Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
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"
|
||||
@@ -33,6 +35,11 @@ type PricingRuleImportItem struct {
|
||||
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"`
|
||||
@@ -43,6 +50,7 @@ type PricingRuleImportItem struct {
|
||||
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"`
|
||||
@@ -52,6 +60,7 @@ type PricingRuleImportItem struct {
|
||||
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"`
|
||||
@@ -61,6 +70,7 @@ type PricingRuleImportItem struct {
|
||||
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 {
|
||||
@@ -77,6 +87,52 @@ type PricingRuleImportResult struct {
|
||||
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")
|
||||
@@ -104,37 +160,130 @@ func SavePricingRulesBulkHandler(pg *sql.DB) http.HandlerFunc {
|
||||
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
|
||||
}
|
||||
id, err := queries.UpsertPricingRule(ctx, tx, it)
|
||||
if err != nil {
|
||||
http.Error(w, "pricing rule save error", http.StatusInternalServerError)
|
||||
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 id != "" {
|
||||
updated++
|
||||
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})
|
||||
}
|
||||
}
|
||||
@@ -163,6 +312,12 @@ func ImportPricingRulesHandler(pg *sql.DB) http.HandlerFunc {
|
||||
}
|
||||
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
|
||||
@@ -171,6 +326,18 @@ func ImportPricingRulesHandler(pg *sql.DB) http.HandlerFunc {
|
||||
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,
|
||||
@@ -195,6 +362,11 @@ func ImportPricingRulesHandler(pg *sql.DB) http.HandlerFunc {
|
||||
|
||||
_, 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,
|
||||
@@ -205,6 +377,7 @@ func ImportPricingRulesHandler(pg *sql.DB) http.HandlerFunc {
|
||||
Try6: raw.Try6,
|
||||
TryWholesaleStep: raw.TryWholesaleStep,
|
||||
TryRetailStep: raw.TryRetailStep,
|
||||
TryRetailMode: queries.NormalizeRetailModeForRoute(raw.TryRetailMode),
|
||||
UsdBase: raw.UsdBase,
|
||||
Usd1: raw.Usd1,
|
||||
Usd2: raw.Usd2,
|
||||
@@ -214,6 +387,7 @@ func ImportPricingRulesHandler(pg *sql.DB) http.HandlerFunc {
|
||||
Usd6: raw.Usd6,
|
||||
UsdWholesaleStep: raw.UsdWholesaleStep,
|
||||
UsdRetailStep: raw.UsdRetailStep,
|
||||
UsdRetailMode: queries.NormalizeRetailModeForRoute(raw.UsdRetailMode),
|
||||
EurBase: raw.EurBase,
|
||||
Eur1: raw.Eur1,
|
||||
Eur2: raw.Eur2,
|
||||
@@ -223,6 +397,7 @@ func ImportPricingRulesHandler(pg *sql.DB) http.HandlerFunc {
|
||||
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)
|
||||
@@ -470,7 +645,34 @@ func sortPricingRuleExportRows(rows []queries.PricingParameterRuleRow, sortBy st
|
||||
return boolRank(liActive) > boolRank(ljActive)
|
||||
}
|
||||
return boolRank(liActive) < boolRank(ljActive)
|
||||
case "askili_yan", "kategori", "urun_ilk_grubu", "urun_ana_grubu", "urun_alt_grubu", "icerik", "marka", "brand_code", "brand_group":
|
||||
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 {
|
||||
@@ -515,6 +717,26 @@ func pricingRuleStringValue(row queries.PricingParameterRuleRow, field string) s
|
||||
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 ""
|
||||
}
|
||||
@@ -523,10 +745,10 @@ func pricingRuleStringValue(row queries.PricingParameterRuleRow, field string) s
|
||||
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",
|
||||
"TRY TOPTAN YUVARLAMA", "TRY PERAKENDE YUVARLAMA", "TRY TABAN", "TRY 1", "TRY 2", "TRY 3", "TRY 4", "TRY 5", "TRY 6",
|
||||
"USD TOPTAN YUVARLAMA", "USD PERAKENDE YUVARLAMA", "USD TABAN", "USD 1", "USD 2", "USD 3", "USD 4", "USD 5", "USD 6",
|
||||
"EUR TOPTAN YUVARLAMA", "EUR PERAKENDE YUVARLAMA", "EUR TABAN", "EUR 1", "EUR 2", "EUR 3", "EUR 4", "EUR 5", "EUR 6",
|
||||
"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 {
|
||||
@@ -551,10 +773,15 @@ func buildPricingRuleCSV(rows []queries.PricingParameterRuleRow) string {
|
||||
row.UrunAnaGrubu,
|
||||
row.UrunAltGrubu,
|
||||
row.Icerik,
|
||||
row.Marka,
|
||||
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")),
|
||||
@@ -564,6 +791,7 @@ func buildPricingRuleCSV(rows []queries.PricingParameterRuleRow) string {
|
||||
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")),
|
||||
@@ -573,6 +801,7 @@ func buildPricingRuleCSV(rows []queries.PricingParameterRuleRow) string {
|
||||
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")),
|
||||
|
||||
Reference in New Issue
Block a user