Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-06-17 21:56:49 +03:00
parent e1e9d4baf1
commit e14c1c176a
34 changed files with 7402 additions and 704 deletions

View File

@@ -3,6 +3,7 @@ package queries
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"strconv"
"strings"
@@ -15,6 +16,22 @@ import (
// - mk_pricex: per-currency multipliers (base + 1..6).
// - mk_priceroll: per-currency rounding steps for wholesale (1-5) and retail (6+).
func normalizeRetailMode(v string) string {
v = strings.ToUpper(strings.TrimSpace(v))
switch v {
case "", "STEP":
return "STEP"
case "END_99", "END_49", "BAND_99", "BAND_49":
return v
default:
return "STEP"
}
}
func NormalizeRetailModeForRoute(v string) string {
return normalizeRetailMode(v)
}
func EnsurePricingRuleTables(pg *sql.DB) error {
stmts := []string{
`
@@ -32,10 +49,26 @@ CREATE TABLE IF NOT EXISTS mk_pricing_rule (
brand_code TEXT[] NOT NULL DEFAULT '{}'::text[],
brand_group TEXT[] NOT NULL DEFAULT '{}'::text[],
strategy_code TEXT NOT NULL DEFAULT 'CORE',
anchor_mode TEXT NOT NULL DEFAULT 'USD',
calc_enabled BOOLEAN NOT NULL DEFAULT TRUE,
publish_postgres BOOLEAN NOT NULL DEFAULT TRUE,
publish_nebim BOOLEAN NOT NULL DEFAULT TRUE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
)`,
`ALTER TABLE mk_pricing_rule ADD COLUMN IF NOT EXISTS strategy_code TEXT NOT NULL DEFAULT 'CORE'`,
`ALTER TABLE mk_pricing_rule ADD COLUMN IF NOT EXISTS anchor_mode TEXT NOT NULL DEFAULT 'USD'`,
`ALTER TABLE mk_pricing_rule ADD COLUMN IF NOT EXISTS calc_enabled BOOLEAN NOT NULL DEFAULT TRUE`,
`ALTER TABLE mk_pricing_rule ADD COLUMN IF NOT EXISTS publish_postgres BOOLEAN NOT NULL DEFAULT TRUE`,
`ALTER TABLE mk_pricing_rule ADD COLUMN IF NOT EXISTS publish_nebim BOOLEAN NOT NULL DEFAULT TRUE`,
`UPDATE mk_pricing_rule SET strategy_code='CORE' WHERE COALESCE(NULLIF(BTRIM(strategy_code), ''), '') = ''`,
`UPDATE mk_pricing_rule SET anchor_mode='USD' WHERE COALESCE(NULLIF(BTRIM(anchor_mode), ''), '') = ''`,
`ALTER TABLE mk_pricing_rule DROP CONSTRAINT IF EXISTS ck_mk_pricing_rule_strategy_code`,
`ALTER TABLE mk_pricing_rule ADD CONSTRAINT ck_mk_pricing_rule_strategy_code CHECK (strategy_code IN ('CORE','PREMIUM','SARTORIAL'))`,
`ALTER TABLE mk_pricing_rule DROP CONSTRAINT IF EXISTS ck_mk_pricing_rule_anchor_mode`,
`ALTER TABLE mk_pricing_rule ADD CONSTRAINT ck_mk_pricing_rule_anchor_mode CHECK (anchor_mode IN ('TRY','USD'))`,
`CREATE INDEX IF NOT EXISTS ix_mk_pricing_rule_active ON mk_pricing_rule (is_active)`,
`
CREATE TABLE IF NOT EXISTS mk_pricex (
@@ -60,13 +93,16 @@ CREATE TABLE IF NOT EXISTS mk_priceroll (
step NUMERIC(18,6) NOT NULL DEFAULT 0,
wholesale_step NUMERIC(18,6) NOT NULL DEFAULT 0,
retail_step NUMERIC(18,6) NOT NULL DEFAULT 0,
retail_mode TEXT NOT NULL DEFAULT 'STEP',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (rule_id, currency)
)`,
`ALTER TABLE mk_priceroll ADD COLUMN IF NOT EXISTS wholesale_step NUMERIC(18,6) NOT NULL DEFAULT 0`,
`ALTER TABLE mk_priceroll ADD COLUMN IF NOT EXISTS retail_step NUMERIC(18,6) NOT NULL DEFAULT 0`,
`ALTER TABLE mk_priceroll ADD COLUMN IF NOT EXISTS retail_mode TEXT NOT NULL DEFAULT 'STEP'`,
`UPDATE mk_priceroll SET wholesale_step = step, retail_step = step WHERE step <> 0 AND wholesale_step = 0 AND retail_step = 0`,
`UPDATE mk_priceroll SET retail_mode='STEP' WHERE COALESCE(NULLIF(BTRIM(retail_mode), ''), '') = ''`,
`CREATE INDEX IF NOT EXISTS ix_mk_priceroll_currency ON mk_priceroll (currency)`,
}
for _, s := range stmts {
@@ -92,7 +128,12 @@ type PricingRuleRow struct {
BrandCode []string `json:"brand_code"`
BrandGroupSec []string `json:"brand_group"`
IsActive bool `json:"is_active"`
StrategyCode string `json:"strategy_code"`
AnchorMode string `json:"anchor_mode"`
CalcEnabled bool `json:"calc_enabled"`
PublishPostgres bool `json:"publish_postgres"`
PublishNebim bool `json:"publish_nebim"`
IsActive bool `json:"is_active"`
// multipliers/rolls are per currency
TryBase float64 `json:"try_base"`
@@ -104,6 +145,7 @@ type PricingRuleRow struct {
Try6 float64 `json:"try6"`
TryWholesaleStep float64 `json:"try_wholesale_step"`
TryRetailStep float64 `json:"try_retail_step"`
TryRetailMode string `json:"try_retail_mode"`
UsdBase float64 `json:"usd_base"`
Usd1 float64 `json:"usd1"`
@@ -114,6 +156,7 @@ type PricingRuleRow struct {
Usd6 float64 `json:"usd6"`
UsdWholesaleStep float64 `json:"usd_wholesale_step"`
UsdRetailStep float64 `json:"usd_retail_step"`
UsdRetailMode string `json:"usd_retail_mode"`
EurBase float64 `json:"eur_base"`
Eur1 float64 `json:"eur1"`
@@ -124,6 +167,7 @@ type PricingRuleRow struct {
Eur6 float64 `json:"eur6"`
EurWholesaleStep float64 `json:"eur_wholesale_step"`
EurRetailStep float64 `json:"eur_retail_step"`
EurRetailMode string `json:"eur_retail_mode"`
}
type PricingRuleSaveItem struct {
@@ -141,7 +185,12 @@ type PricingRuleSaveItem struct {
BrandCode []string `json:"brand_code"`
BrandGroupSec []string `json:"brand_group"`
IsActive bool `json:"is_active"`
StrategyCode string `json:"strategy_code"`
AnchorMode string `json:"anchor_mode"`
CalcEnabled bool `json:"calc_enabled"`
PublishPostgres bool `json:"publish_postgres"`
PublishNebim bool `json:"publish_nebim"`
IsActive bool `json:"is_active"`
TryBase float64 `json:"try_base"`
Try1 float64 `json:"try1"`
@@ -152,6 +201,7 @@ type PricingRuleSaveItem struct {
Try6 float64 `json:"try6"`
TryWholesaleStep float64 `json:"try_wholesale_step"`
TryRetailStep float64 `json:"try_retail_step"`
TryRetailMode string `json:"try_retail_mode"`
UsdBase float64 `json:"usd_base"`
Usd1 float64 `json:"usd1"`
@@ -162,6 +212,7 @@ type PricingRuleSaveItem struct {
Usd6 float64 `json:"usd6"`
UsdWholesaleStep float64 `json:"usd_wholesale_step"`
UsdRetailStep float64 `json:"usd_retail_step"`
UsdRetailMode string `json:"usd_retail_mode"`
EurBase float64 `json:"eur_base"`
Eur1 float64 `json:"eur1"`
@@ -172,6 +223,174 @@ type PricingRuleSaveItem struct {
Eur6 float64 `json:"eur6"`
EurWholesaleStep float64 `json:"eur_wholesale_step"`
EurRetailStep float64 `json:"eur_retail_step"`
EurRetailMode string `json:"eur_retail_mode"`
}
// BulkSavePricingRulesFast persists multipliers + rounding steps in a set-based way.
// This is intentionally "dumb": it updates/creates a mk_pricing_rule row (latest by pricing_parameter_id)
// and upserts mk_pricex/mk_priceroll for TRY/USD/EUR.
func BulkSavePricingRulesFast(ctx context.Context, tx *sql.Tx, items []PricingRuleSaveItem) (int, error) {
if len(items) == 0 {
return 0, nil
}
raw, err := json.Marshal(items)
if err != nil {
return 0, err
}
// Notes:
// - rule_id resolution:
// 1) explicit id (if provided)
// 2) latest rule for pricing_parameter_id (if provided)
// 3) otherwise new UUID
// - mk_pricing_rule has no unique constraint on pricing_parameter_id by design, so we target "latest" row.
// - created_at uses default; updated_at is bumped on every save.
q := `
WITH input AS (
SELECT *
FROM jsonb_to_recordset($1::jsonb) AS x(
id text,
pricing_parameter_id bigint,
calc_enabled boolean,
publish_postgres boolean,
publish_nebim boolean,
is_active boolean,
try_retail_mode text,
usd_retail_mode text,
eur_retail_mode text,
try_base float8, try1 float8, try2 float8, try3 float8, try4 float8, try5 float8, try6 float8,
try_wholesale_step float8, try_retail_step float8,
usd_base float8, usd1 float8, usd2 float8, usd3 float8, usd4 float8, usd5 float8, usd6 float8,
usd_wholesale_step float8, usd_retail_step float8,
eur_base float8, eur1 float8, eur2 float8, eur3 float8, eur4 float8, eur5 float8, eur6 float8,
eur_wholesale_step float8, eur_retail_step float8
)
),
norm AS (
SELECT
NULLIF(BTRIM(id), '') AS id_txt,
COALESCE(pricing_parameter_id, 0) AS pricing_parameter_id,
COALESCE(calc_enabled, TRUE) AS calc_enabled,
COALESCE(publish_postgres, TRUE) AS publish_postgres,
COALESCE(publish_nebim, TRUE) AS publish_nebim,
COALESCE(is_active, TRUE) AS is_active,
COALESCE(NULLIF(UPPER(BTRIM(try_retail_mode)), ''), 'STEP') AS try_retail_mode,
COALESCE(NULLIF(UPPER(BTRIM(usd_retail_mode)), ''), 'STEP') AS usd_retail_mode,
COALESCE(NULLIF(UPPER(BTRIM(eur_retail_mode)), ''), 'STEP') AS eur_retail_mode,
COALESCE(try_base, 0) AS try_base, COALESCE(try1, 0) AS try1, COALESCE(try2, 0) AS try2, COALESCE(try3, 0) AS try3, COALESCE(try4, 0) AS try4, COALESCE(try5, 0) AS try5, COALESCE(try6, 0) AS try6,
COALESCE(try_wholesale_step, 0) AS try_wholesale_step, COALESCE(try_retail_step, 0) AS try_retail_step,
COALESCE(usd_base, 0) AS usd_base, COALESCE(usd1, 0) AS usd1, COALESCE(usd2, 0) AS usd2, COALESCE(usd3, 0) AS usd3, COALESCE(usd4, 0) AS usd4, COALESCE(usd5, 0) AS usd5, COALESCE(usd6, 0) AS usd6,
COALESCE(usd_wholesale_step, 0) AS usd_wholesale_step, COALESCE(usd_retail_step, 0) AS usd_retail_step,
COALESCE(eur_base, 0) AS eur_base, COALESCE(eur1, 0) AS eur1, COALESCE(eur2, 0) AS eur2, COALESCE(eur3, 0) AS eur3, COALESCE(eur4, 0) AS eur4, COALESCE(eur5, 0) AS eur5, COALESCE(eur6, 0) AS eur6,
COALESCE(eur_wholesale_step, 0) AS eur_wholesale_step, COALESCE(eur_retail_step, 0) AS eur_retail_step
FROM input
),
resolved AS (
SELECT
COALESCE(
NULLIF(id_txt, '')::uuid,
latest.id,
gen_random_uuid()
) AS rule_id,
pricing_parameter_id,
calc_enabled,
publish_postgres,
publish_nebim,
is_active,
try_retail_mode,
usd_retail_mode,
eur_retail_mode,
try_base, try1, try2, try3, try4, try5, try6,
try_wholesale_step, try_retail_step,
usd_base, usd1, usd2, usd3, usd4, usd5, usd6,
usd_wholesale_step, usd_retail_step,
eur_base, eur1, eur2, eur3, eur4, eur5, eur6,
eur_wholesale_step, eur_retail_step
FROM norm n
LEFT JOIN LATERAL (
SELECT r.id
FROM mk_pricing_rule r
WHERE r.pricing_parameter_id = n.pricing_parameter_id
ORDER BY r.created_at DESC, r.updated_at DESC, r.id DESC
LIMIT 1
) latest ON (n.id_txt IS NULL AND n.pricing_parameter_id > 0)
),
upsert_rule AS (
INSERT INTO mk_pricing_rule (
id,
pricing_parameter_id,
calc_enabled,
publish_postgres,
publish_nebim,
is_active,
updated_at
)
SELECT
rule_id,
NULLIF(pricing_parameter_id, 0),
calc_enabled,
publish_postgres,
publish_nebim,
is_active,
now()
FROM resolved
ON CONFLICT (id) DO UPDATE SET
pricing_parameter_id = EXCLUDED.pricing_parameter_id,
calc_enabled = EXCLUDED.calc_enabled,
publish_postgres = EXCLUDED.publish_postgres,
publish_nebim = EXCLUDED.publish_nebim,
is_active = EXCLUDED.is_active,
updated_at = now()
RETURNING id
),
upsert_pricex AS (
INSERT INTO mk_pricex (rule_id, currency, base_mult, m1, m2, m3, m4, m5, m6, updated_at)
SELECT rule_id, 'TRY', try_base, try1, try2, try3, try4, try5, try6, now() FROM resolved
UNION ALL
SELECT rule_id, 'USD', usd_base, usd1, usd2, usd3, usd4, usd5, usd6, now() FROM resolved
UNION ALL
SELECT rule_id, 'EUR', eur_base, eur1, eur2, eur3, eur4, eur5, eur6, now() FROM resolved
ON CONFLICT (rule_id, currency) DO UPDATE SET
base_mult = EXCLUDED.base_mult,
m1 = EXCLUDED.m1,
m2 = EXCLUDED.m2,
m3 = EXCLUDED.m3,
m4 = EXCLUDED.m4,
m5 = EXCLUDED.m5,
m6 = EXCLUDED.m6,
updated_at = now()
RETURNING 1
),
upsert_priceroll AS (
INSERT INTO mk_priceroll (rule_id, currency, wholesale_step, retail_step, retail_mode, updated_at)
SELECT rule_id, 'TRY', try_wholesale_step, try_retail_step, try_retail_mode, now() FROM resolved
UNION ALL
SELECT rule_id, 'USD', usd_wholesale_step, usd_retail_step, usd_retail_mode, now() FROM resolved
UNION ALL
SELECT rule_id, 'EUR', eur_wholesale_step, eur_retail_step, eur_retail_mode, now() FROM resolved
ON CONFLICT (rule_id, currency) DO UPDATE SET
wholesale_step = EXCLUDED.wholesale_step,
retail_step = EXCLUDED.retail_step,
retail_mode = EXCLUDED.retail_mode,
updated_at = now()
RETURNING 1
)
SELECT COUNT(*)::int FROM resolved;
`
var updated int
if err := tx.QueryRowContext(ctx, q, raw).Scan(&updated); err != nil {
return 0, err
}
return updated, nil
}
func ListPricingRules(ctx context.Context, pg *sql.DB) ([]PricingRuleRow, error) {
@@ -190,6 +409,11 @@ SELECT
r.marka,
r.brand_code,
r.brand_group,
r.strategy_code,
r.anchor_mode,
r.calc_enabled,
r.publish_postgres,
r.publish_nebim,
r.is_active,
COALESCE(tx.base_mult, 0)::float8 AS try_base,
@@ -201,6 +425,7 @@ SELECT
COALESCE(tx.m6, 0)::float8 AS try6,
COALESCE(NULLIF(tr.wholesale_step, 0), tr.step, 0)::float8 AS try_wholesale_step,
COALESCE(NULLIF(tr.retail_step, 0), tr.step, 0)::float8 AS try_retail_step,
COALESCE(NULLIF(BTRIM(tr.retail_mode), ''), 'STEP') AS try_retail_mode,
COALESCE(ux.base_mult, 0)::float8 AS usd_base,
COALESCE(ux.m1, 0)::float8 AS usd1,
@@ -211,6 +436,7 @@ SELECT
COALESCE(ux.m6, 0)::float8 AS usd6,
COALESCE(NULLIF(ur.wholesale_step, 0), ur.step, 0)::float8 AS usd_wholesale_step,
COALESCE(NULLIF(ur.retail_step, 0), ur.step, 0)::float8 AS usd_retail_step,
COALESCE(NULLIF(BTRIM(ur.retail_mode), ''), 'STEP') AS usd_retail_mode,
COALESCE(ex.base_mult, 0)::float8 AS eur_base,
COALESCE(ex.m1, 0)::float8 AS eur1,
@@ -220,7 +446,8 @@ SELECT
COALESCE(ex.m5, 0)::float8 AS eur5,
COALESCE(ex.m6, 0)::float8 AS eur6,
COALESCE(NULLIF(er.wholesale_step, 0), er.step, 0)::float8 AS eur_wholesale_step,
COALESCE(NULLIF(er.retail_step, 0), er.step, 0)::float8 AS eur_retail_step
COALESCE(NULLIF(er.retail_step, 0), er.step, 0)::float8 AS eur_retail_step,
COALESCE(NULLIF(BTRIM(er.retail_mode), ''), 'STEP') AS eur_retail_mode
FROM mk_pricing_rule r
LEFT JOIN mk_pricex tx ON tx.rule_id = r.id AND tx.currency='TRY'
LEFT JOIN mk_pricex ux ON ux.rule_id = r.id AND ux.currency='USD'
@@ -252,14 +479,22 @@ ORDER BY r.created_at DESC;
pq.Array(&r.Marka),
pq.Array(&r.BrandCode),
pq.Array(&r.BrandGroupSec),
&r.StrategyCode,
&r.AnchorMode,
&r.CalcEnabled,
&r.PublishPostgres,
&r.PublishNebim,
&r.IsActive,
&r.TryBase, &r.Try1, &r.Try2, &r.Try3, &r.Try4, &r.Try5, &r.Try6, &r.TryWholesaleStep, &r.TryRetailStep,
&r.UsdBase, &r.Usd1, &r.Usd2, &r.Usd3, &r.Usd4, &r.Usd5, &r.Usd6, &r.UsdWholesaleStep, &r.UsdRetailStep,
&r.EurBase, &r.Eur1, &r.Eur2, &r.Eur3, &r.Eur4, &r.Eur5, &r.Eur6, &r.EurWholesaleStep, &r.EurRetailStep,
&r.TryBase, &r.Try1, &r.Try2, &r.Try3, &r.Try4, &r.Try5, &r.Try6, &r.TryWholesaleStep, &r.TryRetailStep, &r.TryRetailMode,
&r.UsdBase, &r.Usd1, &r.Usd2, &r.Usd3, &r.Usd4, &r.Usd5, &r.Usd6, &r.UsdWholesaleStep, &r.UsdRetailStep, &r.UsdRetailMode,
&r.EurBase, &r.Eur1, &r.Eur2, &r.Eur3, &r.Eur4, &r.Eur5, &r.Eur6, &r.EurWholesaleStep, &r.EurRetailStep, &r.EurRetailMode,
); err != nil {
return nil, err
}
r.TryRetailMode = normalizeRetailMode(r.TryRetailMode)
r.UsdRetailMode = normalizeRetailMode(r.UsdRetailMode)
r.EurRetailMode = normalizeRetailMode(r.EurRetailMode)
out = append(out, r)
}
return out, rows.Err()
@@ -282,6 +517,42 @@ func normalizeTextList(in []string) []string {
return out
}
func deriveStrategyCodeFromBrandGroup(values []string) string {
for _, value := range values {
normalized := strings.ToUpper(strings.TrimSpace(value))
switch normalized {
case "CORE", "PREMIUM", "SARTORIAL":
return normalized
}
}
return "CORE"
}
func deriveAnchorModeFromBrandGroup(ctx context.Context, tx *sql.Tx, values []string) string {
for _, value := range values {
normalized := strings.TrimSpace(value)
if normalized == "" {
continue
}
var mode string
err := tx.QueryRowContext(ctx, `
SELECT anchor_mode
FROM mk_brandgrp
WHERE UPPER(BTRIM(code)) = UPPER(BTRIM($1))
OR UPPER(BTRIM(title)) = UPPER(BTRIM($1))
ORDER BY id
LIMIT 1
`, normalized).Scan(&mode)
if err == nil {
mode = strings.ToUpper(strings.TrimSpace(mode))
if mode == "TRY" || mode == "USD" {
return mode
}
}
}
return "USD"
}
// UpsertPricingRule persists rule scope + per-currency multipliers/roundings.
// Parameter-backed worksheet saves append a new rule version so older prices
// remain queryable. Legacy rules without a parameter id keep update behavior.
@@ -306,6 +577,11 @@ func UpsertPricingRule(ctx context.Context, tx *sql.Tx, item PricingRuleSaveItem
item.Marka = normalizeTextList(item.Marka)
item.BrandCode = normalizeTextList(item.BrandCode)
item.BrandGroupSec = normalizeTextList(item.BrandGroupSec)
item.StrategyCode = deriveStrategyCodeFromBrandGroup(item.BrandGroupSec)
item.AnchorMode = deriveAnchorModeFromBrandGroup(ctx, tx, item.BrandGroupSec)
item.TryRetailMode = normalizeRetailMode(item.TryRetailMode)
item.UsdRetailMode = normalizeRetailMode(item.UsdRetailMode)
item.EurRetailMode = normalizeRetailMode(item.EurRetailMode)
id := strings.TrimSpace(item.ID)
if item.PricingParameterID > 0 {
@@ -317,12 +593,15 @@ func UpsertPricingRule(ctx context.Context, tx *sql.Tx, item PricingRuleSaveItem
INSERT INTO mk_pricing_rule (
pricing_parameter_id,
askili_yan,kategori,urun_ilk_grubu,urun_ana_grubu,urun_alt_grubu,
icerik,karisim,marka,brand_code,brand_group,is_active,created_at,updated_at
icerik,karisim,marka,brand_code,brand_group,
strategy_code,anchor_mode,calc_enabled,publish_postgres,publish_nebim,
is_active,created_at,updated_at
)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,now(),now())
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,now(),now())
RETURNING id
`, nullablePricingParameterID(item.PricingParameterID), pq.Array(item.AskiliYan), pq.Array(item.Kategori), pq.Array(item.UrunIlkGrubu), pq.Array(item.UrunAnaGrubu), pq.Array(item.UrunAltGrubu),
pq.Array(item.Icerik), pq.Array(item.Karisim), pq.Array(item.Marka), pq.Array(item.BrandCode), pq.Array(item.BrandGroupSec),
item.StrategyCode, item.AnchorMode, item.CalcEnabled, item.PublishPostgres, item.PublishNebim,
item.IsActive,
).Scan(&id); err != nil {
return "", err
@@ -341,13 +620,19 @@ UPDATE mk_pricing_rule SET
marka=$10,
brand_code=$11,
brand_group=$12,
is_active=$13,
strategy_code=$13,
anchor_mode=$14,
calc_enabled=$15,
publish_postgres=$16,
publish_nebim=$17,
is_active=$18,
updated_at=now()
WHERE id=$1
`, id,
nullablePricingParameterID(item.PricingParameterID),
pq.Array(item.AskiliYan), pq.Array(item.Kategori), pq.Array(item.UrunIlkGrubu), pq.Array(item.UrunAnaGrubu), pq.Array(item.UrunAltGrubu),
pq.Array(item.Icerik), pq.Array(item.Karisim), pq.Array(item.Marka), pq.Array(item.BrandCode), pq.Array(item.BrandGroupSec),
item.StrategyCode, item.AnchorMode, item.CalcEnabled, item.PublishPostgres, item.PublishNebim,
item.IsActive,
); err != nil {
return "", err
@@ -371,41 +656,176 @@ ON CONFLICT (rule_id, currency) DO UPDATE SET
`, id, cur, base, m1, m2, m3, m4, m5, m6)
return err
}
upsertRoll := func(cur string, wholesaleStep, retailStep float64) error {
upsertRoll := func(cur string, wholesaleStep, retailStep float64, retailMode string) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO mk_priceroll (rule_id, currency, step, wholesale_step, retail_step, created_at, updated_at)
VALUES ($1,$2,$3,$4,$5,now(),now())
INSERT INTO mk_priceroll (rule_id, currency, step, wholesale_step, retail_step, retail_mode, created_at, updated_at)
VALUES ($1,$2,$3,$4,$5,$6,now(),now())
ON CONFLICT (rule_id, currency) DO UPDATE SET
step=EXCLUDED.step,
wholesale_step=EXCLUDED.wholesale_step,
retail_step=EXCLUDED.retail_step,
retail_mode=EXCLUDED.retail_mode,
updated_at=now()
`, id, cur, wholesaleStep, wholesaleStep, retailStep)
`, id, cur, wholesaleStep, wholesaleStep, retailStep, retailMode)
return err
}
if err := upsertX("TRY", item.TryBase, item.Try1, item.Try2, item.Try3, item.Try4, item.Try5, item.Try6); err != nil {
return "", err
}
if err := upsertRoll("TRY", item.TryWholesaleStep, item.TryRetailStep); err != nil {
if err := upsertRoll("TRY", item.TryWholesaleStep, item.TryRetailStep, item.TryRetailMode); err != nil {
return "", err
}
if err := upsertX("USD", item.UsdBase, item.Usd1, item.Usd2, item.Usd3, item.Usd4, item.Usd5, item.Usd6); err != nil {
return "", err
}
if err := upsertRoll("USD", item.UsdWholesaleStep, item.UsdRetailStep); err != nil {
if err := upsertRoll("USD", item.UsdWholesaleStep, item.UsdRetailStep, item.UsdRetailMode); err != nil {
return "", err
}
if err := upsertX("EUR", item.EurBase, item.Eur1, item.Eur2, item.Eur3, item.Eur4, item.Eur5, item.Eur6); err != nil {
return "", err
}
if err := upsertRoll("EUR", item.EurWholesaleStep, item.EurRetailStep); err != nil {
if err := upsertRoll("EUR", item.EurWholesaleStep, item.EurRetailStep, item.EurRetailMode); err != nil {
return "", err
}
return id, nil
}
// UpdatePricingRuleByIDFast updates an existing rule without parameter versioning/scope fill.
// This is the fast path for worksheet saves where rule_id is already known.
func UpdatePricingRuleByIDFast(ctx context.Context, tx *sql.Tx, item PricingRuleSaveItem) error {
if tx == nil {
return fmt.Errorf("nil tx")
}
ruleID := strings.TrimSpace(item.ID)
if ruleID == "" {
return fmt.Errorf("missing rule id")
}
item.TryRetailMode = normalizeRetailMode(item.TryRetailMode)
item.UsdRetailMode = normalizeRetailMode(item.UsdRetailMode)
item.EurRetailMode = normalizeRetailMode(item.EurRetailMode)
if _, err := tx.ExecContext(ctx, `
UPDATE mk_pricing_rule SET
calc_enabled=$2,
publish_postgres=$3,
publish_nebim=$4,
is_active=$5,
updated_at=now()
WHERE id=$1
`, ruleID, item.CalcEnabled, item.PublishPostgres, item.PublishNebim, item.IsActive); err != nil {
return err
}
upsertX := func(cur string, base, m1, m2, m3, m4, m5, m6 float64) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO mk_pricex (rule_id, currency, base_mult, m1, m2, m3, m4, m5, m6, created_at, updated_at)
VALUES (NULLIF($1,'')::uuid,$2,$3,$4,$5,$6,$7,$8,$9,now(),now())
ON CONFLICT (rule_id, currency) DO UPDATE SET
base_mult=EXCLUDED.base_mult,
m1=EXCLUDED.m1,
m2=EXCLUDED.m2,
m3=EXCLUDED.m3,
m4=EXCLUDED.m4,
m5=EXCLUDED.m5,
m6=EXCLUDED.m6,
updated_at=now()
`, ruleID, cur, base, m1, m2, m3, m4, m5, m6)
return err
}
upsertRoll := func(cur string, wholesaleStep, retailStep float64, retailMode string) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO mk_priceroll (rule_id, currency, step, wholesale_step, retail_step, retail_mode, created_at, updated_at)
VALUES (NULLIF($1,'')::uuid,$2,$3,$4,$5,$6,now(),now())
ON CONFLICT (rule_id, currency) DO UPDATE SET
step=EXCLUDED.step,
wholesale_step=EXCLUDED.wholesale_step,
retail_step=EXCLUDED.retail_step,
retail_mode=EXCLUDED.retail_mode,
updated_at=now()
`, ruleID, cur, wholesaleStep, wholesaleStep, retailStep, retailMode)
return err
}
if err := upsertX("TRY", item.TryBase, item.Try1, item.Try2, item.Try3, item.Try4, item.Try5, item.Try6); err != nil {
return err
}
if err := upsertRoll("TRY", item.TryWholesaleStep, item.TryRetailStep, item.TryRetailMode); err != nil {
return err
}
if err := upsertX("USD", item.UsdBase, item.Usd1, item.Usd2, item.Usd3, item.Usd4, item.Usd5, item.Usd6); err != nil {
return err
}
if err := upsertRoll("USD", item.UsdWholesaleStep, item.UsdRetailStep, item.UsdRetailMode); err != nil {
return err
}
if err := upsertX("EUR", item.EurBase, item.Eur1, item.Eur2, item.Eur3, item.Eur4, item.Eur5, item.Eur6); err != nil {
return err
}
if err := upsertRoll("EUR", item.EurWholesaleStep, item.EurRetailStep, item.EurRetailMode); err != nil {
return err
}
return nil
}
// UpsertPricingRuleByParameterIDFast ensures there is a rule row for a pricing_parameter_id and
// updates its multipliers/roundings in place. This avoids expensive parameter versioning and
// scope fill during worksheet-style bulk saves.
func UpsertPricingRuleByParameterIDFast(ctx context.Context, tx *sql.Tx, item PricingRuleSaveItem) (string, error) {
if tx == nil {
return "", fmt.Errorf("nil tx")
}
if item.PricingParameterID <= 0 {
return "", fmt.Errorf("missing pricing_parameter_id")
}
// Find latest rule for this parameter id (if any).
var ruleID string
_ = tx.QueryRowContext(ctx, `
SELECT id::text
FROM mk_pricing_rule
WHERE pricing_parameter_id = $1
ORDER BY created_at DESC, updated_at DESC, id DESC
LIMIT 1
FOR UPDATE
`, item.PricingParameterID).Scan(&ruleID)
ruleID = strings.TrimSpace(ruleID)
if ruleID == "" {
// Create minimal rule row; other fields have defaults and parameter scope is read from mk_urunpricingprmtr.
if err := tx.QueryRowContext(ctx, `
INSERT INTO mk_pricing_rule (
pricing_parameter_id,
calc_enabled,
publish_postgres,
publish_nebim,
is_active,
created_at,
updated_at
)
VALUES ($1,$2,$3,$4,$5,now(),now())
RETURNING id::text
`, item.PricingParameterID, item.CalcEnabled, item.PublishPostgres, item.PublishNebim, item.IsActive).Scan(&ruleID); err != nil {
return "", err
}
ruleID = strings.TrimSpace(ruleID)
}
if ruleID == "" {
return "", fmt.Errorf("failed to resolve rule id")
}
// Reuse the ID-fast updater now that we have an id.
item.ID = ruleID
if err := UpdatePricingRuleByIDFast(ctx, tx, item); err != nil {
return "", err
}
return ruleID, nil
}
func nullablePricingParameterID(id int64) any {
if id <= 0 {
return nil