857 lines
27 KiB
Go
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
|
|
}
|