diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..082ab29 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "./app/cmd/main.go", + "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env" + } + ] +} \ No newline at end of file diff --git a/app/delivery/web/api/restricted/currency.go b/app/delivery/web/api/restricted/currency.go new file mode 100644 index 0000000..3ffa226 --- /dev/null +++ b/app/delivery/web/api/restricted/currency.go @@ -0,0 +1,97 @@ +package restricted + +import ( + "strconv" + + "git.ma-al.com/goc_daniel/b2b/app/config" + "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/service/currencyService" + "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" + "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" + "git.ma-al.com/goc_daniel/b2b/app/utils/response" + "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" + + "github.com/gofiber/fiber/v3" +) + +type CurrencyHandler struct { + CurrencyService *currencyService.CurrencyService + config *config.Config +} + +func NewCurrencyHandler() *CurrencyHandler { + currencyService := currencyService.New() + return &CurrencyHandler{ + CurrencyService: currencyService, + config: config.Get(), + } +} + +func CurrencyHandlerRoutes(r fiber.Router) fiber.Router { + handler := NewCurrencyHandler() + + r.Post("/currency-rate", handler.PostCurrencyRate) + r.Get("/currency-rate/:id", handler.GetCurrencyRate) + // r.Get("/currencies", handler.GetCurrencyRates) + return r +} + +func (h *CurrencyHandler) PostCurrencyRate(c fiber.Ctx) error { + var currencyRate model.CurrencyRate + if err := c.Bind().Body(¤cyRate); err != nil { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrJSONBody)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrJSONBody))) + } + + err := h.CurrencyService.CreateCurrencyRate(¤cyRate) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(nullable.GetNil(""), 1, i18n.T_(c, response.Message_OK))) +} + +func (h *CurrencyHandler) GetCurrencyRate(c fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.Atoi(idStr) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)).JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + + } + + currency, err := h.CurrencyService.GetCurrency(uint(id)) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)).JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + // err = h.CurrencyService.GetCurrencyRate(userID, uint(productID), uint(productShopID), uint(productLangID), updates) + // if err != nil { + // return c.Status(responseErrors.GetErrorStatus(err)). + // JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + // } + + return c.JSON(response.Make(currency, 0, i18n.T_(c, response.Message_OK))) +} + +func (h *CurrencyHandler) GetCurrencyRates(c fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.Atoi(idStr) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)).JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + + } + + currency, err := h.CurrencyService.GetCurrency(uint(id)) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)).JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + // err = h.CurrencyService.GetCurrencyRate(userID, uint(productID), uint(productShopID), uint(productLangID), updates) + // if err != nil { + // return c.Status(responseErrors.GetErrorStatus(err)). + // JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + // } + + return c.JSON(response.Make(currency, 0, i18n.T_(c, response.Message_OK))) +} diff --git a/app/delivery/web/api/restricted/product.go b/app/delivery/web/api/restricted/product.go new file mode 100644 index 0000000..d9c40a3 --- /dev/null +++ b/app/delivery/web/api/restricted/product.go @@ -0,0 +1,82 @@ +package restricted + +import ( + "strconv" + + "git.ma-al.com/goc_daniel/b2b/app/config" + "git.ma-al.com/goc_daniel/b2b/app/service/productService" + "git.ma-al.com/goc_daniel/b2b/app/utils/i18n" + "git.ma-al.com/goc_daniel/b2b/app/utils/nullable" + "git.ma-al.com/goc_daniel/b2b/app/utils/response" + "git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors" + "github.com/gofiber/fiber/v3" +) + +type ProductsHandler struct { + productService *productService.ProductService + config *config.Config +} + +// NewListProductsHandler creates a new ListProductsHandler instance +func NewProductsHandler() *ProductsHandler { + productService := productService.New() + return &ProductsHandler{ + productService: productService, + config: config.Get(), + } +} + +func ProductsHandlerRoutes(r fiber.Router) fiber.Router { + handler := NewProductsHandler() + + //TODO: WIP doesn't work yet + r.Get("/product/:id/:country_id/:quantity", handler.GetProductJson) + + return r +} + +func (h *ProductsHandler) GetProductJson(c fiber.Ctx) error { + idStr := c.Params("id") + + p_id_product, err := strconv.Atoi(idStr) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + country_idStr := c.Params("country_id") + + b2b_id_country, err := strconv.Atoi(country_idStr) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + quantityStr := c.Params("quantity") + + p_quantity, err := strconv.Atoi(quantityStr) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + id_lang, ok := c.Locals("lang_id").(int) + if !ok { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + p_id_customer, ok := c.Locals("user_id").(int) + if !ok { + return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadAttribute)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, responseErrors.ErrBadAttribute))) + } + + productJson, err := h.productService.GetJSON(p_id_product, id_lang, p_id_customer, b2b_id_country, p_quantity) + if err != nil { + return c.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(c, err))) + } + + return c.JSON(response.Make(&productJson, 1, i18n.T_(c, response.Message_OK))) +} diff --git a/app/delivery/web/init.go b/app/delivery/web/init.go index 94c02a6..b35516a 100644 --- a/app/delivery/web/init.go +++ b/app/delivery/web/init.go @@ -97,6 +97,8 @@ func (s *Server) Setup() error { listProducts := s.restricted.Group("/list-products") restricted.ListProductsHandlerRoutes(listProducts) + restricted.ProductsHandlerRoutes(s.restricted) + // locale selector (restricted) // this is basically for changing user's selected language and country localeSelector := s.restricted.Group("/langs-and-countries") @@ -118,6 +120,8 @@ func (s *Server) Setup() error { return c.SendStatus(fiber.StatusNotFound) }) + restricted.CurrencyHandlerRoutes(s.restricted) + // // Restricted routes example // restricted := s.api.Group("/restricted") // restricted.Use(middleware.AuthMiddleware()) diff --git a/app/model/currency.go b/app/model/currency.go new file mode 100644 index 0000000..18ca8ce --- /dev/null +++ b/app/model/currency.go @@ -0,0 +1,25 @@ +package model + +import "time" + +type Currency struct { + ID int `json:"id"` + PsIDCurrency uint `json:"ps_id_currency"` + IsDefault bool `json:"is_default"` + IsActive bool `json:"is_active"` + ConversionRate *float64 `json:"conversion_rate,omitempty"` +} + +func (Currency) TableName() string { + return "b2b_currencies" +} + +type CurrencyRate struct { + B2bIdCurrency uint `json:"b2b_id_currency"` + CreatedAt time.Time `json:"created_at"` + ConversionRate *float64 `json:"conversion_rate,omitempty"` +} + +func (CurrencyRate) TableName() string { + return "b2b_currency_rates" +} diff --git a/app/model/model.go b/app/model/model.go new file mode 100644 index 0000000..620b57a --- /dev/null +++ b/app/model/model.go @@ -0,0 +1,18 @@ +package model + +import ( + "time" + + "gorm.io/gorm" +) + +type Model struct { + ID uint `gorm:"primarykey;autoIncrement" swaggerignore:"true" json:"id,omitempty" hidden:"true"` + CreatedAt time.Time `gorm:"not null;autoCreateTime" swaggerignore:"true" json:"-"` + UpdatedAt time.Time `gorm:"autoUpdateTime" swaggerignore:"true" json:"-"` + DeletedAt gorm.DeletedAt `gorm:"index" swaggerignore:"true" json:"-"` +} + +// Makes all objects embedding db.Model implementators of ModelWithID interface +func (m Model) ModelWithID() { +} diff --git a/app/repos/currencyRepo/currencyRepo.go b/app/repos/currencyRepo/currencyRepo.go new file mode 100644 index 0000000..97b1b5e --- /dev/null +++ b/app/repos/currencyRepo/currencyRepo.go @@ -0,0 +1,53 @@ +package currencyRepo + +import ( + "git.ma-al.com/goc_daniel/b2b/app/db" + "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" + "git.ma-al.com/goc_daniel/b2b/app/utils/query/find" +) + +type UICurrencyRepo interface { + CreateConversionRate(currencyRate *model.CurrencyRate) error + Get(id uint) (*model.Currency, error) +} + +type CurrencyRepo struct{} + +func New() UICurrencyRepo { + return &CurrencyRepo{} +} + +func (repo *CurrencyRepo) CreateConversionRate(currencyRate *model.CurrencyRate) error { + return db.DB.Debug().Create(currencyRate).Error +} + +func (repo *CurrencyRepo) Get(id uint) (*model.Currency, error) { + var currency model.Currency + + err := db.DB.Table("b2b_currencies c"). + Select("c.*, r.conversion_rate"). + Joins(` + LEFT JOIN b2b_currency_rates r + ON r.b2b_id_currency = c.id + AND r.created_at = ( + SELECT MAX(created_at) + FROM b2b_currency_rates + WHERE b2b_id_currency = c.id + ) + `). + Where("c.id = ?", id). + Scan(¤cy).Error + + return ¤cy, err +} + +func (repo *CurrencyRepo) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.Currency], error) { + + found, err := find.Paginate[model.Currency](langId, p, db.DB. + Model(&model.Currency{}). + Scopes(filt.All()...), + ) + + return &found, err +} diff --git a/app/repos/productsRepo/productsRepo.go b/app/repos/productsRepo/productsRepo.go new file mode 100644 index 0000000..af68a63 --- /dev/null +++ b/app/repos/productsRepo/productsRepo.go @@ -0,0 +1,25 @@ +package productsRepo + +import ( + "git.ma-al.com/goc_daniel/b2b/app/db" +) + +type UIProductsRepo interface { + GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*string, error) +} + +type ProductsRepo struct{} + +func New() UIProductsRepo { + return &ProductsRepo{} +} + +func (repo *ProductsRepo) GetJSON(p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*string, error) { + var product string + + err := db.DB.Raw(`CALL get_full_product(?,?,?,?,?,?)`, p_id_product, p_id_shop, p_id_lang, p_id_customer, b2b_id_country, p_quantity). + Scan(&product). + Error + + return &product, err +} diff --git a/app/service/currencyService/currencyService.go b/app/service/currencyService/currencyService.go new file mode 100644 index 0000000..d4924d8 --- /dev/null +++ b/app/service/currencyService/currencyService.go @@ -0,0 +1,25 @@ +package currencyService + +import ( + "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/repos/currencyRepo" +) + +type CurrencyService struct { + repo currencyRepo.UICurrencyRepo +} + +func (s *CurrencyService) GetCurrency(id uint) (*model.Currency, error) { + return s.repo.Get(id) +} + +func (s *CurrencyService) CreateCurrencyRate(currency *model.CurrencyRate) error { + return s.repo.CreateConversionRate(currency) +} + +func New() *CurrencyService { + repo := currencyRepo.New() + return &CurrencyService{ + repo: repo, + } +} diff --git a/app/service/productService/productService.go b/app/service/productService/productService.go new file mode 100644 index 0000000..364079d --- /dev/null +++ b/app/service/productService/productService.go @@ -0,0 +1,25 @@ +package productService + +import ( + "git.ma-al.com/goc_daniel/b2b/app/repos/productsRepo" + constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" +) + +type ProductService struct { + productsRepo productsRepo.UIProductsRepo +} + +func New() *ProductService { + return &ProductService{ + productsRepo: productsRepo.New(), + } +} + +func (s *ProductService) GetJSON(p_id_product, p_id_lang, p_id_customer, b2b_id_country, p_quantity int) (*string, error) { + products, err := s.productsRepo.GetJSON(p_id_product, constdata.SHOP_ID, p_id_lang, p_id_customer, b2b_id_country, p_quantity) + if err != nil { + return products, err + } + + return products, nil +} diff --git a/app/utils/responseErrors/responseErrors.go b/app/utils/responseErrors/responseErrors.go index f658430..2b8715d 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -56,6 +56,9 @@ var ( ErrMaxAmtOfCartsReached = errors.New("maximal amount of carts reached") ErrUserHasNoSuchCart = errors.New("user does not have cart with given id") ErrProductOrItsVariationDoesNotExist = errors.New("product or its variation with given ids does not exist") + + // Typed errors for data parsing + ErrJSONBody = errors.New("invalid JSON body") ) // Error represents an error with HTTP status code @@ -153,6 +156,9 @@ func GetErrorCode(c fiber.Ctx, err error) string { case errors.Is(err, ErrProductOrItsVariationDoesNotExist): return i18n.T_(c, "error.product_or_its_variation_does_not_exist") + case errors.Is(err, ErrJSONBody): + return i18n.T_(c, "error.err_json_body") + default: return i18n.T_(c, "error.err_internal_server_error") } @@ -191,7 +197,8 @@ func GetErrorStatus(err error) int { errors.Is(err, ErrNoRootFound), errors.Is(err, ErrMaxAmtOfCartsReached), errors.Is(err, ErrUserHasNoSuchCart), - errors.Is(err, ErrProductOrItsVariationDoesNotExist): + errors.Is(err, ErrProductOrItsVariationDoesNotExist), + errors.Is(err, ErrJSONBody): return fiber.StatusBadRequest case errors.Is(err, ErrEmailExists): return fiber.StatusConflict diff --git a/i18n/migrations/20260302163122_create_tables.sql b/i18n/migrations/20260302163122_create_tables.sql index a126730..684af5b 100644 --- a/i18n/migrations/20260302163122_create_tables.sql +++ b/i18n/migrations/20260302163122_create_tables.sql @@ -1,7 +1,7 @@ -- +goose Up CREATE TABLE IF NOT EXISTS b2b_language ( - id INT AUTO_INCREMENT PRIMARY KEY, + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, created_at DATETIME(6) NOT NULL, updated_at DATETIME(6) NULL, deleted_at DATETIME(6) NULL, @@ -19,29 +19,22 @@ CREATE TABLE IF NOT EXISTS b2b_language ( CREATE INDEX IF NOT EXISTS idx_language_deleted_at ON b2b_language (deleted_at); -INSERT IGNORE INTO b2b_language - (id, created_at, updated_at, deleted_at, name, iso_code, lang_code, date_format, date_format_short, rtl, is_default, active, flag) -VALUES - (1, '2022-09-16 17:10:02.837', '2026-03-02 21:24:36.779730', NULL, 'Polski', 'pl', 'pl', '__-__-____', '__-__', 0, 0, 1, 'πŸ‡΅πŸ‡±'), - (2, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'English', 'en', 'en', '__-__-____', '__-__', 0, 1, 1, 'πŸ‡¬πŸ‡§'), - (3, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'Deutsch', 'de', 'de', '__-__-____', '__-__', 0, 0, 1, 'πŸ‡©πŸ‡ͺ'); - CREATE TABLE IF NOT EXISTS b2b_components ( - id INT AUTO_INCREMENT PRIMARY KEY, + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; CREATE UNIQUE INDEX IF NOT EXISTS uk_components_name ON b2b_components (name, id); -- scopes CREATE TABLE IF NOT EXISTS b2b_scopes ( - id INT AUTO_INCREMENT PRIMARY KEY, + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; CREATE UNIQUE INDEX IF NOT EXISTS uk_scopes_name ON b2b_scopes (name); -- translations CREATE TABLE IF NOT EXISTS b2b_translations ( - lang_id INT NOT NULL, - scope_id INT NOT NULL, - component_id INT NOT NULL, + lang_id INT UNSIGNED NOT NULL, + scope_id INT UNSIGNED NOT NULL, + component_id INT UNSIGNED NOT NULL, `key` VARCHAR(255) NOT NULL, data TEXT NULL, PRIMARY KEY (lang_id, scope_id, component_id, `key`), @@ -71,8 +64,8 @@ CREATE TABLE IF NOT EXISTS b2b_customers ( password_reset_expires DATETIME(6) NULL, last_password_reset_request DATETIME(6) NULL, last_login_at DATETIME(6) NULL, - lang_id BIGINT NULL DEFAULT 2, - country_id BIGINT NULL DEFAULT 2, + lang_id INT NULL DEFAULT 2, + country_id INT NULL DEFAULT 2, created_at DATETIME(6) NULL, updated_at DATETIME(6) NULL, deleted_at DATETIME(6) NULL @@ -87,7 +80,7 @@ ON b2b_customers (deleted_at); -- customer_carts CREATE TABLE IF NOT EXISTS b2b_customer_carts ( - cart_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + cart_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL, name VARCHAR(255) NULL, CONSTRAINT fk_customer_carts_customers FOREIGN KEY (user_id) REFERENCES b2b_customers(id) ON DELETE CASCADE ON UPDATE CASCADE @@ -97,10 +90,10 @@ CREATE INDEX IF NOT EXISTS idx_customer_carts_user_id ON b2b_customer_carts (use -- carts_products CREATE TABLE IF NOT EXISTS b2b_carts_products ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, - cart_id BIGINT UNSIGNED NOT NULL, + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + cart_id INT UNSIGNED NOT NULL, product_id INT UNSIGNED NOT NULL, - product_attribute_id BIGINT NULL, + product_attribute_id INT NULL, amount INT UNSIGNED NOT NULL, CONSTRAINT fk_carts_products_customer_carts FOREIGN KEY (cart_id) REFERENCES b2b_customer_carts (cart_id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT fk_carts_products_product FOREIGN KEY (product_id) REFERENCES ps_product (id_product) ON DELETE CASCADE ON UPDATE CASCADE @@ -110,7 +103,7 @@ CREATE INDEX IF NOT EXISTS idx_carts_products_cart_id ON b2b_carts_products (car -- refresh_tokens CREATE TABLE IF NOT EXISTS b2b_refresh_tokens ( - id INT AUTO_INCREMENT PRIMARY KEY, + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, customer_id BIGINT UNSIGNED NOT NULL, token_hash VARCHAR(64) NOT NULL, expires_at DATETIME(6) NOT NULL, @@ -120,24 +113,146 @@ CREATE TABLE IF NOT EXISTS b2b_refresh_tokens ( CREATE UNIQUE INDEX IF NOT EXISTS uk_refresh_tokens_token_hash ON b2b_refresh_tokens (token_hash); CREATE INDEX IF NOT EXISTS idx_refresh_tokens_customer_id ON b2b_refresh_tokens (customer_id); +CREATE TABLE `b2b_currencies` ( + `id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, + `ps_id_currency` INT UNSIGNED NOT NULL, + `is_default` TINYINT NOT NULL, + `is_active` TINYINT NOT NULL, + PRIMARY KEY (`id`) +) +ENGINE = InnoDB; + +ALTER TABLE `b2b_currencies` ADD CONSTRAINT `FK_b2b_currencies_ps_id_currency` FOREIGN KEY (`ps_id_currency`) REFERENCES `ps_currency` (`id_currency`) ON DELETE CASCADE ON UPDATE CASCADE; + +CREATE INDEX `fk_b2b_currencies_ps_currency` +ON `b2b_currencies` ( + `ps_id_currency` ASC +); + +CREATE TABLE `b2b_currency_rates` ( + `id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, + `b2b_id_currency` BIGINT UNSIGNED NOT NULL, + `created_at` DATETIME NOT NULL, + `conversion_rate` DECIMAL(13,6) NULL DEFAULT NULL , + PRIMARY KEY (`id`) +) +ENGINE = InnoDB; +ALTER TABLE `b2b_currency_rates` ADD CONSTRAINT `FK_b2b_currency_rates_b2b_id_currency` FOREIGN KEY (`b2b_id_currency`) REFERENCES `b2b_currencies` (`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +CREATE INDEX `fk_b2b_currency_rates_b2b_currencies` +ON `b2b_currency_rates` ( + `b2b_id_currency` ASC +); -- countries CREATE TABLE IF NOT EXISTS b2b_countries ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(128) NOT NULL, - currency_id INT UNSIGNED NOT NULL, - flag VARCHAR(16) NOT NULL, - CONSTRAINT fk_countries_currency FOREIGN KEY (currency_id) REFERENCES ps_currency(id_currency) ON DELETE RESTRICT ON UPDATE RESTRICT -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + `id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, + `flag` VARCHAR(16) NOT NULL, + `ps_id_country` INT UNSIGNED NOT NULL, + `b2b_id_currency` BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `fk_b2b_countries_ps_country` FOREIGN KEY (`ps_id_country`) REFERENCES `ps_country` (`id_country`) ON DELETE NO ACTION ON UPDATE NO ACTION, + CONSTRAINT `FK_b2b_countries_b2b_id_currency` FOREIGN KEY (`b2b_id_currency`) REFERENCES `b2b_currencies` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION +) +ENGINE = InnoDB; +CREATE INDEX `fk_b2b_countries_ps_country` +ON `b2b_countries` ( + `ps_id_country` ASC +); -INSERT IGNORE INTO b2b_countries - (id, name, currency_id, flag) -VALUES - (1, 'Polska', 1, 'πŸ‡΅πŸ‡±'), - (2, 'England', 2, 'πŸ‡¬πŸ‡§'), - (3, 'ČeΕ‘tina', 2, 'πŸ‡¨πŸ‡Ώ'), - (4, 'Deutschland', 2, 'πŸ‡©πŸ‡ͺ'); +CREATE TABLE b2b_specific_price ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + created_at DATETIME NULL, + updated_at DATETIME NULL, + deleted_at DATETIME NULL, + scope ENUM('shop', 'category', 'product') NOT NULL, + valid_from DATETIME NULL, + valid_till DATETIME NULL, + has_expiration_date BOOLEAN DEFAULT FALSE, + reduction_type ENUM('amount', 'percentage') NOT NULL, + price DECIMAL(10, 2) NULL, + b2b_id_currency BIGINT UNSIGNED NULL, -- specifies which currency is used for the price + percentage_reduction DECIMAL(5, 2) NULL, + from_quantity INT UNSIGNED DEFAULT 1, + is_active BOOLEAN DEFAULT TRUE, + CONSTRAINT fk_b2b_specific_price_country FOREIGN KEY (b2b_id_country) REFERENCES b2b_countries(id) ON DELETE + SET + NULL ON UPDATE CASCADE, + CONSTRAINT fk_b2b_specific_price_customer FOREIGN KEY (b2b_id_customer) REFERENCES b2b_customers(id) ON DELETE + SET + NULL ON UPDATE CASCADE, + CONSTRAINT fk_b2b_specific_price_currency FOREIGN KEY (b2b_id_currency) REFERENCES b2b_currencies(id) ON DELETE + SET + NULL ON UPDATE CASCADE +) ENGINE = InnoDB; +CREATE INDEX idx_b2b_scope ON b2b_specific_price(scope); +CREATE INDEX idx_b2b_customer ON b2b_specific_price(b2b_id_customer); +CREATE INDEX idx_b2b_country ON b2b_specific_price(b2b_id_country); +CREATE INDEX idx_b2b_active_dates ON b2b_specific_price(is_active, valid_from, valid_till); +CREATE INDEX idx_b2b_lookup +ON b2b_specific_price ( + scope, + is_active, + b2b_id_customer, + b2b_id_country, + from_quantity +); +CREATE TABLE b2b_specific_price_product ( + b2b_specific_price_id BIGINT UNSIGNED, + id_product INT UNSIGNED, + PRIMARY KEY (b2b_specific_price_id, id_product), + FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE, + FOREIGN KEY (id_product) REFERENCES ps_product(id_product) ON DELETE CASCADE +); + +CREATE TABLE b2b_specific_price_category ( + b2b_specific_price_id BIGINT UNSIGNED, + id_category INT UNSIGNED, + PRIMARY KEY (b2b_specific_price_id, id_category), + FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE, + FOREIGN KEY (id_category) REFERENCES ps_category(id_category) ON DELETE CASCADE +); + +CREATE TABLE b2b_specific_price_product_attribute ( + b2b_specific_price_id BIGINT UNSIGNED, + id_product_attribute INT UNSIGNED, + PRIMARY KEY (b2b_specific_price_id, id_product_attribute), + FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE, + FOREIGN KEY (id_product_attribute) REFERENCES ps_product_attribute(id_product_attribute) ON DELETE CASCADE +); + +CREATE TABLE b2b_specific_price_customer ( + b2b_specific_price_id BIGINT UNSIGNED, + b2b_id_customer BIGINT UNSIGNED, + PRIMARY KEY (b2b_specific_price_id, b2b_id_customer), + FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE, + FOREIGN KEY (b2b_id_customer) REFERENCES b2b_customers(id) ON DELETE CASCADE +); + +CREATE TABLE b2b_specific_price_country ( + b2b_specific_price_id BIGINT UNSIGNED, + b2b_id_country BIGINT UNSIGNED, + PRIMARY KEY (b2b_specific_price_id, b2b_id_country), + FOREIGN KEY (b2b_specific_price_id) REFERENCES b2b_specific_price(id) ON DELETE CASCADE, + FOREIGN KEY (b2b_id_country) REFERENCES b2b_countries(id) ON DELETE CASCADE +); + +CREATE INDEX idx_b2b_product_rel +ON b2b_specific_price_product (id_product); + +CREATE INDEX idx_b2b_category_rel +ON b2b_specific_price_category (id_category); + +CREATE INDEX idx_b2b_product_attribute_rel +ON b2b_specific_price_product_attribute (id_product_attribute); + +CREATE INDEX idx_bsp_customer +ON b2b_specific_price_customer (b2b_specific_price_id, b2b_id_customer); + +CREATE INDEX idx_bsp_country +ON b2b_specific_price_country (b2b_specific_price_id, b2b_id_country); -- +goose Down DROP TABLE IF EXISTS b2b_countries; @@ -147,3 +262,9 @@ DROP TABLE IF EXISTS b2b_scopes; DROP TABLE IF EXISTS b2b_translations; DROP TABLE IF EXISTS b2b_customers; DROP TABLE IF EXISTS b2b_refresh_tokens; +DROP TABLE IF EXISTS b2b_currencies; +DROP TABLE IF EXISTS b2b_currency_rates; +DROP TABLE IF EXISTS b2b_specific_price; +DROP TABLE IF EXISTS b2b_specific_price_product; +DROP TABLE IF EXISTS b2b_specific_price_category; +DROP TABLE IF EXISTS b2b_specific_price_product_attribute; diff --git a/i18n/migrations/20260302163123_create_tables_data.sql b/i18n/migrations/20260302163123_create_tables_data.sql new file mode 100644 index 0000000..7bae811 --- /dev/null +++ b/i18n/migrations/20260302163123_create_tables_data.sql @@ -0,0 +1,32 @@ +-- +goose Up + +INSERT IGNORE INTO b2b_language + (id, created_at, updated_at, deleted_at, name, iso_code, lang_code, date_format, date_format_short, rtl, is_default, active, flag) +VALUES + (1, '2022-09-16 17:10:02.837', '2026-03-02 21:24:36.779730', NULL, 'Polski', 'pl', 'pl', '__-__-____', '__-__', 0, 0, 1, 'πŸ‡΅πŸ‡±'), + (2, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'English', 'en', 'en', '__-__-____', '__-__', 0, 1, 1, 'πŸ‡¬πŸ‡§'), + (3, '2022-09-16 17:10:02.852', '2026-03-02 21:24:36.779730', NULL, 'Deutsch', 'de', 'de', '__-__-____', '__-__', 0, 0, 1, 'πŸ‡©πŸ‡ͺ'); + + +-- insert sample admin user admin@ma-al.com/Maal12345678 + +INSERT IGNORE INTO b2b_customers (id, email, password, first_name, last_name, role, provider, provider_id, avatar_url, is_active, email_verified, email_verification_token, email_verification_expires, password_reset_token, password_reset_expires, last_password_reset_request, last_login_at, lang_id, country_id, created_at, updated_at, deleted_at) +VALUES + (1, 'admin@ma-al.com', '$2a$10$Owy9DjrS0l3Fz4XoOvh5pulgmOMqdwXmb7hYE9BovnSuWS2plGr82', 'Super', 'Admin', 'admin', 'local', '', '', 1, 1, NULL, NULL, '', NULL, NULL, NULL, 1, 1, '2026-03-02 16:55:10.252740', '2026-03-02 16:55:10.252740', NULL); +ALTER TABLE b2b_customers AUTO_INCREMENT = 1; + +INSERT INTO `b2b_currencies` (`ps_id_currency`, `is_default`, `is_active`) VALUES +('1','1','1'), +('2','0','1'); + +INSERT IGNORE INTO b2b_countries + (id, flag, ps_id_country, b2b_id_currency) +VALUES + (1, 'πŸ‡΅πŸ‡±', 14, 1), + (2, 'πŸ‡¬πŸ‡§', 17, 2), + (3, 'πŸ‡¨πŸ‡Ώ', 16, 2), + (4, 'πŸ‡©πŸ‡ͺ', 1, 2); + + + +-- +goose Down \ No newline at end of file diff --git a/i18n/migrations/20260319163200_procedures.sql b/i18n/migrations/20260319163200_procedures.sql index b10e54d..8207466 100644 --- a/i18n/migrations/20260319163200_procedures.sql +++ b/i18n/migrations/20260319163200_procedures.sql @@ -1,223 +1,362 @@ -- +goose Up -DELIMITER // - -DROP PROCEDURE IF EXISTS get_full_product // - +DELIMITER // +DROP PROCEDURE IF EXISTS get_full_product +// CREATE PROCEDURE get_full_product( - IN p_id_product INT UNSIGNED, - IN p_id_shop INT UNSIGNED, - IN p_id_lang INT UNSIGNED, - IN p_id_customer INT UNSIGNED, - IN p_id_group INT UNSIGNED, - IN p_id_currency INT UNSIGNED, - IN p_id_country INT UNSIGNED, - IN p_quantity INT UNSIGNED -) -BEGIN - - DECLARE v_tax_rate DECIMAL(10,4) DEFAULT 0; - - SELECT COALESCE(t.rate, 0.0000) INTO v_tax_rate -FROM ps_tax_rule tr -INNER JOIN ps_tax t - ON t.id_tax = tr.id_tax -WHERE tr.id_tax_rules_group = ( - SELECT ps.id_tax_rules_group - FROM ps_product_shop ps - WHERE ps.id_product = p_id_product - AND ps.id_shop = p_id_shop - LIMIT 1 -) - AND tr.id_country = p_id_country -ORDER BY - tr.id_state DESC, - tr.zipcode_from != '' DESC, - tr.id_tax_rule DESC + IN p_id_product INT UNSIGNED, + IN p_id_shop INT UNSIGNED, + IN p_id_lang INT UNSIGNED, + IN p_id_customer INT UNSIGNED, + IN b2b_id_country INT UNSIGNED, + IN p_quantity INT UNSIGNED +) +BEGIN +DECLARE v_tax_rate DECIMAL(10, 4) DEFAULT 0; +DECLARE p_id_currency DECIMAL(10, 4) DEFAULT 0; +SELECT + COALESCE(t.rate, 0.0000) INTO v_tax_rate +FROM + ps_tax_rule tr + INNER JOIN ps_tax t ON t.id_tax = tr.id_tax + LEFT JOIN b2b_countries ON b2b_countries.id = b2b_id_country +WHERE + tr.id_tax_rules_group = ( + SELECT + ps.id_tax_rules_group + FROM + ps_product_shop ps + WHERE + ps.id_product = p_id_product + AND ps.id_shop = p_id_shop + LIMIT + 1 + ) + AND tr.id_country = b2b_countries.ps_id_country +ORDER BY + tr.id_state DESC, + tr.zipcode_from != '' DESC, + tr.id_tax_rule DESC +LIMIT + 1; + +SELECT + b2b_currencies.ps_id_currency INTO p_id_currency +FROM + b2b_currencies + LEFT JOIN b2b_countries ON b2b_countries.b2b_id_currency = b2b_currencies.id +WHERE + b2b_countries.id = b2b_id_country LIMIT 1; + +/* FINAL JSON */ +SELECT + JSON_OBJECT( + /* ================= PRODUCT ================= */ + 'id_product', + p.id_product, + 'reference', + p.reference, + 'name', + pl.name, + 'description', + pl.description, + 'short_description', + pl.description_short, + /* ================= PRICE ================= */ +'price', +JSON_OBJECT( + 'base', + COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate), - /* FINAL 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, p.price), - - 'final_tax_excl', + 'final_tax_excl', + ( + CASE + WHEN bsp.id IS NOT NULL THEN + CASE + /* FIXED PRICE */ + WHEN bsp.reduction_type = 'amount' THEN ( - COALESCE(ps.price, p.price) - - IFNULL( - CASE - WHEN sp.reduction_type = 'amount' THEN sp.reduction - WHEN sp.reduction_type = 'percentage' THEN COALESCE(ps.price, p.price) * sp.reduction - ELSE 0 - END, 0 - ) - ), - - 'final_tax_incl', - ( - ( - COALESCE(ps.price, p.price) - - IFNULL( - CASE - WHEN sp.reduction_type = 'amount' THEN sp.reduction - WHEN sp.reduction_type = 'percentage' THEN COALESCE(ps.price, p.price) * sp.reduction - ELSE 0 - END, 0 - ) - ) * (1 + v_tax_rate / 100) + CASE + WHEN bsp.b2b_id_currency IS NULL THEN bsp.price + ELSE bsp.price * br_bsp.conversion_rate + END ) - ), - /* ================= META ================= */ - 'active', COALESCE(ps.active, p.active), - 'visibility', COALESCE(ps.visibility, p.visibility), - 'manufacturer', m.name, - 'category', cl.name, + /* PERCENTAGE */ + WHEN bsp.reduction_type = 'percentage' THEN + COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate) + * (1 - bsp.percentage_reduction / 100) - /* ================= IMAGE ================= */ - 'cover_image', JSON_OBJECT( - 'id', i.id_image, - 'legend', il.legend - ), + ELSE + COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate) + END - /* ================= 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 - ), + ELSE + COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate) + END + ), - /* ================= COMBINATIONS ================= */ - 'combinations', ( - SELECT JSON_ARRAYAGG( - JSON_OBJECT( - 'id_product_attribute', pa.id_product_attribute, - 'reference', pa.reference, + 'final_tax_incl', + ( + ( + CASE + WHEN bsp.id IS NOT NULL THEN + CASE + WHEN bsp.reduction_type = 'amount' THEN + ( + CASE + WHEN bsp.b2b_id_currency IS NULL THEN bsp.price + ELSE bsp.price * br_bsp.conversion_rate + END + ) - 'price', JSON_OBJECT( - 'impact', COALESCE(pas.price, pa.price), + WHEN bsp.reduction_type = 'percentage' THEN + COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate) + * (1 - bsp.percentage_reduction / 100) - '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 + ELSE + COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate) + END + ELSE + COALESCE(ps.price * r.conversion_rate, p.price * r.conversion_rate) + END + ) * (1 + v_tax_rate / 100) + ) +), + /* ================= META ================= */ + 'active', + COALESCE(ps.active, p.active), + 'visibility', + COALESCE(ps.visibility, p.visibility), + 'manufacturer', + m.name, + 'category', + cl.name, + /* ================= IMAGE ================= */ + 'cover_image', + JSON_OBJECT( + 'id', + i.id_image, + 'legend', + il.legend + ), + /* ================= FEATURES ================= */ + 'features', + ( + SELECT + JSON_ARRAYAGG( + JSON_OBJECT( + 'name', + fl.name, + 'value', + fvl.value + ) ) + FROM + ps_feature_product fp + JOIN ps_feature_lang fl ON fl.id_feature = fp.id_feature + AND fl.id_lang = p_id_lang + JOIN ps_feature_value_lang fvl ON fvl.id_feature_value = fp.id_feature_value + AND fvl.id_lang = p_id_lang + WHERE + fp.id_product = p.id_product + ), + /* ================= COMBINATIONS ================= */ + 'combinations', + ( + SELECT + JSON_ARRAYAGG( + JSON_OBJECT( + 'id_product_attribute', + pa.id_product_attribute, + 'reference', + pa.reference, + 'price', + JSON_OBJECT( + 'impact', + COALESCE(pas.price, pa.price), + 'final_tax_excl', + ( + COALESCE(ps.price, p.price) + COALESCE(pas.price, pa.price) + ), + 'final_tax_incl', + ( + ( + COALESCE(ps.price, p.price) + COALESCE(pas.price, pa.price) + ) * (1 + v_tax_rate / 100) + ) + ), + 'stock', + IFNULL(sa.quantity, 0), + 'default_on', + pas.default_on, + /* ATTRIBUTES JSON */ + 'attributes', + ( + SELECT + JSON_ARRAYAGG( + JSON_OBJECT( + 'group', + agl.name, + 'attribute', + al.name + ) + ) + FROM + ps_product_attribute_combination pac + JOIN ps_attribute a ON a.id_attribute = pac.id_attribute + JOIN ps_attribute_lang al ON al.id_attribute = a.id_attribute + AND al.id_lang = p_id_lang + JOIN ps_attribute_group_lang agl ON agl.id_attribute_group = a.id_attribute_group + AND agl.id_lang = p_id_lang + WHERE + pac.id_product_attribute = pa.id_product_attribute + ), + /* IMAGES */ + 'images', + ( + SELECT + JSON_ARRAYAGG(img.id_image) + FROM + ps_product_attribute_image pai + JOIN ps_image img ON img.id_image = pai.id_image + WHERE + pai.id_product_attribute = pa.id_product_attribute + ) + ) + ) + FROM + ps_product_attribute pa + JOIN ps_product_attribute_shop pas ON pas.id_product_attribute = pa.id_product_attribute + AND pas.id_shop = p_id_shop + LEFT JOIN ps_stock_available sa ON sa.id_product = pa.id_product + AND sa.id_product_attribute = pa.id_product_attribute + AND sa.id_shop = p_id_shop + WHERE + pa.id_product = p.id_product + ) + ) AS product_json +FROM + ps_product p + LEFT JOIN ps_product_shop ps ON ps.id_product = p.id_product + AND ps.id_shop = p_id_shop + LEFT JOIN ps_product_lang pl ON pl.id_product = p.id_product + AND pl.id_lang = p_id_lang + AND pl.id_shop = p_id_shop + LEFT JOIN ps_category_lang cl ON cl.id_category = COALESCE(ps.id_category_default, p.id_category_default) + AND cl.id_lang = p_id_lang + AND cl.id_shop = p_id_shop + LEFT JOIN ps_manufacturer m ON m.id_manufacturer = p.id_manufacturer + LEFT JOIN ps_image i ON i.id_product = p.id_product + AND i.cover = 1 + LEFT JOIN ps_image_lang il ON il.id_image = i.id_image + AND il.id_lang = p_id_lang + /* SPECIFIC PRICE */ +LEFT JOIN ( + SELECT bsp.* + FROM b2b_specific_price bsp - ) AS product_json + /* RELATIONS */ + LEFT JOIN b2b_specific_price_product bsp_p + ON bsp_p.b2b_specific_price_id = bsp.id - FROM ps_product p + LEFT JOIN b2b_specific_price_category bsp_c + ON bsp_c.b2b_specific_price_id = bsp.id - LEFT JOIN ps_product_shop ps - ON ps.id_product = p.id_product AND ps.id_shop = p_id_shop + WHERE bsp.is_active = TRUE - 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 + /* SCOPE MATCH */ + AND ( + /* PRODUCT */ + (bsp.scope = 'product' AND bsp_p.id_product = p_id_product) - 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 + /* 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 + ) + ) - LEFT JOIN ps_manufacturer m - ON m.id_manufacturer = p.id_manufacturer + /* SHOP (GLOBAL) */ + OR (bsp.scope = 'shop') + ) - LEFT JOIN ps_image i - ON i.id_product = p.id_product AND i.cover = 1 + /* 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 + ) +) - LEFT JOIN ps_image_lang il - ON il.id_image = i.id_image AND il.id_lang = p_id_lang +/* 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 + ) +) - /* SPECIFIC PRICE */ - LEFT JOIN ( - SELECT sp1.* - FROM ps_specific_price sp1 - WHERE sp1.id_product = p_id_product - AND (sp1.id_customer = 0 OR sp1.id_customer = p_id_customer) - AND (sp1.id_group = 0 OR sp1.id_group = p_id_group) - AND (sp1.id_currency = 0 OR sp1.id_currency = p_id_currency) - AND sp1.from_quantity <= p_quantity - ORDER BY - sp1.id_customer DESC, - sp1.id_group DESC, - sp1.from_quantity DESC, - sp1.id_specific_price DESC - LIMIT 1 - ) sp ON sp.id_product = p.id_product + /* QUANTITY */ + AND bsp.from_quantity <= p_quantity - WHERE p.id_product = p_id_product + /* DATE */ + AND ( + bsp.has_expiration_date = FALSE OR ( + (bsp.valid_from IS NULL OR bsp.valid_from <= NOW()) + AND (bsp.valid_till IS NULL OR bsp.valid_till >= NOW()) + ) + ) - LIMIT 1; + ORDER BY + /* πŸ”₯ STRICT PRIORITY */ + bsp.scope = 'product' DESC, + bsp.scope = 'category' DESC, + bsp.scope = 'shop' DESC, + bsp.b2b_id_customer DESC, + bsp.b2b_id_country DESC, + + bsp.from_quantity DESC, + bsp.id DESC + + LIMIT 1 +) bsp ON 1=1 +LEFT JOIN b2b_currency_rates br_bsp + ON br_bsp.b2b_id_currency = bsp.b2b_id_currency + AND br_bsp.created_at = ( + SELECT MAX(created_at) + FROM b2b_currency_rates + WHERE b2b_id_currency = bsp.b2b_id_currency + ) + LEFT JOIN b2b_countries ON b2b_countries.id = b2b_id_country + LEFT JOIN b2b_currencies ON b2b_currencies.id = p_id_currency + LEFT JOIN b2b_currency_rates r ON r.b2b_id_currency = b2b_currencies.id + AND r.created_at = ( + SELECT + MAX(created_at) + FROM + b2b_currency_rates + WHERE + b2b_id_currency = b2b_currencies.id + ) +WHERE + p.id_product = p_id_product +LIMIT + 1; END // DELIMITER ; diff --git a/i18n/migrations/20260320113729_stuff.sql b/i18n/migrations/20260320113729_stuff.sql new file mode 100644 index 0000000..b9c449e --- /dev/null +++ b/i18n/migrations/20260320113729_stuff.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +SELECT 'up SQL query'; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +SELECT 'down SQL query'; +-- +goose StatementEnd diff --git a/repository/currencyRepo/currencyRepo.go b/repository/currencyRepo/currencyRepo.go new file mode 100644 index 0000000..97b1b5e --- /dev/null +++ b/repository/currencyRepo/currencyRepo.go @@ -0,0 +1,53 @@ +package currencyRepo + +import ( + "git.ma-al.com/goc_daniel/b2b/app/db" + "git.ma-al.com/goc_daniel/b2b/app/model" + "git.ma-al.com/goc_daniel/b2b/app/utils/query/filters" + "git.ma-al.com/goc_daniel/b2b/app/utils/query/find" +) + +type UICurrencyRepo interface { + CreateConversionRate(currencyRate *model.CurrencyRate) error + Get(id uint) (*model.Currency, error) +} + +type CurrencyRepo struct{} + +func New() UICurrencyRepo { + return &CurrencyRepo{} +} + +func (repo *CurrencyRepo) CreateConversionRate(currencyRate *model.CurrencyRate) error { + return db.DB.Debug().Create(currencyRate).Error +} + +func (repo *CurrencyRepo) Get(id uint) (*model.Currency, error) { + var currency model.Currency + + err := db.DB.Table("b2b_currencies c"). + Select("c.*, r.conversion_rate"). + Joins(` + LEFT JOIN b2b_currency_rates r + ON r.b2b_id_currency = c.id + AND r.created_at = ( + SELECT MAX(created_at) + FROM b2b_currency_rates + WHERE b2b_id_currency = c.id + ) + `). + Where("c.id = ?", id). + Scan(¤cy).Error + + return ¤cy, err +} + +func (repo *CurrencyRepo) Find(langId uint, p find.Paging, filt *filters.FiltersList) (*find.Found[model.Currency], error) { + + found, err := find.Paginate[model.Currency](langId, p, db.DB. + Model(&model.Currency{}). + Scopes(filt.All()...), + ) + + return &found, err +} diff --git a/taskfiles/db.yml b/taskfiles/db.yml index b7a349b..21ac231 100644 --- a/taskfiles/db.yml +++ b/taskfiles/db.yml @@ -59,6 +59,7 @@ tasks: - | sed '/-- +goose Down/,$d' i18n/migrations/20260302163100_routes.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}} sed '/-- +goose Down/,$d' i18n/migrations/20260302163122_create_tables.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}} + sed '/-- +goose Down/,$d' i18n/migrations/20260302163123_create_tables_data.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}} sed '/-- +goose Down/,$d' i18n/migrations/20260302163152_translations_backoffice.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}} sed '/-- +goose Down/,$d' i18n/migrations/20260302163157_translations_backend.sql | docker compose -p {{.PROJECT}} exec -T {{.LOCAL_DB_SERVICE}} mariadb -u {{.LOCAL_DB_USER}} --password={{.LOCAL_DB_PASSWORD}} {{.LOCAL_DB_NAME}}