product-procedures #59
@@ -2,27 +2,27 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware/perms"
|
"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/utils/localeExtractor"
|
||||||
|
"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"
|
"github.com/gofiber/fiber/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Require(p perms.Permission) fiber.Handler {
|
func Require(p perms.Permission) fiber.Handler {
|
||||||
return func(c fiber.Ctx) error {
|
return func(c fiber.Ctx) error {
|
||||||
u := c.Locals("user")
|
user, ok := localeExtractor.GetCustomer(c)
|
||||||
if u == nil {
|
|
||||||
return c.SendStatus(fiber.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
user, ok := u.(*model.UserSession)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return c.SendStatus(fiber.StatusInternalServerError)
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, perm := range user.Permissions {
|
for _, perm := range user.Role.Permissions {
|
||||||
if perm == p {
|
if perm.Name == p {
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return c.SendStatus(fiber.StatusForbidden)
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrForbidden)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrForbidden)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ package perms
|
|||||||
type Permission string
|
type Permission string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UserReadAny Permission = "user.read.any"
|
UserReadAny Permission = "user.read.any"
|
||||||
UserWriteAny Permission = "user.write.any"
|
UserWriteAny Permission = "user.write.any"
|
||||||
UserDeleteAny Permission = "user.delete.any"
|
UserDeleteAny Permission = "user.delete.any"
|
||||||
CurrencyWrite Permission = "currency.write"
|
CurrencyWrite Permission = "currency.write"
|
||||||
|
SpecificPriceManage Permission = "specific_price.manage"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
"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/model/dbmodel"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/service/productService"
|
"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/i18n"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/localeExtractor"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/nullable"
|
||||||
@@ -34,6 +35,7 @@ func ProductsHandlerRoutes(r fiber.Router) fiber.Router {
|
|||||||
|
|
||||||
r.Get("/:id/:country_id/:quantity", handler.GetProductJson)
|
r.Get("/:id/:country_id/:quantity", handler.GetProductJson)
|
||||||
r.Get("/list", handler.ListProducts)
|
r.Get("/list", handler.ListProducts)
|
||||||
|
r.Get("/list-variants/:product_id", handler.ListProductVariants)
|
||||||
r.Post("/favorite/:product_id", handler.AddToFavorites)
|
r.Post("/favorite/:product_id", handler.AddToFavorites)
|
||||||
r.Delete("/favorite/:product_id", handler.RemoveFromFavorites)
|
r.Delete("/favorite/:product_id", handler.RemoveFromFavorites)
|
||||||
|
|
||||||
@@ -70,7 +72,7 @@ func (h *ProductsHandler) GetProductJson(c fiber.Ctx) error {
|
|||||||
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)))
|
||||||
}
|
}
|
||||||
productJson, err := h.productService.GetJSON(p_id_product, int(customer.LangID), int(customer.ID), b2b_id_country, p_quantity)
|
productJson, err := h.productService.Get(uint(p_id_product), customer.LangID, customer.ID, uint(b2b_id_country), uint(p_quantity))
|
||||||
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)))
|
||||||
@@ -80,25 +82,19 @@ func (h *ProductsHandler) GetProductJson(c fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ProductsHandler) ListProducts(c fiber.Ctx) error {
|
func (h *ProductsHandler) ListProducts(c fiber.Ctx) error {
|
||||||
paging, filters, err := query_params.ParseFilters[model.Product](c, columnMappingListProducts)
|
paging, filters, err := query_params.ParseFilters[dbmodel.PsProduct](c, columnMappingListProducts)
|
||||||
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)))
|
||||||
}
|
}
|
||||||
|
|
||||||
id_lang, ok := localeExtractor.GetLangID(c)
|
customer, ok := localeExtractor.GetCustomer(c)
|
||||||
if !ok {
|
if !ok || customer == nil {
|
||||||
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)))
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, ok := localeExtractor.GetUserID(c)
|
list, err := h.productService.Find(customer.LangID, customer.ID, paging, filters, customer, constdata.DEFAULT_PRODUCT_QUANTITY, constdata.SHOP_ID)
|
||||||
if !ok {
|
|
||||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
|
||||||
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
|
||||||
}
|
|
||||||
|
|
||||||
list, err := h.productService.Find(id_lang, userID, paging, filters)
|
|
||||||
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)))
|
||||||
@@ -164,3 +160,27 @@ func (h *ProductsHandler) RemoveFromFavorites(c fiber.Ctx) error {
|
|||||||
|
|
||||||
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
return c.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *ProductsHandler) ListProductVariants(c fiber.Ctx) error {
|
||||||
|
productIDStr := c.Params("product_id")
|
||||||
|
|
||||||
|
productID, err := strconv.Atoi(productIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
customer, ok := localeExtractor.GetCustomer(c)
|
||||||
|
if !ok || customer == nil {
|
||||||
|
goc_daniel marked this conversation as resolved
|
|||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := h.productService.GetProductAttributes(customer.LangID, uint(productID), constdata.SHOP_ID, customer.ID, customer.CountryID, constdata.DEFAULT_PRODUCT_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(&list, len(list), i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|||||||
159
app/delivery/web/api/restricted/specificPrice.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
package restricted
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/delivery/middleware"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/service/specificPriceService"
|
||||||
|
"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 SpecificPriceHandler struct {
|
||||||
|
SpecificPriceService *specificPriceService.SpecificPriceService
|
||||||
|
config *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSpecificPriceHandler() *SpecificPriceHandler {
|
||||||
|
SpecificPriceService := specificPriceService.New()
|
||||||
|
return &SpecificPriceHandler{
|
||||||
|
SpecificPriceService: SpecificPriceService,
|
||||||
|
config: config.Get(),
|
||||||
|
}
|
||||||
|
dudzic_wiktor marked this conversation as resolved
goc_daniel
commented
Add permissions everywhere. Also, while I would leave it as is, we transfer c.Context() to service rather than its specific necessary elements. On a more important note, rather than returning: return c.Status(fiber.StatusCreated).JSON(result) we have agreed instead on using: return c.JSON(response.Make(&result, 0, i18n.T_(c, response.Message_OK))) I guess, overall, this whole file and its subcomponents have to be revisited. Add permissions everywhere. Also, while I would leave it as is, we transfer c.Context() to service rather than its specific necessary elements. On a more important note, rather than returning:
return c.Status(fiber.StatusCreated).JSON(result)
we have agreed instead on using:
return c.JSON(response.Make(&result, 0, i18n.T_(c, response.Message_OK)))
I guess, overall, this whole file and its subcomponents have to be revisited.
|
|||||||
|
}
|
||||||
|
|
||||||
|
func SpecificPriceHandlerRoutes(r fiber.Router) fiber.Router {
|
||||||
|
handler := NewSpecificPriceHandler()
|
||||||
|
|
||||||
|
r.Post("/", middleware.Require("specific_price.manage"), handler.Create)
|
||||||
|
r.Put("/:id", middleware.Require("specific_price.manage"), handler.Update)
|
||||||
|
r.Delete("/:id", middleware.Require("specific_price.manage"), handler.Delete)
|
||||||
|
r.Get("/", middleware.Require("specific_price.manage"), handler.List)
|
||||||
|
r.Get("/:id", middleware.Require("specific_price.manage"), handler.GetByID)
|
||||||
|
r.Patch("/:id/activate", middleware.Require("specific_price.manage"), handler.Activate)
|
||||||
|
r.Patch("/:id/deactivate", middleware.Require("specific_price.manage"), handler.Deactivate)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SpecificPriceHandler) Create(c fiber.Ctx) error {
|
||||||
|
var pr model.SpecificPrice
|
||||||
|
if err := c.Bind().Body(&pr); err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.SpecificPriceService.Create(c.Context(), &pr)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(&result, 1, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SpecificPriceHandler) Update(c fiber.Ctx) error {
|
||||||
|
idStr := c.Params("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
var pr model.SpecificPrice
|
||||||
|
if err := c.Bind().Body(&pr); err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody)))
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.SpecificPriceService.Update(c.Context(), id, &pr)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(&result, 1, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SpecificPriceHandler) List(c fiber.Ctx) error {
|
||||||
|
result, err := h.SpecificPriceService.List(c.Context())
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(err)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response.Make(&result, 1, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SpecificPriceHandler) GetByID(c fiber.Ctx) error {
|
||||||
|
idStr := c.Params("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.SpecificPriceService.GetByID(c.Context(), 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(&result, 1, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SpecificPriceHandler) Activate(c fiber.Ctx) error {
|
||||||
|
idStr := c.Params("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.SpecificPriceService.SetActive(c.Context(), id, true)
|
||||||
|
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(""), 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SpecificPriceHandler) Deactivate(c fiber.Ctx) error {
|
||||||
|
idStr := c.Params("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.SpecificPriceService.SetActive(c.Context(), id, false)
|
||||||
|
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(""), 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SpecificPriceHandler) Delete(c fiber.Ctx) error {
|
||||||
|
idStr := c.Params("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)).
|
||||||
|
JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute)))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.SpecificPriceService.Delete(c.Context(), 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(nullable.GetNil(""), 0, i18n.T_(c, response.Message_OK)))
|
||||||
|
}
|
||||||
@@ -132,6 +132,8 @@ func (s *Server) Setup() error {
|
|||||||
carts := s.restricted.Group("/carts")
|
carts := s.restricted.Group("/carts")
|
||||||
restricted.CartsHandlerRoutes(carts)
|
restricted.CartsHandlerRoutes(carts)
|
||||||
|
|
||||||
|
specificPrice := s.restricted.Group("/specific-price")
|
||||||
|
restricted.SpecificPriceHandlerRoutes(specificPrice)
|
||||||
// addresses (restricted)
|
// addresses (restricted)
|
||||||
addresses := s.restricted.Group("/addresses")
|
addresses := s.restricted.Group("/addresses")
|
||||||
restricted.AddressesHandlerRoutes(addresses)
|
restricted.AddressesHandlerRoutes(addresses)
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import "git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
|
|
||||||
|
|
||||||
// Represents a country together with its associated currency
|
// Represents a country together with its associated currency
|
||||||
type Country struct {
|
type Country struct {
|
||||||
ID uint `gorm:"primaryKey;column:id" json:"id"`
|
ID uint `gorm:"primaryKey;column:id" json:"id"`
|
||||||
Name string `gorm:"column:name" json:"name"`
|
Name string `gorm:"column:name" json:"name"`
|
||||||
Flag string `gorm:"size:16;not null;column:flag" json:"flag"`
|
Flag string `gorm:"size:16;not null;column:flag" json:"flag"`
|
||||||
|
|
||||||
PSCurrencyID uint `gorm:"column:currency_id" json:"currency_id"`
|
CurrencyID uint `gorm:"column:b2b_id_currency" json:"currency_id"`
|
||||||
PSCurrency *dbmodel.PsCurrency `gorm:"foreignKey:PSCurrencyID;references:IDCurrency" json:"ps_currency"`
|
Currency *Currency `gorm:"foreignKey:CurrencyID" json:"currency,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Country) TableName() string {
|
func (Country) TableName() string {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type Customer struct {
|
|||||||
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
||||||
LangID uint `gorm:"default:2" json:"lang_id"` // User's preferred language
|
LangID uint `gorm:"default:2" json:"lang_id"` // User's preferred language
|
||||||
CountryID uint `gorm:"default:2" json:"country_id"` // User's selected country
|
CountryID uint `gorm:"default:2" json:"country_id"` // User's selected country
|
||||||
|
Country *Country `gorm:"foreignKey:CountryID" json:"country,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|||||||
@@ -1,76 +1,17 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
// Product contains each and every column from the table ps_product.
|
|
||||||
type Product struct {
|
|
||||||
ProductID uint `gorm:"column:id_product;primaryKey" json:"product_id" form:"product_id"`
|
|
||||||
SupplierID uint `gorm:"column:id_supplier" json:"supplier_id" form:"supplier_id"`
|
|
||||||
ManufacturerID uint `gorm:"column:id_manufacturer" json:"manufacturer_id" form:"manufacturer_id"`
|
|
||||||
CategoryDefaultID uint `gorm:"column:id_category_default" json:"category_default_id" form:"category_default_id"`
|
|
||||||
ShopDefaultID uint `gorm:"column:id_shop_default" json:"shop_default_id" form:"shop_default_id"`
|
|
||||||
TaxRulesGroupID uint `gorm:"column:id_tax_rules_group" json:"tax_rules_group_id" form:"tax_rules_group_id"`
|
|
||||||
OnSale uint `gorm:"column:on_sale" json:"on_sale" form:"on_sale"`
|
|
||||||
OnlineOnly uint `gorm:"column:online_only" json:"online_only" form:"online_only"`
|
|
||||||
EAN13 string `gorm:"column:ean13;type:varchar(13)" json:"ean13" form:"ean13"`
|
|
||||||
ISBN string `gorm:"column:isbn;type:varchar(32)" json:"isbn" form:"isbn"`
|
|
||||||
UPC string `gorm:"column:upc;type:varchar(12)" json:"upc" form:"upc"`
|
|
||||||
EkoTax float32 `gorm:"column:eko_tax;type:decimal(20,6)" json:"eko_tax" form:"eko_tax"`
|
|
||||||
Quantity uint `gorm:"column:quantity" json:"quantity" form:"quantity"`
|
|
||||||
MinimalQuantity uint `gorm:"column:minimal_quantity" json:"minimal_quantity" form:"minimal_quantity"`
|
|
||||||
LowStockThreshold uint `gorm:"column:low_stock_threshold" json:"low_stock_threshold" form:"low_stock_threshold"`
|
|
||||||
LowStockAlert uint `gorm:"column:low_stock_alert" json:"low_stock_alert" form:"low_stock_alert"`
|
|
||||||
Price float32 `gorm:"column:price;type:decimal(20,6)" json:"price" form:"price"`
|
|
||||||
WholesalePrice float32 `gorm:"column:wholesale_price;type:decimal(20,6)" json:"wholesale_price" form:"wholesale_price"`
|
|
||||||
Unity string `gorm:"column:unity;type:varchar(255)" json:"unity" form:"unity"`
|
|
||||||
UnitPriceRatio float32 `gorm:"column:unit_price_ratio;type:decimal(20,6)" json:"unit_price_ratio" form:"unit_price_ratio"`
|
|
||||||
UnitID uint `gorm:"column:id_unit;primaryKey" json:"unit_id" form:"unit_id"`
|
|
||||||
AdditionalShippingCost float32 `gorm:"column:additional_shipping_cost;type:decimal(20,2)" json:"additional_shipping_cost" form:"additional_shipping_cost"`
|
|
||||||
Reference string `gorm:"column:reference;type:varchar(64)" json:"reference" form:"reference"`
|
|
||||||
SupplierReference string `gorm:"column:supplier_reference;type:varchar(64)" json:"supplier_reference" form:"supplier_reference"`
|
|
||||||
Location string `gorm:"column:location;type:varchar(64)" json:"location" form:"location"`
|
|
||||||
|
|
||||||
Width float32 `gorm:"column:width;type:decimal(20,6)" json:"width" form:"width"`
|
|
||||||
Height float32 `gorm:"column:height;type:decimal(20,6)" json:"height" form:"height"`
|
|
||||||
Depth float32 `gorm:"column:depth;type:decimal(20,6)" json:"depth" form:"depth"`
|
|
||||||
Weight float32 `gorm:"column:weight;type:decimal(20,6)" json:"weight" form:"weight"`
|
|
||||||
OutOfStock uint `gorm:"column:out_of_stock" json:"out_of_stock" form:"out_of_stock"`
|
|
||||||
AdditionalDeliveryTimes uint `gorm:"column:additional_delivery_times" json:"additional_delivery_times" form:"additional_delivery_times"`
|
|
||||||
QuantityDiscount uint `gorm:"column:quantity_discount" json:"quantity_discount" form:"quantity_discount"`
|
|
||||||
Customizable uint `gorm:"column:customizable" json:"customizable" form:"customizable"`
|
|
||||||
UploadableFiles uint `gorm:"column:uploadable_files" json:"uploadable_files" form:"uploadable_files"`
|
|
||||||
TextFields uint `gorm:"column:text_fields" json:"text_fields" form:"text_fields"`
|
|
||||||
|
|
||||||
Active uint `gorm:"column:active" json:"active" form:"active"`
|
|
||||||
RedirectType string `gorm:"column:redirect_type;type:enum('','404','301-product','302-product','301-category','302-category')" json:"redirect_type" form:"redirect_type"`
|
|
||||||
TypeRedirectedID int `gorm:"column:id_type_redirected" json:"type_redirected_id" form:"type_redirected_id"`
|
|
||||||
AvailableForOrder uint `gorm:"column:available_for_order" json:"available_for_order" form:"available_for_order"`
|
|
||||||
AvailableDate string `gorm:"column:available_date;type:date" json:"available_date" form:"available_date"`
|
|
||||||
ShowCondition uint `gorm:"column:show_condition" json:"show_condition" form:"show_condition"`
|
|
||||||
Condition string `gorm:"column:condition;type:enum('new','used','refurbished')" json:"condition" form:"condition"`
|
|
||||||
ShowPrice uint `gorm:"column:show_price" json:"show_price" form:"show_price"`
|
|
||||||
|
|
||||||
Indexed uint `gorm:"column:indexed" json:"indexed" form:"indexed"`
|
|
||||||
Visibility string `gorm:"column:visibility;type:enum('both','catalog','search','none')" json:"visibility" form:"visibility"`
|
|
||||||
CacheIsPack uint `gorm:"column:cache_is_pack" json:"cache_is_pack" form:"cache_is_pack"`
|
|
||||||
CacheHasAttachments uint `gorm:"column:cache_has_attachments" json:"cache_has_attachments" form:"cache_has_attachments"`
|
|
||||||
IsVirtual uint `gorm:"column:is_virtual" json:"is_virtual" form:"is_virtual"`
|
|
||||||
CacheDefaultAttribute uint `gorm:"column:cache_default_attribute" json:"cache_default_attribute" form:"cache_default_attribute"`
|
|
||||||
DateAdd string `gorm:"column:date_add;type:datetime" json:"date_add" form:"date_add"`
|
|
||||||
DateUpd string `gorm:"column:date_upd;type:datetime" json:"date_upd" form:"date_upd"`
|
|
||||||
AdvancedStockManagement uint `gorm:"column:advanced_stock_management" json:"advanced_stock_management" form:"advanced_stock_management"`
|
|
||||||
PackStockType uint `gorm:"column:pack_stock_type" json:"pack_stock_type" form:"pack_stock_type"`
|
|
||||||
State uint `gorm:"column:state" json:"state" form:"state"`
|
|
||||||
DeliveryDays uint `gorm:"column:delivery_days" json:"delivery_days" form:"delivery_days"`
|
|
||||||
}
|
|
||||||
type ProductInList struct {
|
type ProductInList struct {
|
||||||
ProductID uint `gorm:"column:product_id" json:"product_id" form:"product_id"`
|
ProductID uint `gorm:"column:product_id" json:"product_id" form:"product_id"`
|
||||||
Name string `gorm:"column:name" json:"name" form:"name"`
|
Name string `gorm:"column:name" json:"name" form:"name"`
|
||||||
LinkRewrite string `gorm:"column:link_rewrite" json:"link_rewrite"`
|
LinkRewrite string `gorm:"column:link_rewrite" json:"link_rewrite"`
|
||||||
ImageLink string `gorm:"column:image_link" json:"image_link"`
|
ImageLink string `gorm:"column:image_link" json:"image_link"`
|
||||||
CategoryName string `gorm:"column:category_name" json:"category_name" form:"category_name"`
|
CategoryName string `gorm:"column:category_name" json:"category_name" form:"category_name"`
|
||||||
Reference string `gorm:"column:reference" json:"reference"`
|
Reference string `gorm:"column:reference" json:"reference"`
|
||||||
VariantsNumber uint `gorm:"column:variants_number" json:"variants_number"`
|
VariantsNumber uint `gorm:"column:variants_number" json:"variants_number"`
|
||||||
Quantity int64 `gorm:"column:quantity" json:"quantity"`
|
Quantity int64 `gorm:"column:quantity" json:"quantity"`
|
||||||
IsFavorite bool `gorm:"column:is_favorite" json:"is_favorite"`
|
PriceTaxExcl float64 `gorm:"column:price_tax_excl" json:"price_tax_excl"`
|
||||||
|
PriceTaxIncl float64 `gorm:"column:price_tax_incl" json:"price_tax_incl"`
|
||||||
|
IsFavorite bool `gorm:"column:is_favorite" json:"is_favorite"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductFilters struct {
|
type ProductFilters struct {
|
||||||
|
|||||||
29
app/model/specificPrice.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type SpecificPrice struct {
|
||||||
|
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
|
Name string `gorm:"type:varchar(255);not null" json:"name"`
|
||||||
|
ValidFrom *time.Time `gorm:"null" json:"valid_from"`
|
||||||
|
ValidTill *time.Time `gorm:"null" json:"valid_till"`
|
||||||
|
HasExpirationDate bool `gorm:"default:false" json:"has_expiration_date"`
|
||||||
|
ReductionType string `gorm:"type:enum('amount','percentage');not null" json:"reduction_type"`
|
||||||
|
Price *float64 `gorm:"type:decimal(10,2);null" json:"price"`
|
||||||
|
CurrencyID *uint64 `gorm:"column:b2b_id_currency;null" json:"currency_id"`
|
||||||
|
PercentageReduction *float64 `gorm:"type:decimal(5,2);null" json:"percentage_reduction"`
|
||||||
|
FromQuantity uint32 `gorm:"default:1" json:"from_quantity"`
|
||||||
|
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||||
|
CreatedAt *time.Time `gorm:"null" json:"created_at"`
|
||||||
|
UpdatedAt *time.Time `gorm:"null" json:"updated_at"`
|
||||||
|
|
||||||
|
ProductIDs []uint64 `gorm:"-" json:"product_ids"`
|
||||||
|
CategoryIDs []uint64 `gorm:"-" json:"category_ids"`
|
||||||
|
ProductAttributeIDs []uint64 `gorm:"-" json:"product_attribute_ids"`
|
||||||
|
CountryIDs []uint64 `gorm:"-" json:"country_ids"`
|
||||||
|
CustomerIDs []uint64 `gorm:"-" json:"customer_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SpecificPrice) TableName() string {
|
||||||
|
return "b2b_specific_price"
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package productsRepo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/config"
|
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/db"
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||||
@@ -10,13 +9,18 @@ import (
|
|||||||
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
|
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/view"
|
||||||
"git.ma-al.com/goc_marek/gormcol"
|
"git.ma-al.com/goc_marek/gormcol"
|
||||||
"github.com/WinterYukky/gorm-extra-clause-plugin/exclause"
|
"github.com/WinterYukky/gorm-extra-clause-plugin/exclause"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UIProductsRepo interface {
|
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)
|
// GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error)
|
||||||
Find(id_lang, userID uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error)
|
Find(id_lang uint, userID uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.ProductInList], error)
|
||||||
|
GetProductVariants(langID uint, productID uint, shopID uint, customerID uint, countryID uint, quantity uint) ([]view.ProductAttribute, error)
|
||||||
|
GetBase(p_id_product, p_id_shop, p_id_lang uint) (view.Product, error)
|
||||||
|
GetPrice(p_id_product uint, productAttributeID *uint, p_id_shop uint, p_id_customer uint, p_id_country uint, p_quantity uint) (view.Price, error)
|
||||||
|
GetVariants(p_id_product, p_id_shop, p_id_lang, p_id_customer, p_id_country, p_quantity uint) ([]view.ProductAttribute, error)
|
||||||
AddToFavorites(userID uint, productID uint) error
|
AddToFavorites(userID uint, productID uint) error
|
||||||
RemoveFromFavorites(userID uint, productID uint) error
|
RemoveFromFavorites(userID uint, productID uint) error
|
||||||
ExistsInFavorites(userID uint, productID uint) (bool, error)
|
ExistsInFavorites(userID uint, productID uint) (bool, error)
|
||||||
@@ -29,30 +33,78 @@ func New() UIProductsRepo {
|
|||||||
return &ProductsRepo{}
|
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) {
|
func (repo *ProductsRepo) GetBase(p_id_product, p_id_shop, p_id_lang uint) (view.Product, error) {
|
||||||
var productStr string // ← Scan as string first
|
var result view.Product
|
||||||
|
|
||||||
err := db.DB.Raw(`CALL get_full_product(?,?,?,?,?,?)`,
|
err := db.DB.Raw(`CALL get_product_base(?,?,?)`,
|
||||||
p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity).
|
p_id_product, p_id_shop, p_id_lang).
|
||||||
Scan(&productStr).
|
Scan(&result).Error
|
||||||
Error
|
|
||||||
|
|
||||||
if err != nil {
|
return result, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !json.Valid([]byte(productStr)) {
|
|
||||||
return nil, fmt.Errorf("invalid json returned from stored procedure")
|
|
||||||
}
|
|
||||||
|
|
||||||
raw := json.RawMessage(productStr)
|
|
||||||
return &raw, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *ProductsRepo) Find(id_lang, userID uint, p find.Paging, filt *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
func (repo *ProductsRepo) GetPrice(
|
||||||
var list []model.ProductInList
|
p_id_product uint, productAttributeID *uint, p_id_shop uint, p_id_customer uint, p_id_country uint, p_quantity uint,
|
||||||
var total int64
|
) (view.Price, error) {
|
||||||
|
|
||||||
|
type row struct {
|
||||||
|
Price json.RawMessage `gorm:"column:price"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var r row
|
||||||
|
err := db.DB.Raw(`
|
||||||
|
SELECT fn_product_price(?,?,?,?,?,?) AS price`,
|
||||||
|
p_id_product, p_id_shop, p_id_customer, p_id_country, p_quantity, productAttributeID).
|
||||||
|
Scan(&r).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return view.Price{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var temp struct {
|
||||||
|
Base json.Number `json:"base"`
|
||||||
|
FinalTaxExcl json.Number `json:"final_tax_excl"`
|
||||||
|
FinalTaxIncl json.Number `json:"final_tax_incl"`
|
||||||
|
TaxRate json.Number `json:"tax_rate"`
|
||||||
|
Priority json.Number `json:"priority"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(r.Price, &temp); err != nil {
|
||||||
|
return view.Price{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
price := view.Price{
|
||||||
|
Base: mustParseFloat(temp.Base),
|
||||||
|
FinalTaxExcl: mustParseFloat(temp.FinalTaxExcl),
|
||||||
|
FinalTaxIncl: mustParseFloat(temp.FinalTaxIncl),
|
||||||
|
TaxRate: mustParseFloat(temp.TaxRate),
|
||||||
|
Priority: mustParseInt(temp.Priority),
|
||||||
|
}
|
||||||
|
|
||||||
|
return price, nil
|
||||||
|
}
|
||||||
|
func mustParseFloat(n json.Number) float64 {
|
||||||
|
f, _ := n.Float64()
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustParseInt(n json.Number) int {
|
||||||
|
i, _ := n.Int64()
|
||||||
|
return int(i)
|
||||||
|
}
|
||||||
|
func (repo *ProductsRepo) GetVariants(p_id_product, p_id_shop, p_id_lang, p_id_customer, p_id_country, p_quantity uint) ([]view.ProductAttribute, error) {
|
||||||
|
|
||||||
|
var results []view.ProductAttribute
|
||||||
|
|
||||||
|
err := db.DB.Raw(`
|
||||||
|
CALL get_product_variants(?,?,?,?,?,?)`,
|
||||||
|
p_id_product, p_id_shop, p_id_lang, p_id_customer, p_id_country, p_quantity).
|
||||||
|
Scan(&results).Error
|
||||||
|
|
||||||
|
return results, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *ProductsRepo) Find(langID uint, userID uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.ProductInList], error) {
|
||||||
query := db.Get().
|
query := db.Get().
|
||||||
Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps").
|
Table(gormcol.Field.Tab(dbmodel.PsProductShopCols.Active)+" AS ps").
|
||||||
Select(`
|
Select(`
|
||||||
@@ -67,9 +119,9 @@ func (repo *ProductsRepo) Find(id_lang, userID uint, p find.Paging, filt *filter
|
|||||||
COALESCE(f.is_favorite, 0) AS is_favorite
|
COALESCE(f.is_favorite, 0) AS is_favorite
|
||||||
`, config.Get().Image.ImagePrefix).
|
`, config.Get().Image.ImagePrefix).
|
||||||
Joins("JOIN "+dbmodel.PsProductCols.IDProduct.Tab()+" p ON p.id_product = ps.id_product").
|
Joins("JOIN "+dbmodel.PsProductCols.IDProduct.Tab()+" p ON p.id_product = ps.id_product").
|
||||||
Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_lang = ?", id_lang).
|
Joins("JOIN ps_product_lang pl ON pl.id_product = ps.id_product AND pl.id_lang = ?", langID).
|
||||||
Joins("JOIN ps_image_shop ims ON ims.id_product = ps.id_product AND ims.cover = 1").
|
Joins("JOIN ps_image_shop ims ON ims.id_product = ps.id_product AND ims.cover = 1").
|
||||||
Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_lang = ?", id_lang).
|
Joins("JOIN ps_category_lang cl ON cl.id_category = ps.id_category_default AND cl.id_lang = ?", langID).
|
||||||
Joins("JOIN ps_category_product cp ON cp.id_product = ps.id_product").
|
Joins("JOIN ps_category_product cp ON cp.id_product = ps.id_product").
|
||||||
Joins("LEFT JOIN variants v ON v.id_product = ps.id_product").
|
Joins("LEFT JOIN variants v ON v.id_product = ps.id_product").
|
||||||
Joins("LEFT JOIN favorites f ON f.id_product = ps.id_product").
|
Joins("LEFT JOIN favorites f ON f.id_product = ps.id_product").
|
||||||
@@ -105,29 +157,64 @@ func (repo *ProductsRepo) Find(id_lang, userID uint, p find.Paging, filt *filter
|
|||||||
}).
|
}).
|
||||||
Order("ps.id_product DESC")
|
Order("ps.id_product DESC")
|
||||||
|
|
||||||
// Apply all filters
|
query = query.Scopes(filt.All()...)
|
||||||
if filt != nil {
|
|
||||||
filt.ApplyAll(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
// run counter first as query is without limit and offset
|
list, err := find.Paginate[model.ProductInList](langID, p, query)
|
||||||
err := query.Count(&total).Error
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return find.Found[model.ProductInList]{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return &list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *ProductsRepo) GetProductVariants(langID uint, productID uint, shopID uint, customerID uint, countryID uint, quantity uint) ([]view.ProductAttribute, error) {
|
||||||
|
var result []view.ProductAttribute
|
||||||
|
err := db.DB.
|
||||||
|
Raw(`
|
||||||
|
CALL get_product_attributes_with_price(?, ?, ?, ?, ?, ?)
|
||||||
|
`,
|
||||||
|
langID,
|
||||||
|
productID,
|
||||||
|
shopID,
|
||||||
|
customerID,
|
||||||
|
countryID,
|
||||||
|
quantity,
|
||||||
|
).
|
||||||
|
Scan(&result).Error
|
||||||
|
|
||||||
err = query.
|
|
||||||
Limit(p.Limit()).
|
|
||||||
Offset(p.Offset()).
|
|
||||||
Find(&list).Error
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return find.Found[model.ProductInList]{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return find.Found[model.ProductInList]{
|
return result, nil
|
||||||
Items: list,
|
}
|
||||||
Count: uint(total),
|
|
||||||
}, nil
|
func (repo *ProductsRepo) PopulateProductPrice(product *model.ProductInList, targetCustomer *model.Customer, quantity int, shopID uint) error {
|
||||||
|
row := db.Get().Raw(
|
||||||
|
"CALL get_product_price(?, ?, ?, ?, ?)",
|
||||||
|
product.ProductID,
|
||||||
|
shopID,
|
||||||
|
targetCustomer.ID,
|
||||||
|
targetCustomer.CountryID,
|
||||||
|
quantity,
|
||||||
|
).Row()
|
||||||
|
|
||||||
|
var (
|
||||||
|
id uint
|
||||||
|
base float64
|
||||||
|
excl float64
|
||||||
|
incl float64
|
||||||
|
tax float64
|
||||||
|
)
|
||||||
|
|
||||||
|
err := row.Scan(&id, &base, &excl, &incl, &tax)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
product.PriceTaxExcl = excl
|
||||||
|
product.PriceTaxIncl = incl
|
||||||
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *ProductsRepo) AddToFavorites(userID uint, productID uint) error {
|
func (repo *ProductsRepo) AddToFavorites(userID uint, productID uint) error {
|
||||||
|
|||||||
247
app/repos/specificPriceRepo/specificPriceRepo.go
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
package specificPriceRepo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UISpecificPriceRepo interface {
|
||||||
|
Create(ctx context.Context, pr *model.SpecificPrice) error
|
||||||
|
Update(ctx context.Context, pr *model.SpecificPrice) error
|
||||||
|
Delete(ctx context.Context, id uint64) error
|
||||||
|
GetByID(ctx context.Context, id uint64) (*model.SpecificPrice, error)
|
||||||
|
List(ctx context.Context) ([]*model.SpecificPrice, error)
|
||||||
|
SetActive(ctx context.Context, id uint64, active bool) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpecificPriceRepo struct{}
|
||||||
|
|
||||||
|
func New() UISpecificPriceRepo {
|
||||||
|
return &SpecificPriceRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *SpecificPriceRepo) Create(ctx context.Context, pr *model.SpecificPrice) error {
|
||||||
|
return db.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
now := time.Now()
|
||||||
|
pr.CreatedAt = &now
|
||||||
|
|
||||||
|
if err := tx.Create(pr).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return repo.insertRelations(tx, pr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *SpecificPriceRepo) Update(ctx context.Context, pr *model.SpecificPrice) error {
|
||||||
|
return db.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
now := time.Now()
|
||||||
|
pr.UpdatedAt = &now
|
||||||
|
|
||||||
|
if err := tx.Save(pr).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo.clearRelations(tx, pr.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return repo.insertRelations(tx, pr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *SpecificPriceRepo) GetByID(ctx context.Context, id uint64) (*model.SpecificPrice, error) {
|
||||||
|
var pr model.SpecificPrice
|
||||||
|
err := db.DB.WithContext(ctx).Where("id = ?", id).First(&pr).Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo.loadRelations(ctx, &pr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *SpecificPriceRepo) List(ctx context.Context) ([]*model.SpecificPrice, error) {
|
||||||
|
var specificPrices []*model.SpecificPrice
|
||||||
|
err := db.DB.WithContext(ctx).Find(&specificPrices).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range specificPrices {
|
||||||
|
if err := repo.loadRelations(ctx, specificPrices[i]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return specificPrices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *SpecificPriceRepo) SetActive(ctx context.Context, id uint64, active bool) error {
|
||||||
|
return db.DB.WithContext(ctx).Model(&model.SpecificPrice{}).Where("id = ?", id).Update("is_active", active).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *SpecificPriceRepo) Delete(ctx context.Context, id uint64) error {
|
||||||
|
return db.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Exec("DELETE FROM b2b_specific_price_product WHERE b2b_specific_price_id = ?", id).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Exec("DELETE FROM b2b_specific_price_category WHERE b2b_specific_price_id = ?", id).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Exec("DELETE FROM b2b_specific_price_product_attribute WHERE b2b_specific_price_id = ?", id).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Exec("DELETE FROM b2b_specific_price_country WHERE b2b_specific_price_id = ?", id).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Exec("DELETE FROM b2b_specific_price_customer WHERE b2b_specific_price_id = ?", id).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Delete(&model.SpecificPrice{}, "id = ?", id).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *SpecificPriceRepo) insertRelations(tx *gorm.DB, pr *model.SpecificPrice) error {
|
||||||
|
if len(pr.ProductIDs) > 0 {
|
||||||
|
for _, productID := range pr.ProductIDs {
|
||||||
|
if err := tx.Exec(`
|
||||||
|
INSERT INTO b2b_specific_price_product (b2b_specific_price_id, id_product) VALUES (?, ?)
|
||||||
|
`, pr.ID, productID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pr.CategoryIDs) > 0 {
|
||||||
|
for _, categoryID := range pr.CategoryIDs {
|
||||||
|
if err := tx.Exec(`
|
||||||
|
INSERT INTO b2b_specific_price_category (b2b_specific_price_id, id_category) VALUES (?, ?)
|
||||||
|
`, pr.ID, categoryID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pr.ProductAttributeIDs) > 0 {
|
||||||
|
for _, attrID := range pr.ProductAttributeIDs {
|
||||||
|
if err := tx.Exec(`
|
||||||
|
INSERT INTO b2b_specific_price_product_attribute (b2b_specific_price_id, id_product_attribute) VALUES (?, ?)
|
||||||
|
`, pr.ID, attrID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pr.CountryIDs) > 0 {
|
||||||
|
for _, countryID := range pr.CountryIDs {
|
||||||
|
if err := tx.Exec(`
|
||||||
|
INSERT INTO b2b_specific_price_country (b2b_specific_price_id, b2b_id_country) VALUES (?, ?)
|
||||||
|
`, pr.ID, countryID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pr.CustomerIDs) > 0 {
|
||||||
|
for _, customerID := range pr.CustomerIDs {
|
||||||
|
if err := tx.Exec(`
|
||||||
|
INSERT INTO b2b_specific_price_customer (b2b_specific_price_id, b2b_id_customer) VALUES (?, ?)
|
||||||
|
`, pr.ID, customerID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *SpecificPriceRepo) clearRelations(tx *gorm.DB, id uint64) error {
|
||||||
|
if err := tx.Exec("DELETE FROM b2b_specific_price_product WHERE b2b_specific_price_id = ?", id).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Exec("DELETE FROM b2b_specific_price_category WHERE b2b_specific_price_id = ?", id).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Exec("DELETE FROM b2b_specific_price_product_attribute WHERE b2b_specific_price_id = ?", id).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Exec("DELETE FROM b2b_specific_price_country WHERE b2b_specific_price_id = ?", id).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Exec("DELETE FROM b2b_specific_price_customer WHERE b2b_specific_price_id = ?", id).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *SpecificPriceRepo) loadRelations(ctx context.Context, pr *model.SpecificPrice) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
var productIDs []struct {
|
||||||
|
IDProduct uint64 `gorm:"column:id_product"`
|
||||||
|
}
|
||||||
|
if err = db.DB.WithContext(ctx).Table("b2b_specific_price_product").Where("b2b_specific_price_id = ?", pr.ID).Select("id_product").Scan(&productIDs).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, p := range productIDs {
|
||||||
|
pr.ProductIDs = append(pr.ProductIDs, p.IDProduct)
|
||||||
|
}
|
||||||
|
|
||||||
|
var categoryIDs []struct {
|
||||||
|
IDCategory uint64 `gorm:"column:id_category"`
|
||||||
|
}
|
||||||
|
if err = db.DB.WithContext(ctx).Table("b2b_specific_price_category").Where("b2b_specific_price_id = ?", pr.ID).Select("id_category").Scan(&categoryIDs).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, c := range categoryIDs {
|
||||||
|
pr.CategoryIDs = append(pr.CategoryIDs, c.IDCategory)
|
||||||
|
}
|
||||||
|
|
||||||
|
var attrIDs []struct {
|
||||||
|
IDAttr uint64 `gorm:"column:id_product_attribute"`
|
||||||
|
}
|
||||||
|
if err = db.DB.WithContext(ctx).Table("b2b_specific_price_product_attribute").Where("b2b_specific_price_id = ?", pr.ID).Select("id_product_attribute").Scan(&attrIDs).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, a := range attrIDs {
|
||||||
|
pr.ProductAttributeIDs = append(pr.ProductAttributeIDs, a.IDAttr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var countryIDs []struct {
|
||||||
|
IDCountry uint64 `gorm:"column:b2b_id_country"`
|
||||||
|
}
|
||||||
|
if err = db.DB.WithContext(ctx).Table("b2b_specific_price_country").Where("b2b_specific_price_id = ?", pr.ID).Select("b2b_id_country").Scan(&countryIDs).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, c := range countryIDs {
|
||||||
|
pr.CountryIDs = append(pr.CountryIDs, c.IDCountry)
|
||||||
|
}
|
||||||
|
|
||||||
|
var customerIDs []struct {
|
||||||
|
IDCustomer uint64 `gorm:"column:b2b_id_customer"`
|
||||||
|
}
|
||||||
|
if err = db.DB.WithContext(ctx).Table("b2b_specific_price_customer").Where("b2b_specific_price_id = ?", pr.ID).Select("b2b_id_customer").Scan(&customerIDs).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, c := range customerIDs {
|
||||||
|
pr.CustomerIDs = append(pr.CustomerIDs, c.IDCustomer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
// JWTClaims represents the JWT claims
|
// JWTClaims represents the JWT claims
|
||||||
@@ -436,7 +437,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.Preload("Role.Permissions").First(&user, userID).Error; err != nil {
|
if err := s.db.Preload("Role.Permissions").Preload(clause.Associations).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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package productService
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/repos/productsRepo"
|
"git.ma-al.com/goc_daniel/b2b/app/repos/productsRepo"
|
||||||
@@ -9,6 +10,7 @@ import (
|
|||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/filters"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/query/find"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/view"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProductService struct {
|
type ProductService struct {
|
||||||
@@ -21,17 +23,108 @@ func New() *ProductService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProductService) GetJSON(p_id_product, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*json.RawMessage, error) {
|
func (s *ProductService) Get(
|
||||||
products, err := s.productsRepo.GetJSON(p_id_product, constdata.SHOP_ID, p_id_lang, p_id_customer, b2b_id_country, p_quantity)
|
p_id_product, p_id_lang, p_id_customer, b2b_id_country, p_quantity uint,
|
||||||
|
) (*json.RawMessage, error) {
|
||||||
|
|
||||||
|
product, err := s.productsRepo.GetBase(p_id_product, constdata.SHOP_ID, p_id_lang)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return products, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return products, nil
|
price, err := s.productsRepo.GetPrice(p_id_product, nil, constdata.SHOP_ID, p_id_customer, b2b_id_country, p_quantity)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
variants, err := s.productsRepo.GetVariants(p_id_product, constdata.SHOP_ID, p_id_lang, p_id_customer, b2b_id_country, p_quantity)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := view.ProductFull{
|
||||||
|
Product: product,
|
||||||
|
Price: price,
|
||||||
|
Variants: variants,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(variants) > 0 {
|
||||||
|
result.Variants = variants
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBytes, err := json.Marshal(result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := json.RawMessage(jsonBytes)
|
||||||
|
return &raw, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProductService) Find(id_lang, userID uint, p find.Paging, filters *filters.FiltersList) (find.Found[model.ProductInList], error) {
|
func (s *ProductService) Find(
|
||||||
return s.productsRepo.Find(id_lang, userID, p, filters)
|
idLang uint,
|
||||||
|
userID uint,
|
||||||
|
p find.Paging,
|
||||||
|
filters *filters.FiltersList,
|
||||||
|
customer *model.Customer,
|
||||||
|
quantity uint,
|
||||||
|
shopID uint,
|
||||||
|
) (*find.Found[model.ProductInList], error) {
|
||||||
|
|
||||||
|
if customer == nil || customer.Country == nil {
|
||||||
|
return nil, errors.New("customer is nil or missing fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
found, err := s.productsRepo.Find(idLang, userID, p, filters)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. collect simple products (no variants)
|
||||||
|
simpleProductIndexes := make([]int, 0, len(found.Items))
|
||||||
|
|
||||||
|
for i := range found.Items {
|
||||||
|
if found.Items[i].VariantsNumber <= 0 {
|
||||||
|
simpleProductIndexes = append(simpleProductIndexes, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. resolve prices ONLY for simple products
|
||||||
|
for _, i := range simpleProductIndexes {
|
||||||
|
price, err := s.productsRepo.GetPrice(
|
||||||
|
found.Items[i].ProductID,
|
||||||
|
nil,
|
||||||
|
shopID,
|
||||||
|
customer.ID,
|
||||||
|
customer.CountryID,
|
||||||
|
quantity,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
found.Items[i].PriceTaxExcl = price.FinalTaxExcl
|
||||||
|
found.Items[i].PriceTaxIncl = price.FinalTaxIncl
|
||||||
|
}
|
||||||
|
|
||||||
|
return found, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductService) GetProductAttributes(
|
||||||
|
langID uint,
|
||||||
|
productID uint,
|
||||||
|
shopID uint,
|
||||||
|
customerID uint,
|
||||||
|
countryID uint,
|
||||||
|
quantity uint,
|
||||||
|
) ([]view.ProductAttribute, error) {
|
||||||
|
variants, err := s.productsRepo.GetVariants(productID, constdata.SHOP_ID, langID, customerID, countryID, quantity)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return variants, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProductService) AddToFavorites(userID uint, productID uint) error {
|
func (s *ProductService) AddToFavorites(userID uint, productID uint) error {
|
||||||
|
|||||||
124
app/service/specificPriceService/specificPriceService.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package specificPriceService
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/repos/specificPriceRepo"
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SpecificPriceService struct {
|
||||||
|
specificPriceRepo specificPriceRepo.UISpecificPriceRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *SpecificPriceService {
|
||||||
|
return &SpecificPriceService{
|
||||||
|
specificPriceRepo: specificPriceRepo.New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpecificPriceService) Create(ctx context.Context, pr *model.SpecificPrice) (*model.SpecificPrice, error) {
|
||||||
|
if err := s.validateRequest(pr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.specificPriceRepo.Create(ctx, pr); err != nil {
|
||||||
|
dudzic_wiktor marked this conversation as resolved
goc_daniel
commented
this is doing nothing this is doing nothing
|
|||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpecificPriceService) Update(ctx context.Context, id uint64, pr *model.SpecificPrice) (*model.SpecificPrice, error) {
|
||||||
|
existing, err := s.specificPriceRepo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if existing == nil {
|
||||||
|
return nil, responseErrors.ErrSpecificPriceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.validateUpdateRequest(pr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pr.ID = id
|
||||||
|
|
||||||
|
if err := s.specificPriceRepo.Update(ctx, pr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dudzic_wiktor marked this conversation as resolved
goc_daniel
commented
also doing nothing also doing nothing
|
|||||||
|
func (s *SpecificPriceService) GetByID(ctx context.Context, id uint64) (*model.SpecificPrice, error) {
|
||||||
|
pr, err := s.specificPriceRepo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if pr == nil {
|
||||||
|
return nil, responseErrors.ErrSpecificPriceNotFound
|
||||||
|
}
|
||||||
|
return pr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpecificPriceService) List(ctx context.Context) ([]*model.SpecificPrice, error) {
|
||||||
|
return s.specificPriceRepo.List(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpecificPriceService) SetActive(ctx context.Context, id uint64, active bool) error {
|
||||||
|
pr, err := s.specificPriceRepo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pr == nil {
|
||||||
|
return responseErrors.ErrSpecificPriceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.specificPriceRepo.SetActive(ctx, id, active)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpecificPriceService) Delete(ctx context.Context, id uint64) error {
|
||||||
|
pr, err := s.specificPriceRepo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pr == nil {
|
||||||
|
return responseErrors.ErrSpecificPriceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.specificPriceRepo.Delete(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpecificPriceService) validateRequest(pr *model.SpecificPrice) error {
|
||||||
|
if pr.ReductionType != "amount" && pr.ReductionType != "percentage" {
|
||||||
|
return responseErrors.ErrInvalidReductionType
|
||||||
|
}
|
||||||
|
|
||||||
|
if pr.ReductionType == "percentage" && pr.PercentageReduction == nil {
|
||||||
|
return responseErrors.ErrPercentageRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
if pr.ReductionType == "amount" && pr.Price == nil {
|
||||||
|
return responseErrors.ErrPriceRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpecificPriceService) validateUpdateRequest(pr *model.SpecificPrice) error {
|
||||||
|
if pr.ReductionType != "" && pr.ReductionType != "amount" && pr.ReductionType != "percentage" {
|
||||||
|
return responseErrors.ErrInvalidReductionType
|
||||||
|
}
|
||||||
|
|
||||||
|
if pr.ReductionType == "percentage" && pr.PercentageReduction == nil {
|
||||||
|
return responseErrors.ErrPercentageRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
if pr.ReductionType == "amount" && pr.Price == nil {
|
||||||
|
return responseErrors.ErrPriceRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package constdata
|
|||||||
// PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads).
|
// PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads).
|
||||||
const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$`
|
const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$`
|
||||||
const SHOP_ID = 1
|
const SHOP_ID = 1
|
||||||
|
const DEFAULT_PRODUCT_QUANTITY = 1
|
||||||
const SHOP_DEFAULT_LANGUAGE = 1
|
const SHOP_DEFAULT_LANGUAGE = 1
|
||||||
const ADMIN_NOTIFICATION_LANGUAGE = 2
|
const ADMIN_NOTIFICATION_LANGUAGE = 2
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ var (
|
|||||||
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 price reduction handler
|
||||||
|
ErrInvalidReductionType = errors.New("invalid reduction type: must be 'amount' or 'percentage'")
|
||||||
|
ErrPercentageRequired = errors.New("percentage_reduction required when reduction_type is percentage")
|
||||||
|
ErrPriceRequired = errors.New("price required when reduction_type is amount")
|
||||||
|
ErrSpecificPriceNotFound = errors.New("price reduction not found")
|
||||||
// Typed errors for storage
|
// Typed errors for storage
|
||||||
ErrAccessDenied = errors.New("access denied!")
|
ErrAccessDenied = errors.New("access denied!")
|
||||||
ErrFolderDoesNotExist = errors.New("folder does not exist")
|
ErrFolderDoesNotExist = errors.New("folder does not exist")
|
||||||
@@ -207,6 +212,15 @@ func GetErrorCode(c fiber.Ctx, err error) string {
|
|||||||
case errors.Is(err, ErrMissingFileFieldDocument):
|
case errors.Is(err, ErrMissingFileFieldDocument):
|
||||||
return i18n.T_(c, "error.err_missing_file_field_document")
|
return i18n.T_(c, "error.err_missing_file_field_document")
|
||||||
|
|
||||||
|
case errors.Is(err, ErrInvalidReductionType):
|
||||||
|
return i18n.T_(c, "error.invalid_reduction_type")
|
||||||
|
case errors.Is(err, ErrPercentageRequired):
|
||||||
|
return i18n.T_(c, "error.percentage_required")
|
||||||
|
case errors.Is(err, ErrPriceRequired):
|
||||||
|
return i18n.T_(c, "error.price_required")
|
||||||
|
case errors.Is(err, ErrSpecificPriceNotFound):
|
||||||
|
return i18n.T_(c, "error.price_reduction_not_found")
|
||||||
|
|
||||||
case errors.Is(err, ErrJSONBody):
|
case errors.Is(err, ErrJSONBody):
|
||||||
return i18n.T_(c, "error.err_json_body")
|
return i18n.T_(c, "error.err_json_body")
|
||||||
|
|
||||||
@@ -268,6 +282,9 @@ func GetErrorStatus(err error) int {
|
|||||||
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, ErrInvalidReductionType),
|
||||||
|
errors.Is(err, ErrPercentageRequired),
|
||||||
|
errors.Is(err, ErrPriceRequired),
|
||||||
errors.Is(err, ErrAccessDenied),
|
errors.Is(err, ErrAccessDenied),
|
||||||
errors.Is(err, ErrFolderDoesNotExist),
|
errors.Is(err, ErrFolderDoesNotExist),
|
||||||
errors.Is(err, ErrFileDoesNotExist),
|
errors.Is(err, ErrFileDoesNotExist),
|
||||||
@@ -279,6 +296,8 @@ func GetErrorStatus(err error) int {
|
|||||||
errors.Is(err, ErrInvalidCountryID),
|
errors.Is(err, ErrInvalidCountryID),
|
||||||
errors.Is(err, ErrInvalidAddressJSON):
|
errors.Is(err, ErrInvalidAddressJSON):
|
||||||
return fiber.StatusBadRequest
|
return fiber.StatusBadRequest
|
||||||
|
case errors.Is(err, ErrSpecificPriceNotFound):
|
||||||
|
return fiber.StatusNotFound
|
||||||
case errors.Is(err, ErrEmailExists):
|
case errors.Is(err, ErrEmailExists):
|
||||||
return fiber.StatusConflict
|
return fiber.StatusConflict
|
||||||
case errors.Is(err, ErrAIResponseFail),
|
case errors.Is(err, ErrAIResponseFail),
|
||||||
|
|||||||
98
app/view/product.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package view
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductAttribute struct {
|
||||||
|
IDProductAttribute uint `gorm:"column:id_product_attribute" json:"id_product_attribute"`
|
||||||
|
Reference string `gorm:"column:reference" json:"reference"`
|
||||||
|
BasePrice float64 `gorm:"column:base_price" json:"base_price"`
|
||||||
|
PriceTaxExcl float64 `gorm:"column:price_tax_excl" json:"price_tax_excl"`
|
||||||
|
PriceTaxIncl float64 `gorm:"column:price_tax_incl" json:"price_tax_incl"`
|
||||||
|
Quantity int64 `gorm:"column:quantity" json:"quantity"`
|
||||||
|
Attributes json.RawMessage `gorm:"column:attributes" json:"attributes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Price struct {
|
||||||
|
Base float64 `json:"base"`
|
||||||
|
FinalTaxExcl float64 `json:"final_tax_excl"`
|
||||||
|
FinalTaxIncl float64 `json:"final_tax_incl"`
|
||||||
|
TaxRate float64 `json:"tax_rate"`
|
||||||
|
Priority int `json:"priority"` // or string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Variant struct {
|
||||||
|
ID uint `json:"id_product_attribute"`
|
||||||
|
Reference string `json:"reference"`
|
||||||
|
|
||||||
|
BasePrice float64 `json:"base_price"`
|
||||||
|
FinalExcl float64 `json:"final_tax_excl"`
|
||||||
|
FinalIncl float64 `json:"final_tax_incl"`
|
||||||
|
|
||||||
|
Stock int `json:"stock"`
|
||||||
|
}
|
||||||
|
type ProductFull struct {
|
||||||
|
Product Product `json:"product"`
|
||||||
|
Price Price `json:"price"`
|
||||||
|
Variants []ProductAttribute `json:"variants,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Product struct {
|
||||||
|
goc_daniel marked this conversation as resolved
goc_daniel
commented
Is there a reason we're not using model.Product here? There's already so many different product types I feel like it's gonna cause a problem down the line. Alternatively we can delete model.Product, since it really is just a copy of ps_product database type. (The latter sounds like a good thing to do) Is there a reason we're not using model.Product here? There's already so many different product types I feel like it's gonna cause a problem down the line. Alternatively we can delete model.Product, since it really is just a copy of ps_product database type. (The latter sounds like a good thing to do)
dudzic_wiktor
commented
yes, view/product.go is being introduced because not every field in product model is meant to be sent and shown to the end user as well as we add fields that are not part of product model such as IsFavorite. this approach allows to make views minimal and flexible without altering underlying data structure. yes, view/product.go is being introduced because not every field in product model is meant to be sent and shown to the end user as well as we add fields that are not part of product model such as IsFavorite. this approach allows to make views minimal and flexible without altering underlying data structure.
dudzic_wiktor
commented
i agree with deleting model.Product since it's redundant i agree with deleting model.Product since it's redundant
|
|||||||
|
ID uint `gorm:"column:id" json:"id"`
|
||||||
|
Reference string `gorm:"column:reference" json:"reference"`
|
||||||
|
SupplierReference string `gorm:"column:supplier_reference" json:"supplier_reference,omitempty"`
|
||||||
|
EAN13 string `gorm:"column:ean13" json:"ean13,omitempty"`
|
||||||
|
UPC string `gorm:"column:upc" json:"upc,omitempty"`
|
||||||
|
ISBN string `gorm:"column:isbn" json:"isbn,omitempty"`
|
||||||
|
|
||||||
|
// Basic Price (from product table)
|
||||||
|
BasePrice float64 `gorm:"column:base_price" json:"base_price"`
|
||||||
|
WholesalePrice float64 `gorm:"column:wholesale_price" json:"wholesale_price,omitempty"`
|
||||||
|
Unity string `gorm:"column:unity" json:"unity,omitempty"`
|
||||||
|
UnitPriceRatio float64 `gorm:"column:unit_price_ratio" json:"unit_price_ratio,omitempty"`
|
||||||
|
|
||||||
|
// Stock & Availability
|
||||||
|
Quantity int `gorm:"column:quantity" json:"quantity"`
|
||||||
|
MinimalQuantity int `gorm:"column:minimal_quantity" json:"minimal_quantity"`
|
||||||
|
AvailableForOrder bool `gorm:"column:available_for_order" json:"available_for_order"`
|
||||||
|
AvailableDate string `gorm:"column:available_date" json:"available_date,omitempty"`
|
||||||
|
OutOfStockBehavior int `gorm:"column:out_of_stock_behavior" json:"out_of_stock_behavior"`
|
||||||
|
|
||||||
|
// Flags
|
||||||
|
OnSale bool `gorm:"column:on_sale" json:"on_sale"`
|
||||||
|
ShowPrice bool `gorm:"column:show_price" json:"show_price"`
|
||||||
|
Condition string `gorm:"column:condition" json:"condition"`
|
||||||
|
IsVirtual bool `gorm:"column:is_virtual" json:"is_virtual"`
|
||||||
|
|
||||||
|
// Physical
|
||||||
|
Weight float64 `gorm:"column:weight" json:"weight"`
|
||||||
|
Width float64 `gorm:"column:width" json:"width"`
|
||||||
|
Height float64 `gorm:"column:height" json:"height"`
|
||||||
|
Depth float64 `gorm:"column:depth" json:"depth"`
|
||||||
|
AdditionalShippingCost float64 `gorm:"column:additional_shipping_cost" json:"additional_shipping_cost,omitempty"`
|
||||||
|
|
||||||
|
// Delivery
|
||||||
|
DeliveryDays int `gorm:"column:delivery_days" json:"delivery_days,omitempty"`
|
||||||
|
|
||||||
|
// Status
|
||||||
|
Active bool `gorm:"column:active" json:"active"`
|
||||||
|
Visibility string `gorm:"column:visibility" json:"visibility"`
|
||||||
|
Indexed bool `gorm:"column:indexed" json:"indexed"`
|
||||||
|
|
||||||
|
// Timestamps
|
||||||
|
DateAdd time.Time `gorm:"column:date_add" json:"date_add"`
|
||||||
|
DateUpd time.Time `gorm:"column:date_upd" json:"date_upd"`
|
||||||
|
|
||||||
|
// Language fields
|
||||||
|
Name string `gorm:"column:name" json:"name"`
|
||||||
|
Description string `gorm:"column:description" json:"description"`
|
||||||
|
DescriptionShort string `gorm:"column:description_short" json:"description_short"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Manufacturer string `gorm:"column:manufacturer" json:"manufacturer"`
|
||||||
|
Category string `gorm:"column:category" json:"category"`
|
||||||
|
|
||||||
|
IsFavorite bool `gorm:"column:is_favorite" json:"is_favorite"`
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: Change Locales
|
name: Change Locales
|
||||||
type: http
|
type: http
|
||||||
seq: 3
|
seq: 5
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: POST
|
method: POST
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: Delete Index - MeiliSearch
|
name: Delete Index - MeiliSearch
|
||||||
type: http
|
type: http
|
||||||
seq: 5
|
seq: 7
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: DELETE
|
method: DELETE
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: Search Index Settings
|
name: Search Index Settings
|
||||||
type: http
|
type: http
|
||||||
seq: 4
|
seq: 6
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: POST
|
method: POST
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: Search Items
|
name: Search Items
|
||||||
type: http
|
type: http
|
||||||
seq: 2
|
seq: 4
|
||||||
|
|
||||||
http:
|
http:
|
||||||
method: POST
|
method: POST
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: auth
|
name: auth
|
||||||
type: folder
|
type: folder
|
||||||
seq: 6
|
seq: 2
|
||||||
|
|
||||||
request:
|
request:
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ http:
|
|||||||
data: |-
|
data: |-
|
||||||
{
|
{
|
||||||
"b2b_id_currency" : 1,
|
"b2b_id_currency" : 1,
|
||||||
"conversion_rate": 4.2
|
"conversion_rate": 3
|
||||||
}
|
}
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ http:
|
|||||||
runtime:
|
runtime:
|
||||||
variables:
|
variables:
|
||||||
- name: id
|
- name: id
|
||||||
value: "1"
|
value: "2"
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
encodeUrl: true
|
encodeUrl: true
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: currency
|
name: currency
|
||||||
type: folder
|
type: folder
|
||||||
seq: 8
|
seq: 9
|
||||||
|
|
||||||
request:
|
request:
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: "{{bas_url}}/restricted/customer?id=1"
|
url: "{{bas_url}}/restricted/customer?id=2"
|
||||||
params:
|
params:
|
||||||
- name: id
|
- name: id
|
||||||
value: "1"
|
value: "2"
|
||||||
type: query
|
type: query
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: "{{bas_url}}/restricted/customer/list?search="
|
url: "{{bas_url}}/restricted/customer/list?search=marek"
|
||||||
params:
|
params:
|
||||||
- name: search
|
- name: search
|
||||||
value: ""
|
value: marek
|
||||||
type: query
|
type: query
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: customer
|
name: customer
|
||||||
type: folder
|
type: folder
|
||||||
seq: 9
|
seq: 10
|
||||||
|
|
||||||
request:
|
request:
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: "{{bas_url}}/restricted/product/200/1/5"
|
url: "{{bas_url}}/restricted/product/51/1/7"
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
|
|||||||
22
bruno/api_v1/product/Product Variants List.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
info:
|
||||||
|
name: Product Variants List
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: "{{bas_url}}/restricted/product/list-variants/{{product_id}}"
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: ""
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
variables:
|
||||||
|
- name: product_id
|
||||||
|
value: "51"
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
@@ -5,7 +5,7 @@ info:
|
|||||||
|
|
||||||
http:
|
http:
|
||||||
method: GET
|
method: GET
|
||||||
url: "{{bas_url}}/restricted/product/list?p=1&elems=30&sort=product_id,asc&category_id_in=243&reference=~62"
|
url: "{{bas_url}}/restricted/product/list?p=1&elems=30&sort=product_id,asc&reference=~NC100"
|
||||||
params:
|
params:
|
||||||
- name: p
|
- name: p
|
||||||
value: "1"
|
value: "1"
|
||||||
@@ -19,8 +19,9 @@ http:
|
|||||||
- name: category_id_in
|
- name: category_id_in
|
||||||
value: "243"
|
value: "243"
|
||||||
type: query
|
type: query
|
||||||
|
disabled: true
|
||||||
- name: reference
|
- name: reference
|
||||||
value: ~62
|
value: ~NC100
|
||||||
type: query
|
type: query
|
||||||
body:
|
body:
|
||||||
type: json
|
type: json
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
info:
|
info:
|
||||||
name: product
|
name: product
|
||||||
type: folder
|
type: folder
|
||||||
seq: 7
|
seq: 8
|
||||||
|
|
||||||
request:
|
request:
|
||||||
auth: inherit
|
auth: inherit
|
||||||
|
|||||||
20
bruno/api_v1/specific_price/Activate.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
info:
|
||||||
|
name: Activate
|
||||||
|
type: http
|
||||||
|
seq: 5
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: PATCH
|
||||||
|
url: "{{bas_url}}/restricted/specific-price/{{id}}/activate"
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
variables:
|
||||||
|
- name: id
|
||||||
|
value: "1"
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
27
bruno/api_v1/specific_price/Create.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
info:
|
||||||
|
name: Create
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: "{{bas_url}}/restricted/specific-price"
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"name": "Summer Sale 3",
|
||||||
|
"scope": "shop",
|
||||||
|
"reduction_type": "amount",
|
||||||
|
"price": 69,
|
||||||
|
"from_quantity": 1,
|
||||||
|
"is_active": true,
|
||||||
|
"currency_id": 2
|
||||||
|
}
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
20
bruno/api_v1/specific_price/Deactivate.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
info:
|
||||||
|
name: Deactivate
|
||||||
|
type: http
|
||||||
|
seq: 6
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: PATCH
|
||||||
|
url: "{{bas_url}}/restricted/specific-price/{{id}}/deactivate"
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
variables:
|
||||||
|
- name: id
|
||||||
|
value: "1"
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
20
bruno/api_v1/specific_price/Delete.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
info:
|
||||||
|
name: Delete
|
||||||
|
type: http
|
||||||
|
seq: 7
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: DELETE
|
||||||
|
url: "{{bas_url}}/restricted/price-reductions/{{id}}"
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
variables:
|
||||||
|
- name: id
|
||||||
|
value: "1"
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
20
bruno/api_v1/specific_price/Get.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
info:
|
||||||
|
name: Get
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: "{{bas_url}}/restricted/specific-price/{{id}}"
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
variables:
|
||||||
|
- name: id
|
||||||
|
value: "1"
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
15
bruno/api_v1/specific_price/List.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
info:
|
||||||
|
name: List
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: "{{bas_url}}/restricted/specific-price"
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
38
bruno/api_v1/specific_price/Update.yml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
info:
|
||||||
|
name: Update
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: PUT
|
||||||
|
url: "{{bas_url}}/restricted/specific-price/{{id}}"
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"name": "Summer Sale Updated",
|
||||||
|
"reduction_type": "amount",
|
||||||
|
"percentage_reduction": 50.0,
|
||||||
|
"price": 69,
|
||||||
|
"currency_id": 1,
|
||||||
|
"scope": "shop",
|
||||||
|
"is_active": true,
|
||||||
|
"from_quantity":1
|
||||||
|
// "product_ids": [51,53],
|
||||||
|
// "category_ids": [1],
|
||||||
|
// "product_attribute_ids": [1114],
|
||||||
|
// "country_ids": [1],
|
||||||
|
// "customer_ids": [2,1]
|
||||||
|
}
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
variables:
|
||||||
|
- name: id
|
||||||
|
value: "3"
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
73
bruno/api_v1/specific_price/folder.yml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
info:
|
||||||
|
name: specific_price
|
||||||
|
type: folder
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |
|
||||||
|
# Specific Price API
|
||||||
|
|
||||||
|
Endpoints for managing specific price rules (price reductions).
|
||||||
|
|
||||||
|
## Scopes
|
||||||
|
|
||||||
|
Specific prices can be **global** or **scoped**:
|
||||||
|
|
||||||
|
- **Global**: If all scope arrays (`product_ids`, `category_ids`, `product_attribute_ids`, `country_ids`, `customer_ids`) are empty, the price reduction applies to everything.
|
||||||
|
|
||||||
|
- **Scoped**: If ANY scope array has values, the price reduction applies only when ANY condition matches (UNION logic).
|
||||||
|
|
||||||
|
### Scope Fields
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `product_ids` | uint[] | Specific products |
|
||||||
|
| `category_ids` | uint[] | Products in categories |
|
||||||
|
| `product_attribute_ids` | uint[] | Product variants (e.g., size, color) |
|
||||||
|
| `country_ids` | uint[] | Customers in countries |
|
||||||
|
| `customer_ids` | uint[] | Specific customers |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
**Global** (applies to all products):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Global Sale",
|
||||||
|
"reduction_type": "percentage",
|
||||||
|
"percentage_reduction": 10,
|
||||||
|
"from_quantity": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Scoped to specific products**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Product Sale",
|
||||||
|
"reduction_type": "percentage",
|
||||||
|
"percentage_reduction": 20,
|
||||||
|
"product_ids": [1, 2, 3]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Scoped to category + country**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Category Country Sale",
|
||||||
|
"reduction_type": "amount",
|
||||||
|
"price": 9.99,
|
||||||
|
"category_ids": [5],
|
||||||
|
"country_ids": [1]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reduction Types
|
||||||
|
|
||||||
|
- `percentage`: Requires `percentage_reduction` (e.g., 10.5 = 10.5% off)
|
||||||
|
- `amount`: Requires `price` (fixed price after reduction)
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- `reduction_type` is required and must be "percentage" or "amount"
|
||||||
|
- If `reduction_type` is "percentage", then `percentage_reduction` is required
|
||||||
|
- If `reduction_type` is "amount", then `price` is required
|
||||||
|
type: text/markdown
|
||||||
@@ -238,7 +238,6 @@ CREATE TABLE b2b_specific_price (
|
|||||||
created_at DATETIME NULL,
|
created_at DATETIME NULL,
|
||||||
updated_at DATETIME NULL,
|
updated_at DATETIME NULL,
|
||||||
deleted_at DATETIME NULL,
|
deleted_at DATETIME NULL,
|
||||||
scope ENUM('shop', 'category', 'product') NOT NULL,
|
|
||||||
valid_from DATETIME NULL,
|
valid_from DATETIME NULL,
|
||||||
valid_till DATETIME NULL,
|
valid_till DATETIME NULL,
|
||||||
has_expiration_date BOOLEAN DEFAULT FALSE,
|
has_expiration_date BOOLEAN DEFAULT FALSE,
|
||||||
@@ -249,11 +248,9 @@ CREATE TABLE b2b_specific_price (
|
|||||||
from_quantity INT UNSIGNED DEFAULT 1,
|
from_quantity INT UNSIGNED DEFAULT 1,
|
||||||
is_active BOOLEAN DEFAULT TRUE
|
is_active BOOLEAN DEFAULT TRUE
|
||||||
) ENGINE = InnoDB;
|
) ENGINE = InnoDB;
|
||||||
CREATE INDEX idx_b2b_scope ON b2b_specific_price(scope);
|
|
||||||
CREATE INDEX idx_b2b_active_dates ON b2b_specific_price(is_active, valid_from, valid_till);
|
CREATE INDEX idx_b2b_active_dates ON b2b_specific_price(is_active, valid_from, valid_till);
|
||||||
CREATE INDEX idx_b2b_lookup
|
CREATE INDEX idx_b2b_lookup
|
||||||
ON b2b_specific_price (
|
ON b2b_specific_price (
|
||||||
scope,
|
|
||||||
is_active,
|
is_active,
|
||||||
from_quantity
|
from_quantity
|
||||||
);
|
);
|
||||||
@@ -307,11 +304,11 @@ ON b2b_specific_price_category (id_category);
|
|||||||
CREATE INDEX idx_b2b_product_attribute_rel
|
CREATE INDEX idx_b2b_product_attribute_rel
|
||||||
ON b2b_specific_price_product_attribute (id_product_attribute);
|
ON b2b_specific_price_product_attribute (id_product_attribute);
|
||||||
|
|
||||||
CREATE INDEX idx_bsp_customer
|
CREATE INDEX idx_bsp_customer_rel
|
||||||
ON b2b_specific_price_customer (b2b_specific_price_id, b2b_id_customer);
|
ON b2b_specific_price_customer (b2b_id_customer);
|
||||||
|
|
||||||
CREATE INDEX idx_bsp_country
|
CREATE INDEX idx_bsp_country_rel
|
||||||
ON b2b_specific_price_country (b2b_specific_price_id, b2b_id_country);
|
ON b2b_specific_price_country (b2b_id_country);
|
||||||
|
|
||||||
DELIMITER //
|
DELIMITER //
|
||||||
|
|
||||||
|
|||||||
@@ -34,13 +34,16 @@ INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('1', 'user.read.any');
|
|||||||
INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('2', 'user.write.any');
|
INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('2', 'user.write.any');
|
||||||
INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('3', 'user.delete.any');
|
INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('3', 'user.delete.any');
|
||||||
INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('4', 'currency.write');
|
INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('4', 'currency.write');
|
||||||
|
INSERT INTO `b2b_permissions` (`id`, `name`) VALUES ('5', 'specific_price.manage');
|
||||||
|
|
||||||
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '1');
|
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '1');
|
||||||
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '2');
|
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '2');
|
||||||
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '3');
|
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '3');
|
||||||
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '4');
|
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '4');
|
||||||
|
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('2', '5');
|
||||||
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '1');
|
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '1');
|
||||||
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '2');
|
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '2');
|
||||||
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '3');
|
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '3');
|
||||||
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '4');
|
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '4');
|
||||||
|
INSERT INTO `b2b_role_permissions` (`role_id`, `permission_id`) VALUES ('3', '5');
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
@@ -1,386 +1,410 @@
|
|||||||
-- +goose Up
|
-- +goose Up
|
||||||
DELIMITER //
|
DELIMITER //
|
||||||
DROP PROCEDURE IF EXISTS get_full_product
|
|
||||||
//
|
DROP FUNCTION IF EXISTS fn_product_price //
|
||||||
CREATE PROCEDURE get_full_product(
|
CREATE FUNCTION fn_product_price(
|
||||||
IN p_id_product INT UNSIGNED,
|
p_id_product INT UNSIGNED,
|
||||||
IN p_id_shop INT UNSIGNED,
|
p_id_shop INT UNSIGNED,
|
||||||
IN p_id_lang INT UNSIGNED,
|
p_id_customer INT UNSIGNED,
|
||||||
IN p_id_customer INT UNSIGNED,
|
p_id_country INT UNSIGNED,
|
||||||
IN b2b_id_country INT UNSIGNED,
|
p_quantity INT UNSIGNED,
|
||||||
IN p_quantity INT UNSIGNED
|
p_id_product_attribute INT UNSIGNED
|
||||||
)
|
)
|
||||||
BEGIN
|
RETURNS JSON
|
||||||
DECLARE v_tax_rate DECIMAL(10, 4) DEFAULT 0;
|
DETERMINISTIC
|
||||||
DECLARE p_id_currency DECIMAL(10, 4) DEFAULT 0;
|
READS SQL DATA
|
||||||
SELECT
|
BEGIN
|
||||||
COALESCE(t.rate, 0.0000) INTO v_tax_rate
|
|
||||||
FROM
|
DECLARE v_tax_rate DECIMAL(10,4) DEFAULT 0;
|
||||||
ps_tax_rule tr
|
|
||||||
INNER JOIN ps_tax t ON t.id_tax = tr.id_tax
|
DECLARE v_base_raw DECIMAL(20,6);
|
||||||
LEFT JOIN b2b_countries ON b2b_countries.id = b2b_id_country
|
DECLARE v_base DECIMAL(20,6);
|
||||||
WHERE
|
DECLARE v_excl DECIMAL(20,6);
|
||||||
tr.id_tax_rules_group = (
|
DECLARE v_incl DECIMAL(20,6);
|
||||||
SELECT
|
|
||||||
ps.id_tax_rules_group
|
DECLARE v_reduction_type VARCHAR(20);
|
||||||
FROM
|
DECLARE v_percentage DECIMAL(10,4);
|
||||||
ps_product_shop ps
|
DECLARE v_fixed_price DECIMAL(20,6);
|
||||||
WHERE
|
DECLARE v_specific_currency_id BIGINT;
|
||||||
ps.id_product = p_id_product
|
|
||||||
|
DECLARE v_has_specific INT DEFAULT 0;
|
||||||
|
|
||||||
|
-- currency
|
||||||
|
DECLARE v_target_currency BIGINT;
|
||||||
|
DECLARE v_target_rate DECIMAL(13,6) DEFAULT 1;
|
||||||
|
DECLARE v_specific_rate DECIMAL(13,6) DEFAULT 1;
|
||||||
|
|
||||||
|
SET p_id_product_attribute = NULLIF(p_id_product_attribute, 0);
|
||||||
|
|
||||||
|
-- ================= TAX =================
|
||||||
|
SELECT COALESCE(t.rate, 0)
|
||||||
|
INTO v_tax_rate
|
||||||
|
FROM ps_tax_rule tr
|
||||||
|
JOIN ps_tax t ON t.id_tax = tr.id_tax
|
||||||
|
LEFT JOIN b2b_countries c ON c.id = p_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
|
AND ps.id_shop = p_id_shop
|
||||||
LIMIT
|
LIMIT 1
|
||||||
1
|
|
||||||
)
|
)
|
||||||
AND tr.id_country = b2b_countries.ps_id_country
|
AND tr.id_country = c.ps_id_country
|
||||||
ORDER BY
|
LIMIT 1;
|
||||||
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',
|
-- ================= TARGET CURRENCY =================
|
||||||
(
|
SELECT c.b2b_id_currency
|
||||||
CASE
|
INTO v_target_currency
|
||||||
WHEN bsp.id IS NOT NULL THEN
|
FROM b2b_countries c
|
||||||
CASE
|
WHERE c.id = p_id_country
|
||||||
/* FIXED PRICE */
|
LIMIT 1;
|
||||||
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 */
|
-- latest target rate
|
||||||
WHEN bsp.reduction_type = 'percentage' THEN
|
SELECT r.conversion_rate
|
||||||
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate)
|
INTO v_target_rate
|
||||||
* (1 - bsp.percentage_reduction / 100)
|
FROM b2b_currency_rates r
|
||||||
|
WHERE r.b2b_id_currency = v_target_currency
|
||||||
|
ORDER BY r.created_at DESC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
ELSE
|
-- ================= BASE PRICE (RAW) =================
|
||||||
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate)
|
SELECT
|
||||||
END
|
COALESCE(ps.price, p.price) + COALESCE(pas.price, 0)
|
||||||
|
INTO v_base_raw
|
||||||
|
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_attribute_shop pas
|
||||||
|
ON pas.id_product_attribute = p_id_product_attribute
|
||||||
|
AND pas.id_shop = p_id_shop
|
||||||
|
WHERE p.id_product = p_id_product;
|
||||||
|
|
||||||
ELSE
|
-- convert base to target currency
|
||||||
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate)
|
SET v_base = v_base_raw * v_target_rate;
|
||||||
END
|
|
||||||
),
|
|
||||||
|
|
||||||
'final_tax_incl',
|
-- ================= RULE SELECTION =================
|
||||||
(
|
SELECT
|
||||||
(
|
1,
|
||||||
CASE
|
bsp.reduction_type,
|
||||||
WHEN bsp.id IS NOT NULL THEN
|
bsp.percentage_reduction,
|
||||||
CASE
|
bsp.price,
|
||||||
WHEN bsp.reduction_type = 'amount' THEN
|
bsp.b2b_id_currency
|
||||||
(
|
|
||||||
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
|
INTO
|
||||||
COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate)
|
v_has_specific,
|
||||||
* (1 - bsp.percentage_reduction / 100)
|
v_reduction_type,
|
||||||
|
v_percentage,
|
||||||
|
v_fixed_price,
|
||||||
|
v_specific_currency_id
|
||||||
|
|
||||||
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,
|
|
||||||
/* ================= FAVORITE ================= */
|
|
||||||
'is_favorite',
|
|
||||||
EXISTS(
|
|
||||||
SELECT 1 FROM b2b_favorites f
|
|
||||||
WHERE f.user_id = p_id_customer AND f.product_id = p_id_product
|
|
||||||
),
|
|
||||||
/* ================= 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
|
FROM b2b_specific_price bsp
|
||||||
|
|
||||||
/* RELATIONS */
|
WHERE bsp.is_active = 1
|
||||||
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
|
AND bsp.from_quantity <= p_quantity
|
||||||
|
|
||||||
/* DATE */
|
-- intersection rules (unchanged)
|
||||||
AND (
|
AND (
|
||||||
bsp.has_expiration_date = FALSE OR (
|
NOT EXISTS (SELECT 1 FROM b2b_specific_price_product x WHERE x.b2b_specific_price_id = bsp.id)
|
||||||
(bsp.valid_from IS NULL OR bsp.valid_from <= NOW())
|
OR EXISTS (SELECT 1 FROM b2b_specific_price_product x WHERE x.b2b_specific_price_id = bsp.id AND x.id_product = p_id_product)
|
||||||
AND (bsp.valid_till IS NULL OR bsp.valid_till >= NOW())
|
)
|
||||||
|
|
||||||
|
AND (
|
||||||
|
NOT EXISTS (SELECT 1 FROM b2b_specific_price_product_attribute x WHERE x.b2b_specific_price_id = bsp.id)
|
||||||
|
OR EXISTS (SELECT 1 FROM b2b_specific_price_product_attribute x WHERE x.b2b_specific_price_id = bsp.id AND x.id_product_attribute = p_id_product_attribute)
|
||||||
|
)
|
||||||
|
|
||||||
|
AND (
|
||||||
|
NOT EXISTS (SELECT 1 FROM b2b_specific_price_category x WHERE x.b2b_specific_price_id = bsp.id)
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM b2b_specific_price_category x
|
||||||
|
JOIN ps_category_product cp ON cp.id_category = x.id_category
|
||||||
|
WHERE x.b2b_specific_price_id = bsp.id AND cp.id_product = p_id_product
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
ORDER BY
|
AND (
|
||||||
/* 🔥 SCOPE PRIORITY */
|
NOT EXISTS (SELECT 1 FROM b2b_specific_price_customer x WHERE x.b2b_specific_price_id = bsp.id)
|
||||||
bsp.scope = 'product' DESC,
|
OR EXISTS (SELECT 1 FROM b2b_specific_price_customer x WHERE x.b2b_specific_price_id = bsp.id AND x.b2b_id_customer = p_id_customer)
|
||||||
bsp.scope = 'category' DESC,
|
)
|
||||||
bsp.scope = 'shop' DESC,
|
|
||||||
|
|
||||||
/* 🔥 CUSTOMER PRIORITY */
|
AND (
|
||||||
(
|
NOT EXISTS (SELECT 1 FROM b2b_specific_price_country x WHERE x.b2b_specific_price_id = bsp.id)
|
||||||
EXISTS (
|
OR EXISTS (SELECT 1 FROM b2b_specific_price_country x WHERE x.b2b_specific_price_id = bsp.id AND x.b2b_id_country = p_id_country)
|
||||||
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 */
|
ORDER BY
|
||||||
(
|
-- customer wins
|
||||||
EXISTS (
|
(EXISTS (SELECT 1 FROM b2b_specific_price_customer x WHERE x.b2b_specific_price_id = bsp.id AND x.b2b_id_customer = p_id_customer)) DESC,
|
||||||
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 */
|
-- attribute
|
||||||
|
(EXISTS (SELECT 1 FROM b2b_specific_price_product_attribute x WHERE x.b2b_specific_price_id = bsp.id AND x.id_product_attribute = p_id_product_attribute)) DESC,
|
||||||
|
|
||||||
|
-- product
|
||||||
|
(EXISTS (SELECT 1 FROM b2b_specific_price_product x WHERE x.b2b_specific_price_id = bsp.id AND x.id_product = p_id_product)) DESC,
|
||||||
|
|
||||||
|
-- category
|
||||||
|
(EXISTS (
|
||||||
|
SELECT 1 FROM b2b_specific_price_category x
|
||||||
|
JOIN ps_category_product cp ON cp.id_category = x.id_category
|
||||||
|
WHERE x.b2b_specific_price_id = bsp.id AND cp.id_product = p_id_product
|
||||||
|
)) DESC,
|
||||||
|
|
||||||
|
-- country
|
||||||
|
(EXISTS (SELECT 1 FROM b2b_specific_price_country x WHERE x.b2b_specific_price_id = bsp.id AND x.b2b_id_country = p_id_country)) DESC,
|
||||||
|
|
||||||
bsp.from_quantity DESC,
|
|
||||||
bsp.id DESC
|
bsp.id DESC
|
||||||
|
|
||||||
LIMIT 1
|
LIMIT 1;
|
||||||
) bsp ON 1=1
|
|
||||||
LEFT JOIN b2b_currency_rates br_bsp
|
-- ================= APPLY =================
|
||||||
ON br_bsp.b2b_id_currency = bsp.b2b_id_currency
|
SET v_excl = v_base;
|
||||||
AND br_bsp.created_at = (
|
|
||||||
SELECT MAX(created_at)
|
IF v_has_specific = 1 THEN
|
||||||
FROM b2b_currency_rates
|
|
||||||
WHERE b2b_id_currency = bsp.b2b_id_currency
|
IF v_reduction_type = 'amount' THEN
|
||||||
)
|
|
||||||
LEFT JOIN b2b_countries ON b2b_countries.id = b2b_id_country
|
-- convert specific price currency if needed
|
||||||
LEFT JOIN b2b_currencies ON b2b_currencies.id = p_id_currency
|
IF v_specific_currency_id IS NOT NULL AND v_specific_currency_id != v_target_currency THEN
|
||||||
LEFT JOIN b2b_currency_rates r ON r.b2b_id_currency = b2b_currencies.id
|
|
||||||
AND r.created_at = (
|
SELECT r.conversion_rate
|
||||||
SELECT
|
INTO v_specific_rate
|
||||||
MAX(created_at)
|
FROM b2b_currency_rates r
|
||||||
FROM
|
WHERE r.b2b_id_currency = v_specific_currency_id
|
||||||
b2b_currency_rates
|
ORDER BY r.created_at DESC
|
||||||
WHERE
|
LIMIT 1;
|
||||||
b2b_id_currency = b2b_currencies.id
|
|
||||||
)
|
-- normalize → then convert to target
|
||||||
WHERE
|
SET v_excl = (v_fixed_price / v_specific_rate) * v_target_rate;
|
||||||
p.id_product = p_id_product
|
|
||||||
LIMIT
|
ELSE
|
||||||
1;
|
SET v_excl = v_fixed_price;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ELSEIF v_reduction_type = 'percentage' THEN
|
||||||
|
SET v_excl = v_base * (1 - v_percentage / 100);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SET v_incl = v_excl * (1 + v_tax_rate / 100);
|
||||||
|
|
||||||
|
RETURN JSON_OBJECT(
|
||||||
|
'base', v_base,
|
||||||
|
'final_tax_excl', v_excl,
|
||||||
|
'final_tax_incl', v_incl,
|
||||||
|
'tax_rate', v_tax_rate,
|
||||||
|
'rate', v_target_rate
|
||||||
|
);
|
||||||
|
|
||||||
END //
|
END //
|
||||||
|
|
||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
|
|
||||||
|
DELIMITER //
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS get_product_variants //
|
||||||
|
CREATE PROCEDURE get_product_variants(
|
||||||
|
IN p_id_product INT,
|
||||||
|
IN p_id_shop INT,
|
||||||
|
IN p_id_lang INT,
|
||||||
|
IN p_id_customer INT,
|
||||||
|
IN p_id_country INT,
|
||||||
|
IN p_quantity INT
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
pa.id_product_attribute,
|
||||||
|
pa.reference,
|
||||||
|
|
||||||
|
-- PRICE (computed once per row via correlated subquery)
|
||||||
|
CAST(JSON_UNQUOTE(JSON_EXTRACT(
|
||||||
|
fn_product_price(
|
||||||
|
p_id_product,
|
||||||
|
p_id_shop,
|
||||||
|
p_id_customer,
|
||||||
|
p_id_country,
|
||||||
|
p_quantity,
|
||||||
|
pa.id_product_attribute
|
||||||
|
),
|
||||||
|
'$.base'
|
||||||
|
)) AS DECIMAL(20,6)) AS base_price,
|
||||||
|
|
||||||
|
CAST(JSON_UNQUOTE(JSON_EXTRACT(
|
||||||
|
fn_product_price(
|
||||||
|
p_id_product,
|
||||||
|
p_id_shop,
|
||||||
|
p_id_customer,
|
||||||
|
p_id_country,
|
||||||
|
p_quantity,
|
||||||
|
pa.id_product_attribute
|
||||||
|
),
|
||||||
|
'$.final_tax_excl'
|
||||||
|
)) AS DECIMAL(20,6)) AS price_tax_excl,
|
||||||
|
|
||||||
|
CAST(JSON_UNQUOTE(JSON_EXTRACT(
|
||||||
|
fn_product_price(
|
||||||
|
p_id_product,
|
||||||
|
p_id_shop,
|
||||||
|
p_id_customer,
|
||||||
|
p_id_country,
|
||||||
|
p_quantity,
|
||||||
|
pa.id_product_attribute
|
||||||
|
),
|
||||||
|
'$.final_tax_incl'
|
||||||
|
)) AS DECIMAL(20,6)) AS price_tax_incl,
|
||||||
|
|
||||||
|
IFNULL(sa.quantity, 0) AS quantity,
|
||||||
|
|
||||||
|
(
|
||||||
|
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
|
||||||
|
) AS attributes
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
END //
|
||||||
|
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
DELIMITER //
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS get_product_price //
|
||||||
|
CREATE PROCEDURE get_product_price(
|
||||||
|
IN p_id_product INT,
|
||||||
|
IN p_id_shop INT,
|
||||||
|
IN p_id_customer INT,
|
||||||
|
IN p_id_country INT,
|
||||||
|
IN p_quantity INT
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
|
||||||
|
SELECT fn_product_price(
|
||||||
|
p_id_product,
|
||||||
|
p_id_shop,
|
||||||
|
p_id_customer,
|
||||||
|
p_id_country,
|
||||||
|
p_quantity,
|
||||||
|
NULL
|
||||||
|
) AS price;
|
||||||
|
|
||||||
|
END //
|
||||||
|
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
DELIMITER //
|
||||||
|
DROP PROCEDURE IF EXISTS get_product_base //
|
||||||
|
CREATE PROCEDURE get_product_base(
|
||||||
|
IN p_id_product INT,
|
||||||
|
IN p_id_shop INT,
|
||||||
|
IN p_id_lang INT
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
SELECT
|
||||||
|
p.id_product AS id, -- matches view.Product.ID
|
||||||
|
|
||||||
|
p.reference,
|
||||||
|
p.supplier_reference,
|
||||||
|
p.ean13,
|
||||||
|
p.upc,
|
||||||
|
p.isbn,
|
||||||
|
|
||||||
|
-- Price related (basic)
|
||||||
|
p.price AS base_price,
|
||||||
|
p.wholesale_price,
|
||||||
|
p.unity,
|
||||||
|
p.unit_price_ratio,
|
||||||
|
|
||||||
|
-- Stock & Availability
|
||||||
|
p.quantity,
|
||||||
|
p.minimal_quantity,
|
||||||
|
p.available_for_order,
|
||||||
|
p.available_date,
|
||||||
|
p.out_of_stock AS out_of_stock_behavior, -- 0=deny, 1=allow, 2=default
|
||||||
|
|
||||||
|
-- Flags
|
||||||
|
COALESCE(ps.on_sale, 0) AS on_sale,
|
||||||
|
COALESCE(ps.show_price, 1) AS show_price,
|
||||||
|
p.condition,
|
||||||
|
p.is_virtual,
|
||||||
|
|
||||||
|
-- Physical
|
||||||
|
p.weight,
|
||||||
|
p.width,
|
||||||
|
p.height,
|
||||||
|
p.depth,
|
||||||
|
p.additional_shipping_cost,
|
||||||
|
|
||||||
|
-- Delivery
|
||||||
|
p.additional_delivery_times AS delivery_days, -- you can adjust if needed
|
||||||
|
|
||||||
|
-- Status
|
||||||
|
COALESCE(ps.active, p.active) AS active,
|
||||||
|
COALESCE(ps.visibility, p.visibility) AS visibility,
|
||||||
|
p.indexed,
|
||||||
|
|
||||||
|
-- Other useful
|
||||||
|
p.date_add,
|
||||||
|
p.date_upd,
|
||||||
|
|
||||||
|
-- Language data
|
||||||
|
pl.name,
|
||||||
|
pl.description,
|
||||||
|
pl.description_short,
|
||||||
|
|
||||||
|
-- Relations
|
||||||
|
m.name AS manufacturer,
|
||||||
|
cl.name AS category
|
||||||
|
|
||||||
|
-- This doesn't fit to base product, I'll add proper is_favorite to product later
|
||||||
|
|
||||||
|
-- EXISTS(
|
||||||
|
-- SELECT 1 FROM b2b_favorites f
|
||||||
|
-- WHERE f.user_id = p_id_customer AND f.product_id = p_id_product
|
||||||
|
-- ) AS is_favorite
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
WHERE p.id_product = p_id_product
|
||||||
|
LIMIT 1;
|
||||||
|
END //
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
-- +goose Up
|
|
||||||
-- +goose StatementBegin
|
|
||||||
SELECT 'up SQL query';
|
|
||||||
-- +goose StatementEnd
|
|
||||||
|
|
||||||
-- +goose Down
|
|
||||||
-- +goose StatementBegin
|
|
||||||
SELECT 'down SQL query';
|
|
||||||
-- +goose StatementEnd
|
|
||||||
idk if we want to keep a close eye on the returned errors? Because most files return responseErrors.ErrInvalidBody when localeExtractor fails. Similarly, when attribute can not be parsed, most handlers return esponseErrors.ErrBadAttribute.
I think if request fails to extract locale it should be probably internal server error because the server put the data in there in the first place