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",
|
"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",
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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' })
|
||||||
|
|||||||
Reference in New Issue
Block a user