680 lines
20 KiB
Go
680 lines
20 KiB
Go
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[:])
|
|
}
|