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

@@ -0,0 +1,362 @@
package queries
import (
"bssapp-backend/models"
"context"
"database/sql"
"encoding/json"
"fmt"
"math"
"strings"
)
type FxDeltaPublishStats struct {
RateDate string
Queued int
Updated int // sdprc rows updated/inserted
Skipped int // missing anchor or rule
Failures int
}
type sdprcPublishRow struct {
ProductCode string `json:"product_code"`
Currency string `json:"currency"`
LevelNo int `json:"level_no"`
Price float64 `json:"price"`
}
func round2fx(v float64) float64 {
if math.IsNaN(v) || math.IsInf(v, 0) {
return 0
}
return math.Round(v*100) / 100
}
func roundDerivedWithRule(rule *PricingRuleRow, currency string, level int, raw float64) float64 {
currency = strings.ToUpper(strings.TrimSpace(currency))
if level < 1 || level > 6 {
return 0
}
if rule == nil {
// Fallback: keep a stable 2-decimal behavior when no rule exists.
return round2fx(raw)
}
whStep := 0.0
rtStep := 0.0
rtMode := ""
switch currency {
case "TRY":
whStep, rtStep, rtMode = rule.TryWholesaleStep, rule.TryRetailStep, rule.TryRetailMode
case "USD":
whStep, rtStep, rtMode = rule.UsdWholesaleStep, rule.UsdRetailStep, rule.UsdRetailMode
case "EUR":
whStep, rtStep, rtMode = rule.EurWholesaleStep, rule.EurRetailStep, rule.EurRetailMode
default:
return 0
}
// In our model: level 1-5 = wholesale rounding, level 6 = retail rounding.
if level >= 6 {
return applyRetailRounding(raw, whStep, rtStep, rtMode)
}
return roundUpStep(raw, whStep)
}
// PublishDerivedPricesFromAnchor recalculates derived currency tiers from the stored anchor tiers in sdprc.
// Rule selection determines anchor_mode (USD/TRY). Anchor tiers are never modified here.
func PublishDerivedPricesFromAnchor(ctx context.Context, pg *sql.DB, productCodes []string, rateDate string, forceFxRefresh bool) (int, int, error) {
if len(productCodes) == 0 {
return 0, 0, nil
}
rateRow, err := resolvePricingFxRateByDate(ctx, pg, rateDate, forceFxRefresh, true)
if err != nil {
return 0, 0, err
}
if rateRow.UsdTry <= 0 || rateRow.EurTry <= 0 || rateRow.UsdEur <= 0 {
return 0, 0, fmt.Errorf("invalid fx rates for date %s", rateRow.RateDate)
}
// Load rule map once.
ruleRows, err := ListPricingParameterRules(ctx, pg, PricingRuleOptionFilters{})
if err != nil {
return 0, 0, err
}
rulesByScope := make(map[string]PricingParameterRuleRow, len(ruleRows))
for _, item := range ruleRows {
rulesByScope[item.ScopeKey] = item
}
// Fetch product metadata (scope) from MSSQL.
products, err := GetAllProductPricingRows(ctx, 1000, ProductPricingFilters{ProductCode: productCodes}, "productCode", false)
if err != nil {
return 0, 0, err
}
byCode := make(map[string]models.ProductPricing, len(products))
for _, p := range products {
code := strings.TrimSpace(p.ProductCode)
if code == "" {
continue
}
byCode[code] = p
}
derivedTargets := make([]sdprcPublishRow, 0, len(productCodes)*12) // derived: 2 currencies * 6 levels
skipped := 0
for _, codeRaw := range productCodes {
code := strings.TrimSpace(codeRaw)
if code == "" {
continue
}
product, ok := byCode[code]
if !ok {
skipped++
continue
}
scopeKey := pricingParameterScopeKey(pricingParameterRow{
AskiliYan: strings.TrimSpace(product.AskiliYan),
Kategori: strings.TrimSpace(product.Kategori),
UrunIlkGrubu: strings.TrimSpace(product.UrunIlkGrubu),
UrunAnaGrubu: strings.TrimSpace(product.UrunAnaGrubu),
UrunAltGrubu: strings.TrimSpace(product.UrunAltGrubu),
Icerik: strings.TrimSpace(product.Icerik),
Marka: strings.TrimSpace(product.Marka),
BrandCode: strings.TrimSpace(product.BrandCode),
BrandGroupSec: strings.TrimSpace(product.BrandGroupSec),
})
ruleItem, ok := rulesByScope[scopeKey]
if !ok || ruleItem.Rule == nil || !ruleItem.Rule.IsActive || !ruleItem.Rule.CalcEnabled {
skipped++
continue
}
anchorMode := strings.ToUpper(strings.TrimSpace(ruleItem.Rule.AnchorMode))
if anchorMode != "USD" && anchorMode != "TRY" {
anchorMode = "USD"
}
anchor, ok, err := loadLatestSdprcTiers(ctx, pg, code, anchorMode)
if err != nil || !ok {
skipped++
continue
}
switch anchorMode {
case "USD":
for i := 0; i < 6; i++ {
level := i + 1
usd := anchor[i]
tryV := roundDerivedWithRule(ruleItem.Rule, "TRY", level, usd*rateRow.UsdTry)
eurV := roundDerivedWithRule(ruleItem.Rule, "EUR", level, usd*rateRow.UsdEur)
if tryV > 0 {
derivedTargets = append(derivedTargets, sdprcPublishRow{ProductCode: code, Currency: "TRY", LevelNo: level, Price: tryV})
}
if eurV > 0 {
derivedTargets = append(derivedTargets, sdprcPublishRow{ProductCode: code, Currency: "EUR", LevelNo: level, Price: eurV})
}
}
default: // TRY
for i := 0; i < 6; i++ {
level := i + 1
tryV := anchor[i]
usd := roundDerivedWithRule(ruleItem.Rule, "USD", level, tryV/rateRow.UsdTry)
eurV := roundDerivedWithRule(ruleItem.Rule, "EUR", level, tryV/rateRow.EurTry)
if usd > 0 {
derivedTargets = append(derivedTargets, sdprcPublishRow{ProductCode: code, Currency: "USD", LevelNo: level, Price: usd})
}
if eurV > 0 {
derivedTargets = append(derivedTargets, sdprcPublishRow{ProductCode: code, Currency: "EUR", LevelNo: level, Price: eurV})
}
}
}
}
if len(derivedTargets) == 0 {
return 0, skipped, nil
}
written, err := bulkUpsertSdprcDerived(ctx, pg, derivedTargets)
return written, skipped, err
}
func loadLatestSdprcTiers(ctx context.Context, pg *sql.DB, productCode string, currency string) ([6]float64, bool, error) {
var out [6]float64
productCode = strings.TrimSpace(productCode)
currency = strings.ToUpper(strings.TrimSpace(currency))
if productCode == "" {
return out, false, nil
}
if currency != "USD" && currency != "TRY" {
return out, false, nil
}
rows, err := pg.QueryContext(ctx, `
WITH latest AS (
SELECT DISTINCT ON (sdprc.sdprcgrp_id)
sdprc.sdprcgrp_id AS grp,
COALESCE(sdprc.prc, 0)::float8 AS prc
FROM sdprc
JOIN mmitem ON mmitem.id = sdprc.mmitem_id
WHERE mmitem.code = $1
AND sdprc.crn = $2
AND sdprc.sdprcgrp_id BETWEEN 1 AND 6
AND sdprc.prc IS NOT NULL
AND sdprc.prc > 0
ORDER BY sdprc.sdprcgrp_id, sdprc.zlins_dttm DESC
)
SELECT grp, prc FROM latest ORDER BY grp;
`, productCode, currency)
if err != nil {
return out, false, err
}
defer rows.Close()
found := 0
for rows.Next() {
var grp int
var prc float64
if err := rows.Scan(&grp, &prc); err != nil {
return out, false, err
}
if grp >= 1 && grp <= 6 && prc > 0 {
out[grp-1] = prc
found++
}
}
if err := rows.Err(); err != nil {
return out, false, err
}
return out, found == 6, nil
}
func bulkUpsertSdprcDerived(ctx context.Context, pg *sql.DB, targets []sdprcPublishRow) (int, error) {
raw, err := json.Marshal(targets)
if err != nil {
return 0, err
}
tx, err := pg.BeginTx(ctx, nil)
if err != nil {
return 0, err
}
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, `SELECT pg_advisory_xact_lock(2003, 1)`); err != nil {
return 0, err
}
q := `
WITH input AS (
SELECT *
FROM jsonb_to_recordset($1::jsonb) AS x(product_code text, currency text, level_no int, price float8)
),
norm AS (
SELECT
NULLIF(BTRIM(product_code), '') AS product_code,
UPPER(NULLIF(BTRIM(currency), '')) AS currency,
COALESCE(level_no, 0) AS level_no,
COALESCE(price, 0) AS price
FROM input
),
dims_cache AS (
SELECT
NULLIF(BTRIM(c.product_code), '') AS product_code,
c.dim1,
c.dim3
FROM mk_mmitem_dim_combo c
JOIN norm
ON norm.product_code = c.product_code
WHERE c.dim1 IS NOT NULL
),
dims_sdprc AS (
SELECT
norm.product_code AS product_code,
s.dim1 AS dim1,
s.dim3 AS dim3
FROM norm
JOIN mmitem mm
ON mm.code = norm.product_code
JOIN sdprc s
ON s.mmitem_id = mm.id
WHERE s.dim1 IS NOT NULL
AND s.dim1 > 0
GROUP BY norm.product_code, s.dim1, s.dim3
),
dims AS (
SELECT product_code, dim1, dim3 FROM dims_cache
UNION
SELECT product_code, dim1, dim3 FROM dims_sdprc
),
mapped AS (
SELECT
mm.id AS mmitem_id,
m.sdprcgrp_id AS sdprcgrp_id,
norm.currency AS crn,
d.dim1 AS dim1,
d.dim3 AS dim3,
norm.price AS prc
FROM norm
JOIN dims d
ON d.product_code = norm.product_code
JOIN mk_price_target_map_pg m
ON m.is_active = TRUE
AND m.currency = norm.currency
AND m.level_no = norm.level_no
JOIN mmitem mm
ON mm.code = norm.product_code
WHERE norm.product_code IS NOT NULL
AND norm.currency IN ('USD','EUR','TRY')
AND norm.level_no BETWEEN 1 AND 6
AND norm.price > 0
AND m.sdprcgrp_id IS NOT NULL
),
latest AS (
SELECT DISTINCT ON (s.mmitem_id, s.sdprcgrp_id, s.crn, s.dim1, COALESCE(s.dim3, 0))
s.id,
s.mmitem_id,
s.sdprcgrp_id,
s.crn,
s.dim1,
s.dim3
FROM sdprc s
JOIN mapped m
ON m.mmitem_id = s.mmitem_id
AND m.sdprcgrp_id = s.sdprcgrp_id
AND m.crn = s.crn
AND m.dim1 = s.dim1
AND ((m.dim3 IS NULL AND s.dim3 IS NULL) OR (m.dim3 = s.dim3))
ORDER BY s.mmitem_id, s.sdprcgrp_id, s.crn, s.dim1, COALESCE(s.dim3, 0), s.zlins_dttm DESC
),
updated AS (
UPDATE sdprc s
SET prc = m.prc,
zlins_dttm = now()
FROM latest l
JOIN mapped m
ON m.mmitem_id=l.mmitem_id AND m.sdprcgrp_id=l.sdprcgrp_id AND m.crn=l.crn
AND m.dim1 = l.dim1 AND ((m.dim3 IS NULL AND l.dim3 IS NULL) OR (m.dim3 = l.dim3))
WHERE s.id = l.id
AND s.prc IS DISTINCT FROM m.prc
RETURNING 1
),
inserted AS (
INSERT INTO sdprc (mmitem_id, sdprcgrp_id, crn, dim1, dim3, prc, zlins_dttm)
SELECT m.mmitem_id, m.sdprcgrp_id, m.crn, m.dim1, m.dim3, m.prc, now()
FROM mapped m
LEFT JOIN latest l
ON l.mmitem_id=m.mmitem_id AND l.sdprcgrp_id=m.sdprcgrp_id AND l.crn=m.crn
AND l.dim1 = m.dim1 AND ((l.dim3 IS NULL AND m.dim3 IS NULL) OR (l.dim3 = m.dim3))
WHERE l.id IS NULL
RETURNING 1
)
SELECT (SELECT COUNT(*) FROM updated)::int + (SELECT COUNT(*) FROM inserted)::int;
`
var written int
if err := tx.QueryRowContext(ctx, q, raw).Scan(&written); err != nil {
return 0, err
}
if err := tx.Commit(); err != nil {
return 0, err
}
return written, nil
}