Files
bssapp/svc/routes/pricing_rules.go
2026-06-17 21:57:02 +03:00

857 lines
27 KiB
Go

package routes
import (
"bssapp-backend/auth"
"bssapp-backend/queries"
"bssapp-backend/utils"
"database/sql"
"encoding/json"
"fmt"
"github.com/lib/pq"
"net/http"
"sort"
"strconv"
"strings"
"time"
)
// Step-1/2 scope (distinct+cascade) comes from the PostgreSQL parameter cache.
// For now we implement:
// - Postgres tables (bootstrap)
// - List/Save rules (bulk)
// - Options endpoint for cascade (mk_urunpricingprmtr)
type PricingRuleBulkSavePayload struct {
Items []queries.PricingRuleSaveItem `json:"items"`
}
type PricingRuleImportItem struct {
AskiliYan string `json:"askili_yan"`
Kategori string `json:"kategori"`
UrunIlkGrubu string `json:"urun_ilk_grubu"`
UrunAnaGrubu string `json:"urun_ana_grubu"`
UrunAltGrubu string `json:"urun_alt_grubu"`
Icerik string `json:"icerik"`
Marka string `json:"marka"`
BrandCode string `json:"brand_code"`
BrandGroupSec string `json:"brand_group"`
StrategyCode string `json:"strategy_code"`
AnchorMode string `json:"anchor_mode"`
CalcEnabled bool `json:"calc_enabled"`
PublishPostgres bool `json:"publish_postgres"`
PublishNebim bool `json:"publish_nebim"`
IsActive bool `json:"is_active"`
TryBase float64 `json:"try_base"`
Try1 float64 `json:"try1"`
Try2 float64 `json:"try2"`
Try3 float64 `json:"try3"`
Try4 float64 `json:"try4"`
Try5 float64 `json:"try5"`
Try6 float64 `json:"try6"`
TryWholesaleStep float64 `json:"try_wholesale_step"`
TryRetailStep float64 `json:"try_retail_step"`
TryRetailMode string `json:"try_retail_mode"`
UsdBase float64 `json:"usd_base"`
Usd1 float64 `json:"usd1"`
Usd2 float64 `json:"usd2"`
Usd3 float64 `json:"usd3"`
Usd4 float64 `json:"usd4"`
Usd5 float64 `json:"usd5"`
Usd6 float64 `json:"usd6"`
UsdWholesaleStep float64 `json:"usd_wholesale_step"`
UsdRetailStep float64 `json:"usd_retail_step"`
UsdRetailMode string `json:"usd_retail_mode"`
EurBase float64 `json:"eur_base"`
Eur1 float64 `json:"eur1"`
Eur2 float64 `json:"eur2"`
Eur3 float64 `json:"eur3"`
Eur4 float64 `json:"eur4"`
Eur5 float64 `json:"eur5"`
Eur6 float64 `json:"eur6"`
EurWholesaleStep float64 `json:"eur_wholesale_step"`
EurRetailStep float64 `json:"eur_retail_step"`
EurRetailMode string `json:"eur_retail_mode"`
}
type PricingRuleImportPayload struct {
Items []PricingRuleImportItem `json:"items"`
}
type PricingRuleImportResult struct {
Success bool `json:"success"`
Processed int `json:"processed"`
Matched int `json:"matched"`
Skipped int `json:"skipped"`
Updated int `json:"updated"`
ActivatedScopeCount int `json:"activated_scope_count"`
ErrorCount int `json:"error_count"`
}
func normalizePricingStrategyCode(v string) string {
v = strings.ToUpper(strings.TrimSpace(v))
if v == "" {
return "CORE"
}
return v
}
func normalizePricingAnchorMode(v string) string {
v = strings.ToUpper(strings.TrimSpace(v))
if v == "" {
return "USD"
}
return v
}
func isValidPricingStrategyCode(v string) bool {
if strings.TrimSpace(v) == "" {
return true
}
switch normalizePricingStrategyCode(v) {
case "CORE", "PREMIUM", "SARTORIAL":
return true
default:
return false
}
}
func isValidPricingAnchorMode(v string) bool {
switch normalizePricingAnchorMode(v) {
case "TRY", "USD":
return true
default:
return false
}
}
func isValidPricingRetailMode(v string) bool {
switch queries.NormalizeRetailModeForRoute(v) {
case "STEP", "END_99", "END_49", "BAND_99", "BAND_49":
return true
default:
return false
}
}
func GetPricingRulesHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
rows, err := queries.ListPricingRules(ctx, pg)
if err != nil {
http.Error(w, "pricing rules list error", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
}
// Very small “bulk upsert” for step-1/2: we only need to persist the multipliers+roundings for now.
// Rules are identified by UUID; new rows can be created via empty id (server generates).
func SavePricingRulesBulkHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
var payload PricingRuleBulkSavePayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "invalid payload", http.StatusBadRequest)
return
}
started := time.Now()
traceID := utils.TraceIDFromRequest(r)
w.Header().Set("X-Trace-ID", traceID)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
logger := utils.SlogFromContext(ctx).With("handler", "pricing-rules.bulk-save")
claims, _ := auth.GetClaimsFromContext(ctx)
if claims != nil {
logger = logger.With("user", claims.Username, "user_id", claims.ID)
}
existingIDCount := 0
newIDCount := 0
for _, it := range payload.Items {
if strings.TrimSpace(it.ID) != "" {
existingIDCount++
} else {
newIDCount++
}
}
logger.Info("bulk-save:start",
"items", len(payload.Items),
"existing_id", existingIDCount,
"new_id", newIDCount,
)
tx, err := pg.BeginTx(ctx, nil)
if err != nil {
logger.Error("bulk-save:tx-begin:error", "err", err)
http.Error(w, "pg transaction start error", http.StatusInternalServerError)
return
}
defer tx.Rollback()
// Serialize writes touching mk_urunpricingprmtr/mk_pricing_rule/mk_pricex/mk_priceroll
// to avoid deadlocks with pricing-parameter sync and concurrent bulk-saves.
lockWaitStarted := time.Now()
if _, err := tx.ExecContext(ctx, `SELECT pg_advisory_xact_lock(1001, 1)`); err != nil {
logger.Error("bulk-save:advisory-lock:error", "err", err)
http.Error(w, "pg advisory lock error", http.StatusInternalServerError)
return
}
logger.Info("bulk-save:advisory-lock:acquired", "wait_ms", time.Since(lockWaitStarted).Milliseconds())
logPgErr := func(msg string, err error, it queries.PricingRuleSaveItem) {
fields := []any{
"pricing_parameter_id", it.PricingParameterID,
"id", strings.TrimSpace(it.ID),
"err", err,
}
if pe, ok := err.(*pq.Error); ok && pe != nil {
fields = append(fields,
"sqlstate", string(pe.Code),
"constraint", pe.Constraint,
"table", pe.Table,
"column", pe.Column,
"detail", pe.Detail,
"where", pe.Where,
)
}
logger.Error(msg, fields...)
}
updated := 0
for _, it := range payload.Items {
// Zero means that no rounding rule has been configured yet.
if it.TryWholesaleStep < 0 || it.TryRetailStep < 0 || it.UsdWholesaleStep < 0 || it.UsdRetailStep < 0 || it.EurWholesaleStep < 0 || it.EurRetailStep < 0 {
logger.Warn("bulk-save:invalid-rounding-step",
"pricing_parameter_id", it.PricingParameterID,
"id", strings.TrimSpace(it.ID),
)
http.Error(w, "invalid rounding step", http.StatusBadRequest)
return
}
if !isValidPricingStrategyCode(it.StrategyCode) {
logger.Warn("bulk-save:invalid-strategy-code",
"pricing_parameter_id", it.PricingParameterID,
"id", strings.TrimSpace(it.ID),
"strategy_code", it.StrategyCode,
)
http.Error(w, "invalid strategy_code", http.StatusBadRequest)
return
}
if !isValidPricingAnchorMode(it.AnchorMode) {
logger.Warn("bulk-save:invalid-anchor-mode",
"pricing_parameter_id", it.PricingParameterID,
"id", strings.TrimSpace(it.ID),
"anchor_mode", it.AnchorMode,
)
http.Error(w, "invalid anchor_mode", http.StatusBadRequest)
return
}
if !isValidPricingRetailMode(it.TryRetailMode) || !isValidPricingRetailMode(it.UsdRetailMode) || !isValidPricingRetailMode(it.EurRetailMode) {
logger.Warn("bulk-save:invalid-retail-mode",
"pricing_parameter_id", it.PricingParameterID,
"id", strings.TrimSpace(it.ID),
"try_retail_mode", it.TryRetailMode,
"usd_retail_mode", it.UsdRetailMode,
"eur_retail_mode", it.EurRetailMode,
)
http.Error(w, "invalid retail_mode", http.StatusBadRequest)
return
}
}
dbStarted := time.Now()
updated, err = queries.BulkSavePricingRulesFast(ctx, tx, payload.Items)
if err != nil {
// best-effort: log first item context
if len(payload.Items) > 0 {
logPgErr("bulk-save:bulk-fast:error", err, payload.Items[0])
} else {
logger.Error("bulk-save:bulk-fast:error", "err", err)
}
http.Error(w, "pricing rule save error: "+err.Error(), http.StatusInternalServerError)
return
}
logger.Info("bulk-save:db:done", "updated", updated, "duration_ms", time.Since(dbStarted).Milliseconds())
if err := tx.Commit(); err != nil {
logger.Error("bulk-save:commit:error", "err", err)
http.Error(w, "pg transaction commit error", http.StatusInternalServerError)
return
}
logger.Info("bulk-save:done", "updated", updated, "duration_ms", time.Since(started).Milliseconds())
_ = json.NewEncoder(w).Encode(map[string]any{"success": true, "updated": updated})
}
}
func ImportPricingRulesHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
var payload PricingRuleImportPayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "invalid payload", http.StatusBadRequest)
return
}
if len(payload.Items) == 0 {
_ = json.NewEncoder(w).Encode(PricingRuleImportResult{Success: true})
return
}
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
tx, err := pg.BeginTx(ctx, nil)
if err != nil {
http.Error(w, "pg transaction start error", http.StatusInternalServerError)
return
}
defer tx.Rollback()
// Same global lock as bulk-save: prevents deadlocks with concurrent updates/sync.
if _, err := tx.ExecContext(ctx, `SELECT pg_advisory_xact_lock(1001, 1)`); err != nil {
http.Error(w, "pg advisory lock error", http.StatusInternalServerError)
return
}
updated := 0
matched := 0
skipped := 0
for _, raw := range payload.Items {
if raw.TryWholesaleStep < 0 || raw.TryRetailStep < 0 || raw.UsdWholesaleStep < 0 || raw.UsdRetailStep < 0 || raw.EurWholesaleStep < 0 || raw.EurRetailStep < 0 {
http.Error(w, "invalid rounding step", http.StatusBadRequest)
return
}
if !isValidPricingStrategyCode(raw.StrategyCode) {
http.Error(w, "invalid strategy_code", http.StatusBadRequest)
return
}
if !isValidPricingAnchorMode(raw.AnchorMode) {
http.Error(w, "invalid anchor_mode", http.StatusBadRequest)
return
}
if !isValidPricingRetailMode(raw.TryRetailMode) || !isValidPricingRetailMode(raw.UsdRetailMode) || !isValidPricingRetailMode(raw.EurRetailMode) {
http.Error(w, "invalid retail_mode", http.StatusBadRequest)
return
}
pricingParameterID, err := queries.FindActivePricingParameterByScope(ctx, tx, queries.PricingParameterRowForImport(
raw.AskiliYan,
raw.Kategori,
raw.UrunIlkGrubu,
raw.UrunAnaGrubu,
raw.UrunAltGrubu,
raw.Icerik,
raw.Marka,
raw.BrandCode,
raw.BrandGroupSec,
))
if err == sql.ErrNoRows {
skipped++
continue
}
if err != nil {
http.Error(w, "pricing parameter resolve error", http.StatusInternalServerError)
return
}
matched++
_, err = queries.UpsertPricingRule(ctx, tx, queries.PricingRuleSaveItem{
PricingParameterID: pricingParameterID,
StrategyCode: normalizePricingStrategyCode(raw.StrategyCode),
AnchorMode: normalizePricingAnchorMode(raw.AnchorMode),
CalcEnabled: raw.CalcEnabled,
PublishPostgres: raw.PublishPostgres,
PublishNebim: raw.PublishNebim,
IsActive: raw.IsActive,
TryBase: raw.TryBase,
Try1: raw.Try1,
Try2: raw.Try2,
Try3: raw.Try3,
Try4: raw.Try4,
Try5: raw.Try5,
Try6: raw.Try6,
TryWholesaleStep: raw.TryWholesaleStep,
TryRetailStep: raw.TryRetailStep,
TryRetailMode: queries.NormalizeRetailModeForRoute(raw.TryRetailMode),
UsdBase: raw.UsdBase,
Usd1: raw.Usd1,
Usd2: raw.Usd2,
Usd3: raw.Usd3,
Usd4: raw.Usd4,
Usd5: raw.Usd5,
Usd6: raw.Usd6,
UsdWholesaleStep: raw.UsdWholesaleStep,
UsdRetailStep: raw.UsdRetailStep,
UsdRetailMode: queries.NormalizeRetailModeForRoute(raw.UsdRetailMode),
EurBase: raw.EurBase,
Eur1: raw.Eur1,
Eur2: raw.Eur2,
Eur3: raw.Eur3,
Eur4: raw.Eur4,
Eur5: raw.Eur5,
Eur6: raw.Eur6,
EurWholesaleStep: raw.EurWholesaleStep,
EurRetailStep: raw.EurRetailStep,
EurRetailMode: queries.NormalizeRetailModeForRoute(raw.EurRetailMode),
})
if err != nil {
http.Error(w, "pricing rule import error", http.StatusInternalServerError)
return
}
updated++
}
if err := tx.Commit(); err != nil {
http.Error(w, "pg transaction commit error", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(PricingRuleImportResult{
Success: true,
Processed: len(payload.Items),
Matched: matched,
Skipped: skipped,
Updated: updated,
ActivatedScopeCount: 0,
ErrorCount: 0,
})
}
}
func GetPricingRuleOptionsHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
field := strings.TrimSpace(r.URL.Query().Get("field"))
if field == "" {
http.Error(w, "missing field", http.StatusBadRequest)
return
}
limit := 500
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
if n, err := strconv.Atoi(raw); err == nil && n > 0 && n <= 5000 {
limit = n
}
}
f := pricingRuleFiltersFromRequest(r)
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
opts, err := queries.ListPricingParameterDistinctOptions(ctx, pg, field, f, limit)
if err != nil {
http.Error(w, "options lookup error", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{
"field": field,
"options": opts,
})
}
}
func GetPricingParameterRulesHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
rows, err := queries.ListPricingParameterRules(ctx, pg, pricingRuleFiltersFromRequest(r))
if err != nil {
http.Error(w, "pricing parameter rules list error", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(rows)
}
}
func ExportPricingRulesHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
traceID := utils.TraceIDFromRequest(r)
ctx := utils.ContextWithTraceID(r.Context(), traceID)
rows, err := queries.ListPricingParameterRules(ctx, pg, pricingRuleFiltersFromRequest(r))
if err != nil {
http.Error(w, "pricing parameter rules export error", http.StatusInternalServerError)
return
}
rows = filterPricingRuleExportRows(rows, r)
sortPricingRuleExportRows(rows, strings.TrimSpace(r.URL.Query().Get("sort_by")), strings.TrimSpace(r.URL.Query().Get("desc")) != "0")
filename := fmt.Sprintf("pricing_rules_all_%s.csv", time.Now().Format("2006-01-02"))
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
_, _ = w.Write([]byte("\uFEFF"))
_, _ = w.Write([]byte(buildPricingRuleCSV(rows)))
}
}
func pricingRuleFiltersFromRequest(r *http.Request) queries.PricingRuleOptionFilters {
return queries.PricingRuleOptionFilters{
AskiliYan: splitCSV(r.URL.Query().Get("askili_yan")),
Kategori: splitCSV(r.URL.Query().Get("kategori")),
UrunIlkGrubu: splitCSV(r.URL.Query().Get("urun_ilk_grubu")),
UrunAnaGrubu: splitCSV(r.URL.Query().Get("urun_ana_grubu")),
UrunAltGrubu: splitCSV(r.URL.Query().Get("urun_alt_grubu")),
Icerik: splitCSV(r.URL.Query().Get("icerik")),
Marka: splitCSV(r.URL.Query().Get("marka")),
BrandCode: splitCSV(r.URL.Query().Get("brand_code")),
BrandGroupSec: splitCSV(r.URL.Query().Get("brand_group")),
}
}
func filterPricingRuleExportRows(rows []queries.PricingParameterRuleRow, r *http.Request) []queries.PricingParameterRuleRow {
rangeFilter := func(prefix string) (*float64, *float64) {
parse := func(raw string) *float64 {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
v, err := strconv.ParseFloat(strings.ReplaceAll(raw, ",", "."), 64)
if err != nil {
return nil
}
return &v
}
return parse(r.URL.Query().Get(prefix + "_min")), parse(r.URL.Query().Get(prefix + "_max"))
}
fields := []string{
"try_base", "try1", "try2", "try3", "try4", "try5", "try6", "try_wholesale_step", "try_retail_step",
"usd_base", "usd1", "usd2", "usd3", "usd4", "usd5", "usd6", "usd_wholesale_step", "usd_retail_step",
"eur_base", "eur1", "eur2", "eur3", "eur4", "eur5", "eur6", "eur_wholesale_step", "eur_retail_step",
}
minMap := map[string]*float64{}
maxMap := map[string]*float64{}
for _, field := range fields {
minMap[field], maxMap[field] = rangeFilter(field)
}
out := make([]queries.PricingParameterRuleRow, 0, len(rows))
for _, row := range rows {
ok := true
for _, field := range fields {
value := pricingRuleNumericValue(row, field)
if minMap[field] != nil && value < *minMap[field] {
ok = false
break
}
if maxMap[field] != nil && value > *maxMap[field] {
ok = false
break
}
}
if ok {
out = append(out, row)
}
}
return out
}
func pricingRuleNumericValue(row queries.PricingParameterRuleRow, field string) float64 {
if row.Rule == nil {
return 0
}
switch field {
case "try_base":
return row.Rule.TryBase
case "try1":
return row.Rule.Try1
case "try2":
return row.Rule.Try2
case "try3":
return row.Rule.Try3
case "try4":
return row.Rule.Try4
case "try5":
return row.Rule.Try5
case "try6":
return row.Rule.Try6
case "try_wholesale_step":
return row.Rule.TryWholesaleStep
case "try_retail_step":
return row.Rule.TryRetailStep
case "usd_base":
return row.Rule.UsdBase
case "usd1":
return row.Rule.Usd1
case "usd2":
return row.Rule.Usd2
case "usd3":
return row.Rule.Usd3
case "usd4":
return row.Rule.Usd4
case "usd5":
return row.Rule.Usd5
case "usd6":
return row.Rule.Usd6
case "usd_wholesale_step":
return row.Rule.UsdWholesaleStep
case "usd_retail_step":
return row.Rule.UsdRetailStep
case "eur_base":
return row.Rule.EurBase
case "eur1":
return row.Rule.Eur1
case "eur2":
return row.Rule.Eur2
case "eur3":
return row.Rule.Eur3
case "eur4":
return row.Rule.Eur4
case "eur5":
return row.Rule.Eur5
case "eur6":
return row.Rule.Eur6
case "eur_wholesale_step":
return row.Rule.EurWholesaleStep
case "eur_retail_step":
return row.Rule.EurRetailStep
default:
return 0
}
}
func sortPricingRuleExportRows(rows []queries.PricingParameterRuleRow, sortBy string, desc bool) {
sortBy = strings.TrimSpace(sortBy)
if sortBy == "" {
return
}
sort.SliceStable(rows, func(i, j int) bool {
li, lj := rows[i], rows[j]
switch sortBy {
case "has_rule":
if desc {
return boolRank(li.HasRule) > boolRank(lj.HasRule)
}
return boolRank(li.HasRule) < boolRank(lj.HasRule)
case "is_active":
liActive, ljActive := false, false
if li.Rule != nil {
liActive = li.Rule.IsActive
}
if lj.Rule != nil {
ljActive = lj.Rule.IsActive
}
if desc {
return boolRank(liActive) > boolRank(ljActive)
}
return boolRank(liActive) < boolRank(ljActive)
case "calc_enabled", "publish_postgres", "publish_nebim":
liValue, ljValue := false, false
if li.Rule != nil {
switch sortBy {
case "calc_enabled":
liValue = li.Rule.CalcEnabled
case "publish_postgres":
liValue = li.Rule.PublishPostgres
case "publish_nebim":
liValue = li.Rule.PublishNebim
}
}
if lj.Rule != nil {
switch sortBy {
case "calc_enabled":
ljValue = lj.Rule.CalcEnabled
case "publish_postgres":
ljValue = lj.Rule.PublishPostgres
case "publish_nebim":
ljValue = lj.Rule.PublishNebim
}
}
if desc {
return boolRank(liValue) > boolRank(ljValue)
}
return boolRank(liValue) < boolRank(ljValue)
case "askili_yan", "kategori", "urun_ilk_grubu", "urun_ana_grubu", "urun_alt_grubu", "icerik", "marka", "brand_code", "brand_group", "anchor_mode",
"try_retail_mode", "usd_retail_mode", "eur_retail_mode":
vi := pricingRuleStringValue(li, sortBy)
vj := pricingRuleStringValue(lj, sortBy)
if desc {
return strings.Compare(vi, vj) > 0
}
return strings.Compare(vi, vj) < 0
default:
vi := pricingRuleNumericValue(li, sortBy)
vj := pricingRuleNumericValue(lj, sortBy)
if desc {
return vi > vj
}
return vi < vj
}
})
}
func boolRank(v bool) int {
if v {
return 1
}
return 0
}
func pricingRuleStringValue(row queries.PricingParameterRuleRow, field string) string {
switch field {
case "askili_yan":
return row.AskiliYan
case "kategori":
return row.Kategori
case "urun_ilk_grubu":
return row.UrunIlkGrubu
case "urun_ana_grubu":
return row.UrunAnaGrubu
case "urun_alt_grubu":
return row.UrunAltGrubu
case "icerik":
return row.Icerik
case "marka":
return row.Marka
case "brand_code":
return row.BrandCode
case "brand_group":
return row.BrandGroupSec
case "anchor_mode":
if row.Rule == nil {
return "USD"
}
return row.Rule.AnchorMode
case "try_retail_mode":
if row.Rule == nil {
return "STEP"
}
return queries.NormalizeRetailModeForRoute(row.Rule.TryRetailMode)
case "usd_retail_mode":
if row.Rule == nil {
return "STEP"
}
return queries.NormalizeRetailModeForRoute(row.Rule.UsdRetailMode)
case "eur_retail_mode":
if row.Rule == nil {
return "STEP"
}
return queries.NormalizeRetailModeForRoute(row.Rule.EurRetailMode)
default:
return ""
}
}
func buildPricingRuleCSV(rows []queries.PricingParameterRuleRow) string {
headers := []string{
"DURUM", "AKTIF", "ASKILI YAN", "KATEGORI", "URUN ILK GRUBU", "URUN ANA GRUBU", "URUN ALT GRUBU",
"ICERIK", "MARKA", "BRAND CODE", "MARKA GRUBU", "ANCHOR MODE", "HESAP AKTIF", "PG YAYIN", "NEBIM YAYIN",
"TRY TOPTAN YUVARLAMA", "TRY PERAKENDE MODU", "TRY PERAKENDE DEGERI", "TRY TABAN", "TRY 1", "TRY 2", "TRY 3", "TRY 4", "TRY 5", "TRY 6",
"USD TOPTAN YUVARLAMA", "USD PERAKENDE MODU", "USD PERAKENDE DEGERI", "USD TABAN", "USD 1", "USD 2", "USD 3", "USD 4", "USD 5", "USD 6",
"EUR TOPTAN YUVARLAMA", "EUR PERAKENDE MODU", "EUR PERAKENDE DEGERI", "EUR TABAN", "EUR 1", "EUR 2", "EUR 3", "EUR 4", "EUR 5", "EUR 6",
}
var b strings.Builder
for i, h := range headers {
b.WriteString(csvEscapeValue(h))
if i == len(headers)-1 {
b.WriteString("\n")
} else {
b.WriteString(";")
}
}
for _, row := range rows {
active := "Pasif"
if row.Rule == nil || row.Rule.IsActive {
active = "Aktif"
}
values := []string{
map[bool]string{true: "Tanimli", false: "Yeni"}[row.HasRule],
active,
row.AskiliYan,
row.Kategori,
row.UrunIlkGrubu,
row.UrunAnaGrubu,
row.UrunAltGrubu,
row.Icerik,
csvExcelTextValue(row.Marka),
csvExcelTextValue(row.BrandCode),
row.BrandGroupSec,
pricingRuleStringValue(row, "anchor_mode"),
map[bool]string{true: "Aktif", false: "Pasif"}[row.Rule == nil || row.Rule.CalcEnabled],
map[bool]string{true: "Evet", false: "Hayir"}[row.Rule == nil || row.Rule.PublishPostgres],
map[bool]string{true: "Evet", false: "Hayir"}[row.Rule == nil || row.Rule.PublishNebim],
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try_wholesale_step")),
pricingRuleStringValue(row, "try_retail_mode"),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try_retail_step")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try_base")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try1")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try2")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try3")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try4")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try5")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try6")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd_wholesale_step")),
pricingRuleStringValue(row, "usd_retail_mode"),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd_retail_step")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd_base")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd1")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd2")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd3")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd4")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd5")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd6")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur_wholesale_step")),
pricingRuleStringValue(row, "eur_retail_mode"),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur_retail_step")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur_base")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur1")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur2")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur3")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur4")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur5")),
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur6")),
}
for i, value := range values {
b.WriteString(csvEscapeValue(value))
if i == len(values)-1 {
b.WriteString("\n")
} else {
b.WriteString(";")
}
}
}
return b.String()
}
func csvEscapeValue(value string) string {
text := strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(value), "\r", " "), "\n", " ")
if strings.Contains(text, ";") || strings.Contains(text, "\"") {
text = `"` + strings.ReplaceAll(text, `"`, `""`) + `"`
}
return text
}
func csvExcelTextValue(value string) string {
text := strings.TrimSpace(value)
if text == "" {
return ""
}
return `="` + strings.ReplaceAll(text, `"`, `""`) + `"`
}
func splitCSV(raw string) []string {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
out = append(out, p)
}
}
return out
}