diff --git a/app/delivery/web/api/restricted/customer.go b/app/delivery/web/api/restricted/customer.go index e280d4f..9d15c11 100644 --- a/app/delivery/web/api/restricted/customer.go +++ b/app/delivery/web/api/restricted/customer.go @@ -32,6 +32,7 @@ func CustomerHandlerRoutes(r fiber.Router) fiber.Router { r.Get("", handler.customerData) r.Get("/list", middleware.Require(perms.UserReadAny), handler.listCustomers) + r.Patch("/no-vat", middleware.Require(perms.UserWriteAny), handler.setCustomerNoVatStatus) return r } @@ -100,3 +101,28 @@ var columnMappingListUsers map[string]string = map[string]string{ "first_name": "users.first_name", "last_name": "users.last_name", } + +func (h *customerHandler) setCustomerNoVatStatus(fc fiber.Ctx) error { + user, ok := localeExtractor.GetCustomer(fc) + if !ok || user == nil { + return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrInvalidBody))) + } + + var req struct { + CustomerID uint `json:"customer_id"` + IsNoVat bool `json:"is_no_vat"` + } + + if err := fc.Bind().Body(&req); err != nil { + return fc.Status(responseErrors.GetErrorStatus(responseErrors.ErrJSONBody)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, responseErrors.ErrJSONBody))) + } + + if err := h.service.SetCustomerNoVatStatus(req.CustomerID, req.IsNoVat); err != nil { + return fc.Status(responseErrors.GetErrorStatus(err)). + JSON(response.Make(nullable.GetNil(""), 0, responseErrors.GetErrorCode(fc, err))) + } + + return fc.JSON(response.Make(nullable.GetNil(""), 0, i18n.T_(fc, response.Message_OK))) +} diff --git a/app/model/customer.go b/app/model/customer.go index cc4b9f1..b15b145 100644 --- a/app/model/customer.go +++ b/app/model/customer.go @@ -35,6 +35,7 @@ type Customer struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + IsNoVat bool `gorm:"default:false" json:"is_no_vat"` } func (u *Customer) HasPermission(permission perms.Permission) bool { diff --git a/app/repos/customerRepo/customerRepo.go b/app/repos/customerRepo/customerRepo.go index 8e55063..8cc11a4 100644 --- a/app/repos/customerRepo/customerRepo.go +++ b/app/repos/customerRepo/customerRepo.go @@ -17,6 +17,7 @@ type UICustomerRepo interface { Find(langId uint, p find.Paging, filt *filters.FiltersList, search string) (*find.Found[model.UserInList], error) Save(customer *model.Customer) error Create(customer *model.Customer) error + SetCustomerNoVatStatus(customerID uint, isNoVat bool) error } type CustomerRepo struct{} @@ -114,3 +115,7 @@ func (repo *CustomerRepo) Save(customer *model.Customer) error { func (repo *CustomerRepo) Create(customer *model.Customer) error { return db.DB.Create(customer).Error } + +func (repo *CustomerRepo) SetCustomerNoVatStatus(customerID uint, isNoVat bool) error { + return db.DB.Model(&model.Customer{}).Where("id = ?", customerID).Update("is_no_vat", isNoVat).Error +} diff --git a/app/service/customerService/customerService.go b/app/service/customerService/customerService.go index bce463d..be8cc9c 100644 --- a/app/service/customerService/customerService.go +++ b/app/service/customerService/customerService.go @@ -24,3 +24,7 @@ func (s *CustomerService) GetById(id uint) (*model.Customer, error) { func (s *CustomerService) Find(langId uint, p find.Paging, filt *filters.FiltersList, search string) (*find.Found[model.UserInList], error) { return s.repo.Find(langId, p, filt, search) } + +func (s *CustomerService) SetCustomerNoVatStatus(customerID uint, isNoVat bool) error { + return s.repo.SetCustomerNoVatStatus(customerID, isNoVat) +} diff --git a/bruno/api_v1/customer/Set is_no_vat.yml b/bruno/api_v1/customer/Set is_no_vat.yml new file mode 100644 index 0000000..eaa70ff --- /dev/null +++ b/bruno/api_v1/customer/Set is_no_vat.yml @@ -0,0 +1,22 @@ +info: + name: Set is_no_vat + type: http + seq: 4 + +http: + method: PATCH + url: "{{bas_url}}/restricted/customer/no-vat" + body: + type: json + data: |- + { + "customer_id":1, + "is_no_vat": false + } + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/i18n/migrations/20260302163122_create_tables.sql b/i18n/migrations/20260302163122_create_tables.sql index e25205d..6226f32 100644 --- a/i18n/migrations/20260302163122_create_tables.sql +++ b/i18n/migrations/20260302163122_create_tables.sql @@ -112,7 +112,8 @@ CREATE TABLE IF NOT EXISTS b2b_customers ( country_id INT NULL DEFAULT 2, created_at DATETIME(6) NULL, updated_at DATETIME(6) NULL, - deleted_at DATETIME(6) NULL + deleted_at DATETIME(6) NULL, + is_no_vat TINYINT(1) NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_email diff --git a/i18n/migrations/20260319163200_procedures.sql b/i18n/migrations/20260319163200_procedures.sql index 267985a..b823a3a 100644 --- a/i18n/migrations/20260319163200_procedures.sql +++ b/i18n/migrations/20260319163200_procedures.sql @@ -16,6 +16,7 @@ READS SQL DATA BEGIN DECLARE v_tax_rate DECIMAL(10,4) DEFAULT 0; + DECLARE v_tax_group INT; DECLARE v_base_raw DECIMAL(20,6); DECLARE v_base DECIMAL(20,6); @@ -29,45 +30,54 @@ BEGIN 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; + DECLARE v_is_no_vat TINYINT DEFAULT 0; + SET p_id_product_attribute = NULLIF(p_id_product_attribute, 0); + -- ================= CUSTOMER VAT ================= + SELECT COALESCE(c.is_no_vat, 0) + INTO v_is_no_vat + FROM b2b_customers c + WHERE c.id = p_id_customer + LIMIT 1; + + -- ================= TAX GROUP ================= + SELECT ps.id_tax_rules_group + INTO v_tax_group + FROM ps_product_shop ps + WHERE ps.id_product = p_id_product + AND ps.id_shop = p_id_shop + LIMIT 1; + -- ================= 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 - LIMIT 1 - ) - AND tr.id_country = c.ps_id_country + WHERE tr.id_tax_rules_group = v_tax_group + AND tr.id_country = c.ps_id_country LIMIT 1; - -- ================= TARGET CURRENCY ================= - SELECT c.b2b_id_currency - INTO v_target_currency + IF v_is_no_vat = 1 THEN + SET v_tax_rate = 0; + END IF; + + -- ================= CURRENCY ================= + SELECT c.b2b_id_currency, r.conversion_rate + INTO v_target_currency, v_target_rate FROM b2b_countries c + LEFT JOIN b2b_currency_rates r + ON r.b2b_id_currency = c.b2b_id_currency WHERE c.id = p_id_country - LIMIT 1; - - -- latest target rate - SELECT r.conversion_rate - INTO v_target_rate - FROM b2b_currency_rates r - WHERE r.b2b_id_currency = v_target_currency ORDER BY r.created_at DESC LIMIT 1; - -- ================= BASE PRICE (RAW) ================= + -- ================= BASE PRICE ================= SELECT COALESCE(ps.price, p.price) + COALESCE(pas.price, 0) INTO v_base_raw @@ -79,8 +89,8 @@ BEGIN AND pas.id_shop = p_id_shop WHERE p.id_product = p_id_product; - -- convert base to target currency SET v_base = v_base_raw * v_target_rate; + SET v_excl = v_base; -- ================= RULE SELECTION ================= SELECT @@ -99,71 +109,67 @@ BEGIN FROM b2b_specific_price bsp + LEFT JOIN b2b_specific_price_product spp + ON spp.b2b_specific_price_id = bsp.id + AND spp.id_product = p_id_product + + LEFT JOIN b2b_specific_price_product_attribute spa + ON spa.b2b_specific_price_id = bsp.id + AND spa.id_product_attribute = p_id_product_attribute + + LEFT JOIN b2b_specific_price_customer spc + ON spc.b2b_specific_price_id = bsp.id + AND spc.b2b_id_customer = p_id_customer + + LEFT JOIN b2b_specific_price_country spco + ON spco.b2b_specific_price_id = bsp.id + AND spco.b2b_id_country = p_id_country + + LEFT JOIN b2b_specific_price_category spcat + ON spcat.b2b_specific_price_id = bsp.id + + LEFT JOIN ps_category_product cp + ON cp.id_category = spcat.id_category + AND cp.id_product = p_id_product + WHERE bsp.is_active = 1 AND bsp.from_quantity <= p_quantity - -- intersection rules (unchanged) - AND ( - NOT EXISTS (SELECT 1 FROM b2b_specific_price_product x WHERE x.b2b_specific_price_id = bsp.id) - 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 (spp.id_product IS NOT NULL OR NOT EXISTS ( + SELECT 1 FROM b2b_specific_price_product x WHERE x.b2b_specific_price_id = bsp.id + )) - 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 (spa.id_product_attribute IS NOT NULL OR NOT EXISTS ( + SELECT 1 FROM b2b_specific_price_product_attribute x WHERE x.b2b_specific_price_id = bsp.id + )) - 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 - ) - ) + AND (spc.b2b_id_customer IS NOT NULL OR NOT EXISTS ( + SELECT 1 FROM b2b_specific_price_customer x WHERE x.b2b_specific_price_id = bsp.id + )) - AND ( - NOT EXISTS (SELECT 1 FROM b2b_specific_price_customer x WHERE x.b2b_specific_price_id = bsp.id) - 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) - ) + AND (spco.b2b_id_country IS NOT NULL OR NOT EXISTS ( + SELECT 1 FROM b2b_specific_price_country x WHERE x.b2b_specific_price_id = bsp.id + )) - AND ( - NOT EXISTS (SELECT 1 FROM b2b_specific_price_country x WHERE x.b2b_specific_price_id = bsp.id) - 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) - ) + AND (cp.id_product IS NOT NULL OR NOT EXISTS ( + SELECT 1 FROM b2b_specific_price_category x WHERE x.b2b_specific_price_id = bsp.id + )) ORDER BY - -- customer wins - (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, - - -- 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, - + (spc.b2b_id_customer IS NOT NULL) DESC, + (spa.id_product_attribute IS NOT NULL) DESC, + (spp.id_product IS NOT NULL) DESC, + (cp.id_product IS NOT NULL) DESC, + (spco.b2b_id_country IS NOT NULL) DESC, bsp.id DESC LIMIT 1; -- ================= APPLY ================= - SET v_excl = v_base; - IF v_has_specific = 1 THEN IF v_reduction_type = 'amount' THEN - -- convert specific price currency if needed IF v_specific_currency_id IS NOT NULL AND v_specific_currency_id != v_target_currency THEN SELECT r.conversion_rate @@ -173,7 +179,6 @@ BEGIN ORDER BY r.created_at DESC LIMIT 1; - -- normalize → then convert to target SET v_excl = (v_fixed_price / v_specific_rate) * v_target_rate; ELSE