Merge remote-tracking branch 'origin/master'
This commit is contained in:
362
svc/queries/product_pricing_fx_publish.go
Normal file
362
svc/queries/product_pricing_fx_publish.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user