Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-06-04 17:43:21 +03:00
parent 6aea7f4012
commit fea4938a9d
4 changed files with 268 additions and 26 deletions

View File

@@ -840,6 +840,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"pricing", "update", "pricing", "update",
wrapV3(routes.SavePricingRulesBulkHandler(pgDB)), wrapV3(routes.SavePricingRulesBulkHandler(pgDB)),
) )
bindV3(r, pgDB,
"/api/pricing/pricing-rules/import", "POST",
"pricing", "update",
wrapV3(routes.ImportPricingRulesHandler(pgDB)),
)
bindV3(r, pgDB, bindV3(r, pgDB,
"/api/pricing/pricing-rules/options", "GET", "/api/pricing/pricing-rules/options", "GET",
"pricing", "view", "pricing", "view",

View File

@@ -31,6 +31,23 @@ type pricingParameterRow struct {
BrandGroupSec string 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 { type PricingParameterRuleRow struct {
PricingParameterID int64 `json:"pricing_parameter_id"` PricingParameterID int64 `json:"pricing_parameter_id"`
ScopeKey string `json:"scope_key"` ScopeKey string `json:"scope_key"`
@@ -238,6 +255,77 @@ func pricingParameterScopeValue(value string) []string {
return []string{value} 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) { func SyncPricingParametersFromMSSQL(ctx context.Context, mssql *sql.DB, pg *sql.DB) (PricingParameterSyncResult, error) {
out := PricingParameterSyncResult{} out := PricingParameterSyncResult{}
startedAt := time.Now() startedAt := time.Now()

View File

@@ -23,6 +23,58 @@ type PricingRuleBulkSavePayload struct {
Items []queries.PricingRuleSaveItem `json:"items"` 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 { func GetPricingRulesHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") 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 { func GetPricingRuleOptionsHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")

View File

@@ -874,10 +874,7 @@ async function onImportFileChange (event) {
return return
} }
const rowMap = new Map(rows.value.map(row => [buildImportRowKeyFromObject(row), row])) const importItems = []
let matched = 0
let updated = 0
let unmatched = 0
for (let i = 1; i < matrix.length; i++) { for (let i = 1; i < matrix.length; i++) {
const csvRow = matrix[i] const csvRow = matrix[i]
@@ -886,14 +883,10 @@ async function onImportFileChange (event) {
identity[field] = csvRow[keyHeaderIndexes[label]] ?? '' identity[field] = csvRow[keyHeaderIndexes[label]] ?? ''
} }
const target = rowMap.get(buildImportRowKeyFromObject(identity)) const item = {
if (!target) { ...identity,
unmatched += 1 is_active: true
continue
} }
matched += 1
let changed = false
for (const [headerLabel, field] of Object.entries(importFieldMap)) { for (const [headerLabel, field] of Object.entries(importFieldMap)) {
const idx = headers.indexOf(headerLabel) const idx = headers.indexOf(headerLabel)
if (idx < 0) continue if (idx < 0) continue
@@ -901,32 +894,33 @@ async function onImportFileChange (event) {
if (field === 'is_active') { if (field === 'is_active') {
const next = parseImportedBoolean(rawValue) const next = parseImportedBoolean(rawValue)
if (next === null || next === target.is_active) continue if (next !== null) item.is_active = next
target.is_active = next
changed = true
continue continue
} }
const next = parseImportedNumber(rawValue) item[field] = parseImportedNumber(rawValue)
if (Number(target[field] || 0) === next) continue }
target[field] = next importItems.push(item)
changed = true
} }
if (changed) { if (importItems.length === 0) {
markDirty(target) Notify.create({ type: 'warning', message: 'CSV icinde islenecek satir bulunamadi' })
updated += 1
}
}
if (updated === 0 && matched === 0) {
Notify.create({ type: 'warning', message: 'CSV satirlari ekrandaki kurallarla eslesmedi' })
return 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({ Notify.create({
type: 'positive', 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) { } catch (err) {
Notify.create({ type: 'negative', message: err?.message || 'CSV okunamadi' }) Notify.create({ type: 'negative', message: err?.message || 'CSV okunamadi' })