Merge remote-tracking branch 'origin/product-procedures' into front-styles

This commit is contained in:
2026-04-03 09:23:51 +02:00
49 changed files with 1496 additions and 96 deletions

14
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "./app/cmd/main.go",
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/.env"
}
]
}

View File

@@ -63,7 +63,7 @@ func AuthMiddleware() fiber.Handler {
// Set user in context // Set user in context
c.Locals(constdata.USER_LOCALES_NAME, user.ToSession()) c.Locals(constdata.USER_LOCALES_NAME, user.ToSession())
c.Locals(constdata.USER_LOCALES_ID, user.ID) c.Locals(constdata.USER_LOCALES_ID, user.ID)
c.Locals(constdata.LANG_LOCALES_ID, user.LangID)
return c.Next() return c.Next()
} }
} }
@@ -85,7 +85,7 @@ func RequireAdmin() fiber.Handler {
}) })
} }
if userSession.Role != model.RoleAdmin { if model.CustomerRole(userSession.RoleName) != model.RoleAdmin {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "admin access required", "error": "admin access required",
}) })

View File

@@ -0,0 +1,28 @@
package middleware
import (
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/model"
"github.com/gofiber/fiber/v3"
)
func Require(p perms.Permission) fiber.Handler {
return func(c fiber.Ctx) error {
u := c.Locals("user")
if u == nil {
return c.SendStatus(fiber.StatusUnauthorized)
}
user, ok := u.(*model.UserSession)
if !ok {
return c.SendStatus(fiber.StatusInternalServerError)
}
for _, perm := range user.Permissions {
if perm == p {
return c.Next()
}
}
return c.SendStatus(fiber.StatusForbidden)
}
}

View File

@@ -0,0 +1,11 @@
package perms
type Permission string
const (
UserRead Permission = "user.read"
UserWrite Permission = "user.write"
UserReadAny Permission = "user.read.any"
UserWriteAny Permission = "user.write.any"
UserDeleteAny Permission = "user.delete.any"
)

View File

@@ -360,7 +360,7 @@ func (h *AuthHandler) UpdateJWTToken(c fiber.Ctx) error {
user := model.Customer{ user := model.Customer{
ID: userLocals.UserID, ID: userLocals.UserID,
Email: userLocals.Email, Email: userLocals.Email,
Role: userLocals.Role, Role: model.Role{ID: userLocals.RoleID, Name: userLocals.RoleName},
LangID: userLocals.LangID, LangID: userLocals.LangID,
CountryID: userLocals.CountryID, CountryID: userLocals.CountryID,
IsActive: userLocals.IsActive, IsActive: userLocals.IsActive,

View File

@@ -0,0 +1,68 @@
package restricted
import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/currencyService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3"
)
type CurrencyHandler struct {
CurrencyService *currencyService.CurrencyService
config *config.Config
}
func NewCurrencyHandler() *CurrencyHandler {
currencyService := currencyService.New()
return &CurrencyHandler{
CurrencyService: currencyService,
config: config.Get(),
}
}
func CurrencyHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewCurrencyHandler()
r.Post("/currency-rate", handler.PostCurrencyRate)
r.Get("/currency-rate/:id", handler.GetCurrencyRate)
return r
}
func (h *CurrencyHandler) PostCurrencyRate(c fiber.Ctx) error {
var currencyRate model.CurrencyRate
if err := c.Bind().Body(&currencyRate); err != nil {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrJSONBody)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrJSONBody)))
}
err := h.CurrencyService.CreateCurrencyRate(&currencyRate)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(nullable.GetNil(""), 1, i18n.T_(c, response.Message_OK)))
}
func (h *CurrencyHandler) GetCurrencyRate(c fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.Atoi(idStr)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
currency, err := h.CurrencyService.GetCurrency(uint(id))
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(currency, 0, i18n.T_(c, response.Message_OK)))
}

View File

@@ -0,0 +1,70 @@
package restricted
import (
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/customerService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3"
)
type customerHandler struct {
service *customerService.CustomerService
}
func NewCustomerHandler() *customerHandler {
customerService := customerService.New()
return &customerHandler{
service: customerService,
}
}
func CustomerHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewCustomerHandler()
r.Get("", handler.customerData)
return r
}
func (h *customerHandler) customerData(fc fiber.Ctx) error {
var customerId uint
customerIdStr := fc.Query("id")
if customerIdStr != "" {
user, ok := fc.Locals("user").(*model.UserSession)
if !ok {
return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute)))
}
id, err := strconv.ParseUint(customerIdStr, 10, 64)
if err != nil {
return fiber.ErrBadRequest
}
if user.UserID != uint(id) && !user.HasPermission(perms.UserReadAny) {
return fc.Status(fiber.StatusForbidden).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrForbidden)))
}
customerId = uint(id)
} else {
id, ok := fc.Locals("userID").(uint)
if !ok {
return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrBadAttribute)))
}
customerId = id
}
customer, err := h.service.GetById(customerId)
if err != nil {
return fc.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err)))
}
return fc.JSON(response.Make(&customer, 0, i18n.T_(fc, response.Message_OK)))
}

View File

@@ -3,6 +3,8 @@ package restricted
import ( import (
"strconv" "strconv"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/service/menuService" "git.ma-al.com/goc_daniel/b2b/app/service/menuService"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n" "git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable" "git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
@@ -86,12 +88,12 @@ func (h *MenuHandler) GetBreadcrumb(c fiber.Ctx) error {
} }
func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error { func (h *MenuHandler) GetTopMenu(c fiber.Ctx) error {
lang_id, ok := c.Locals("langID").(uint) session, ok := c.Locals("user").(*model.UserSession)
if !ok { if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
} }
menu, err := h.menuService.GetTopMenu(lang_id) menu, err := h.menuService.GetTopMenu(session.LangID, session.RoleID)
if err != nil { if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)). return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))

View File

@@ -0,0 +1,82 @@
package restricted
import (
"fmt"
"strconv"
"git.ma-al.com/goc_daniel/b2b/app/config"
"git.ma-al.com/goc_daniel/b2b/app/service/productService"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
"git.ma-al.com/goc_daniel/b2b/app/utils/i18n"
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
"git.ma-al.com/goc_daniel/b2b/app/utils/response"
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
"github.com/gofiber/fiber/v3"
)
type ProductsHandler struct {
productService *productService.ProductService
config *config.Config
}
// NewListProductsHandler creates a new ListProductsHandler instance
func NewProductsHandler() *ProductsHandler {
productService := productService.New()
return &ProductsHandler{
productService: productService,
config: config.Get(),
}
}
func ProductsHandlerRoutes(r fiber.Router) fiber.Router {
handler := NewProductsHandler()
r.Get("/:id/:country_id/:quantity", handler.GetProductJson)
return r
}
func (h *ProductsHandler) GetProductJson(c fiber.Ctx) error {
idStr := c.Params("id")
p_id_product, err := strconv.Atoi(idStr)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
country_idStr := c.Params("country_id")
b2b_id_country, err := strconv.Atoi(country_idStr)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
quantityStr := c.Params("quantity")
p_quantity, err := strconv.Atoi(quantityStr)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
p_id_customer, ok := c.Locals(constdata.USER_LOCALES_ID).(uint)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
fmt.Printf("p_id_customer: %v\n", p_id_customer)
id_lang, ok := c.Locals(constdata.LANG_LOCALES_ID).(uint)
if !ok {
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
}
productJson, err := h.productService.GetJSON(p_id_product, int(id_lang), int(p_id_customer), b2b_id_country, p_quantity)
if err != nil {
return c.Status(responseErrors.GetErrorStatus(err)).
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
}
return c.JSON(response.Make(&productJson, 1, i18n.T_(c, response.Message_OK)))
}

View File

@@ -90,6 +90,9 @@ func (s *Server) Setup() error {
menuRouting := s.public.Group("/menu") menuRouting := s.public.Group("/menu")
public.RoutingHandlerRoutes(menuRouting) public.RoutingHandlerRoutes(menuRouting)
pCustomer := s.restricted.Group("/customer")
restricted.CustomerHandlerRoutes(pCustomer)
// product translation routes (restricted) // product translation routes (restricted)
productTranslation := s.restricted.Group("/product-translation") productTranslation := s.restricted.Group("/product-translation")
restricted.ProductTranslationHandlerRoutes(productTranslation) restricted.ProductTranslationHandlerRoutes(productTranslation)
@@ -98,6 +101,9 @@ func (s *Server) Setup() error {
list := s.restricted.Group("/list") list := s.restricted.Group("/list")
restricted.ListHandlerRoutes(list) restricted.ListHandlerRoutes(list)
product := s.restricted.Group("/product")
restricted.ProductsHandlerRoutes(product)
// locale selector (restricted) // locale selector (restricted)
// this is basically for changing user's selected language and country // this is basically for changing user's selected language and country
localeSelector := s.restricted.Group("/langs-and-countries") localeSelector := s.restricted.Group("/langs-and-countries")
@@ -119,6 +125,8 @@ func (s *Server) Setup() error {
return c.SendStatus(fiber.StatusNotFound) return c.SendStatus(fiber.StatusNotFound)
}) })
restricted.CurrencyHandlerRoutes(s.restricted)
// // Restricted routes example // // Restricted routes example
// restricted := s.api.Group("/restricted") // restricted := s.api.Group("/restricted")
// restricted.Use(middleware.AuthMiddleware()) // restricted.Use(middleware.AuthMiddleware())

25
app/model/currency.go Normal file
View File

@@ -0,0 +1,25 @@
package model
import "time"
type Currency struct {
ID int `json:"id"`
PsIDCurrency uint `json:"ps_id_currency"`
IsDefault bool `json:"is_default"`
IsActive bool `json:"is_active"`
ConversionRate *float64 `json:"conversion_rate,omitempty"`
}
func (Currency) TableName() string {
return "b2b_currencies"
}
type CurrencyRate struct {
B2bIdCurrency uint `json:"b2b_id_currency"`
CreatedAt time.Time `json:"created_at"`
ConversionRate *float64 `json:"conversion_rate,omitempty"`
}
func (CurrencyRate) TableName() string {
return "b2b_currency_rates"
}

View File

@@ -3,6 +3,7 @@ package model
import ( import (
"time" "time"
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -13,7 +14,8 @@ type Customer struct {
Password string `gorm:"size:255" json:"-"` // Hashed password, not exposed in JSON Password string `gorm:"size:255" json:"-"` // Hashed password, not exposed in JSON
FirstName string `gorm:"size:100" json:"first_name"` FirstName string `gorm:"size:100" json:"first_name"`
LastName string `gorm:"size:100" json:"last_name"` LastName string `gorm:"size:100" json:"last_name"`
Role CustomerRole `gorm:"type:varchar(20);default:'user'" json:"role"` RoleID uint `gorm:"column:role_id;not null;default:1" json:"-"`
Role Role `gorm:"foreignKey:RoleID" json:"role"`
Provider AuthProvider `gorm:"type:varchar(20);default:'local'" json:"provider"` Provider AuthProvider `gorm:"type:varchar(20);default:'local'" json:"provider"`
ProviderID string `gorm:"size:255" json:"provider_id,omitempty"` // ID from OAuth provider ProviderID string `gorm:"size:255" json:"provider_id,omitempty"` // ID from OAuth provider
AvatarURL string `gorm:"size:500" json:"avatar_url,omitempty"` AvatarURL string `gorm:"size:500" json:"avatar_url,omitempty"`
@@ -32,14 +34,6 @@ type Customer struct {
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
} }
// CustomerRole represents the role of a user
type CustomerRole string
const (
RoleUser CustomerRole = "user"
RoleAdmin CustomerRole = "admin"
)
// AuthProvider represents the authentication provider // AuthProvider represents the authentication provider
type AuthProvider string type AuthProvider string
@@ -53,16 +47,6 @@ func (Customer) TableName() string {
return "b2b_customers" return "b2b_customers"
} }
// IsAdmin checks if the user has admin role
func (u *Customer) IsAdmin() bool {
return u.Role == RoleAdmin
}
// CanManageUsers checks if the user can manage other users
func (u *Customer) CanManageUsers() bool {
return u.Role == RoleAdmin
}
// FullName returns the user's full name // FullName returns the user's full name
func (u *Customer) FullName() string { func (u *Customer) FullName() string {
if u.FirstName == "" && u.LastName == "" { if u.FirstName == "" && u.LastName == "" {
@@ -76,24 +60,48 @@ type UserSession struct {
UserID uint `json:"user_id"` UserID uint `json:"user_id"`
Email string `json:"email"` Email string `json:"email"`
Username string `json:"username"` Username string `json:"username"`
Role CustomerRole `json:"role"` RoleID uint `json:"role_id"`
RoleName string `json:"role_name"`
LangID uint `json:"lang_id"` LangID uint `json:"lang_id"`
CountryID uint `json:"country_id"` CountryID uint `json:"country_id"`
IsActive bool `json:"is_active"` IsActive bool `json:"is_active"`
Permissions []perms.Permission `json:"permissions"`
}
func (us *UserSession) HasPermission(permission perms.Permission) bool {
for _, p := range us.Permissions {
if p == permission {
return true
}
}
return false
} }
// ToSession converts User to UserSession // ToSession converts User to UserSession
func (u *Customer) ToSession() *UserSession { func (u *Customer) ToSession() *UserSession {
return &UserSession{ return &UserSession{
UserID: u.ID, UserID: u.ID,
Email: u.Email, Email: u.Email,
Role: u.Role, RoleID: u.Role.ID,
RoleName: u.Role.Name,
Permissions: BuildPermissionSlice(u),
LangID: u.LangID, LangID: u.LangID,
CountryID: u.CountryID, CountryID: u.CountryID,
IsActive: u.IsActive, IsActive: u.IsActive,
} }
} }
func BuildPermissionSlice(user *Customer) []perms.Permission {
var perms []perms.Permission
for _, p := range user.Role.Permissions {
perms = append(perms, p.Name)
}
return perms
}
// LoginRequest represents the login form data // LoginRequest represents the login form data
type LoginRequest struct { type LoginRequest struct {
Email string `json:"email" form:"email"` Email string `json:"email" form:"email"`

18
app/model/model.go Normal file
View File

@@ -0,0 +1,18 @@
package model
import (
"time"
"gorm.io/gorm"
)
type Model struct {
ID uint `gorm:"primarykey;autoIncrement" swaggerignore:"true" json:"id,omitempty" hidden:"true"`
CreatedAt time.Time `gorm:"not null;autoCreateTime" swaggerignore:"true" json:"-"`
UpdatedAt time.Time `gorm:"autoUpdateTime" swaggerignore:"true" json:"-"`
DeletedAt gorm.DeletedAt `gorm:"index" swaggerignore:"true" json:"-"`
}
// Makes all objects embedding db.Model implementators of ModelWithID interface
func (m Model) ModelWithID() {
}

12
app/model/permission.go Normal file
View File

@@ -0,0 +1,12 @@
package model
import "git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
type Permission struct {
ID uint
Name perms.Permission
}
func (Permission) TableName() string {
return "b2b_permissions"
}

19
app/model/role.go Normal file
View File

@@ -0,0 +1,19 @@
package model
type Role struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:64" json:"name"`
Permissions []Permission `gorm:"many2many:b2b_role_permissions;" json:"-"`
}
func (Role) TableName() string {
return "b2b_roles"
}
type CustomerRole string
const (
RoleUser CustomerRole = "user"
RoleAdmin CustomerRole = "admin"
RoleSuperAdmin CustomerRole = "super_admin"
)

View File

@@ -0,0 +1,53 @@
package currencyRepo
import (
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
)
type UICurrencyRepo interface {
CreateConversionRate(currencyRate *model.CurrencyRate) error
Get(id uint) (*model.Currency, error)
}
type CurrencyRepo struct{}
func New() UICurrencyRepo {
return &CurrencyRepo{}
}
func (repo *CurrencyRepo) CreateConversionRate(currencyRate *model.CurrencyRate) error {
return db.DB.Create(currencyRate).Error
}
func (repo *CurrencyRepo) Get(id uint) (*model.Currency, error) {
var currency model.Currency
err := db.DB.Table("b2b_currencies c").
Select("c.*, r.conversion_rate").
Joins(`
LEFT JOIN b2b_currency_rates r
ON r.b2b_id_currency = c.id
AND r.created_at = (
SELECT MAX(created_at)
FROM b2b_currency_rates
WHERE b2b_id_currency = c.id
)
`).
Where("c.id = ?", id).
Scan(&currency).Error
return &currency, err
}
func (repo *CurrencyRepo) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.Currency], error) {
found, err := find.Paginate[model.Currency](langId, p, db.DB.
Model(&model.Currency{}).
Scopes(filt.All()...),
)
return &found, err
}

View File

@@ -0,0 +1,27 @@
package customerRepo
import (
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
)
type UICustomerRepo interface {
Get(id uint) (*model.Customer, error)
}
type CustomerRepo struct{}
func New() UICustomerRepo {
return &CustomerRepo{}
}
func (repo *CustomerRepo) Get(id uint) (*model.Customer, error) {
var customer model.Customer
err := db.DB.
Preload("Role").
First(&customer, id).
Error
return &customer, err
}

View File

@@ -0,0 +1,39 @@
package productsRepo
import (
"encoding/json"
"fmt"
"git.ma-al.com/goc_daniel/b2b/app/db"
)
type UIProductsRepo interface {
GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error)
}
type ProductsRepo struct{}
func New() UIProductsRepo {
return &ProductsRepo{}
}
func (repo *ProductsRepo) GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) {
var productStr string // ← Scan as string first
err := db.DB.Raw(`CALL get_full_product(?,?,?,?,?,?)`,
p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity).
Scan(&productStr).
Error
if err != nil {
return nil, err
}
// Optional: validate it's valid JSON
if !json.Valid([]byte(productStr)) {
return nil, fmt.Errorf("invalid json returned from stored procedure")
}
raw := json.RawMessage(productStr)
return &raw, nil
}

View File

@@ -8,7 +8,7 @@ import (
type UIRoutesRepo interface { type UIRoutesRepo interface {
GetRoutes(langId uint) ([]model.Route, error) GetRoutes(langId uint) ([]model.Route, error)
GetTopMenu(id uint) ([]model.B2BTopMenu, error) GetTopMenu(id uint, roleId uint) ([]model.B2BTopMenu, error)
} }
type RoutesRepo struct{} type RoutesRepo struct{}
@@ -26,12 +26,16 @@ func (p *RoutesRepo) GetRoutes(langId uint) ([]model.Route, error) {
return routes, nil return routes, nil
} }
func (p *RoutesRepo) GetTopMenu(id uint) ([]model.B2BTopMenu, error) { func (p *RoutesRepo) GetTopMenu(langId uint, roleId uint) ([]model.B2BTopMenu, error) {
var menus []model.B2BTopMenu var menus []model.B2BTopMenu
err := db.Get(). err := db.
Where("active = ?", 1). Get().
Order("parent_id ASC, position ASC"). Model(model.B2BTopMenu{}).
Joins("JOIN b2b_top_menu_roles tmr ON tmr.top_menu_id = b2b_top_menu.menu_id").
Where(model.B2BTopMenu{Active: 1}).
Where("tmr.role_id = ?", roleId).
Order("b2b_top_menu.parent_id ASC, b2b_top_menu.position ASC").
Find(&menus).Error Find(&menus).Error
return menus, err return menus, err

View File

@@ -26,7 +26,7 @@ type JWTClaims struct {
UserID uint `json:"user_id"` UserID uint `json:"user_id"`
Email string `json:"email"` Email string `json:"email"`
Username string `json:"username"` Username string `json:"username"`
Role model.CustomerRole `json:"customer_role"` Role string `json:"customer_role"`
CartsIDs []uint `json:"carts_ids"` CartsIDs []uint `json:"carts_ids"`
LangID uint `json:"lang_id"` LangID uint `json:"lang_id"`
CountryID uint `json:"country_id"` CountryID uint `json:"country_id"`
@@ -59,7 +59,7 @@ func (s *AuthService) Login(req *model.LoginRequest) (*model.AuthResponse, strin
var user model.Customer var user model.Customer
// Find user by email // Find user by email
if err := s.db.Where("email = ?", req.Email).First(&user).Error; err != nil { if err := s.db.Preload("Role.Permissions").Where("email = ?", req.Email).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, "", responseErrors.ErrInvalidCredentials return nil, "", responseErrors.ErrInvalidCredentials
} }
@@ -144,7 +144,7 @@ func (s *AuthService) Register(req *model.RegisterRequest) error {
Password: string(hashedPassword), Password: string(hashedPassword),
FirstName: req.FirstName, FirstName: req.FirstName,
LastName: req.LastName, LastName: req.LastName,
Role: model.RoleUser, Role: model.Role{},
Provider: model.ProviderLocal, Provider: model.ProviderLocal,
IsActive: false, IsActive: false,
EmailVerified: false, EmailVerified: false,
@@ -422,7 +422,7 @@ func (s *AuthService) RevokeAllRefreshTokens(userID uint) {
// GetUserByID retrieves a user by ID // GetUserByID retrieves a user by ID
func (s *AuthService) GetUserByID(userID uint) (*model.Customer, error) { func (s *AuthService) GetUserByID(userID uint) (*model.Customer, error) {
var user model.Customer var user model.Customer
if err := s.db.First(&user, userID).Error; err != nil { if err := s.db.Preload("Role.Permissions").First(&user, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, responseErrors.ErrUserNotFound return nil, responseErrors.ErrUserNotFound
} }
@@ -489,7 +489,7 @@ func (s *AuthService) generateAccessToken(user *model.Customer) (string, error)
UserID: user.ID, UserID: user.ID,
Email: user.Email, Email: user.Email,
Username: user.Email, Username: user.Email,
Role: user.Role, Role: user.Role.Name,
CartsIDs: []uint{}, CartsIDs: []uint{},
LangID: user.LangID, LangID: user.LangID,
CountryID: user.CountryID, CountryID: user.CountryID,

View File

@@ -150,7 +150,7 @@ func (s *AuthService) findOrCreateGoogleUser(info *view.GoogleUserInfo) (*model.
Provider: model.ProviderGoogle, Provider: model.ProviderGoogle,
ProviderID: info.ID, ProviderID: info.ID,
AvatarURL: info.Picture, AvatarURL: info.Picture,
Role: model.RoleUser, Role: model.Role{},
IsActive: true, IsActive: true,
EmailVerified: true, EmailVerified: true,
LangID: 2, // default is english LangID: 2, // default is english

View File

@@ -0,0 +1,25 @@
package currencyService
import (
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/currencyRepo"
)
type CurrencyService struct {
repo currencyRepo.UICurrencyRepo
}
func (s *CurrencyService) GetCurrency(id uint) (*model.Currency, error) {
return s.repo.Get(id)
}
func (s *CurrencyService) CreateCurrencyRate(currency *model.CurrencyRate) error {
return s.repo.CreateConversionRate(currency)
}
func New() *CurrencyService {
repo := currencyRepo.New()
return &CurrencyService{
repo: repo,
}
}

View File

@@ -0,0 +1,20 @@
package customerService
import (
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/repos/customerRepo"
)
type CustomerService struct {
repo customerRepo.UICustomerRepo
}
func New() *CustomerService {
return &CustomerService{
repo: customerRepo.New(),
}
}
func (s *CustomerService) GetById(id uint) (*model.Customer, error) {
return s.repo.Get(id)
}

View File

@@ -176,8 +176,8 @@ func (s *MenuService) GetBreadcrumb(root_category_id uint, start_category_id uin
return breadcrumb, nil return breadcrumb, nil
} }
func (s *MenuService) GetTopMenu(id uint) ([]*model.B2BTopMenu, error) { func (s *MenuService) GetTopMenu(languageId uint, roleId uint) ([]*model.B2BTopMenu, error) {
items, err := s.routesRepo.GetTopMenu(id) items, err := s.routesRepo.GetTopMenu(languageId, roleId)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -0,0 +1,27 @@
package productService
import (
"encoding/json"
"git.ma-al.com/goc_daniel/b2b/app/repos/productsRepo"
constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data"
)
type ProductService struct {
productsRepo productsRepo.UIProductsRepo
}
func New() *ProductService {
return &ProductService{
productsRepo: productsRepo.New(),
}
}
func (s *ProductService) GetJSON(p_id_product, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) {
products, err := s.productsRepo.GetJSON(p_id_product, constdata.SHOP_ID, p_id_lang, p_id_customer, b2b_id_country, p_quantity)
if err != nil {
return products, err
}
return products, nil
}

View File

@@ -13,3 +13,4 @@ const DEFAULT_NEW_CART_NAME = "new cart"
const USER_LOCALES_NAME = "user" const USER_LOCALES_NAME = "user"
const USER_LOCALES_ID = "userID" const USER_LOCALES_ID = "userID"
const LANG_LOCALES_ID = "langID"

View File

@@ -9,6 +9,7 @@ import (
var ( var (
// Typed errors for request validation and authentication // Typed errors for request validation and authentication
ErrForbidden = errors.New("forbidden")
ErrInvalidBody = errors.New("invalid request body") ErrInvalidBody = errors.New("invalid request body")
ErrNotAuthenticated = errors.New("not authenticated") ErrNotAuthenticated = errors.New("not authenticated")
ErrUserNotFound = errors.New("user not found") ErrUserNotFound = errors.New("user not found")
@@ -59,6 +60,9 @@ var (
ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached") ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached")
ErrUserHasNoSuchCart = errors.New("user does not have cart with given id") ErrUserHasNoSuchCart = errors.New("user does not have cart with given id")
ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist") ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist")
// Typed errors for data parsing
ErrJSONBody = errors.New("invalid JSON body")
) )
// Error represents an error with HTTP status code // Error represents an error with HTTP status code
@@ -83,6 +87,8 @@ func NewError(err error, status int) *Error {
// GetErrorCode returns the error code string for HTTP response mapping // GetErrorCode returns the error code string for HTTP response mapping
func GetErrorCode(c fiber.Ctx, err error) string { func GetErrorCode(c fiber.Ctx, err error) string {
switch { switch {
case errors.Is(err, ErrForbidden):
return i18n.T_(c, "error.err_forbidden")
case errors.Is(err, ErrInvalidBody): case errors.Is(err, ErrInvalidBody):
return i18n.T_(c, "error.err_invalid_body") return i18n.T_(c, "error.err_invalid_body")
case errors.Is(err, ErrInvalidCredentials): case errors.Is(err, ErrInvalidCredentials):
@@ -162,6 +168,9 @@ func GetErrorCode(c fiber.Ctx, err error) string {
case errors.Is(err, ErrProductOrItsVariationDoesNotExist): case errors.Is(err, ErrProductOrItsVariationDoesNotExist):
return i18n.T_(c, "error.product_or_its_variation_does_not_exist") return i18n.T_(c, "error.product_or_its_variation_does_not_exist")
case errors.Is(err, ErrJSONBody):
return i18n.T_(c, "error.err_json_body")
default: default:
return i18n.T_(c, "error.err_internal_server_error") return i18n.T_(c, "error.err_internal_server_error")
} }
@@ -170,6 +179,8 @@ func GetErrorCode(c fiber.Ctx, err error) string {
// GetErrorStatus returns the HTTP status code for the given error // GetErrorStatus returns the HTTP status code for the given error
func GetErrorStatus(err error) int { func GetErrorStatus(err error) int {
switch { switch {
case errors.Is(err, ErrForbidden):
return fiber.StatusForbidden
case errors.Is(err, ErrInvalidCredentials), case errors.Is(err, ErrInvalidCredentials),
errors.Is(err, ErrNotAuthenticated), errors.Is(err, ErrNotAuthenticated),
errors.Is(err, ErrInvalidToken), errors.Is(err, ErrInvalidToken),
@@ -203,7 +214,8 @@ func GetErrorStatus(err error) int {
errors.Is(err, ErrRootNeverReached), errors.Is(err, ErrRootNeverReached),
errors.Is(err, ErrMaxAmtOfCartsReached), errors.Is(err, ErrMaxAmtOfCartsReached),
errors.Is(err, ErrUserHasNoSuchCart), errors.Is(err, ErrUserHasNoSuchCart),
errors.Is(err, ErrProductOrItsVariationDoesNotExist): errors.Is(err, ErrProductOrItsVariationDoesNotExist),
errors.Is(err, ErrJSONBody):
return fiber.StatusBadRequest return fiber.StatusBadRequest
case errors.Is(err, ErrEmailExists): case errors.Is(err, ErrEmailExists):
return fiber.StatusConflict return fiber.StatusConflict

View File

@@ -1,7 +1,7 @@
info: info:
name: Change Locales name: Change Locales
type: http type: http
seq: 4 seq: 3
http: http:
method: POST method: POST

View File

@@ -1,7 +1,7 @@
info: info:
name: Create Search Index name: Create Search Index
type: http type: http
seq: 2 seq: 1
http: http:
method: GET method: GET

View File

@@ -1,7 +1,7 @@
info: info:
name: Delete Index - MeiliSearch name: Delete Index - MeiliSearch
type: http type: http
seq: 7 seq: 5
http: http:
method: DELETE method: DELETE

View File

@@ -1,7 +1,7 @@
info: info:
name: Search Index Settings name: Search Index Settings
type: http type: http
seq: 5 seq: 4
http: http:
method: POST method: POST

View File

@@ -1,7 +1,7 @@
info: info:
name: Search Items name: Search Items
type: http type: http
seq: 3 seq: 2
http: http:
method: POST method: POST

View File

@@ -0,0 +1,29 @@
info:
name: Login
type: http
seq: 1
http:
method: POST
url: "{{bas_url}}/public/auth/login"
body:
type: json
data: |-
{
"email":"{{email}}",
"password":"{{password}}"
}
auth: inherit
runtime:
variables:
- name: email
value: admin@ma-al.com
- name: password
value: Maal12345678
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,7 @@
info:
name: auth
type: folder
seq: 6
request:
auth: inherit

View File

@@ -0,0 +1,15 @@
info:
name: currency-rate
type: http
seq: 2
http:
method: POST
url: "{{bas_url}}/restricted/currency-rate"
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,20 @@
info:
name: currency
type: http
seq: 1
http:
method: GET
url: "{{bas_url}}/restricted/currency-rate/{{id}}"
auth: inherit
runtime:
variables:
- name: id
value: "1"
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,7 @@
info:
name: currency
type: folder
seq: 8
request:
auth: inherit

View File

@@ -0,0 +1,19 @@
info:
name: Customer (me)
type: http
seq: 2
http:
method: GET
url: "{{bas_url}}/restricted/customer?id=1"
params:
- name: id
value: "1"
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,19 @@
info:
name: Customer (other)
type: http
seq: 9
http:
method: GET
url: "{{bas_url}}/restricted/customer?id=1"
params:
- name: id
value: "1"
type: query
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,7 @@
info:
name: customer
type: folder
seq: 9
request:
auth: inherit

View File

@@ -0,0 +1,15 @@
info:
name: Get Product
type: http
seq: 1
http:
method: GET
url: "{{bas_url}}/restricted/product/200/1/5"
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5

View File

@@ -0,0 +1,7 @@
info:
name: product
type: folder
seq: 7
request:
auth: inherit

View File

@@ -1,7 +1,7 @@
-- +goose Up -- +goose Up
CREATE TABLE IF NOT EXISTS b2b_language ( CREATE TABLE IF NOT EXISTS b2b_language (
id INT AUTO_INCREMENT PRIMARY KEY, id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
created_at DATETIME(6) NOT NULL, created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NULL, updated_at DATETIME(6) NULL,
deleted_at DATETIME(6) NULL, deleted_at DATETIME(6) NULL,
@@ -19,29 +19,22 @@ CREATE TABLE IF NOT EXISTS b2b_language (
CREATE INDEX IF NOT EXISTS idx_language_deleted_at CREATE INDEX IF NOT EXISTS idx_language_deleted_at
ON b2b_language (deleted_at); ON b2b_language (deleted_at);
INSERT IGNORE INTO b2b_language
(id, created_at, updated_at, deleted_at, name, iso_code, lang_code, date_format, date_format_short, rtl, is_default, active, flag)
VALUES
(1, '2022-09-16 17:10:02.837', '2026-03-02 21:24:36.779730', NULL, 'Polski', 'pl', 'pl', '__-__-____', '__-__', 0, 0, 1, '🇵🇱'),
(2, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'English', 'en', 'en', '__-__-____', '__-__', 0, 1, 1, '🇬🇧'),
(3, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'Deutsch', 'de', 'de', '__-__-____', '__-__', 0, 0, 1, '🇩🇪');
CREATE TABLE IF NOT EXISTS b2b_components ( CREATE TABLE IF NOT EXISTS b2b_components (
id INT AUTO_INCREMENT PRIMARY KEY, id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL name VARCHAR(255) NOT NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE UNIQUE INDEX IF NOT EXISTS uk_components_name ON b2b_components (name, id); CREATE UNIQUE INDEX IF NOT EXISTS uk_components_name ON b2b_components (name, id);
-- scopes -- scopes
CREATE TABLE IF NOT EXISTS b2b_scopes ( CREATE TABLE IF NOT EXISTS b2b_scopes (
id INT AUTO_INCREMENT PRIMARY KEY, id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL name VARCHAR(255) NOT NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE UNIQUE INDEX IF NOT EXISTS uk_scopes_name ON b2b_scopes (name); CREATE UNIQUE INDEX IF NOT EXISTS uk_scopes_name ON b2b_scopes (name);
-- translations -- translations
CREATE TABLE IF NOT EXISTS b2b_translations ( CREATE TABLE IF NOT EXISTS b2b_translations (
lang_id INT NOT NULL, lang_id INT UNSIGNED NOT NULL,
scope_id INT NOT NULL, scope_id INT UNSIGNED NOT NULL,
component_id INT NOT NULL, component_id INT UNSIGNED NOT NULL,
`key` VARCHAR(255) NOT NULL, `key` VARCHAR(255) NOT NULL,
data TEXT NULL, data TEXT NULL,
PRIMARY KEY (lang_id, scope_id, component_id, `key`), PRIMARY KEY (lang_id, scope_id, component_id, `key`),
@@ -50,7 +43,42 @@ CREATE TABLE IF NOT EXISTS b2b_translations (
CONSTRAINT fk_translations_component FOREIGN KEY (component_id) REFERENCES b2b_components(id) ON DELETE RESTRICT ON UPDATE RESTRICT CONSTRAINT fk_translations_component FOREIGN KEY (component_id) REFERENCES b2b_components(id) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `b2b_roles` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(63) NULL,
PRIMARY KEY (`id`)
);
CREATE UNIQUE INDEX `IX_b2b_roles_id`
ON `b2b_roles` (
`id` ASC
);
CREATE TABLE b2b_permissions (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL UNIQUE
);
CREATE TABLE b2b_role_permissions (
role_id BIGINT UNSIGNED NOT NULL,
permission_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (role_id, permission_id),
CONSTRAINT fk_role_permissions_role
FOREIGN KEY (role_id)
REFERENCES b2b_roles(id)
ON DELETE CASCADE,
CONSTRAINT fk_role_permissions_permission
FOREIGN KEY (permission_id)
REFERENCES b2b_permissions(id)
ON DELETE CASCADE
);
CREATE TABLE `b2b_top_menu_roles` (
`top_menu_id` BIGINT UNSIGNED NOT NULL,
`role_id` BIGINT UNSIGNED NOT NULL
);
-- customers -- customers
CREATE TABLE IF NOT EXISTS b2b_customers ( CREATE TABLE IF NOT EXISTS b2b_customers (
@@ -59,7 +87,7 @@ CREATE TABLE IF NOT EXISTS b2b_customers (
password VARCHAR(255) NULL, password VARCHAR(255) NULL,
first_name VARCHAR(100) NULL, first_name VARCHAR(100) NULL,
last_name VARCHAR(100) NULL, last_name VARCHAR(100) NULL,
role VARCHAR(20) NULL DEFAULT 'user', role_id BIGINT UNSIGNED NOT NULL DEFAULT 1,
provider VARCHAR(20) NULL DEFAULT 'local', provider VARCHAR(20) NULL DEFAULT 'local',
provider_id VARCHAR(255) NULL, provider_id VARCHAR(255) NULL,
avatar_url VARCHAR(500) NULL, avatar_url VARCHAR(500) NULL,
@@ -71,8 +99,8 @@ CREATE TABLE IF NOT EXISTS b2b_customers (
password_reset_expires DATETIME(6) NULL, password_reset_expires DATETIME(6) NULL,
last_password_reset_request DATETIME(6) NULL, last_password_reset_request DATETIME(6) NULL,
last_login_at DATETIME(6) NULL, last_login_at DATETIME(6) NULL,
lang_id BIGINT NULL DEFAULT 2, lang_id INT NULL DEFAULT 2,
country_id BIGINT NULL DEFAULT 2, country_id INT NULL DEFAULT 2,
created_at DATETIME(6) NULL, created_at DATETIME(6) NULL,
updated_at DATETIME(6) NULL, updated_at DATETIME(6) NULL,
deleted_at DATETIME(6) NULL deleted_at DATETIME(6) NULL
@@ -84,10 +112,13 @@ ON b2b_customers (email);
CREATE INDEX IF NOT EXISTS idx_customers_deleted_at CREATE INDEX IF NOT EXISTS idx_customers_deleted_at
ON b2b_customers (deleted_at); ON b2b_customers (deleted_at);
ALTER TABLE b2b_customers
ADD CONSTRAINT fk_customer_role
FOREIGN KEY (role_id) REFERENCES b2b_roles(id);
-- customer_carts -- customer_carts
CREATE TABLE IF NOT EXISTS b2b_customer_carts ( CREATE TABLE IF NOT EXISTS b2b_customer_carts (
cart_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, cart_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(255) NULL, name VARCHAR(255) NULL,
CONSTRAINT fk_customer_carts_customers FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE CASCADE ON UPDATE CASCADE CONSTRAINT fk_customer_carts_customers FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE CASCADE ON UPDATE CASCADE
@@ -97,10 +128,10 @@ CREATE INDEX IF NOT EXISTS idx_customer_carts_user_id ON b2b_customer_carts (use
-- carts_products -- carts_products
CREATE TABLE IF NOT EXISTS b2b_carts_products ( CREATE TABLE IF NOT EXISTS b2b_carts_products (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
cart_id BIGINT UNSIGNED NOT NULL, cart_id INT UNSIGNED NOT NULL,
product_id INT UNSIGNED NOT NULL, product_id INT UNSIGNED NOT NULL,
product_attribute_id BIGINT NULL, product_attribute_id INT NULL,
amount INT UNSIGNED NOT NULL, amount INT UNSIGNED NOT NULL,
CONSTRAINT fk_carts_products_customer_carts FOREIGN KEY (cart_id) REFERENCES b2b_customer_carts (cart_id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT fk_carts_products_customer_carts FOREIGN KEY (cart_id) REFERENCES b2b_customer_carts (cart_id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_carts_products_product FOREIGN KEY (product_id) REFERENCES ps_product (id_product) ON DELETE CASCADE ON UPDATE CASCADE CONSTRAINT fk_carts_products_product FOREIGN KEY (product_id) REFERENCES ps_product (id_product) ON DELETE CASCADE ON UPDATE CASCADE
@@ -110,7 +141,7 @@ CREATE INDEX IF NOT EXISTS idx_carts_products_cart_id ON b2b_carts_products (car
-- refresh_tokens -- refresh_tokens
CREATE TABLE IF NOT EXISTS b2b_refresh_tokens ( CREATE TABLE IF NOT EXISTS b2b_refresh_tokens (
id INT AUTO_INCREMENT PRIMARY KEY, id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
customer_id BIGINT UNSIGNED NOT NULL, customer_id BIGINT UNSIGNED NOT NULL,
token_hash VARCHAR(64) NOT NULL, token_hash VARCHAR(64) NOT NULL,
expires_at DATETIME(6) NOT NULL, expires_at DATETIME(6) NOT NULL,
@@ -120,24 +151,137 @@ CREATE TABLE IF NOT EXISTS b2b_refresh_tokens (
CREATE UNIQUE INDEX IF NOT EXISTS uk_refresh_tokens_token_hash ON b2b_refresh_tokens (token_hash); CREATE UNIQUE INDEX IF NOT EXISTS uk_refresh_tokens_token_hash ON b2b_refresh_tokens (token_hash);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_customer_id ON b2b_refresh_tokens (customer_id); CREATE INDEX IF NOT EXISTS idx_refresh_tokens_customer_id ON b2b_refresh_tokens (customer_id);
CREATE TABLE `b2b_currencies` (
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL,
`ps_id_currency` INT UNSIGNED NOT NULL,
`is_default` TINYINT NOT NULL,
`is_active` TINYINT NOT NULL,
PRIMARY KEY (`id`)
)
ENGINE = InnoDB;
ALTER TABLE `b2b_currencies` ADD CONSTRAINT `FK_b2b_currencies_ps_id_currency` FOREIGN KEY (`ps_id_currency`) REFERENCES `ps_currency` (`id_currency`) ON DELETE CASCADE ON UPDATE CASCADE;
CREATE INDEX `fk_b2b_currencies_ps_currency`
ON `b2b_currencies` (
`ps_id_currency` ASC
);
CREATE TABLE `b2b_currency_rates` (
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL,
`b2b_id_currency` BIGINT UNSIGNED NOT NULL,
`created_at` DATETIME NOT NULL,
`conversion_rate` DECIMAL(13,6) NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)
ENGINE = InnoDB;
ALTER TABLE `b2b_currency_rates` ADD CONSTRAINT `FK_b2b_currency_rates_b2b_id_currency` FOREIGN KEY (`b2b_id_currency`) REFERENCES `b2b_currencies` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
CREATE INDEX `fk_b2b_currency_rates_b2b_currencies`
ON `b2b_currency_rates` (
`b2b_id_currency` ASC
);
-- countries -- countries
CREATE TABLE IF NOT EXISTS b2b_countries ( CREATE TABLE IF NOT EXISTS b2b_countries (
id INT AUTO_INCREMENT PRIMARY KEY, `id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL,
name VARCHAR(128) NOT NULL, `flag` VARCHAR(16) NOT NULL,
currency_id INT UNSIGNED NOT NULL, `ps_id_country` INT UNSIGNED NOT NULL,
flag VARCHAR(16) NOT NULL, `b2b_id_currency` BIGINT UNSIGNED NOT NULL,
CONSTRAINT fk_countries_currency FOREIGN KEY (currency_id) REFERENCES ps_currency(id_currency) ON DELETE RESTRICT ON UPDATE RESTRICT PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CONSTRAINT `fk_b2b_countries_ps_country` FOREIGN KEY (`ps_id_country`) REFERENCES `ps_country` (`id_country`) ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT `FK_b2b_countries_b2b_id_currency` FOREIGN KEY (`b2b_id_currency`) REFERENCES `b2b_currencies` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
)
ENGINE = InnoDB;
CREATE INDEX `fk_b2b_countries_ps_country`
ON `b2b_countries` (
`ps_id_country` ASC
);
INSERT IGNORE INTO b2b_countries CREATE TABLE b2b_specific_price (
(id, name, currency_id, flag) id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
VALUES name VARCHAR(255) NOT NULL,
(1, 'Polska', 1, '🇵🇱'), created_at DATETIME NULL,
(2, 'England', 2, '🇬🇧'), updated_at DATETIME NULL,
(3, 'Čeština', 2, '🇨🇿'), deleted_at DATETIME NULL,
(4, 'Deutschland', 2, '🇩🇪'); scope ENUM('shop', 'category', 'product') NOT NULL,
valid_from DATETIME NULL,
valid_till DATETIME NULL,
has_expiration_date BOOLEAN DEFAULT FALSE,
reduction_type ENUM('amount', 'percentage') NOT NULL,
price DECIMAL(10, 2) NULL,
b2b_id_currency BIGINT UNSIGNED NULL, -- specifies which currency is used for the price
percentage_reduction DECIMAL(5, 2) NULL,
from_quantity INT UNSIGNED DEFAULT 1,
is_active BOOLEAN DEFAULT TRUE
) ENGINE = InnoDB;
CREATE INDEX idx_b2b_scope ON b2b_specific_price(scope);
CREATE INDEX idx_b2b_customer ON b2b_specific_price(b2b_id_customer);
CREATE INDEX idx_b2b_country ON b2b_specific_price(b2b_id_country);
CREATE INDEX idx_b2b_active_dates ON b2b_specific_price(is_active, valid_from, valid_till);
CREATE INDEX idx_b2b_lookup
ON b2b_specific_price (
scope,
is_active,
b2b_id_customer,
b2b_id_country,
from_quantity
);
CREATE TABLE b2b_specific_price_product (
b2b_specific_price_id BIGINT UNSIGNED,
id_product INT UNSIGNED,
PRIMARY KEY (b2b_specific_price_id, id_product),
FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE,
FOREIGN KEY (id_product) REFERENCES ps_product(id_product) ON DELETE CASCADE
);
CREATE TABLE b2b_specific_price_category (
b2b_specific_price_id BIGINT UNSIGNED,
id_category INT UNSIGNED,
PRIMARY KEY (b2b_specific_price_id, id_category),
FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE,
FOREIGN KEY (id_category) REFERENCES ps_category(id_category) ON DELETE CASCADE
);
CREATE TABLE b2b_specific_price_product_attribute (
b2b_specific_price_id BIGINT UNSIGNED,
id_product_attribute INT UNSIGNED,
PRIMARY KEY (b2b_specific_price_id, id_product_attribute),
FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE,
FOREIGN KEY (id_product_attribute) REFERENCES ps_product_attribute(id_product_attribute) ON DELETE CASCADE
);
CREATE TABLE b2b_specific_price_customer (
b2b_specific_price_id BIGINT UNSIGNED,
b2b_id_customer BIGINT UNSIGNED,
PRIMARY KEY (b2b_specific_price_id, b2b_id_customer),
FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE,
FOREIGN KEY (b2b_id_customer) REFERENCES b2b_customers(id) ON DELETE CASCADE
);
CREATE TABLE b2b_specific_price_country (
b2b_specific_price_id BIGINT UNSIGNED,
b2b_id_country BIGINT UNSIGNED,
PRIMARY KEY (b2b_specific_price_id, b2b_id_country),
FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE,
FOREIGN KEY (b2b_id_country) REFERENCES b2b_countries(id) ON DELETE CASCADE
);
CREATE INDEX idx_b2b_product_rel
ON b2b_specific_price_product (id_product);
CREATE INDEX idx_b2b_category_rel
ON b2b_specific_price_category (id_category);
CREATE INDEX idx_b2b_product_attribute_rel
ON b2b_specific_price_product_attribute (id_product_attribute);
CREATE INDEX idx_bsp_customer
ON b2b_specific_price_customer (b2b_specific_price_id, b2b_id_customer);
CREATE INDEX idx_bsp_country
ON b2b_specific_price_country (b2b_specific_price_id, b2b_id_country);
DELIMITER // DELIMITER //
@@ -229,6 +373,60 @@ END$$
DELIMITER ; DELIMITER ;
CREATE TABLE b2b_specific_price_product (
b2b_specific_price_id BIGINT UNSIGNED,
id_product INT UNSIGNED,
PRIMARY KEY (b2b_specific_price_id, id_product),
FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE,
FOREIGN KEY (id_product) REFERENCES ps_product(id_product) ON DELETE CASCADE
);
CREATE TABLE b2b_specific_price_category (
b2b_specific_price_id BIGINT UNSIGNED,
id_category INT UNSIGNED,
PRIMARY KEY (b2b_specific_price_id, id_category),
FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE,
FOREIGN KEY (id_category) REFERENCES ps_category(id_category) ON DELETE CASCADE
);
CREATE TABLE b2b_specific_price_product_attribute (
b2b_specific_price_id BIGINT UNSIGNED,
id_product_attribute INT UNSIGNED,
PRIMARY KEY (b2b_specific_price_id, id_product_attribute),
FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE,
FOREIGN KEY (id_product_attribute) REFERENCES ps_product_attribute(id_product_attribute) ON DELETE CASCADE
);
CREATE TABLE b2b_specific_price_customer (
b2b_specific_price_id BIGINT UNSIGNED,
b2b_id_customer BIGINT UNSIGNED,
PRIMARY KEY (b2b_specific_price_id, b2b_id_customer),
FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE,
FOREIGN KEY (b2b_id_customer) REFERENCES b2b_customers(id) ON DELETE CASCADE
);
CREATE TABLE b2b_specific_price_country (
b2b_specific_price_id BIGINT UNSIGNED,
b2b_id_country BIGINT UNSIGNED,
PRIMARY KEY (b2b_specific_price_id, b2b_id_country),
FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE,
FOREIGN KEY (b2b_id_country) REFERENCES b2b_countries(id) ON DELETE CASCADE
);
CREATE INDEX idx_b2b_product_rel
ON b2b_specific_price_product (id_product);
CREATE INDEX idx_b2b_category_rel
ON b2b_specific_price_category (id_category);
CREATE INDEX idx_b2b_product_attribute_rel
ON b2b_specific_price_product_attribute (id_product_attribute);
CREATE INDEX idx_bsp_customer
ON b2b_specific_price_customer (b2b_specific_price_id, b2b_id_customer);
CREATE INDEX idx_bsp_country
ON b2b_specific_price_country (b2b_specific_price_id, b2b_id_country);
-- +goose Down -- +goose Down
DROP TABLE IF EXISTS b2b_countries; DROP TABLE IF EXISTS b2b_countries;
@@ -238,3 +436,9 @@ DROP TABLE IF EXISTS b2b_scopes;
DROP TABLE IF EXISTS b2b_translations; DROP TABLE IF EXISTS b2b_translations;
DROP TABLE IF EXISTS b2b_customers; DROP TABLE IF EXISTS b2b_customers;
DROP TABLE IF EXISTS b2b_refresh_tokens; DROP TABLE IF EXISTS b2b_refresh_tokens;
DROP TABLE IF EXISTS b2b_currencies;
DROP TABLE IF EXISTS b2b_currency_rates;
DROP TABLE IF EXISTS b2b_specific_price;
DROP TABLE IF EXISTS b2b_specific_price_product;
DROP TABLE IF EXISTS b2b_specific_price_category;
DROP TABLE IF EXISTS b2b_specific_price_product_attribute;

View File

@@ -0,0 +1,35 @@
-- +goose Up
INSERT IGNORE INTO b2b_language
(id, created_at, updated_at, deleted_at, name, iso_code, lang_code, date_format, date_format_short, rtl, is_default, active, flag)
VALUES
(1, '2022-09-16 17:10:02.837', '2026-03-02 21:24:36.779730', NULL, 'Polski', 'pl', 'pl', '__-__-____', '__-__', 0, 0, 1, '🇵🇱'),
(2, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'English', 'en', 'en', '__-__-____', '__-__', 0, 1, 1, '🇬🇧'),
(3, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'Deutsch', 'de', 'de', '__-__-____', '__-__', 0, 0, 1, '🇩🇪');
INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('user','1');
INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('admin','2');
INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('super_admin','3');
-- insert sample admin user admin@ma-al.com/Maal12345678
INSERT IGNORE INTO b2b_customers (id, email, password, first_name, last_name, role_id, provider, provider_id, avatar_url, is_active, email_verified, email_verification_token, email_verification_expires, password_reset_token, password_reset_expires, last_password_reset_request, last_login_at, lang_id, country_id, created_at, updated_at, deleted_at)
VALUES
(1, 'admin@ma-al.com', '$2a$10$Owy9DjrS0l3Fz4XoOvh5pulgmOMqdwXmb7hYE9BovnSuWS2plGr82', 'Super', 'Admin', 2, 'local', '', '', 1, 1, NULL, NULL, '', NULL, NULL, NULL, 1, 1, '2026-03-02 16:55:10.252740', '2026-03-02 16:55:10.252740', NULL);
ALTER TABLE b2b_customers AUTO_INCREMENT = 1;
INSERT INTO `b2b_currencies` (`ps_id_currency`, `is_default`, `is_active`) VALUES
('1','1','1'),
('2','0','1');
INSERT IGNORE INTO b2b_countries
(id, flag, ps_id_country, b2b_id_currency)
VALUES
(1, '🇵🇱', 14, 1),
(2, '🇬🇧', 17, 2),
(3, '🇨🇿', 16, 2),
(4, '🇩🇪', 1, 2);
-- +goose Down

View File

@@ -0,0 +1,380 @@
-- +goose Up
DELIMITER //
DROP PROCEDURE IF EXISTS get_full_product
//
CREATE PROCEDURE get_full_product(
IN p_id_product INT UNSIGNED,
IN p_id_shop INT UNSIGNED,
IN p_id_lang INT UNSIGNED,
IN p_id_customer INT UNSIGNED,
IN b2b_id_country INT UNSIGNED,
IN p_quantity INT UNSIGNED
)
BEGIN
DECLARE v_tax_rate DECIMAL(10, 4) DEFAULT 0;
DECLARE p_id_currency DECIMAL(10, 4) DEFAULT 0;
SELECT
COALESCE(t.rate, 0.0000) INTO v_tax_rate
FROM
ps_tax_rule tr
INNER JOIN ps_tax t ON t.id_tax = tr.id_tax
LEFT JOIN b2b_countries ON b2b_countries.id = b2b_id_country
WHERE
tr.id_tax_rules_group = (
SELECT
ps.id_tax_rules_group
FROM
ps_product_shop ps
WHERE
ps.id_product = p_id_product
AND ps.id_shop = p_id_shop
LIMIT
1
)
AND tr.id_country = b2b_countries.ps_id_country
ORDER BY
tr.id_state DESC,
tr.zipcode_from != '' DESC,
tr.id_tax_rule DESC
LIMIT
1;
SELECT
b2b_currencies.ps_id_currency INTO p_id_currency
FROM
b2b_currencies
LEFT JOIN b2b_countries ON b2b_countries.b2b_id_currency = b2b_currencies.id
WHERE
b2b_countries.id = b2b_id_country
LIMIT 1;
/* FINAL JSON */
SELECT
JSON_OBJECT(
/* ================= PRODUCT ================= */
'id_product',
p.id_product,
'reference',
p.reference,
'name',
pl.name,
'description',
pl.description,
'short_description',
pl.description_short,
/* ================= PRICE ================= */
'price',
JSON_OBJECT(
'base',
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate),
'final_tax_excl',
(
CASE
WHEN bsp.id IS NOT NULL THEN
CASE
/* FIXED PRICE */
WHEN bsp.reduction_type = 'amount' THEN
(
CASE
WHEN bsp.b2b_id_currency IS NULL THEN bsp.price
ELSE bsp.price * br_bsp.conversion_rate
END
)
/* PERCENTAGE */
WHEN bsp.reduction_type = 'percentage' THEN
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate)
* (1 - bsp.percentage_reduction / 100)
ELSE
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate)
END
ELSE
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate)
END
),
'final_tax_incl',
(
(
CASE
WHEN bsp.id IS NOT NULL THEN
CASE
WHEN bsp.reduction_type = 'amount' THEN
(
CASE
WHEN bsp.b2b_id_currency IS NULL THEN bsp.price
ELSE bsp.price * br_bsp.conversion_rate
END
)
WHEN bsp.reduction_type = 'percentage' THEN
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate)
* (1 - bsp.percentage_reduction / 100)
ELSE
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate)
END
ELSE
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate)
END
) * (1 + v_tax_rate / 100)
)
),
/* ================= META ================= */
'active',
COALESCE(ps.active, p.active),
'visibility',
COALESCE(ps.visibility, p.visibility),
'manufacturer',
m.name,
'category',
cl.name,
/* ================= IMAGE ================= */
'cover_image',
JSON_OBJECT(
'id',
i.id_image,
'legend',
il.legend
),
/* ================= FEATURES ================= */
'features',
(
SELECT
JSON_ARRAYAGG(
JSON_OBJECT(
'name',
fl.name,
'value',
fvl.value
)
)
FROM
ps_feature_product fp
JOIN ps_feature_lang fl ON fl.id_feature = fp.id_feature
AND fl.id_lang = p_id_lang
JOIN ps_feature_value_lang fvl ON fvl.id_feature_value = fp.id_feature_value
AND fvl.id_lang = p_id_lang
WHERE
fp.id_product = p.id_product
),
/* ================= COMBINATIONS ================= */
'combinations',
(
SELECT
JSON_ARRAYAGG(
JSON_OBJECT(
'id_product_attribute',
pa.id_product_attribute,
'reference',
pa.reference,
'price',
JSON_OBJECT(
'impact',
COALESCE(pas.price, pa.price),
'final_tax_excl',
(
COALESCE(ps.price, p.price) + COALESCE(pas.price, pa.price)
),
'final_tax_incl',
(
(
COALESCE(ps.price, p.price) + COALESCE(pas.price, pa.price)
) * (1 + v_tax_rate / 100)
)
),
'stock',
IFNULL(sa.quantity, 0),
'default_on',
pas.default_on,
/* ATTRIBUTES JSON */
'attributes',
(
SELECT
JSON_ARRAYAGG(
JSON_OBJECT(
'group',
agl.name,
'attribute',
al.name
)
)
FROM
ps_product_attribute_combination pac
JOIN ps_attribute a ON a.id_attribute = pac.id_attribute
JOIN ps_attribute_lang al ON al.id_attribute = a.id_attribute
AND al.id_lang = p_id_lang
JOIN ps_attribute_group_lang agl ON agl.id_attribute_group = a.id_attribute_group
AND agl.id_lang = p_id_lang
WHERE
pac.id_product_attribute = pa.id_product_attribute
),
/* IMAGES */
'images',
(
SELECT
JSON_ARRAYAGG(img.id_image)
FROM
ps_product_attribute_image pai
JOIN ps_image img ON img.id_image = pai.id_image
WHERE
pai.id_product_attribute = pa.id_product_attribute
)
)
)
FROM
ps_product_attribute pa
JOIN ps_product_attribute_shop pas ON pas.id_product_attribute = pa.id_product_attribute
AND pas.id_shop = p_id_shop
LEFT JOIN ps_stock_available sa ON sa.id_product = pa.id_product
AND sa.id_product_attribute = pa.id_product_attribute
AND sa.id_shop = p_id_shop
WHERE
pa.id_product = p.id_product
)
) AS product_json
FROM
ps_product p
LEFT JOIN ps_product_shop ps ON ps.id_product = p.id_product
AND ps.id_shop = p_id_shop
LEFT JOIN ps_product_lang pl ON pl.id_product = p.id_product
AND pl.id_lang = p_id_lang
AND pl.id_shop = p_id_shop
LEFT JOIN ps_category_lang cl ON cl.id_category = COALESCE(ps.id_category_default, p.id_category_default)
AND cl.id_lang = p_id_lang
AND cl.id_shop = p_id_shop
LEFT JOIN ps_manufacturer m ON m.id_manufacturer = p.id_manufacturer
LEFT JOIN ps_image i ON i.id_product = p.id_product
AND i.cover = 1
LEFT JOIN ps_image_lang il ON il.id_image = i.id_image
AND il.id_lang = p_id_lang
/* SPECIFIC PRICE */
LEFT JOIN (
SELECT bsp.*
FROM b2b_specific_price bsp
/* RELATIONS */
LEFT JOIN b2b_specific_price_product bsp_p
ON bsp_p.b2b_specific_price_id = bsp.id
LEFT JOIN b2b_specific_price_category bsp_c
ON bsp_c.b2b_specific_price_id = bsp.id
WHERE bsp.is_active = TRUE
/* SCOPE MATCH */
AND (
/* PRODUCT */
(bsp.scope = 'product' AND bsp_p.id_product = p_id_product)
/* CATEGORY */
OR (
bsp.scope = 'category'
AND bsp_c.id_category IN (
SELECT cp.id_category
FROM ps_category_product cp
WHERE cp.id_product = p_id_product
)
)
/* SHOP (GLOBAL) */
OR (bsp.scope = 'shop')
)
/* CUSTOMER MATCH */
AND (
NOT EXISTS (
SELECT 1 FROM b2b_specific_price_customer c
WHERE c.b2b_specific_price_id = bsp.id
)
OR EXISTS (
SELECT 1 FROM b2b_specific_price_customer c
WHERE c.b2b_specific_price_id = bsp.id
AND c.b2b_id_customer = p_id_customer
)
)
/* COUNTRY MATCH */
AND (
NOT EXISTS (
SELECT 1 FROM b2b_specific_price_country ctry
WHERE ctry.b2b_specific_price_id = bsp.id
)
OR EXISTS (
SELECT 1 FROM b2b_specific_price_country ctry
WHERE ctry.b2b_specific_price_id = bsp.id
AND ctry.b2b_id_country = b2b_id_country
)
)
/* QUANTITY */
AND bsp.from_quantity <= p_quantity
/* DATE */
AND (
bsp.has_expiration_date = FALSE OR (
(bsp.valid_from IS NULL OR bsp.valid_from <= NOW())
AND (bsp.valid_till IS NULL OR bsp.valid_till >= NOW())
)
)
ORDER BY
/* 🔥 SCOPE PRIORITY */
bsp.scope = 'product' DESC,
bsp.scope = 'category' DESC,
bsp.scope = 'shop' DESC,
/* 🔥 CUSTOMER PRIORITY */
(
EXISTS (
SELECT 1 FROM b2b_specific_price_customer c
WHERE c.b2b_specific_price_id = bsp.id
AND c.b2b_id_customer = p_id_customer
)
) DESC,
/* 🔥 COUNTRY PRIORITY */
(
EXISTS (
SELECT 1 FROM b2b_specific_price_country ctry
WHERE ctry.b2b_specific_price_id = bsp.id
AND ctry.b2b_id_country = b2b_id_country
)
) DESC,
/* GLOBAL fallback (no restrictions) naturally goes last */
bsp.from_quantity DESC,
bsp.id DESC
LIMIT 1
) bsp ON 1=1
LEFT JOIN b2b_currency_rates br_bsp
ON br_bsp.b2b_id_currency = bsp.b2b_id_currency
AND br_bsp.created_at = (
SELECT MAX(created_at)
FROM b2b_currency_rates
WHERE b2b_id_currency = bsp.b2b_id_currency
)
LEFT JOIN b2b_countries ON b2b_countries.id = b2b_id_country
LEFT JOIN b2b_currencies ON b2b_currencies.id = p_id_currency
LEFT JOIN b2b_currency_rates r ON r.b2b_id_currency = b2b_currencies.id
AND r.created_at = (
SELECT
MAX(created_at)
FROM
b2b_currency_rates
WHERE
b2b_id_currency = b2b_currencies.id
)
WHERE
p.id_product = p_id_product
LIMIT
1;
END //
DELIMITER ;
-- +goose Down

View File

@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
SELECT 'up SQL query';
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd

View File

@@ -0,0 +1,53 @@
package currencyRepo
import (
"git.ma-al.com/goc_daniel/b2b/app/db"
"git.ma-al.com/goc_daniel/b2b/app/model"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
)
type UICurrencyRepo interface {
CreateConversionRate(currencyRate *model.CurrencyRate) error
Get(id uint) (*model.Currency, error)
}
type CurrencyRepo struct{}
func New() UICurrencyRepo {
return &CurrencyRepo{}
}
func (repo *CurrencyRepo) CreateConversionRate(currencyRate *model.CurrencyRate) error {
return db.DB.Debug().Create(currencyRate).Error
}
func (repo *CurrencyRepo) Get(id uint) (*model.Currency, error) {
var currency model.Currency
err := db.DB.Table("b2b_currencies c").
Select("c.*, r.conversion_rate").
Joins(`
LEFT JOIN b2b_currency_rates r
ON r.b2b_id_currency = c.id
AND r.created_at = (
SELECT MAX(created_at)
FROM b2b_currency_rates
WHERE b2b_id_currency = c.id
)
`).
Where("c.id = ?", id).
Scan(&currency).Error
return &currency, err
}
func (repo *CurrencyRepo) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.Currency], error) {
found, err := find.Paginate[model.Currency](langId, p, db.DB.
Model(&model.Currency{}).
Scopes(filt.All()...),
)
return &found, err
}

View File

@@ -59,6 +59,7 @@ tasks:
- | - |
sed '/-- +goose Down/,$d' i18n/migrations/20260302163100_routes.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}} sed '/-- +goose Down/,$d' i18n/migrations/20260302163100_routes.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}}
sed '/-- +goose Down/,$d' i18n/migrations/20260302163122_create_tables.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}} sed '/-- +goose Down/,$d' i18n/migrations/20260302163122_create_tables.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}}
sed '/-- +goose Down/,$d' i18n/migrations/20260302163123_create_tables_data.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}}
sed '/-- +goose Down/,$d' i18n/migrations/20260302163152_translations_backoffice.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}} sed '/-- +goose Down/,$d' i18n/migrations/20260302163152_translations_backoffice.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}}
sed '/-- +goose Down/,$d' i18n/migrations/20260302163157_translations_backend.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}} sed '/-- +goose Down/,$d' i18n/migrations/20260302163157_translations_backend.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}}