diff --git a/svc/product_series_auto_scheduler.go b/svc/product_series_auto_scheduler.go index e80e52b..e1e8c14 100644 --- a/svc/product_series_auto_scheduler.go +++ b/svc/product_series_auto_scheduler.go @@ -92,6 +92,15 @@ func productSeriesFallbackEnabled() bool { return raw == "1" || raw == "true" || raw == "on" || raw == "yes" } +func productSeriesFallbackAutoCreateEnabled() bool { + // Default on: avoid manual intervention by auto-creating the fallback dfgrp row when missing. + raw := strings.TrimSpace(strings.ToLower(os.Getenv("PRODUCT_SERIES_FALLBACK_AUTO_CREATE"))) + 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 == "" { @@ -141,13 +150,61 @@ LIMIT 1 `, code).Scan(&id, &gotCode) if err != nil { if err == sql.ErrNoRows { - // cache negative for a short period too + if !productSeriesFallbackAutoCreateEnabled() { + // cache negative for a short period too + productSeriesFallbackMu.Lock() + productSeriesFallbackCachedCode = code + productSeriesFallbackCachedID = 0 + productSeriesFallbackCachedAt = time.Now() + productSeriesFallbackMu.Unlock() + return 0, code, nil + } + + // Auto-create the fallback series definition (best-effort) to avoid manual steps. + // We guard with the same mutex to avoid duplicate inserts within this process. productSeriesFallbackMu.Lock() + defer productSeriesFallbackMu.Unlock() + + // Re-check under lock in case another goroutine created it while we were waiting. + if productSeriesFallbackCachedCode == code && productSeriesFallbackCachedID > 0 && + productSeriesFallbackCachedAt.After(time.Now().Add(-10*time.Minute)) { + return productSeriesFallbackCachedID, code, nil + } + + var createdID int64 + createErr := pg.QueryRowContext(ctx, ` +INSERT INTO dfgrp (code, title, is_active, typ, master, parent_filter, sort_order, is_required, notes) +SELECT $1, $1, TRUE, 'opt', 'zbggseri', '', 0, FALSE, 'auto-created fallback series' +WHERE NOT EXISTS ( + SELECT 1 FROM dfgrp WHERE master='zbggseri' AND code=$1 +) +RETURNING id +`, code).Scan(&createdID) + if createErr != nil { + // If RETURNING didn't return because it already exists, select it now. + var sid int64 + var scode string + selErr := pg.QueryRowContext(ctx, ` +SELECT id, COALESCE(code,'') +FROM dfgrp +WHERE master='zbggseri' AND code=$1 +ORDER BY id +LIMIT 1 +`, code).Scan(&sid, &scode) + if selErr != nil { + // still missing; treat as disabled for now + productSeriesFallbackCachedCode = code + productSeriesFallbackCachedID = 0 + productSeriesFallbackCachedAt = time.Now() + return 0, code, nil + } + createdID = sid + } + productSeriesFallbackCachedCode = code - productSeriesFallbackCachedID = 0 + productSeriesFallbackCachedID = createdID productSeriesFallbackCachedAt = time.Now() - productSeriesFallbackMu.Unlock() - return 0, code, nil + return createdID, code, nil } return 0, code, err }