diff --git a/svc/product_series_auto_scheduler.go b/svc/product_series_auto_scheduler.go index 7563fb7..e80e52b 100644 --- a/svc/product_series_auto_scheduler.go +++ b/svc/product_series_auto_scheduler.go @@ -14,6 +14,7 @@ import ( "sort" "strconv" "strings" + "sync" "sync/atomic" "time" @@ -77,6 +78,90 @@ type productSeriesAutoStats struct { Skipped int } +var productSeriesFallbackMu sync.Mutex +var productSeriesFallbackCachedCode string +var productSeriesFallbackCachedID int64 +var productSeriesFallbackCachedAt time.Time + +func productSeriesFallbackEnabled() bool { + // Default on: user explicitly requested "stok var ama seri yoksa 1- ata". + raw := strings.TrimSpace(strings.ToLower(os.Getenv("PRODUCT_SERIES_FALLBACK_ON_EMPTY"))) + if raw == "" { + return true + } + return raw == "1" || raw == "true" || raw == "on" || raw == "yes" +} + +func productSeriesFallbackCode() string { + code := strings.TrimSpace(os.Getenv("PRODUCT_SERIES_FALLBACK_SERIES_CODE")) + if code == "" { + code = "1-" + } + return code +} + +func productSeriesResolveFallbackSeries(ctx context.Context, pg *sql.DB) (int64, string, error) { + if !productSeriesFallbackEnabled() { + return 0, "", nil + } + code := productSeriesFallbackCode() + if code == "" { + return 0, "", nil + } + + productSeriesFallbackMu.Lock() + // Cache for a bit to avoid repeated lookups under high throughput. + if productSeriesFallbackCachedCode == code && productSeriesFallbackCachedAt.After(time.Now().Add(-10*time.Minute)) { + id := productSeriesFallbackCachedID + productSeriesFallbackMu.Unlock() + return id, code, nil + } + productSeriesFallbackMu.Unlock() + + var id int64 + var gotCode string + err := pg.QueryRowContext(ctx, ` +SELECT id, COALESCE(code,'') +FROM dfgrp +WHERE master='zbggseri' + AND COALESCE(is_active, TRUE)=TRUE + AND ( + code = $1 + OR title = $1 + OR title LIKE $1 || '%' + ) +ORDER BY + CASE + WHEN code = $1 THEN 0 + WHEN title = $1 THEN 1 + ELSE 2 + END, + id +LIMIT 1 +`, code).Scan(&id, &gotCode) + if err != nil { + if err == sql.ErrNoRows { + // cache negative for a short period too + productSeriesFallbackMu.Lock() + productSeriesFallbackCachedCode = code + productSeriesFallbackCachedID = 0 + productSeriesFallbackCachedAt = time.Now() + productSeriesFallbackMu.Unlock() + return 0, code, nil + } + return 0, code, err + } + if gotCode != "" { + code = gotCode + } + productSeriesFallbackMu.Lock() + productSeriesFallbackCachedCode = code + productSeriesFallbackCachedID = id + productSeriesFallbackCachedAt = time.Now() + productSeriesFallbackMu.Unlock() + return id, code, nil +} + var productSeriesSizeGroups = 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", "46"}, @@ -345,6 +430,35 @@ func productSeriesApplyVariant(ctx context.Context, pg *sql.DB, v productSeriesA return 0, 1, nil } selected := productSeriesSelectRules(v, rules) + if len(selected) == 0 { + fallbackID, _, err := productSeriesResolveFallbackSeries(ctx, pg) + if err != nil { + return 0, 1, err + } + if fallbackID > 0 { + // Only apply fallback when the variant has no assignments yet. + var exists int + checkErr := pg.QueryRowContext(ctx, ` +SELECT 1 +FROM zbggseri +WHERE mmitem_id=$1 + AND dim1=$2 + AND (($3::bigint IS NULL AND dim3 IS NULL) OR dim3=$3::bigint) +LIMIT 1 +`, mmitemID, dim1ID, nullableInt64ForAuto(dim3ID)).Scan(&exists) + if checkErr == nil { + // keep existing manual/previous assignment; nothing to do + return 0, 0, nil + } + if checkErr != nil && checkErr != sql.ErrNoRows { + return 0, 1, checkErr + } + // Use the fallback series as the single selected rule. + selected = []productSeriesAutoRule{{SeriesID: fallbackID}} + } else { + return 0, 1, nil + } + } if !apply { return len(selected), 0, nil }