diff --git a/svc/cmd/product_performance_refresh/main.go b/svc/cmd/product_performance_refresh/main.go new file mode 100644 index 0000000..843c36d --- /dev/null +++ b/svc/cmd/product_performance_refresh/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "bssapp-backend/db" + "bssapp-backend/queries" + "context" + "flag" + "fmt" + "log" + "os" + "strconv" + "strings" + "time" + + "github.com/joho/godotenv" +) + +func main() { + mode := flag.String("mode", "delta", "refresh mode: full or delta") + stage := flag.String("stage", "all", "start stage: sales, stock, price, kpi, all") + startRaw := flag.String("start", "", "start date YYYY-MM-DD") + endRaw := flag.String("end", "", "end date YYYY-MM-DD") + prefix := flag.String("prefix", "", "optional product code prefix") + mssqlTimeoutSec := flag.Int("mssql-timeout-sec", 0, "override MSSQL connection/dial timeout seconds") + skipDelete := flag.Bool("skip-delete", false, "skip deleting existing cache rows") + resumeAfter := flag.Int("resume-after", 0, "skip this many MSSQL rows before inserting") + flag.Parse() + + _ = godotenv.Load(".env", "mail.env", ".env.local") + + if *mssqlTimeoutSec > 0 { + timeout := strconv.Itoa(*mssqlTimeoutSec) + _ = os.Setenv("MSSQL_CONNECTION_TIMEOUT_SEC", timeout) + _ = os.Setenv("MSSQL_DIAL_TIMEOUT_SEC", timeout) + } + + if err := db.ConnectMSSQL(); err != nil { + log.Fatalf("mssql connect failed: %v", err) + } + pgDB, err := db.ConnectPostgres() + if err != nil { + log.Fatalf("postgres connect failed: %v", err) + } + defer pgDB.Close() + + startDate, err := parseDateFlag(*startRaw) + if err != nil { + log.Fatalf("invalid -start: %v", err) + } + endDate, err := parseDateFlag(*endRaw) + if err != nil { + log.Fatalf("invalid -end: %v", err) + } + + ctx := context.Background() + result, err := queries.RefreshProductPerformance(ctx, pgDB, queries.ProductPerformanceRefreshRequest{ + Mode: strings.TrimSpace(*mode), + Stage: strings.TrimSpace(*stage), + StartDate: startDate, + EndDate: endDate, + ProductPrefix: strings.TrimSpace(*prefix), + SkipDelete: *skipDelete, + ResumeAfter: *resumeAfter, + }) + if err != nil { + log.Fatalf("refresh failed: %v", err) + } + + fmt.Printf("product performance refresh done: sales=%d stock=%d kpi=%d\n", + result.SalesRows, + result.StockRows, + result.KpiRows, + ) +} + +func parseDateFlag(raw string) (time.Time, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return time.Time{}, nil + } + return time.ParseInLocation("2006-01-02", raw, time.Local) +} diff --git a/svc/queries/product_performance.go b/svc/queries/product_performance.go index 04acaaa..79bb305 100644 --- a/svc/queries/product_performance.go +++ b/svc/queries/product_performance.go @@ -528,6 +528,11 @@ func refreshProductPerformanceStockChunked(ctx context.Context, pg *sql.DB, star return 0, err } defer tx.Rollback() + copyStmt, err := prepareProductPerformanceStockCopy(ctx, tx) + if err != nil { + return 0, err + } + defer copyStmt.Close() for rows.Next() { var r productPerformanceStockDaily @@ -545,13 +550,23 @@ func refreshProductPerformanceStockChunked(ctx context.Context, pg *sql.DB, star } continue } - if err := insertProductPerformanceStock(ctx, tx, r); err != nil { + if _, err := copyStmt.ExecContext(ctx, + r.StockDate, r.ProductCode, r.ColorCode, r.YakaKodu, r.StockQty, + r.InQty, r.OutQty, r.KpiInQty, r.KpiOutQty, r.SalesMovementQty, + r.ProductionInQty, r.PurchaseInQty, r.ConsumptionOutQty, r.CountDiffQty, + ); err != nil { return count, err } count++ chunkRows++ if chunkRows >= chunkSize { log.Printf("[ProductPerformanceRefresh] stock chunk commit start inserted_rows=%d scanned_rows=%d", count, seen) + if _, err := copyStmt.ExecContext(ctx); err != nil { + return count, err + } + if err := copyStmt.Close(); err != nil { + return count, err + } if err := tx.Commit(); err != nil { return count, err } @@ -560,6 +575,10 @@ func refreshProductPerformanceStockChunked(ctx context.Context, pg *sql.DB, star if err != nil { return count, err } + copyStmt, err = prepareProductPerformanceStockCopy(ctx, tx) + if err != nil { + return count, err + } chunkRows = 0 } } @@ -568,11 +587,18 @@ func refreshProductPerformanceStockChunked(ctx context.Context, pg *sql.DB, star } if chunkRows > 0 { log.Printf("[ProductPerformanceRefresh] stock final chunk commit start inserted_rows=%d scanned_rows=%d", count, seen) + if _, err := copyStmt.ExecContext(ctx); err != nil { + return count, err + } + if err := copyStmt.Close(); err != nil { + return count, err + } if err := tx.Commit(); err != nil { return count, err } log.Printf("[ProductPerformanceRefresh] stock final chunk commit done inserted_rows=%d scanned_rows=%d elapsed=%s", count, seen, time.Since(started).Round(time.Second)) } else { + _ = copyStmt.Close() _ = tx.Rollback() } log.Printf("[ProductPerformanceRefresh] stock refresh done inserted_rows=%d scanned_rows=%d elapsed=%s", count, seen, time.Since(started).Round(time.Second)) @@ -583,6 +609,26 @@ type productPerformanceStockExec interface { ExecContext(context.Context, string, ...any) (sql.Result, error) } +func prepareProductPerformanceStockCopy(ctx context.Context, tx *sql.Tx) (*sql.Stmt, error) { + return tx.PrepareContext(ctx, pq.CopyIn( + "mk_product_performance_stock_daily", + "stock_date", + "product_code", + "color_code", + "yaka_kodu", + "stock_qty", + "in_qty", + "out_qty", + "kpi_in_qty", + "kpi_out_qty", + "sales_movement_qty", + "production_in_qty", + "purchase_in_qty", + "consumption_out_qty", + "count_diff_qty", + )) +} + func insertProductPerformanceStock(ctx context.Context, exec productPerformanceStockExec, r productPerformanceStockDaily) error { _, err := exec.ExecContext(ctx, ` INSERT INTO mk_product_performance_stock_daily ( diff --git a/svc/routes/translations.go b/svc/routes/translations.go index 9c6e50a..22105f2 100644 --- a/svc/routes/translations.go +++ b/svc/routes/translations.go @@ -327,6 +327,15 @@ WITH base AS ( COALESCE(NULLIF(source_text_tr, ''), translated_text, '') AS source_text_tr FROM mk_translator WHERE COALESCE(NULLIF(source_text_tr, ''), translated_text, '') <> '' + AND status = 'approved' + AND ( + COALESCE(NULLIF(provider_meta->>'source_type', ''), 'dummy') <> 'dummy' + OR is_manual = true + ) + AND char_length(COALESCE(NULLIF(source_text_tr, ''), translated_text, '')) <= 180 + AND COALESCE(NULLIF(source_text_tr, ''), translated_text, '') !~ '[<>{}\[\]]' + AND position(chr(96) in COALESCE(NULLIF(source_text_tr, ''), translated_text, '')) = 0 + AND COALESCE(NULLIF(source_text_tr, ''), translated_text, '') !~ '(=>|\|\||&&|\?\?|/\*|\*/|\.trim\(|\.replace\(|\.map\(|\.filter\()' ORDER BY t_key, CASE WHEN lang_code='tr' THEN 0 ELSE 1 END, updated_at DESC ), target AS ( @@ -335,6 +344,7 @@ target AS ( COALESCE(translated_text, '') AS translated_text FROM mk_translator WHERE lang_code=$1 + AND (status = 'approved' OR is_manual = true) ORDER BY t_key, is_manual DESC, updated_at DESC ) SELECT @@ -993,10 +1003,12 @@ func collectSourceSeeds(pgDB *sql.DB, mssqlDB *sql.DB, limit int) []sourceSeed { return out } } - for _, row := range collectDummySeeds(limit - len(out)) { - appendSeed(row) - if len(out) >= limit { - return out + if strings.EqualFold(strings.TrimSpace(os.Getenv("TRANSLATION_DUMMY_SEEDS_ENABLED")), "true") { + for _, row := range collectDummySeeds(limit - len(out)) { + appendSeed(row) + if len(out) >= limit { + return out + } } }