no-vat-customers #71

Merged
dudzic_wiktor merged 8 commits from no-vat-customers into main 2026-04-15 11:32:14 +00:00
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("", handler.customerData)
r.Get("/list", middleware.Require(perms.UserReadAny), handler.listCustomers) r.Get("/list", middleware.Require(perms.UserReadAny), handler.listCustomers)
r.Patch("/no-vat", middleware.Require(perms.UserWriteAny), handler.setCustomerNoVatStatus)
return r return r
} }
@@ -100,3 +101,28 @@ var columnMappingListUsers map[string]string = map[string]string{
"first_name": "users.first_name", "first_name": "users.first_name",
"last_name": "users.last_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"` 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:"-"`
IsNoVat bool `gorm:"default:false" json:"is_no_vat"`
} }
func (u *Customer) HasPermission(permission perms.Permission) bool { 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) Find(langId uint, p find.Paging, filt *filters.FiltersList, search string) (*find.Found[model.UserInList], error)
Save(customer *model.Customer) error Save(customer *model.Customer) error
Create(customer *model.Customer) error Create(customer *model.Customer) error
SetCustomerNoVatStatus(customerID uint, isNoVat bool) error
} }
type CustomerRepo struct{} type CustomerRepo struct{}
@@ -114,3 +115,7 @@ func (repo *CustomerRepo) Save(customer *model.Customer) error {
func (repo *CustomerRepo) Create(customer *model.Customer) error { func (repo *CustomerRepo) Create(customer *model.Customer) error {
return db.DB.Create(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) { 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) 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, country_id INT NULL DEFAULT 2,
created_at DATETIME(6) NULL, created_at DATETIME(6) NULL,
updated_at DATETIME(6) NULL, updated_at DATETIME(6) NULL,
deleted_at DATETIME(6) NULL deleted_at DATETIME(6) NULL,
is_no_vat TINYINT(1) NULL DEFAULT 0
Review

if it has default value, then perhaps make it NOT NULL?

if it has default value, then perhaps make it NOT NULL?
Review

that's true, it makes more sense to set it to NOT NULL. the PR already auto-merged so I'll make the change in next unrelated PR

that's true, it makes more sense to set it to NOT NULL. the PR already auto-merged so I'll make the change in next unrelated PR
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_email CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_email

View File

@@ -16,6 +16,7 @@ READS SQL DATA
BEGIN BEGIN
DECLARE v_tax_rate DECIMAL(10,4) DEFAULT 0; DECLARE v_tax_rate DECIMAL(10,4) DEFAULT 0;
DECLARE v_tax_group INT;
DECLARE v_base_raw DECIMAL(20,6); DECLARE v_base_raw DECIMAL(20,6);
DECLARE v_base DECIMAL(20,6); DECLARE v_base DECIMAL(20,6);
@@ -29,45 +30,54 @@ BEGIN
DECLARE v_has_specific INT DEFAULT 0; DECLARE v_has_specific INT DEFAULT 0;
-- currency
DECLARE v_target_currency BIGINT; DECLARE v_target_currency BIGINT;
DECLARE v_target_rate DECIMAL(13,6) DEFAULT 1; DECLARE v_target_rate DECIMAL(13,6) DEFAULT 1;
DECLARE v_specific_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); 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 ================= -- ================= TAX =================
SELECT COALESCE(t.rate, 0) SELECT COALESCE(t.rate, 0)
INTO v_tax_rate INTO v_tax_rate
FROM ps_tax_rule tr FROM ps_tax_rule tr
JOIN ps_tax t ON t.id_tax = tr.id_tax JOIN ps_tax t ON t.id_tax = tr.id_tax
LEFT JOIN b2b_countries c ON c.id = p_id_country LEFT JOIN b2b_countries c ON c.id = p_id_country
WHERE tr.id_tax_rules_group = ( WHERE tr.id_tax_rules_group = v_tax_group
SELECT ps.id_tax_rules_group AND tr.id_country = c.ps_id_country
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
LIMIT 1; LIMIT 1;
-- ================= TARGET CURRENCY ================= IF v_is_no_vat = 1 THEN
SELECT c.b2b_id_currency SET v_tax_rate = 0;
INTO v_target_currency END IF;
-- ================= CURRENCY =================
SELECT c.b2b_id_currency, r.conversion_rate
INTO v_target_currency, v_target_rate
FROM b2b_countries c 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 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 ORDER BY r.created_at DESC
LIMIT 1; LIMIT 1;
-- ================= BASE PRICE (RAW) ================= -- ================= BASE PRICE =================
SELECT SELECT
COALESCE(ps.price, p.price) + COALESCE(pas.price, 0) COALESCE(ps.price, p.price) + COALESCE(pas.price, 0)
INTO v_base_raw INTO v_base_raw
@@ -79,8 +89,8 @@ BEGIN
AND pas.id_shop = p_id_shop AND pas.id_shop = p_id_shop
WHERE p.id_product = p_id_product; WHERE p.id_product = p_id_product;
-- convert base to target currency
SET v_base = v_base_raw * v_target_rate; SET v_base = v_base_raw * v_target_rate;
SET v_excl = v_base;
-- ================= RULE SELECTION ================= -- ================= RULE SELECTION =================
SELECT SELECT
@@ -99,71 +109,67 @@ BEGIN
FROM b2b_specific_price bsp 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 WHERE bsp.is_active = 1
AND bsp.from_quantity <= p_quantity AND bsp.from_quantity <= p_quantity
-- intersection rules (unchanged) AND (spp.id_product IS NOT NULL OR NOT EXISTS (
AND ( SELECT 1 FROM b2b_specific_price_product x WHERE x.b2b_specific_price_id = bsp.id
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 ( AND (spa.id_product_attribute IS NOT NULL OR NOT EXISTS (
NOT EXISTS (SELECT 1 FROM b2b_specific_price_product_attribute x WHERE x.b2b_specific_price_id = bsp.id) 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 ( AND (spc.b2b_id_customer IS NOT NULL OR NOT EXISTS (
NOT EXISTS (SELECT 1 FROM b2b_specific_price_category x WHERE x.b2b_specific_price_id = bsp.id) SELECT 1 FROM b2b_specific_price_customer 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 ( AND (spco.b2b_id_country IS NOT NULL OR NOT EXISTS (
NOT EXISTS (SELECT 1 FROM b2b_specific_price_customer x WHERE x.b2b_specific_price_id = bsp.id) SELECT 1 FROM b2b_specific_price_country 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 ( AND (cp.id_product IS NOT NULL OR NOT EXISTS (
NOT EXISTS (SELECT 1 FROM b2b_specific_price_country x WHERE x.b2b_specific_price_id = bsp.id) SELECT 1 FROM b2b_specific_price_category 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) ))
)
ORDER BY ORDER BY
-- customer wins (spc.b2b_id_customer IS NOT NULL) DESC,
(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, (spa.id_product_attribute IS NOT NULL) DESC,
(spp.id_product IS NOT NULL) DESC,
-- attribute (cp.id_product IS NOT NULL) DESC,
(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, (spco.b2b_id_country IS NOT NULL) 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.id DESC bsp.id DESC
LIMIT 1; LIMIT 1;
-- ================= APPLY ================= -- ================= APPLY =================
SET v_excl = v_base;
IF v_has_specific = 1 THEN IF v_has_specific = 1 THEN
IF v_reduction_type = 'amount' 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 IF v_specific_currency_id IS NOT NULL AND v_specific_currency_id != v_target_currency THEN
SELECT r.conversion_rate SELECT r.conversion_rate
@@ -173,7 +179,6 @@ BEGIN
ORDER BY r.created_at DESC ORDER BY r.created_at DESC
LIMIT 1; LIMIT 1;
-- normalize → then convert to target
SET v_excl = (v_fixed_price / v_specific_rate) * v_target_rate; SET v_excl = (v_fixed_price / v_specific_rate) * v_target_rate;
ELSE ELSE