diff --git a/svc/main.go b/svc/main.go index 2c11fea..fda030b 100644 --- a/svc/main.go +++ b/svc/main.go @@ -840,6 +840,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "pricing", "update", wrapV3(routes.SavePricingRulesBulkHandler(pgDB)), ) + bindV3(r, pgDB, + "/api/pricing/pricing-rules/import", "POST", + "pricing", "update", + wrapV3(routes.ImportPricingRulesHandler(pgDB)), + ) bindV3(r, pgDB, "/api/pricing/pricing-rules/options", "GET", "pricing", "view", diff --git a/svc/queries/pricing_parameters.go b/svc/queries/pricing_parameters.go index 8c6115e..0f50083 100644 --- a/svc/queries/pricing_parameters.go +++ b/svc/queries/pricing_parameters.go @@ -31,6 +31,23 @@ type pricingParameterRow struct { BrandGroupSec string } +func PricingParameterRowForImport( + askiliYan, kategori, urunIlkGrubu, urunAnaGrubu, urunAltGrubu, + icerik, marka, brandCode, brandGroupSec string, +) pricingParameterRow { + return pricingParameterRow{ + AskiliYan: askiliYan, + Kategori: kategori, + UrunIlkGrubu: urunIlkGrubu, + UrunAnaGrubu: urunAnaGrubu, + UrunAltGrubu: urunAltGrubu, + Icerik: icerik, + Marka: marka, + BrandCode: brandCode, + BrandGroupSec: brandGroupSec, + } +} + type PricingParameterRuleRow struct { PricingParameterID int64 `json:"pricing_parameter_id"` ScopeKey string `json:"scope_key"` @@ -238,6 +255,77 @@ func pricingParameterScopeValue(value string) []string { return []string{value} } +func EnsureActivePricingParameterByScope(ctx context.Context, tx *sql.Tx, row pricingParameterRow) (int64, bool, error) { + row.AskiliYan = strings.TrimSpace(row.AskiliYan) + row.Kategori = strings.TrimSpace(row.Kategori) + row.UrunIlkGrubu = strings.TrimSpace(row.UrunIlkGrubu) + row.UrunAnaGrubu = strings.TrimSpace(row.UrunAnaGrubu) + row.UrunAltGrubu = strings.TrimSpace(row.UrunAltGrubu) + row.Icerik = strings.TrimSpace(row.Icerik) + row.Marka = strings.TrimSpace(row.Marka) + row.BrandCode = strings.TrimSpace(row.BrandCode) + row.BrandGroupSec = strings.TrimSpace(row.BrandGroupSec) + + var existingID int64 + var isActive bool + err := tx.QueryRowContext(ctx, ` +SELECT id, is_active +FROM mk_urunpricingprmtr +WHERE askili_yan=$1 + AND kategori=$2 + AND urun_ilk_grubu=$3 + AND urun_ana_grubu=$4 + AND urun_alt_grubu=$5 + AND icerik=$6 + AND marka=$7 + AND brand_code=$8 + AND brand_group_sec=$9 +ORDER BY is_active DESC, last_seen_at DESC, id DESC +LIMIT 1 +`, + row.AskiliYan, + row.Kategori, + row.UrunIlkGrubu, + row.UrunAnaGrubu, + row.UrunAltGrubu, + row.Icerik, + row.Marka, + row.BrandCode, + row.BrandGroupSec, + ).Scan(&existingID, &isActive) + if err == nil { + if isActive { + return existingID, false, nil + } + } else if err != sql.ErrNoRows { + return 0, false, err + } + + var newID int64 + if err := tx.QueryRowContext(ctx, ` +INSERT INTO mk_urunpricingprmtr ( + askili_yan, kategori, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, + icerik, marka, brand_code, brand_group_sec, + is_active, first_seen_at, last_seen_at +) +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,TRUE,now(),now()) +RETURNING id +`, + row.AskiliYan, + row.Kategori, + row.UrunIlkGrubu, + row.UrunAnaGrubu, + row.UrunAltGrubu, + row.Icerik, + row.Marka, + row.BrandCode, + row.BrandGroupSec, + ).Scan(&newID); err != nil { + return 0, false, err + } + return newID, true, nil +} + func SyncPricingParametersFromMSSQL(ctx context.Context, mssql *sql.DB, pg *sql.DB) (PricingParameterSyncResult, error) { out := PricingParameterSyncResult{} startedAt := time.Now() diff --git a/svc/routes/pricing_rules.go b/svc/routes/pricing_rules.go index 9e6f636..edff335 100644 --- a/svc/routes/pricing_rules.go +++ b/svc/routes/pricing_rules.go @@ -23,6 +23,58 @@ 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"` + 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"` + 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"` + 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"` +} + +type PricingRuleImportPayload struct { + Items []PricingRuleImportItem `json:"items"` +} + +type PricingRuleImportResult struct { + Success bool `json:"success"` + Processed int `json:"processed"` + Updated int `json:"updated"` + ActivatedScopeCount int `json:"activated_scope_count"` + ErrorCount int `json:"error_count"` +} + 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") @@ -85,6 +137,109 @@ func SavePricingRulesBulkHandler(pg *sql.DB) http.HandlerFunc { } } +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() + + updated := 0 + activatedScopeCount := 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 + } + + pricingParameterID, activated, err := queries.EnsureActivePricingParameterByScope(ctx, tx, queries.PricingParameterRowForImport( + raw.AskiliYan, + raw.Kategori, + raw.UrunIlkGrubu, + raw.UrunAnaGrubu, + raw.UrunAltGrubu, + raw.Icerik, + raw.Marka, + raw.BrandCode, + raw.BrandGroupSec, + )) + if err != nil { + http.Error(w, "pricing parameter resolve error", http.StatusInternalServerError) + return + } + if activated { + activatedScopeCount++ + } + + _, err = queries.UpsertPricingRule(ctx, tx, queries.PricingRuleSaveItem{ + PricingParameterID: pricingParameterID, + 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, + 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, + 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, + }) + 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), + Updated: updated, + ActivatedScopeCount: activatedScopeCount, + 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") diff --git a/ui/src/pages/PricingRules.vue b/ui/src/pages/PricingRules.vue index 851f4b2..83ff355 100644 --- a/ui/src/pages/PricingRules.vue +++ b/ui/src/pages/PricingRules.vue @@ -874,10 +874,7 @@ async function onImportFileChange (event) { return } - const rowMap = new Map(rows.value.map(row => [buildImportRowKeyFromObject(row), row])) - let matched = 0 - let updated = 0 - let unmatched = 0 + const importItems = [] for (let i = 1; i < matrix.length; i++) { const csvRow = matrix[i] @@ -886,14 +883,10 @@ async function onImportFileChange (event) { identity[field] = csvRow[keyHeaderIndexes[label]] ?? '' } - const target = rowMap.get(buildImportRowKeyFromObject(identity)) - if (!target) { - unmatched += 1 - continue + const item = { + ...identity, + is_active: true } - matched += 1 - - let changed = false for (const [headerLabel, field] of Object.entries(importFieldMap)) { const idx = headers.indexOf(headerLabel) if (idx < 0) continue @@ -901,32 +894,33 @@ async function onImportFileChange (event) { if (field === 'is_active') { const next = parseImportedBoolean(rawValue) - if (next === null || next === target.is_active) continue - target.is_active = next - changed = true + if (next !== null) item.is_active = next continue } - const next = parseImportedNumber(rawValue) - if (Number(target[field] || 0) === next) continue - target[field] = next - changed = true - } - - if (changed) { - markDirty(target) - updated += 1 + item[field] = parseImportedNumber(rawValue) } + importItems.push(item) } - if (updated === 0 && matched === 0) { - Notify.create({ type: 'warning', message: 'CSV satirlari ekrandaki kurallarla eslesmedi' }) + if (importItems.length === 0) { + Notify.create({ type: 'warning', message: 'CSV icinde islenecek satir bulunamadi' }) return } + const response = await api.request({ + method: 'POST', + url: '/pricing/pricing-rules/import', + data: { items: importItems }, + timeout: 180000 + }) + + await loadRows() + + const stats = response?.data || {} Notify.create({ type: 'positive', - message: `CSV yüklendi. Eslesen: ${matched}, guncellenen: ${updated}, eslesmeyen: ${unmatched}` + message: `CSV yuklendi. Islenen: ${stats.processed ?? importItems.length}, kaydedilen: ${stats.updated ?? importItems.length}, yeni aktiflestirilen: ${stats.activated_scope_count ?? 0}, hata: ${stats.error_count ?? 0}` }) } catch (err) { Notify.create({ type: 'negative', message: err?.message || 'CSV okunamadi' })