449 lines
13 KiB
Go
449 lines
13 KiB
Go
package routes
|
|
|
|
import (
|
|
"bssapp-backend/queries"
|
|
"bssapp-backend/utils"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Step-1/2 scope (distinct+cascade) comes from the PostgreSQL parameter cache.
|
|
// For now we implement:
|
|
// - Postgres tables (bootstrap)
|
|
// - List/Save rules (bulk)
|
|
// - Options endpoint for cascade (mk_urunpricingprmtr)
|
|
|
|
type PricingRuleBulkSavePayload struct {
|
|
Items []queries.PricingRuleSaveItem `json:"items"`
|
|
}
|
|
|
|
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")
|
|
traceID := utils.TraceIDFromRequest(r)
|
|
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
|
|
|
rows, err := queries.ListPricingRules(ctx, pg)
|
|
if err != nil {
|
|
http.Error(w, "pricing rules list error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(rows)
|
|
}
|
|
}
|
|
|
|
// Very small “bulk upsert” for step-1/2: we only need to persist the multipliers+roundings for now.
|
|
// Rules are identified by UUID; new rows can be created via empty id (server generates).
|
|
func SavePricingRulesBulkHandler(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 PricingRuleBulkSavePayload
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
http.Error(w, "invalid payload", http.StatusBadRequest)
|
|
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
|
|
for _, it := range payload.Items {
|
|
// Zero means that no rounding rule has been configured yet.
|
|
if it.TryStep < 0 || it.UsdStep < 0 || it.EurStep < 0 {
|
|
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)
|
|
return
|
|
}
|
|
if id != "" {
|
|
updated++
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
http.Error(w, "pg transaction commit error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"success": true, "updated": updated})
|
|
}
|
|
}
|
|
|
|
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")
|
|
|
|
field := strings.TrimSpace(r.URL.Query().Get("field"))
|
|
if field == "" {
|
|
http.Error(w, "missing field", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
limit := 500
|
|
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
|
|
if n, err := strconv.Atoi(raw); err == nil && n > 0 && n <= 5000 {
|
|
limit = n
|
|
}
|
|
}
|
|
|
|
f := pricingRuleFiltersFromRequest(r)
|
|
|
|
traceID := utils.TraceIDFromRequest(r)
|
|
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
|
|
|
opts, err := queries.ListPricingParameterDistinctOptions(ctx, pg, field, f, limit)
|
|
if err != nil {
|
|
http.Error(w, "options lookup error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"field": field,
|
|
"options": opts,
|
|
})
|
|
}
|
|
}
|
|
|
|
func GetPricingParameterRulesHandler(pg *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
traceID := utils.TraceIDFromRequest(r)
|
|
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
|
rows, err := queries.ListPricingParameterRules(ctx, pg, pricingRuleFiltersFromRequest(r))
|
|
if err != nil {
|
|
http.Error(w, "pricing parameter rules list error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(rows)
|
|
}
|
|
}
|
|
|
|
func ExportPricingRulesHandler(pg *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
traceID := utils.TraceIDFromRequest(r)
|
|
ctx := utils.ContextWithTraceID(r.Context(), traceID)
|
|
|
|
rows, err := queries.ListPricingParameterRules(ctx, pg, pricingRuleFiltersFromRequest(r))
|
|
if err != nil {
|
|
http.Error(w, "pricing parameter rules export error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
rows = filterPricingRuleExportRows(rows, r)
|
|
sortPricingRuleExportRows(rows, strings.TrimSpace(r.URL.Query().Get("sort_by")), strings.TrimSpace(r.URL.Query().Get("desc")) != "0")
|
|
|
|
filename := fmt.Sprintf("pricing_rules_all_%s.csv", time.Now().Format("2006-01-02"))
|
|
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
|
|
_, _ = w.Write([]byte("\uFEFF"))
|
|
_, _ = w.Write([]byte(buildPricingRuleCSV(rows)))
|
|
}
|
|
}
|
|
|
|
func pricingRuleFiltersFromRequest(r *http.Request) queries.PricingRuleOptionFilters {
|
|
return queries.PricingRuleOptionFilters{
|
|
AskiliYan: splitCSV(r.URL.Query().Get("askili_yan")),
|
|
Kategori: splitCSV(r.URL.Query().Get("kategori")),
|
|
UrunIlkGrubu: splitCSV(r.URL.Query().Get("urun_ilk_grubu")),
|
|
UrunAnaGrubu: splitCSV(r.URL.Query().Get("urun_ana_grubu")),
|
|
UrunAltGrubu: splitCSV(r.URL.Query().Get("urun_alt_grubu")),
|
|
Icerik: splitCSV(r.URL.Query().Get("icerik")),
|
|
Marka: splitCSV(r.URL.Query().Get("marka")),
|
|
BrandCode: splitCSV(r.URL.Query().Get("brand_code")),
|
|
BrandGroupSec: splitCSV(r.URL.Query().Get("brand_group")),
|
|
}
|
|
}
|
|
|
|
func filterPricingRuleExportRows(rows []queries.PricingParameterRuleRow, r *http.Request) []queries.PricingParameterRuleRow {
|
|
rangeFilter := func(prefix string) (*float64, *float64) {
|
|
parse := func(raw string) *float64 {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return nil
|
|
}
|
|
v, err := strconv.ParseFloat(strings.ReplaceAll(raw, ",", "."), 64)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return &v
|
|
}
|
|
return parse(r.URL.Query().Get(prefix + "_min")), parse(r.URL.Query().Get(prefix + "_max"))
|
|
}
|
|
|
|
fields := []string{
|
|
"try_base", "try1", "try2", "try3", "try4", "try5", "try6", "try_step",
|
|
"usd_base", "usd1", "usd2", "usd3", "usd4", "usd5", "usd6", "usd_step",
|
|
"eur_base", "eur1", "eur2", "eur3", "eur4", "eur5", "eur6", "eur_step",
|
|
}
|
|
minMap := map[string]*float64{}
|
|
maxMap := map[string]*float64{}
|
|
for _, field := range fields {
|
|
minMap[field], maxMap[field] = rangeFilter(field)
|
|
}
|
|
|
|
out := make([]queries.PricingParameterRuleRow, 0, len(rows))
|
|
for _, row := range rows {
|
|
ok := true
|
|
for _, field := range fields {
|
|
value := pricingRuleNumericValue(row, field)
|
|
if minMap[field] != nil && value < *minMap[field] {
|
|
ok = false
|
|
break
|
|
}
|
|
if maxMap[field] != nil && value > *maxMap[field] {
|
|
ok = false
|
|
break
|
|
}
|
|
}
|
|
if ok {
|
|
out = append(out, row)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func pricingRuleNumericValue(row queries.PricingParameterRuleRow, field string) float64 {
|
|
if row.Rule == nil {
|
|
return 0
|
|
}
|
|
switch field {
|
|
case "try_base":
|
|
return row.Rule.TryBase
|
|
case "try1":
|
|
return row.Rule.Try1
|
|
case "try2":
|
|
return row.Rule.Try2
|
|
case "try3":
|
|
return row.Rule.Try3
|
|
case "try4":
|
|
return row.Rule.Try4
|
|
case "try5":
|
|
return row.Rule.Try5
|
|
case "try6":
|
|
return row.Rule.Try6
|
|
case "try_step":
|
|
return row.Rule.TryStep
|
|
case "usd_base":
|
|
return row.Rule.UsdBase
|
|
case "usd1":
|
|
return row.Rule.Usd1
|
|
case "usd2":
|
|
return row.Rule.Usd2
|
|
case "usd3":
|
|
return row.Rule.Usd3
|
|
case "usd4":
|
|
return row.Rule.Usd4
|
|
case "usd5":
|
|
return row.Rule.Usd5
|
|
case "usd6":
|
|
return row.Rule.Usd6
|
|
case "usd_step":
|
|
return row.Rule.UsdStep
|
|
case "eur_base":
|
|
return row.Rule.EurBase
|
|
case "eur1":
|
|
return row.Rule.Eur1
|
|
case "eur2":
|
|
return row.Rule.Eur2
|
|
case "eur3":
|
|
return row.Rule.Eur3
|
|
case "eur4":
|
|
return row.Rule.Eur4
|
|
case "eur5":
|
|
return row.Rule.Eur5
|
|
case "eur6":
|
|
return row.Rule.Eur6
|
|
case "eur_step":
|
|
return row.Rule.EurStep
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func sortPricingRuleExportRows(rows []queries.PricingParameterRuleRow, sortBy string, desc bool) {
|
|
sortBy = strings.TrimSpace(sortBy)
|
|
if sortBy == "" {
|
|
return
|
|
}
|
|
sort.SliceStable(rows, func(i, j int) bool {
|
|
li, lj := rows[i], rows[j]
|
|
switch sortBy {
|
|
case "has_rule":
|
|
if desc {
|
|
return boolRank(li.HasRule) > boolRank(lj.HasRule)
|
|
}
|
|
return boolRank(li.HasRule) < boolRank(lj.HasRule)
|
|
case "is_active":
|
|
liActive, ljActive := false, false
|
|
if li.Rule != nil {
|
|
liActive = li.Rule.IsActive
|
|
}
|
|
if lj.Rule != nil {
|
|
ljActive = lj.Rule.IsActive
|
|
}
|
|
if desc {
|
|
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":
|
|
vi := pricingRuleStringValue(li, sortBy)
|
|
vj := pricingRuleStringValue(lj, sortBy)
|
|
if desc {
|
|
return strings.Compare(vi, vj) > 0
|
|
}
|
|
return strings.Compare(vi, vj) < 0
|
|
default:
|
|
vi := pricingRuleNumericValue(li, sortBy)
|
|
vj := pricingRuleNumericValue(lj, sortBy)
|
|
if desc {
|
|
return vi > vj
|
|
}
|
|
return vi < vj
|
|
}
|
|
})
|
|
}
|
|
|
|
func boolRank(v bool) int {
|
|
if v {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func pricingRuleStringValue(row queries.PricingParameterRuleRow, field string) string {
|
|
switch field {
|
|
case "askili_yan":
|
|
return row.AskiliYan
|
|
case "kategori":
|
|
return row.Kategori
|
|
case "urun_ilk_grubu":
|
|
return row.UrunIlkGrubu
|
|
case "urun_ana_grubu":
|
|
return row.UrunAnaGrubu
|
|
case "urun_alt_grubu":
|
|
return row.UrunAltGrubu
|
|
case "icerik":
|
|
return row.Icerik
|
|
case "marka":
|
|
return row.Marka
|
|
case "brand_code":
|
|
return row.BrandCode
|
|
case "brand_group":
|
|
return row.BrandGroupSec
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
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 YUVARLAMA", "TRY TABAN", "TRY 1", "TRY 2", "TRY 3", "TRY 4", "TRY 5", "TRY 6",
|
|
"USD YUVARLAMA", "USD TABAN", "USD 1", "USD 2", "USD 3", "USD 4", "USD 5", "USD 6",
|
|
"EUR YUVARLAMA", "EUR TABAN", "EUR 1", "EUR 2", "EUR 3", "EUR 4", "EUR 5", "EUR 6",
|
|
}
|
|
var b strings.Builder
|
|
for i, h := range headers {
|
|
b.WriteString(csvEscapeValue(h))
|
|
if i == len(headers)-1 {
|
|
b.WriteString("\n")
|
|
} else {
|
|
b.WriteString(";")
|
|
}
|
|
}
|
|
for _, row := range rows {
|
|
active := "Pasif"
|
|
if row.Rule == nil || row.Rule.IsActive {
|
|
active = "Aktif"
|
|
}
|
|
values := []string{
|
|
map[bool]string{true: "Tanimli", false: "Yeni"}[row.HasRule],
|
|
active,
|
|
row.AskiliYan,
|
|
row.Kategori,
|
|
row.UrunIlkGrubu,
|
|
row.UrunAnaGrubu,
|
|
row.UrunAltGrubu,
|
|
row.Icerik,
|
|
row.Marka,
|
|
row.BrandCode,
|
|
row.BrandGroupSec,
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try_step")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try_base")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try1")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try2")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try3")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try4")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try5")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try6")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd_step")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd_base")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd1")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd2")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd3")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd4")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd5")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd6")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur_step")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur_base")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur1")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur2")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur3")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur4")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur5")),
|
|
fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur6")),
|
|
}
|
|
for i, value := range values {
|
|
b.WriteString(csvEscapeValue(value))
|
|
if i == len(values)-1 {
|
|
b.WriteString("\n")
|
|
} else {
|
|
b.WriteString(";")
|
|
}
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func csvEscapeValue(value string) string {
|
|
text := strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(value), "\r", " "), "\n", " ")
|
|
if strings.Contains(text, ";") || strings.Contains(text, "\"") {
|
|
text = `"` + strings.ReplaceAll(text, `"`, `""`) + `"`
|
|
}
|
|
return text
|
|
}
|
|
|
|
func splitCSV(raw string) []string {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return nil
|
|
}
|
|
parts := strings.Split(raw, ",")
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|