10 Commits

7 changed files with 134 additions and 70 deletions

View File

@@ -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)))
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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