Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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' })
|
||||
|
||||
Reference in New Issue
Block a user