Merge remote-tracking branch 'origin/master'
This commit is contained in:
679
svc/queries/pricing_calc_engine.go
Normal file
679
svc/queries/pricing_calc_engine.go
Normal file
@@ -0,0 +1,679 @@
|
||||
package queries
|
||||
|
||||
import (
|
||||
"bssapp-backend/db"
|
||||
"bssapp-backend/models"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PricingFxRateCacheRow struct {
|
||||
RateDate string `json:"rate_date"`
|
||||
UsdTry float64 `json:"usd_try"`
|
||||
EurTry float64 `json:"eur_try"`
|
||||
UsdEur float64 `json:"usd_eur"`
|
||||
}
|
||||
|
||||
type ProductPricingSnapshotCalcRequest struct {
|
||||
ProductCodes []string
|
||||
Filters ProductPricingFilters
|
||||
RateDate string
|
||||
ForceFxRefresh bool
|
||||
}
|
||||
|
||||
type ProductPricingSnapshotCalcResult struct {
|
||||
RateDate string `json:"rate_date"`
|
||||
UsdTry float64 `json:"usd_try"`
|
||||
EurTry float64 `json:"eur_try"`
|
||||
UsdEur float64 `json:"usd_eur"`
|
||||
Requested int `json:"requested"`
|
||||
Calculated int `json:"calculated"`
|
||||
Skipped int `json:"skipped"`
|
||||
}
|
||||
|
||||
type ProductPricingSnapshotPreviewRow struct {
|
||||
ProductCode string `json:"product_code"`
|
||||
AnchorMode string `json:"anchor_mode"`
|
||||
BasePriceUsd float64 `json:"base_price_usd"`
|
||||
BasePriceTry float64 `json:"base_price_try"`
|
||||
USD1 float64 `json:"usd1"`
|
||||
USD2 float64 `json:"usd2"`
|
||||
USD3 float64 `json:"usd3"`
|
||||
USD4 float64 `json:"usd4"`
|
||||
USD5 float64 `json:"usd5"`
|
||||
USD6 float64 `json:"usd6"`
|
||||
EUR1 float64 `json:"eur1"`
|
||||
EUR2 float64 `json:"eur2"`
|
||||
EUR3 float64 `json:"eur3"`
|
||||
EUR4 float64 `json:"eur4"`
|
||||
EUR5 float64 `json:"eur5"`
|
||||
EUR6 float64 `json:"eur6"`
|
||||
TRY1 float64 `json:"try1"`
|
||||
TRY2 float64 `json:"try2"`
|
||||
TRY3 float64 `json:"try3"`
|
||||
TRY4 float64 `json:"try4"`
|
||||
TRY5 float64 `json:"try5"`
|
||||
TRY6 float64 `json:"try6"`
|
||||
}
|
||||
|
||||
type ProductPricingSnapshotPreviewResult struct {
|
||||
RateDate string `json:"rate_date"`
|
||||
UsdTry float64 `json:"usd_try"`
|
||||
EurTry float64 `json:"eur_try"`
|
||||
UsdEur float64 `json:"usd_eur"`
|
||||
Requested int `json:"requested"`
|
||||
Calculated int `json:"calculated"`
|
||||
Skipped int `json:"skipped"`
|
||||
Rows []ProductPricingSnapshotPreviewRow `json:"rows"`
|
||||
}
|
||||
|
||||
func resolvePricingFxRateByDate(ctx context.Context, pg *sql.DB, rateDate string, forceRefresh bool, persist bool) (PricingFxRateCacheRow, error) {
|
||||
var out PricingFxRateCacheRow
|
||||
rateDate = normalizeCalcDate(rateDate)
|
||||
if rateDate == "" {
|
||||
rateDate = time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
if !forceRefresh {
|
||||
err := pg.QueryRowContext(ctx, `
|
||||
SELECT
|
||||
TO_CHAR(rate_date, 'YYYY-MM-DD'),
|
||||
usd_try::float8,
|
||||
eur_try::float8,
|
||||
usd_eur::float8
|
||||
FROM mk_fx_rate_cache
|
||||
WHERE rate_date=$1::date
|
||||
`, rateDate).Scan(&out.RateDate, &out.UsdTry, &out.EurTry, &out.UsdEur)
|
||||
if err == nil {
|
||||
return out, nil
|
||||
}
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return out, err
|
||||
}
|
||||
}
|
||||
|
||||
if db.MssqlDB == nil {
|
||||
return out, fmt.Errorf("mssql pricing db not available")
|
||||
}
|
||||
row, err := GetProductionHasCostDetailExchangeRatesByDate(ctx, db.MssqlDB, rateDate)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
var (
|
||||
rateDateResolved string
|
||||
usdTry float64
|
||||
eurTry float64
|
||||
gbpIgnored float64
|
||||
)
|
||||
if err := row.Scan(&rateDateResolved, &usdTry, &eurTry, &gbpIgnored); err != nil {
|
||||
return out, err
|
||||
}
|
||||
rateDateResolved = normalizeCalcDate(rateDateResolved)
|
||||
if rateDateResolved == "" {
|
||||
rateDateResolved = rateDate
|
||||
}
|
||||
usdEur := 0.0
|
||||
if usdTry > 0 && eurTry > 0 {
|
||||
usdEur = roundCalcValue(usdTry / eurTry)
|
||||
}
|
||||
|
||||
if persist {
|
||||
if _, err := pg.ExecContext(ctx, `
|
||||
INSERT INTO mk_fx_rate_cache (
|
||||
rate_date, usd_try, eur_try, usd_eur, source_system, source_updated_at, created_at, updated_at
|
||||
)
|
||||
VALUES ($1::date, $2, $3, $4, 'MSSQL', now(), now(), now())
|
||||
ON CONFLICT (rate_date)
|
||||
DO UPDATE SET
|
||||
usd_try=EXCLUDED.usd_try,
|
||||
eur_try=EXCLUDED.eur_try,
|
||||
usd_eur=EXCLUDED.usd_eur,
|
||||
source_system=EXCLUDED.source_system,
|
||||
source_updated_at=EXCLUDED.source_updated_at,
|
||||
updated_at=now()
|
||||
`, rateDateResolved, usdTry, eurTry, usdEur); err != nil {
|
||||
return out, err
|
||||
}
|
||||
}
|
||||
|
||||
out = PricingFxRateCacheRow{
|
||||
RateDate: rateDateResolved,
|
||||
UsdTry: usdTry,
|
||||
EurTry: eurTry,
|
||||
UsdEur: usdEur,
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func SyncPricingFxRateCacheByDate(ctx context.Context, pg *sql.DB, rateDate string, forceRefresh bool) (PricingFxRateCacheRow, error) {
|
||||
return resolvePricingFxRateByDate(ctx, pg, rateDate, forceRefresh, true)
|
||||
}
|
||||
|
||||
func CalculateProductPricingSnapshots(ctx context.Context, pg *sql.DB, req ProductPricingSnapshotCalcRequest) (ProductPricingSnapshotCalcResult, error) {
|
||||
var result ProductPricingSnapshotCalcResult
|
||||
rateRow, err := resolvePricingFxRateByDate(ctx, pg, req.RateDate, req.ForceFxRefresh, true)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.RateDate = rateRow.RateDate
|
||||
result.UsdTry = rateRow.UsdTry
|
||||
result.EurTry = rateRow.EurTry
|
||||
result.UsdEur = rateRow.UsdEur
|
||||
if rateRow.UsdTry <= 0 || rateRow.EurTry <= 0 || rateRow.UsdEur <= 0 {
|
||||
return result, fmt.Errorf("invalid fx rates for date %s", rateRow.RateDate)
|
||||
}
|
||||
|
||||
filters := req.Filters
|
||||
if len(req.ProductCodes) > 0 {
|
||||
filters.ProductCode = dedupeTrimmedStrings(req.ProductCodes)
|
||||
}
|
||||
|
||||
rows, err := GetAllProductPricingRows(ctx, 1000, filters, "productCode", false)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.Requested = len(rows)
|
||||
if len(rows) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
ruleRows, err := ListPricingParameterRules(ctx, pg, PricingRuleOptionFilters{})
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
rulesByScope := make(map[string]PricingParameterRuleRow, len(ruleRows))
|
||||
for _, item := range ruleRows {
|
||||
rulesByScope[item.ScopeKey] = item
|
||||
}
|
||||
|
||||
tx, err := pg.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
for _, product := range rows {
|
||||
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 {
|
||||
result.Skipped++
|
||||
continue
|
||||
}
|
||||
if !ruleItem.Rule.CalcEnabled || !ruleItem.Rule.IsActive {
|
||||
result.Skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
snapshot, ok := buildPricingSnapshotRow(product, ruleItem, rateRow)
|
||||
if !ok {
|
||||
result.Skipped++
|
||||
continue
|
||||
}
|
||||
if err := upsertPricingSnapshot(ctx, tx, snapshot); err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.Calculated++
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return result, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func PreviewProductPricingSnapshots(ctx context.Context, pg *sql.DB, req ProductPricingSnapshotCalcRequest) (ProductPricingSnapshotPreviewResult, error) {
|
||||
var result ProductPricingSnapshotPreviewResult
|
||||
rateRow, err := resolvePricingFxRateByDate(ctx, pg, req.RateDate, req.ForceFxRefresh, false)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.RateDate = rateRow.RateDate
|
||||
result.UsdTry = rateRow.UsdTry
|
||||
result.EurTry = rateRow.EurTry
|
||||
result.UsdEur = rateRow.UsdEur
|
||||
if rateRow.UsdTry <= 0 || rateRow.EurTry <= 0 || rateRow.UsdEur <= 0 {
|
||||
return result, fmt.Errorf("invalid fx rates for date %s", rateRow.RateDate)
|
||||
}
|
||||
|
||||
filters := req.Filters
|
||||
if len(req.ProductCodes) > 0 {
|
||||
filters.ProductCode = dedupeTrimmedStrings(req.ProductCodes)
|
||||
}
|
||||
|
||||
rows, err := GetAllProductPricingRows(ctx, 1000, filters, "productCode", false)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.Requested = len(rows)
|
||||
if len(rows) == 0 {
|
||||
result.Rows = []ProductPricingSnapshotPreviewRow{}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
ruleRows, err := ListPricingParameterRules(ctx, pg, PricingRuleOptionFilters{})
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
rulesByScope := make(map[string]PricingParameterRuleRow, len(ruleRows))
|
||||
for _, item := range ruleRows {
|
||||
rulesByScope[item.ScopeKey] = item
|
||||
}
|
||||
|
||||
outRows := make([]ProductPricingSnapshotPreviewRow, 0, len(rows))
|
||||
for _, product := range rows {
|
||||
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 {
|
||||
result.Skipped++
|
||||
continue
|
||||
}
|
||||
if !ruleItem.Rule.CalcEnabled || !ruleItem.Rule.IsActive {
|
||||
result.Skipped++
|
||||
continue
|
||||
}
|
||||
snapshot, ok := buildPricingSnapshotRow(product, ruleItem, rateRow)
|
||||
if !ok {
|
||||
result.Skipped++
|
||||
continue
|
||||
}
|
||||
outRows = append(outRows, previewRowFromSnapshot(snapshot))
|
||||
result.Calculated++
|
||||
}
|
||||
result.Rows = outRows
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type pricingSnapshotRow struct {
|
||||
ProductCode string
|
||||
PricingParameterID int64
|
||||
RuleID string
|
||||
StrategyCode string
|
||||
AnchorMode string
|
||||
FxDate string
|
||||
CostDate string
|
||||
BasePriceTry float64
|
||||
BasePriceUsd float64
|
||||
Try [6]float64
|
||||
Usd [6]float64
|
||||
Eur [6]float64
|
||||
CalcHash string
|
||||
}
|
||||
|
||||
func buildPricingSnapshotRow(product models.ProductPricing, ruleItem PricingParameterRuleRow, fx PricingFxRateCacheRow) (pricingSnapshotRow, bool) {
|
||||
var out pricingSnapshotRow
|
||||
rule := ruleItem.Rule
|
||||
if rule == nil {
|
||||
return out, false
|
||||
}
|
||||
|
||||
anchorMode := strings.ToUpper(strings.TrimSpace(rule.AnchorMode))
|
||||
if anchorMode != "TRY" && anchorMode != "USD" {
|
||||
anchorMode = "USD"
|
||||
}
|
||||
strategyCode := strings.ToUpper(strings.TrimSpace(rule.StrategyCode))
|
||||
if strategyCode != "CORE" && strategyCode != "PREMIUM" && strategyCode != "SARTORIAL" {
|
||||
strategyCode = strings.ToUpper(strings.TrimSpace(product.BrandGroupSec))
|
||||
}
|
||||
if strategyCode != "CORE" && strategyCode != "PREMIUM" && strategyCode != "SARTORIAL" {
|
||||
strategyCode = "CORE"
|
||||
}
|
||||
|
||||
costUSD := roundCalcValue(product.CostPrice)
|
||||
if costUSD <= 0 {
|
||||
return out, false
|
||||
}
|
||||
|
||||
baseUSD := 0.0
|
||||
baseTRY := 0.0
|
||||
switch anchorMode {
|
||||
case "TRY":
|
||||
if rule.TryBase > 0 {
|
||||
baseTRY = roundCalcValue(costUSD * fx.UsdTry * rule.TryBase)
|
||||
} else if product.BasePriceTry > 0 {
|
||||
baseTRY = roundCalcValue(product.BasePriceTry)
|
||||
} else if product.BasePriceUsd > 0 {
|
||||
baseTRY = roundCalcValue(product.BasePriceUsd * fx.UsdTry)
|
||||
} else if rule.UsdBase > 0 {
|
||||
baseTRY = roundCalcValue(costUSD * rule.UsdBase * fx.UsdTry)
|
||||
}
|
||||
if baseTRY <= 0 {
|
||||
return out, false
|
||||
}
|
||||
baseUSD = roundCalcValue(baseTRY / fx.UsdTry)
|
||||
default:
|
||||
if rule.UsdBase > 0 {
|
||||
baseUSD = roundCalcValue(costUSD * rule.UsdBase)
|
||||
} else if product.BasePriceUsd > 0 {
|
||||
baseUSD = roundCalcValue(product.BasePriceUsd)
|
||||
} else if product.BasePriceTry > 0 {
|
||||
baseUSD = roundCalcValue(product.BasePriceTry / fx.UsdTry)
|
||||
}
|
||||
if baseUSD <= 0 {
|
||||
return out, false
|
||||
}
|
||||
baseTRY = roundCalcValue(baseUSD * fx.UsdTry)
|
||||
}
|
||||
baseEUR := roundCalcValue(baseUSD * fx.UsdEur)
|
||||
|
||||
tryBaseForCalc := baseTRY
|
||||
usdBaseForCalc := baseUSD
|
||||
eurBaseForCalc := baseEUR
|
||||
if tryBaseForCalc <= 0 || usdBaseForCalc <= 0 || eurBaseForCalc <= 0 {
|
||||
return out, false
|
||||
}
|
||||
|
||||
tryMultipliers := [6]float64{rule.Try1, rule.Try2, rule.Try3, rule.Try4, rule.Try5, rule.Try6}
|
||||
usdMultipliers := [6]float64{rule.Usd1, rule.Usd2, rule.Usd3, rule.Usd4, rule.Usd5, rule.Usd6}
|
||||
eurMultipliers := [6]float64{rule.Eur1, rule.Eur2, rule.Eur3, rule.Eur4, rule.Eur5, rule.Eur6}
|
||||
|
||||
prevTry := tryBaseForCalc
|
||||
prevUsd := usdBaseForCalc
|
||||
prevEur := eurBaseForCalc
|
||||
for i := 0; i < 6; i++ {
|
||||
tryRaw := prevTry * tryMultipliers[i]
|
||||
usdRaw := prevUsd * usdMultipliers[i]
|
||||
eurRaw := prevEur * eurMultipliers[i]
|
||||
|
||||
tryStep := rule.TryWholesaleStep
|
||||
usdStep := rule.UsdWholesaleStep
|
||||
eurStep := rule.EurWholesaleStep
|
||||
if i == 5 {
|
||||
out.Try[i] = applyRetailRounding(tryRaw, rule.TryWholesaleStep, rule.TryRetailStep, rule.TryRetailMode)
|
||||
out.Usd[i] = applyRetailRounding(usdRaw, rule.UsdWholesaleStep, rule.UsdRetailStep, rule.UsdRetailMode)
|
||||
out.Eur[i] = applyRetailRounding(eurRaw, rule.EurWholesaleStep, rule.EurRetailStep, rule.EurRetailMode)
|
||||
prevTry = out.Try[i]
|
||||
prevUsd = out.Usd[i]
|
||||
prevEur = out.Eur[i]
|
||||
continue
|
||||
}
|
||||
|
||||
out.Try[i] = roundUpStep(tryRaw, tryStep)
|
||||
out.Usd[i] = roundUpStep(usdRaw, usdStep)
|
||||
out.Eur[i] = roundUpStep(eurRaw, eurStep)
|
||||
prevTry = out.Try[i]
|
||||
prevUsd = out.Usd[i]
|
||||
prevEur = out.Eur[i]
|
||||
}
|
||||
|
||||
out.ProductCode = strings.TrimSpace(product.ProductCode)
|
||||
out.PricingParameterID = ruleItem.PricingParameterID
|
||||
out.RuleID = strings.TrimSpace(rule.ID)
|
||||
out.StrategyCode = strategyCode
|
||||
out.AnchorMode = anchorMode
|
||||
out.FxDate = fx.RateDate
|
||||
out.CostDate = normalizeCalcDate(product.LastCostingDate)
|
||||
out.BasePriceTry = baseTRY
|
||||
out.BasePriceUsd = baseUSD
|
||||
out.CalcHash = pricingSnapshotHash(out, fx)
|
||||
return out, true
|
||||
}
|
||||
|
||||
func applyRetailRounding(raw, wholesaleStep, retailStep float64, retailMode string) float64 {
|
||||
baseRounded := roundUpStep(raw, wholesaleStep)
|
||||
mode := normalizeRetailMode(retailMode)
|
||||
switch mode {
|
||||
case "END_99":
|
||||
return roundUpToEnding(baseRounded, 99)
|
||||
case "END_49":
|
||||
return roundUpToEnding(baseRounded, 49)
|
||||
case "BAND_99":
|
||||
return roundUpToBandEnding(baseRounded, retailStep, 99)
|
||||
case "BAND_49":
|
||||
return roundUpToBandEnding(baseRounded, retailStep, 49)
|
||||
default:
|
||||
if retailStep > 0 {
|
||||
return roundUpStep(baseRounded, retailStep)
|
||||
}
|
||||
return baseRounded
|
||||
}
|
||||
}
|
||||
|
||||
func roundUpToEnding(value float64, ending int) float64 {
|
||||
value = roundCalcValue(value)
|
||||
if value <= 0 {
|
||||
return 0
|
||||
}
|
||||
switch ending {
|
||||
case 99:
|
||||
return roundCalcValue(psychologicalEnding99(value))
|
||||
case 49:
|
||||
return roundCalcValue(psychologicalEnding49(value))
|
||||
default:
|
||||
whole := math.Floor(value + 1e-9)
|
||||
candidate := whole + (float64(ending) / 100.0)
|
||||
if candidate+1e-9 < value {
|
||||
candidate = whole + 1 + (float64(ending) / 100.0)
|
||||
}
|
||||
return roundCalcValue(candidate)
|
||||
}
|
||||
}
|
||||
|
||||
func roundUpToBandEnding(value, band float64, ending int) float64 {
|
||||
value = roundCalcValue(value)
|
||||
band = roundCalcValue(band)
|
||||
if value <= 0 {
|
||||
return 0
|
||||
}
|
||||
if band <= 0 {
|
||||
return roundUpToEnding(value, ending)
|
||||
}
|
||||
units := math.Ceil((value - 1e-9) / band)
|
||||
candidate := (units * band) - 1 + (float64(ending) / 100.0)
|
||||
if candidate+1e-9 < value {
|
||||
candidate = ((units + 1) * band) - 1 + (float64(ending) / 100.0)
|
||||
}
|
||||
return roundCalcValue(candidate)
|
||||
}
|
||||
|
||||
func psychologicalEnding99(value float64) float64 {
|
||||
whole := math.Floor(value + 1e-9)
|
||||
fraction := value - whole
|
||||
if fraction >= 0.90 {
|
||||
return whole + 0.99
|
||||
}
|
||||
return whole - 0.01
|
||||
}
|
||||
|
||||
func psychologicalEnding49(value float64) float64 {
|
||||
whole := math.Floor(value + 1e-9)
|
||||
fraction := value - whole
|
||||
if fraction >= 0.40 {
|
||||
return whole + 0.49
|
||||
}
|
||||
return whole - 0.51
|
||||
}
|
||||
|
||||
func upsertPricingSnapshot(ctx context.Context, tx *sql.Tx, row pricingSnapshotRow) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO mk_price_snapshot (
|
||||
product_code, pricing_parameter_id, rule_id, strategy_code, anchor_mode, fx_date, cost_date,
|
||||
base_price_try, base_price_usd,
|
||||
try1, try2, try3, try4, try5, try6,
|
||||
usd1, usd2, usd3, usd4, usd5, usd6,
|
||||
eur1, eur2, eur3, eur4, eur5, eur6,
|
||||
calc_hash, created_at, updated_at
|
||||
)
|
||||
VALUES (
|
||||
$1,$2,NULLIF($3,'')::uuid,$4,$5,$6::date,NULLIF($7,'')::date,
|
||||
$8,$9,
|
||||
$10,$11,$12,$13,$14,$15,
|
||||
$16,$17,$18,$19,$20,$21,
|
||||
$22,$23,$24,$25,$26,$27,
|
||||
$28,now(),now()
|
||||
)
|
||||
ON CONFLICT (product_code, pricing_parameter_id)
|
||||
DO UPDATE SET
|
||||
rule_id=NULLIF(EXCLUDED.rule_id::text,'')::uuid,
|
||||
strategy_code=EXCLUDED.strategy_code,
|
||||
anchor_mode=EXCLUDED.anchor_mode,
|
||||
fx_date=EXCLUDED.fx_date,
|
||||
cost_date=EXCLUDED.cost_date,
|
||||
base_price_try=EXCLUDED.base_price_try,
|
||||
base_price_usd=EXCLUDED.base_price_usd,
|
||||
try1=EXCLUDED.try1,
|
||||
try2=EXCLUDED.try2,
|
||||
try3=EXCLUDED.try3,
|
||||
try4=EXCLUDED.try4,
|
||||
try5=EXCLUDED.try5,
|
||||
try6=EXCLUDED.try6,
|
||||
usd1=EXCLUDED.usd1,
|
||||
usd2=EXCLUDED.usd2,
|
||||
usd3=EXCLUDED.usd3,
|
||||
usd4=EXCLUDED.usd4,
|
||||
usd5=EXCLUDED.usd5,
|
||||
usd6=EXCLUDED.usd6,
|
||||
eur1=EXCLUDED.eur1,
|
||||
eur2=EXCLUDED.eur2,
|
||||
eur3=EXCLUDED.eur3,
|
||||
eur4=EXCLUDED.eur4,
|
||||
eur5=EXCLUDED.eur5,
|
||||
eur6=EXCLUDED.eur6,
|
||||
calc_hash=EXCLUDED.calc_hash,
|
||||
updated_at=now()
|
||||
`, row.ProductCode, row.PricingParameterID, row.RuleID, row.StrategyCode, row.AnchorMode, row.FxDate, row.CostDate,
|
||||
row.BasePriceTry, row.BasePriceUsd,
|
||||
row.Try[0], row.Try[1], row.Try[2], row.Try[3], row.Try[4], row.Try[5],
|
||||
row.Usd[0], row.Usd[1], row.Usd[2], row.Usd[3], row.Usd[4], row.Usd[5],
|
||||
row.Eur[0], row.Eur[1], row.Eur[2], row.Eur[3], row.Eur[4], row.Eur[5],
|
||||
row.CalcHash,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func previewRowFromSnapshot(row pricingSnapshotRow) ProductPricingSnapshotPreviewRow {
|
||||
return ProductPricingSnapshotPreviewRow{
|
||||
ProductCode: row.ProductCode,
|
||||
AnchorMode: row.AnchorMode,
|
||||
BasePriceUsd: roundCalcValue(row.BasePriceUsd),
|
||||
BasePriceTry: roundCalcValue(row.BasePriceTry),
|
||||
USD1: roundCalcValue(row.Usd[0]),
|
||||
USD2: roundCalcValue(row.Usd[1]),
|
||||
USD3: roundCalcValue(row.Usd[2]),
|
||||
USD4: roundCalcValue(row.Usd[3]),
|
||||
USD5: roundCalcValue(row.Usd[4]),
|
||||
USD6: roundCalcValue(row.Usd[5]),
|
||||
EUR1: roundCalcValue(row.Eur[0]),
|
||||
EUR2: roundCalcValue(row.Eur[1]),
|
||||
EUR3: roundCalcValue(row.Eur[2]),
|
||||
EUR4: roundCalcValue(row.Eur[3]),
|
||||
EUR5: roundCalcValue(row.Eur[4]),
|
||||
EUR6: roundCalcValue(row.Eur[5]),
|
||||
TRY1: roundCalcValue(row.Try[0]),
|
||||
TRY2: roundCalcValue(row.Try[1]),
|
||||
TRY3: roundCalcValue(row.Try[2]),
|
||||
TRY4: roundCalcValue(row.Try[3]),
|
||||
TRY5: roundCalcValue(row.Try[4]),
|
||||
TRY6: roundCalcValue(row.Try[5]),
|
||||
}
|
||||
}
|
||||
|
||||
func roundUpStep(value, step float64) float64 {
|
||||
value = roundCalcValue(value)
|
||||
if value <= 0 {
|
||||
return 0
|
||||
}
|
||||
step = roundCalcValue(step)
|
||||
if step <= 0 {
|
||||
return value
|
||||
}
|
||||
units := math.Ceil((value - 1e-9) / step)
|
||||
return roundCalcValue(units * step)
|
||||
}
|
||||
|
||||
func roundCalcValue(value float64) float64 {
|
||||
if !isFiniteCalc(value) {
|
||||
return 0
|
||||
}
|
||||
return math.Round(value*1_000_000) / 1_000_000
|
||||
}
|
||||
|
||||
func isFiniteCalc(value float64) bool {
|
||||
return !math.IsNaN(value) && !math.IsInf(value, 0)
|
||||
}
|
||||
|
||||
func normalizeCalcDate(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
if len(value) >= 10 {
|
||||
value = value[:10]
|
||||
}
|
||||
if _, err := time.Parse("2006-01-02", value); err != nil {
|
||||
return ""
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func dedupeTrimmedStrings(values []string) []string {
|
||||
seen := map[string]struct{}{}
|
||||
out := make([]string, 0, len(values))
|
||||
for _, raw := range values {
|
||||
val := strings.TrimSpace(raw)
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[val]; ok {
|
||||
continue
|
||||
}
|
||||
seen[val] = struct{}{}
|
||||
out = append(out, val)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func pricingSnapshotHash(row pricingSnapshotRow, fx PricingFxRateCacheRow) string {
|
||||
parts := []string{
|
||||
row.ProductCode,
|
||||
fmt.Sprintf("%d", row.PricingParameterID),
|
||||
row.RuleID,
|
||||
row.StrategyCode,
|
||||
row.AnchorMode,
|
||||
row.FxDate,
|
||||
row.CostDate,
|
||||
fmt.Sprintf("%.6f", row.BasePriceTry),
|
||||
fmt.Sprintf("%.6f", row.BasePriceUsd),
|
||||
fmt.Sprintf("%.6f", fx.UsdTry),
|
||||
fmt.Sprintf("%.6f", fx.EurTry),
|
||||
fmt.Sprintf("%.6f", fx.UsdEur),
|
||||
}
|
||||
for _, value := range row.Try {
|
||||
parts = append(parts, fmt.Sprintf("%.6f", value))
|
||||
}
|
||||
for _, value := range row.Usd {
|
||||
parts = append(parts, fmt.Sprintf("%.6f", value))
|
||||
}
|
||||
for _, value := range row.Eur {
|
||||
parts = append(parts, fmt.Sprintf("%.6f", value))
|
||||
}
|
||||
sum := md5.Sum([]byte(strings.Join(parts, string(rune(31)))))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
Reference in New Issue
Block a user