387 lines
11 KiB
Go
387 lines
11 KiB
Go
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
|
|
),
|
|
-- Prefer PG's authoritative variant dimension table (mmitem_dim). Fall back to cache table if needed.
|
|
dims_mmitem_dim AS (
|
|
SELECT
|
|
norm.product_code AS product_code,
|
|
md.val1::bigint AS dim1,
|
|
CASE
|
|
WHEN md.val3 IS NULL OR md.val3 = 0 THEN NULL
|
|
ELSE md.val3::bigint
|
|
END AS dim3
|
|
FROM norm
|
|
JOIN mmitem mm
|
|
ON mm.code = norm.product_code
|
|
JOIN mmitem_dim md
|
|
ON md.mmitem_id = mm.id
|
|
AND COALESCE(md.is_active, TRUE) = TRUE
|
|
WHERE md.val1 IS NOT NULL
|
|
AND md.val1 > 0
|
|
GROUP BY norm.product_code, md.val1, md.val2
|
|
),
|
|
dims_cache_table AS (
|
|
SELECT
|
|
NULLIF(BTRIM(c.product_code), '') AS product_code,
|
|
c.dim1::bigint AS dim1,
|
|
c.dim3::bigint AS dim3
|
|
FROM mk_mmitem_dim_combo c
|
|
JOIN norm
|
|
ON norm.product_code = c.product_code
|
|
WHERE c.dim1 IS NOT NULL
|
|
),
|
|
dims_cache AS (
|
|
SELECT product_code, dim1, dim3 FROM dims_mmitem_dim
|
|
UNION
|
|
SELECT product_code, dim1, dim3 FROM dims_cache_table
|
|
),
|
|
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
|
|
}
|