Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -71,7 +72,7 @@ func SendOrderMarketMailHandler(pg *sql.DB, mssql *sql.DB, ml *mailer.GraphMaile
|
||||
return
|
||||
}
|
||||
|
||||
pdfBytes, header, err := buildOrderPDFBytesForMail(mssql, orderID)
|
||||
pdfBytes, header, err := buildOrderPDFBytesForMail(mssql, pg, orderID)
|
||||
if err != nil {
|
||||
http.Error(w, "pdf build error: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -241,7 +242,7 @@ ORDER BY email
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func buildOrderPDFBytesForMail(db *sql.DB, orderID string) ([]byte, *OrderHeader, error) {
|
||||
func buildOrderPDFBytesForMail(db *sql.DB, pgDB *sql.DB, orderID string) ([]byte, *OrderHeader, error) {
|
||||
header, err := getOrderHeaderFromDB(db, orderID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -262,7 +263,19 @@ func buildOrderPDFBytesForMail(db *sql.DB, orderID string) ([]byte, *OrderHeader
|
||||
}
|
||||
}
|
||||
|
||||
rows := normalizeOrderLinesForPdf(lines)
|
||||
if pgDB == nil {
|
||||
return nil, nil, errors.New("product-size-match db not initialized")
|
||||
}
|
||||
sizeMatchData, err := loadProductSizeMatchData(pgDB)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
rows := normalizeOrderLinesForPdf(lines, sizeMatchData)
|
||||
for _, rr := range rows {
|
||||
if strings.TrimSpace(rr.Category) == "" {
|
||||
return nil, nil, fmt.Errorf("product-size-match unmapped row: %s/%s/%s", rr.Model, rr.GroupMain, rr.GroupSub)
|
||||
}
|
||||
}
|
||||
|
||||
pdf, err := newOrderPdf()
|
||||
if err != nil {
|
||||
|
||||
@@ -87,6 +87,7 @@ type OrderLineRaw struct {
|
||||
LineDescription sql.NullString
|
||||
UrunAnaGrubu sql.NullString
|
||||
UrunAltGrubu sql.NullString
|
||||
YetiskinGarson sql.NullString
|
||||
IsClosed sql.NullBool
|
||||
WithHoldingTaxType sql.NullString
|
||||
DOVCode sql.NullString
|
||||
@@ -101,20 +102,21 @@ type OrderLineRaw struct {
|
||||
=========================================================== */
|
||||
|
||||
type PdfRow struct {
|
||||
Model string
|
||||
Color string
|
||||
GroupMain string
|
||||
GroupSub string
|
||||
Description string
|
||||
Category string
|
||||
SizeQty map[string]int
|
||||
TotalQty int
|
||||
Price float64
|
||||
Currency string
|
||||
Amount float64
|
||||
Termin string
|
||||
IsClosed bool
|
||||
OrderLineIDs map[string]string
|
||||
Model string
|
||||
Color string
|
||||
GroupMain string
|
||||
GroupSub string
|
||||
YetiskinGarson string
|
||||
Description string
|
||||
Category string
|
||||
SizeQty map[string]int
|
||||
TotalQty int
|
||||
Price float64
|
||||
Currency string
|
||||
Amount float64
|
||||
Termin string
|
||||
IsClosed bool
|
||||
OrderLineIDs map[string]string
|
||||
|
||||
ClosedSizes map[string]bool // 🆕 her beden için IsClosed bilgisi
|
||||
}
|
||||
@@ -332,7 +334,140 @@ func parseNumericSize(v string) (int, bool) {
|
||||
return n, true
|
||||
}
|
||||
|
||||
func detectBedenGroupGo(bedenList []string, ana, alt string) string {
|
||||
func deriveKategoriTokenGo(urunKategori, yetiskinGarson string) string {
|
||||
kat := normalizeTextForMatchGo(urunKategori)
|
||||
if strings.Contains(kat, "GARSON") {
|
||||
return "GARSON"
|
||||
}
|
||||
if strings.Contains(kat, "YETISKIN") {
|
||||
return "YETISKIN"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeRuleAltGroupGo(urunAltGrubu string) string {
|
||||
return normalizeTextForMatchGo(urunAltGrubu)
|
||||
}
|
||||
|
||||
func pickBestGroupFromCandidatesGo(groupKeys, bedenList []string, schemas map[string][]string) string {
|
||||
if len(groupKeys) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(groupKeys) == 1 {
|
||||
return strings.TrimSpace(groupKeys[0])
|
||||
}
|
||||
|
||||
normalizedBeden := make([]string, 0, len(bedenList))
|
||||
for _, b := range bedenList {
|
||||
n := normalizeBedenLabelGo(b)
|
||||
if strings.TrimSpace(n) == "" {
|
||||
n = " "
|
||||
}
|
||||
normalizedBeden = append(normalizedBeden, n)
|
||||
}
|
||||
|
||||
if len(normalizedBeden) == 0 {
|
||||
return strings.TrimSpace(groupKeys[0])
|
||||
}
|
||||
|
||||
bestKey := strings.TrimSpace(groupKeys[0])
|
||||
bestScore := -1
|
||||
for _, key := range groupKeys {
|
||||
k := strings.TrimSpace(key)
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
normalizedSchema := map[string]bool{}
|
||||
for _, sv := range schemas[k] {
|
||||
ns := normalizeBedenLabelGo(sv)
|
||||
if strings.TrimSpace(ns) == "" {
|
||||
ns = " "
|
||||
}
|
||||
normalizedSchema[ns] = true
|
||||
}
|
||||
score := 0
|
||||
for _, b := range normalizedBeden {
|
||||
if normalizedSchema[b] {
|
||||
score++
|
||||
}
|
||||
}
|
||||
if score > bestScore {
|
||||
bestScore = score
|
||||
bestKey = k
|
||||
}
|
||||
}
|
||||
return bestKey
|
||||
}
|
||||
|
||||
func resolveGroupFromProductSizeMatchRulesGo(
|
||||
matchData *ProductSizeMatchResponse,
|
||||
bedenList []string,
|
||||
urunAnaGrubu, urunKategori, yetiskinGarson, urunAltGrubu string,
|
||||
) string {
|
||||
if matchData == nil || len(matchData.Rules) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
kategoriToken := deriveKategoriTokenGo(urunKategori, yetiskinGarson)
|
||||
ana := normalizeTextForMatchGo(urunAnaGrubu)
|
||||
alt := normalizeRuleAltGroupGo(urunAltGrubu)
|
||||
if kategoriToken == "" || ana == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
candidateGroupKeys := make([]string, 0, 2)
|
||||
seen := map[string]bool{}
|
||||
|
||||
for i := range matchData.Rules {
|
||||
rule := &matchData.Rules[i]
|
||||
if normalizeTextForMatchGo(rule.UrunAnaGrubu) != ana {
|
||||
continue
|
||||
}
|
||||
ruleKategori := normalizeTextForMatchGo(rule.Kategori)
|
||||
if ruleKategori != kategoriToken {
|
||||
continue
|
||||
}
|
||||
|
||||
ruleAlt := normalizeTextForMatchGo(rule.UrunAltGrubu)
|
||||
if ruleAlt != alt {
|
||||
continue
|
||||
}
|
||||
for _, g := range rule.GroupKeys {
|
||||
key := strings.TrimSpace(g)
|
||||
if key == "" || seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
candidateGroupKeys = append(candidateGroupKeys, key)
|
||||
}
|
||||
}
|
||||
|
||||
if len(candidateGroupKeys) == 0 {
|
||||
return ""
|
||||
}
|
||||
return pickBestGroupFromCandidatesGo(candidateGroupKeys, bedenList, matchData.Schemas)
|
||||
}
|
||||
|
||||
func detectBedenGroupGo(
|
||||
matchData *ProductSizeMatchResponse,
|
||||
bedenList []string,
|
||||
ana, alt, urunKategori, yetiskinGarson string,
|
||||
) string {
|
||||
ruleBased := resolveGroupFromProductSizeMatchRulesGo(
|
||||
matchData,
|
||||
bedenList,
|
||||
ana,
|
||||
urunKategori,
|
||||
yetiskinGarson,
|
||||
alt,
|
||||
)
|
||||
if ruleBased != "" {
|
||||
return ruleBased
|
||||
}
|
||||
if matchData != nil && len(matchData.Rules) > 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
ana = normalizeTextForMatchGo(ana)
|
||||
alt = normalizeTextForMatchGo(alt)
|
||||
|
||||
@@ -368,6 +503,15 @@ func detectBedenGroupGo(bedenList []string, ana, alt string) string {
|
||||
strings.Contains(ana, "YETİŞKIN/GARSON") || strings.Contains(alt, "YETİŞKIN/GARSON") ||
|
||||
strings.Contains(ana, "YETİŞKİN/GARSON") || strings.Contains(alt, "YETİŞKİN/GARSON")
|
||||
|
||||
// Ayakkabi kurali garsondan once uygulanmali:
|
||||
// GARSON + AYAKKABI => ayk_garson, digerleri => ayk
|
||||
if strings.Contains(ana, "AYAKKABI") || strings.Contains(alt, "AYAKKABI") {
|
||||
if hasGarson {
|
||||
return catAykGar
|
||||
}
|
||||
return catAyk
|
||||
}
|
||||
|
||||
// ✅ Garson → yaş (ürün tipi fark etmeksizin)
|
||||
if hasGarson {
|
||||
return catYas
|
||||
@@ -618,6 +762,7 @@ func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) {
|
||||
L.LineDescription,
|
||||
P.ProductAtt01Desc,
|
||||
P.ProductAtt02Desc,
|
||||
P.ProductAtt44Desc,
|
||||
L.IsClosed,
|
||||
L.WithHoldingTaxTypeCode,
|
||||
L.DOVCode,
|
||||
@@ -656,6 +801,7 @@ func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) {
|
||||
&l.LineDescription,
|
||||
&l.UrunAnaGrubu,
|
||||
&l.UrunAltGrubu,
|
||||
&l.YetiskinGarson,
|
||||
&l.IsClosed,
|
||||
&l.WithHoldingTaxType,
|
||||
&l.DOVCode,
|
||||
@@ -675,7 +821,7 @@ func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) {
|
||||
4) NORMALIZE + CATEGORY MAP
|
||||
=========================================================== */
|
||||
|
||||
func normalizeOrderLinesForPdf(lines []OrderLineRaw) []PdfRow {
|
||||
func normalizeOrderLinesForPdf(lines []OrderLineRaw, matchData *ProductSizeMatchResponse) []PdfRow {
|
||||
type comboKey struct {
|
||||
Model, Color, Color2 string
|
||||
}
|
||||
@@ -701,16 +847,17 @@ func normalizeOrderLinesForPdf(lines []OrderLineRaw) []PdfRow {
|
||||
|
||||
if _, ok := merged[key]; !ok {
|
||||
merged[key] = &PdfRow{
|
||||
Model: model,
|
||||
Color: displayColor,
|
||||
GroupMain: s64(raw.UrunAnaGrubu),
|
||||
GroupSub: s64(raw.UrunAltGrubu),
|
||||
Description: s64(raw.LineDescription),
|
||||
SizeQty: make(map[string]int),
|
||||
Currency: s64(raw.DocCurrencyCode),
|
||||
Price: f64(raw.Price),
|
||||
OrderLineIDs: make(map[string]string),
|
||||
ClosedSizes: make(map[string]bool), // 🆕
|
||||
Model: model,
|
||||
Color: displayColor,
|
||||
GroupMain: s64(raw.UrunAnaGrubu),
|
||||
GroupSub: s64(raw.UrunAltGrubu),
|
||||
YetiskinGarson: s64(raw.YetiskinGarson),
|
||||
Description: s64(raw.LineDescription),
|
||||
SizeQty: make(map[string]int),
|
||||
Currency: s64(raw.DocCurrencyCode),
|
||||
Price: f64(raw.Price),
|
||||
OrderLineIDs: make(map[string]string),
|
||||
ClosedSizes: make(map[string]bool), // 🆕
|
||||
}
|
||||
}
|
||||
row := merged[key]
|
||||
@@ -751,7 +898,7 @@ func normalizeOrderLinesForPdf(lines []OrderLineRaw) []PdfRow {
|
||||
for s := range r.SizeQty {
|
||||
sizes = append(sizes, s)
|
||||
}
|
||||
r.Category = detectBedenGroupGo(sizes, r.GroupMain, r.GroupSub)
|
||||
r.Category = detectBedenGroupGo(matchData, sizes, r.GroupMain, r.GroupSub, r.YetiskinGarson, r.YetiskinGarson)
|
||||
r.Amount = float64(r.TotalQty) * r.Price
|
||||
out = append(out, *r)
|
||||
}
|
||||
@@ -1555,7 +1702,7 @@ func renderOrderGrid(pdf *gofpdf.Fpdf, header *OrderHeader, rows []PdfRow, hasVa
|
||||
HTTP HANDLER → /api/order/pdf/{id}
|
||||
=========================================================== */
|
||||
|
||||
func OrderPDFHandler(db *sql.DB) http.Handler {
|
||||
func OrderPDFHandler(db *sql.DB, pgDB *sql.DB) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
orderID := mux.Vars(r)["id"]
|
||||
@@ -1612,7 +1759,30 @@ func OrderPDFHandler(db *sql.DB) http.Handler {
|
||||
}
|
||||
|
||||
// Normalize
|
||||
rows := normalizeOrderLinesForPdf(lines)
|
||||
var sizeMatchData *ProductSizeMatchResponse
|
||||
if pgDB == nil {
|
||||
http.Error(w, "product-size-match db not initialized", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if m, err := loadProductSizeMatchData(pgDB); err != nil {
|
||||
log.Printf("❌ OrderPDF product-size-match load failed orderID=%s: %v", orderID, err)
|
||||
http.Error(w, "product-size-match load failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
} else {
|
||||
sizeMatchData = m
|
||||
}
|
||||
rows := normalizeOrderLinesForPdf(lines, sizeMatchData)
|
||||
unmapped := make([]string, 0)
|
||||
for _, rr := range rows {
|
||||
if strings.TrimSpace(rr.Category) == "" {
|
||||
unmapped = append(unmapped, fmt.Sprintf("%s/%s/%s", rr.Model, rr.GroupMain, rr.GroupSub))
|
||||
}
|
||||
}
|
||||
if len(unmapped) > 0 {
|
||||
log.Printf("❌ OrderPDF product-size-match unmapped orderID=%s rows=%v", orderID, unmapped)
|
||||
http.Error(w, "product-size-match unmapped rows", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Printf("📄 OrderPDF normalized rows orderID=%s rowCount=%d", orderID, len(rows))
|
||||
for i, rr := range rows {
|
||||
if i >= 30 {
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"bssapp-backend/queries"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -79,6 +81,16 @@ func GetOrderInventoryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Debug: beden/adet özetini tek satırda yazdır (saha doğrulaması için)
|
||||
if len(list) > 0 {
|
||||
keys := make([]string, 0, len(list))
|
||||
for _, it := range list {
|
||||
keys = append(keys, fmt.Sprintf("%s:%g", it.Beden, it.KullanilabilirAdet))
|
||||
}
|
||||
sort.Strings(keys)
|
||||
log.Printf("🔎 [ORDERINV] beden/qty -> %s", keys)
|
||||
}
|
||||
|
||||
log.Printf("✅ [ORDERINV] %s / %s / %s -> %d kayıt döndü", code, color, color2, len(list))
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
106
svc/routes/product_size_match.go
Normal file
106
svc/routes/product_size_match.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
type ProductSizeMatchRule struct {
|
||||
ProductGroupID int `json:"product_group_id"`
|
||||
Kategori string `json:"kategori"`
|
||||
UrunAnaGrubu string `json:"urun_ana_grubu"`
|
||||
UrunAltGrubu string `json:"urun_alt_grubu"`
|
||||
GroupKeys []string `json:"group_keys"`
|
||||
}
|
||||
|
||||
type ProductSizeMatchResponse struct {
|
||||
Rules []ProductSizeMatchRule `json:"rules"`
|
||||
Schemas map[string][]string `json:"schemas"`
|
||||
}
|
||||
|
||||
func defaultSizeSchemas() map[string][]string {
|
||||
return map[string][]string{
|
||||
"tak": {"44", "46", "48", "50", "52", "54", "56", "58", "60", "62", "64", "66", "68", "70", "72", "74"},
|
||||
"ayk": {"39", "40", "41", "42", "43", "44", "45"},
|
||||
"ayk_garson": {"22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "STD"},
|
||||
"yas": {"2", "4", "6", "8", "10", "12", "14"},
|
||||
"pan": {"38", "40", "42", "44", "46", "48", "50", "52", "54", "56", "58", "60", "62", "64", "66", "68"},
|
||||
"gom": {"XS", "S", "M", "L", "XL", "2XL", "3XL", "4XL", "5XL", "6XL", "7XL"},
|
||||
"aksbir": {" ", "44", "STD", "110", "115", "120", "125", "130", "135"},
|
||||
}
|
||||
}
|
||||
|
||||
func loadProductSizeMatchData(pgDB *sql.DB) (*ProductSizeMatchResponse, error) {
|
||||
rows, err := pgDB.Query(`
|
||||
SELECT
|
||||
pg.id AS product_group_id,
|
||||
COALESCE(pg.kategori, ''),
|
||||
COALESCE(pg.urun_ana_grubu, ''),
|
||||
COALESCE(pg.urun_alt_grubu, ''),
|
||||
COALESCE(
|
||||
array_agg(DISTINCT sm.size_group_key ORDER BY sm.size_group_key)
|
||||
FILTER (WHERE sm.size_group_key IS NOT NULL),
|
||||
ARRAY[]::text[]
|
||||
) AS group_keys
|
||||
FROM mk_product_size_match sm
|
||||
JOIN mk_product_group pg
|
||||
ON pg.id = sm.product_group_id
|
||||
GROUP BY
|
||||
pg.id, pg.kategori, pg.urun_ana_grubu, pg.urun_alt_grubu
|
||||
ORDER BY pg.id
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
resp := &ProductSizeMatchResponse{
|
||||
Rules: make([]ProductSizeMatchRule, 0),
|
||||
Schemas: defaultSizeSchemas(),
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var item ProductSizeMatchRule
|
||||
var arr pq.StringArray
|
||||
if err := rows.Scan(
|
||||
&item.ProductGroupID,
|
||||
&item.Kategori,
|
||||
&item.UrunAnaGrubu,
|
||||
&item.UrunAltGrubu,
|
||||
&arr,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.GroupKeys = make([]string, 0, len(arr))
|
||||
for _, g := range arr {
|
||||
g = strings.TrimSpace(g)
|
||||
if g == "" {
|
||||
continue
|
||||
}
|
||||
item.GroupKeys = append(item.GroupKeys, g)
|
||||
}
|
||||
resp.Rules = append(resp.Rules, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GET /api/product-size-match/rules
|
||||
func GetProductSizeMatchRulesHandler(pgDB *sql.DB) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp, err := loadProductSizeMatchData(pgDB)
|
||||
if err != nil {
|
||||
http.Error(w, "product-size-match load failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user