Files
bssapp/svc/routes/orderproductionitems.go
2026-04-15 15:54:44 +03:00

918 lines
35 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/internal/mailer"
"bssapp-backend/models"
"bssapp-backend/queries"
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"regexp"
"strings"
"time"
"github.com/gorilla/mux"
mssql "github.com/microsoft/go-mssqldb"
)
var baggiModelCodeRegex = regexp.MustCompile(`^[A-Z][0-9]{3}-[A-Z]{3}[0-9]{5}$`)
const productionBarcodeTypeCode = "BAGGI3"
// ======================================================
// 📌 OrderProductionItemsRoute — U ürün satırları
// ======================================================
func OrderProductionItemsRoute(mssql *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
id := mux.Vars(r)["id"]
if id == "" {
http.Error(w, "OrderHeaderID bulunamadı", http.StatusBadRequest)
return
}
rows, err := queries.GetOrderProductionItems(mssql, id)
if err != nil {
log.Printf("❌ SQL sorgu hatası: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
defer rows.Close()
list := make([]models.OrderProductionItem, 0, 100)
for rows.Next() {
var o models.OrderProductionItem
if err := rows.Scan(
&o.OrderHeaderID,
&o.OrderLineID,
&o.ItemTypeCode,
&o.OldDim1,
&o.OldDim3,
&o.OldItemCode,
&o.OldColor,
&o.OldColorDescription,
&o.OldDim2,
&o.OldDesc,
&o.OldQty,
&o.NewItemCode,
&o.NewColor,
&o.NewDim2,
&o.NewDesc,
&o.OldDueDate,
&o.NewDueDate,
&o.IsVariantMissing,
); err != nil {
log.Printf("⚠️ SCAN HATASI: %v", err)
continue
}
list = append(list, o)
}
if err := rows.Err(); err != nil {
log.Printf("⚠️ rows.Err(): %v", err)
}
if err := json.NewEncoder(w).Encode(list); err != nil {
log.Printf("❌ encode error: %v", err)
}
})
}
func OrderProductionCdItemLookupsRoute(mssql *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
rid := fmt.Sprintf("opl-%d", time.Now().UnixNano())
w.Header().Set("X-Debug-Request-Id", rid)
log.Printf("[OrderProductionCdItemLookupsRoute] rid=%s started", rid)
lookups, err := queries.GetOrderProductionLookupOptions(mssql)
if err != nil {
log.Printf("[OrderProductionCdItemLookupsRoute] rid=%s lookup error: %v", rid, err)
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(map[string]any{
"message": "Veritabani hatasi",
"step": "cditem-lookups",
"detail": err.Error(),
"requestId": rid,
})
return
}
log.Printf("[OrderProductionCdItemLookupsRoute] rid=%s success", rid)
if err := json.NewEncoder(w).Encode(lookups); err != nil {
log.Printf("[OrderProductionCdItemLookupsRoute] rid=%s encode error: %v", rid, err)
}
})
}
// ======================================================
// 📌 OrderProductionInsertMissingRoute — eksik varyantları ekler
// ======================================================
func OrderProductionInsertMissingRoute(mssql *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
id := mux.Vars(r)["id"]
if id == "" {
http.Error(w, "OrderHeaderID bulunamadı", http.StatusBadRequest)
return
}
claims, _ := auth.GetClaimsFromContext(r.Context())
username := ""
if claims != nil {
username = claims.Username
}
if username == "" {
username = "system"
}
affected, err := queries.InsertMissingProductionVariants(mssql, id, username)
if err != nil {
log.Printf("❌ INSERT varyant hatası: %v", err)
http.Error(w, "Veritabanı hatası", http.StatusInternalServerError)
return
}
resp := map[string]any{
"inserted": affected,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("❌ encode error: %v", err)
}
})
}
// ======================================================
// OrderProductionValidateRoute - yeni model varyant kontrolu
// ======================================================
func OrderProductionValidateRoute(mssql *sql.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
rid := fmt.Sprintf("opv-%d", time.Now().UnixNano())
w.Header().Set("X-Debug-Request-Id", rid)
start := time.Now()
id := mux.Vars(r)["id"]
if id == "" {
http.Error(w, "OrderHeaderID bulunamadi", http.StatusBadRequest)
return
}
var payload models.OrderProductionUpdatePayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "Gecersiz istek", http.StatusBadRequest)
return
}
if err := validateUpdateLines(payload.Lines); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Printf("[OrderProductionValidateRoute] rid=%s orderHeaderID=%s payload lineCount=%d insertMissing=%t cdItemCount=%d attributeCount=%d",
rid, id, len(payload.Lines), payload.InsertMissing, len(payload.CdItems), len(payload.ProductAttributes))
newLines, existingLines := splitLinesByCdItemDraft(payload.Lines, payload.CdItems)
newCodes := uniqueCodesFromLines(newLines)
existingCodes := uniqueCodesFromLines(existingLines)
missing := make([]models.OrderProductionMissingVariant, 0)
targets := make([]models.OrderProductionMissingVariant, 0)
stepStart := time.Now()
if len(newLines) > 0 {
err := runWithTransientMSSQLRetry("validate_build_targets_missing", 3, 500*time.Millisecond, func() error {
var stepErr error
targets, stepErr = buildTargetVariants(mssql, id, newLines)
if stepErr != nil {
return stepErr
}
missing, stepErr = buildMissingVariantsFromTargets(mssql, id, targets)
return stepErr
})
if err != nil {
log.Printf("[OrderProductionValidateRoute] rid=%s orderHeaderID=%s step=build_missing failed duration_ms=%d err=%v",
rid, id, time.Since(stepStart).Milliseconds(), err)
writeDBError(w, http.StatusInternalServerError, "validate_missing_variants", id, "", len(newLines), err)
return
}
}
log.Printf("[OrderProductionValidateRoute] rid=%s orderHeaderID=%s lineCount=%d newLineCount=%d existingLineCount=%d targetVariantCount=%d missingCount=%d build_missing_ms=%d total_ms=%d",
rid, id, len(payload.Lines), len(newLines), len(existingLines), len(targets), len(missing), time.Since(stepStart).Milliseconds(), time.Since(start).Milliseconds())
log.Printf("[OrderProductionValidateRoute] rid=%s orderHeaderID=%s scope newCodes=%v existingCodes=%v",
rid, id, newCodes, existingCodes)
resp := map[string]any{
"missingCount": len(missing),
"missing": missing,
"barcodeValidationCount": 0,
"barcodeValidations": []models.OrderProductionBarcodeValidation{},
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("❌ encode error: %v", err)
}
})
}
// ======================================================
// OrderProductionApplyRoute - yeni model varyant guncelleme
// ======================================================
func OrderProductionApplyRoute(mssql *sql.DB, ml *mailer.GraphMailer) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
rid := fmt.Sprintf("opa-%d", time.Now().UnixNano())
w.Header().Set("X-Debug-Request-Id", rid)
start := time.Now()
id := mux.Vars(r)["id"]
if id == "" {
http.Error(w, "OrderHeaderID bulunamadi", http.StatusBadRequest)
return
}
var payload models.OrderProductionUpdatePayload
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "Gecersiz istek", http.StatusBadRequest)
return
}
if err := validateUpdateLines(payload.Lines); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s payload lineCount=%d insertMissing=%t cdItemCount=%d attributeCount=%d",
rid, id, len(payload.Lines), payload.InsertMissing, len(payload.CdItems), len(payload.ProductAttributes))
if len(payload.Lines) > 0 {
limit := 5
if len(payload.Lines) < limit {
limit = len(payload.Lines)
}
samples := make([]string, 0, limit)
for i := 0; i < limit; i++ {
ln := payload.Lines[i]
dim1 := ""
if ln.ItemDim1Code != nil {
dim1 = strings.TrimSpace(*ln.ItemDim1Code)
}
samples = append(samples, fmt.Sprintf(
"lineID=%s newItem=%s newColor=%s newDim1=%s newDim2=%s",
strings.TrimSpace(ln.OrderLineID),
strings.ToUpper(strings.TrimSpace(ln.NewItemCode)),
strings.ToUpper(strings.TrimSpace(ln.NewColor)),
strings.ToUpper(strings.TrimSpace(dim1)),
strings.ToUpper(strings.TrimSpace(ln.NewDim2)),
))
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s payload lineSamples=%v", rid, id, samples)
}
newLines, existingLines := splitLinesByCdItemDraft(payload.Lines, payload.CdItems)
newCodes := uniqueCodesFromLines(newLines)
existingCodes := uniqueCodesFromLines(existingLines)
stepMissingStart := time.Now()
missing := make([]models.OrderProductionMissingVariant, 0)
barcodeTargets := make([]models.OrderProductionMissingVariant, 0)
if len(newLines) > 0 {
err := runWithTransientMSSQLRetry("apply_build_targets_missing", 3, 500*time.Millisecond, func() error {
var stepErr error
barcodeTargets, stepErr = buildTargetVariants(mssql, id, newLines)
if stepErr != nil {
return stepErr
}
missing, stepErr = buildMissingVariantsFromTargets(mssql, id, barcodeTargets)
return stepErr
})
if err != nil {
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=build_missing failed duration_ms=%d err=%v",
rid, id, time.Since(stepMissingStart).Milliseconds(), err)
writeDBError(w, http.StatusInternalServerError, "apply_validate_missing_variants", id, "", len(newLines), err)
return
}
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s lineCount=%d newLineCount=%d existingLineCount=%d targetVariantCount=%d missingCount=%d build_missing_ms=%d",
rid, id, len(payload.Lines), len(newLines), len(existingLines), len(barcodeTargets), len(missing), time.Since(stepMissingStart).Milliseconds())
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s scope newCodes=%v existingCodes=%v",
rid, id, newCodes, existingCodes)
if len(missing) > 0 && !payload.InsertMissing {
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s early_exit=missing_variants total_ms=%d",
rid, id, time.Since(start).Milliseconds())
w.WriteHeader(http.StatusConflict)
_ = json.NewEncoder(w).Encode(map[string]any{
"missingCount": len(missing),
"missing": missing,
"message": "Eksik varyantlar var",
})
return
}
claims, _ := auth.GetClaimsFromContext(r.Context())
username := ""
if claims != nil {
username = claims.Username
}
if strings.TrimSpace(username) == "" {
username = "system"
}
stepBeginStart := time.Now()
tx, err := mssql.Begin()
if err != nil {
writeDBError(w, http.StatusInternalServerError, "begin_tx", id, username, len(payload.Lines), err)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=begin_tx duration_ms=%d", rid, id, time.Since(stepBeginStart).Milliseconds())
committed := false
currentStep := "begin_tx"
applyTxSettings := func(tx *sql.Tx) error {
// XACT_ABORT OFF:
// Barcode insert path intentionally tolerates duplicate-key errors (fallback/skip duplicate).
// With XACT_ABORT ON, that expected error aborts the whole transaction and causes COMMIT 3902.
_, execErr := tx.Exec(`SET XACT_ABORT OFF; SET LOCK_TIMEOUT 15000;`)
return execErr
}
defer func() {
if committed {
return
}
rbStart := time.Now()
if rbErr := tx.Rollback(); rbErr != nil && rbErr != sql.ErrTxDone {
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s rollback step=%s failed duration_ms=%d err=%v",
rid, id, currentStep, time.Since(rbStart).Milliseconds(), rbErr)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s rollback step=%s ok duration_ms=%d",
rid, id, currentStep, time.Since(rbStart).Milliseconds())
}()
stepTxSettingsStart := time.Now()
currentStep = "tx_settings"
if err := applyTxSettings(tx); err != nil {
writeDBError(w, http.StatusInternalServerError, "tx_settings", id, username, len(payload.Lines), err)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=tx_settings duration_ms=%d", rid, id, time.Since(stepTxSettingsStart).Milliseconds())
if err := ensureTxAlive(tx, "after_tx_settings"); err != nil {
writeDBError(w, http.StatusInternalServerError, "tx_not_active_after_tx_settings", id, username, len(payload.Lines), err)
return
}
var inserted int64
if payload.InsertMissing && len(newLines) > 0 {
currentStep = "insert_missing_variants"
cdItemByCode := buildCdItemDraftMap(payload.CdItems)
stepInsertMissingStart := time.Now()
inserted, err = queries.InsertMissingVariantsTx(tx, missing, username, cdItemByCode)
if err != nil && isTransientMSSQLNetworkErr(err) {
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=insert_missing transient_error retry=1 err=%v",
rid, id, err)
_ = tx.Rollback()
tx, err = mssql.Begin()
if err != nil {
writeDBError(w, http.StatusInternalServerError, "begin_tx_retry_insert_missing", id, username, len(payload.Lines), err)
return
}
currentStep = "tx_settings_retry_insert_missing"
if err = applyTxSettings(tx); err != nil {
writeDBError(w, http.StatusInternalServerError, "tx_settings_retry_insert_missing", id, username, len(payload.Lines), err)
return
}
if err = ensureTxAlive(tx, "after_tx_settings_retry_insert_missing"); err != nil {
writeDBError(w, http.StatusInternalServerError, "tx_not_active_after_tx_settings_retry_insert_missing", id, username, len(payload.Lines), err)
return
}
currentStep = "insert_missing_variants_retry"
inserted, err = queries.InsertMissingVariantsTx(tx, missing, username, cdItemByCode)
}
if err != nil {
writeDBError(w, http.StatusInternalServerError, "insert_missing_variants", id, username, len(missing), err)
return
}
if err := ensureTxAlive(tx, "after_insert_missing_variants"); err != nil {
writeDBError(w, http.StatusInternalServerError, "tx_not_active_after_insert_missing_variants", id, username, len(missing), err)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=insert_missing inserted=%d duration_ms=%d",
rid, id, inserted, time.Since(stepInsertMissingStart).Milliseconds())
}
stepValidateAttrStart := time.Now()
currentStep = "validate_attributes"
if err := validateProductAttributes(payload.ProductAttributes); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=validate_attributes count=%d duration_ms=%d",
rid, id, len(payload.ProductAttributes), time.Since(stepValidateAttrStart).Milliseconds())
stepUpsertAttrStart := time.Now()
currentStep = "upsert_item_attributes"
attributeAffected, err := queries.UpsertItemAttributesTx(tx, payload.ProductAttributes, username)
if err != nil {
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=upsert_attributes failed duration_ms=%d err=%v",
rid, id, time.Since(stepUpsertAttrStart).Milliseconds(), err)
writeDBError(w, http.StatusInternalServerError, "upsert_item_attributes", id, username, len(payload.ProductAttributes), err)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=upsert_attributes affected=%d duration_ms=%d",
rid, id, attributeAffected, time.Since(stepUpsertAttrStart).Milliseconds())
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s table=prItemAttribute inputRows=%d affectedRows=%d",
rid, id, len(payload.ProductAttributes), attributeAffected)
if err := ensureTxAlive(tx, "after_upsert_item_attributes"); err != nil {
writeDBError(w, http.StatusInternalServerError, "tx_not_active_after_upsert_item_attributes", id, username, len(payload.ProductAttributes), err)
return
}
var barcodeInserted int64
// Barkod adimi:
// - Eski kodlara girmemeli
// - Yeni kod satirlari icin, varyant daha once olusmus olsa bile eksik barkod varsa tamamlamali
// Bu nedenle "inserted > 0" yerine "newLineCount > 0" kosulu kullanilir.
if len(newLines) > 0 && len(barcodeTargets) > 0 {
stepUpsertBarcodeStart := time.Now()
currentStep = "upsert_item_barcodes"
barcodeInserted, err = queries.InsertItemBarcodesByTargetsTx(tx, barcodeTargets, username)
if err != nil {
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=upsert_barcodes failed duration_ms=%d err=%v",
rid, id, time.Since(stepUpsertBarcodeStart).Milliseconds(), err)
writeDBError(w, http.StatusInternalServerError, "upsert_item_barcodes", id, username, len(barcodeTargets), err)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=upsert_barcodes inserted=%d duration_ms=%d",
rid, id, barcodeInserted, time.Since(stepUpsertBarcodeStart).Milliseconds())
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s table=prItemBarcode targetVariantRows=%d insertedRows=%d",
rid, id, len(barcodeTargets), barcodeInserted)
if err := ensureTxAlive(tx, "after_upsert_item_barcodes"); err != nil {
writeDBError(w, http.StatusInternalServerError, "tx_not_active_after_upsert_item_barcodes", id, username, len(barcodeTargets), err)
return
}
} else {
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=upsert_barcodes skipped newLineCount=%d targetVariantRows=%d",
rid, id, len(newLines), len(barcodeTargets))
}
stepUpdateHeaderStart := time.Now()
currentStep = "update_order_header_average_due_date"
if err := queries.UpdateOrderHeaderAverageDueDateTx(tx, id, payload.HeaderAverageDueDate, username); err != nil {
writeDBError(w, http.StatusInternalServerError, "update_order_header_average_due_date", id, username, 0, err)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=update_header_average_due_date changed=%t duration_ms=%d",
rid, id, payload.HeaderAverageDueDate != nil, time.Since(stepUpdateHeaderStart).Milliseconds())
if err := ensureTxAlive(tx, "after_update_order_header_average_due_date"); err != nil {
writeDBError(w, http.StatusInternalServerError, "tx_not_active_after_update_order_header_average_due_date", id, username, 0, err)
return
}
currentStep = "touch_order_header"
headerTouched, err := queries.TouchOrderHeaderTx(tx, id, username)
if err != nil {
writeDBError(w, http.StatusInternalServerError, "touch_order_header", id, username, len(payload.Lines), err)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s table=trOrderHeader touchedRows=%d",
rid, id, headerTouched)
if err := ensureTxAlive(tx, "after_touch_order_header"); err != nil {
writeDBError(w, http.StatusInternalServerError, "tx_not_active_after_touch_order_header", id, username, len(payload.Lines), err)
return
}
stepUpdateLinesStart := time.Now()
currentStep = "update_order_lines"
updated, err := queries.UpdateOrderLinesTx(tx, id, payload.Lines, username)
if err != nil {
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=update_lines failed duration_ms=%d err=%v",
rid, id, time.Since(stepUpdateLinesStart).Milliseconds(), err)
writeDBError(w, http.StatusInternalServerError, "update_order_lines", id, username, len(payload.Lines), err)
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=update_lines updated=%d duration_ms=%d",
rid, id, updated, time.Since(stepUpdateLinesStart).Milliseconds())
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s table=trOrderLine targetRows=%d updatedRows=%d",
rid, id, len(payload.Lines), updated)
if err := ensureTxAlive(tx, "after_update_order_lines"); err != nil {
writeDBError(w, http.StatusInternalServerError, "tx_not_active_after_update_order_lines", id, username, len(payload.Lines), err)
return
}
currentStep = "verify_order_lines"
verifyMismatchCount, verifySamples, verifyErr := queries.VerifyOrderLineUpdatesTx(tx, id, payload.Lines)
if verifyErr != nil {
writeDBError(w, http.StatusInternalServerError, "verify_order_lines", id, username, len(payload.Lines), verifyErr)
return
}
if verifyMismatchCount > 0 {
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s table=trOrderLine verifyMismatchCount=%d samples=%v",
rid, id, verifyMismatchCount, verifySamples)
currentStep = "verify_order_lines_mismatch"
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(map[string]any{
"message": "Order satirlari beklenen kod/renk degerlerine guncellenemedi",
"step": "verify_order_lines_mismatch",
"detail": fmt.Sprintf("mismatchCount=%d", verifyMismatchCount),
"samples": verifySamples,
})
return
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s table=trOrderLine verifyMismatchCount=0",
rid, id)
if err := ensureTxAlive(tx, "before_commit_tx"); err != nil {
writeDBError(w, http.StatusInternalServerError, "tx_not_active_before_commit_tx", id, username, len(payload.Lines), err)
return
}
stepCommitStart := time.Now()
currentStep = "commit_tx"
if err := tx.Commit(); err != nil {
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=commit failed duration_ms=%d err=%v",
rid, id, time.Since(stepCommitStart).Milliseconds(), err)
writeDBError(w, http.StatusInternalServerError, "commit_tx", id, username, len(payload.Lines), err)
return
}
committed = true
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s step=commit duration_ms=%d total_ms=%d",
rid, id, time.Since(stepCommitStart).Milliseconds(), time.Since(start).Milliseconds())
// Mail gönderim mantığı
if false && ml != nil {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[OrderProductionApplyRoute] mail panic recover: %v", r)
}
}()
sendProductionUpdateMails(mssql, ml, id, username, payload.Lines)
}()
}
resp := map[string]any{
"updated": updated,
"inserted": inserted,
"barcodeInserted": barcodeInserted,
"attributeUpserted": attributeAffected,
"headerUpdated": payload.HeaderAverageDueDate != nil,
}
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s result updated=%d inserted=%d barcodeInserted=%d attributeUpserted=%d",
rid, id, updated, inserted, barcodeInserted, attributeAffected)
log.Printf("[OrderProductionApplyRoute] rid=%s orderHeaderID=%s summary tables cdItem/prItemVariant(newOnly)=%d trOrderLine(updated)=%d prItemBarcode(inserted,newOnly)=%d prItemAttribute(affected)=%d trOrderHeader(touched)=%d",
rid, id, inserted, updated, barcodeInserted, attributeAffected, headerTouched)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("❌ encode error: %v", err)
}
})
}
func validateProductAttributes(attrs []models.OrderProductionItemAttributeRow) error {
for _, a := range attrs {
if strings.TrimSpace(a.ItemCode) == "" {
return errors.New("Urun ozellikleri icin ItemCode zorunlu")
}
if !baggiModelCodeRegex.MatchString(strings.ToUpper(strings.TrimSpace(a.ItemCode))) {
return errors.New("Girdiginiz kod BAGGI kod sistemine uyumlu degil. Format: X999-XXX99999")
}
if a.ItemTypeCode <= 0 {
return errors.New("Urun ozellikleri icin ItemTypeCode zorunlu")
}
if a.AttributeTypeCode <= 0 {
return errors.New("Urun ozellikleri icin AttributeTypeCode zorunlu")
}
if strings.TrimSpace(a.AttributeCode) == "" {
return errors.New("Urun ozellikleri icin AttributeCode zorunlu")
}
}
return nil
}
func buildCdItemDraftMap(list []models.OrderProductionCdItemDraft) map[string]models.OrderProductionCdItemDraft {
out := make(map[string]models.OrderProductionCdItemDraft, len(list))
for _, item := range list {
code := strings.ToUpper(strings.TrimSpace(item.ItemCode))
if code == "" {
continue
}
item.ItemCode = code
if item.ItemTypeCode == 0 {
item.ItemTypeCode = 1
}
key := queries.NormalizeCdItemMapKey(item.ItemTypeCode, item.ItemCode)
out[key] = item
}
return out
}
func isNoCorrespondingBeginTxErr(err error) bool {
if err == nil {
return false
}
msg := strings.ToLower(strings.TrimSpace(err.Error()))
return strings.Contains(msg, "commit transaction request has no corresponding begin transaction")
}
func buildTargetVariants(mssql *sql.DB, orderHeaderID string, lines []models.OrderProductionUpdateLine) ([]models.OrderProductionMissingVariant, error) {
start := time.Now()
lineDimsMap, err := queries.GetOrderLineDimsMap(mssql, orderHeaderID)
if err != nil {
return nil, err
}
out := make([]models.OrderProductionMissingVariant, 0, len(lines))
seen := make(map[string]struct{}, len(lines))
for _, line := range lines {
lineID := strings.TrimSpace(line.OrderLineID)
newItem := strings.ToUpper(strings.TrimSpace(line.NewItemCode))
newColor := strings.ToUpper(strings.TrimSpace(line.NewColor))
newDim2 := strings.ToUpper(strings.TrimSpace(line.NewDim2))
if lineID == "" || newItem == "" {
continue
}
dims, ok := lineDimsMap[lineID]
if !ok {
continue
}
dim1 := strings.ToUpper(strings.TrimSpace(dims.ItemDim1Code))
if line.ItemDim1Code != nil {
dim1 = strings.ToUpper(strings.TrimSpace(*line.ItemDim1Code))
}
dim3 := strings.ToUpper(strings.TrimSpace(dims.ItemDim3Code))
key := fmt.Sprintf("%d|%s|%s|%s|%s|%s", dims.ItemTypeCode, newItem, newColor, dim1, newDim2, dim3)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, models.OrderProductionMissingVariant{
OrderLineID: lineID,
ItemTypeCode: dims.ItemTypeCode,
ItemCode: newItem,
ColorCode: newColor,
ItemDim1Code: dim1,
ItemDim2Code: newDim2,
ItemDim3Code: dim3,
})
}
log.Printf("[buildTargetVariants] orderHeaderID=%s lineCount=%d dimMapCount=%d targetCount=%d total_ms=%d",
orderHeaderID, len(lines), len(lineDimsMap), len(out), time.Since(start).Milliseconds())
return out, nil
}
func buildMissingVariants(mssql *sql.DB, orderHeaderID string, lines []models.OrderProductionUpdateLine) ([]models.OrderProductionMissingVariant, error) {
targets, err := buildTargetVariants(mssql, orderHeaderID, lines)
if err != nil {
return nil, err
}
return buildMissingVariantsFromTargets(mssql, orderHeaderID, targets)
}
func buildMissingVariantsFromTargets(mssql *sql.DB, orderHeaderID string, targets []models.OrderProductionMissingVariant) ([]models.OrderProductionMissingVariant, error) {
start := time.Now()
missing := make([]models.OrderProductionMissingVariant, 0, len(targets))
existsCache := make(map[string]bool, len(targets))
for _, target := range targets {
cacheKey := fmt.Sprintf("%d|%s|%s|%s|%s|%s",
target.ItemTypeCode,
target.ItemCode,
target.ColorCode,
target.ItemDim1Code,
target.ItemDim2Code,
target.ItemDim3Code,
)
exists, cached := existsCache[cacheKey]
if !cached {
var checkErr error
exists, checkErr = queries.VariantExists(mssql, target.ItemTypeCode, target.ItemCode, target.ColorCode, target.ItemDim1Code, target.ItemDim2Code, target.ItemDim3Code)
if checkErr != nil {
return nil, checkErr
}
existsCache[cacheKey] = exists
}
if !exists {
missing = append(missing, target)
}
}
log.Printf("[buildMissingVariants] orderHeaderID=%s targetCount=%d missingCount=%d total_ms=%d",
orderHeaderID, len(targets), len(missing), time.Since(start).Milliseconds())
return missing, nil
}
func runWithTransientMSSQLRetry(op string, maxAttempts int, baseDelay time.Duration, fn func() error) error {
if maxAttempts <= 1 {
return fn()
}
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
err := fn()
if err == nil {
return nil
}
lastErr = err
if !isTransientMSSQLNetworkErr(err) || attempt == maxAttempts {
return err
}
wait := time.Duration(attempt) * baseDelay
log.Printf("[MSSQLRetry] op=%s attempt=%d/%d wait_ms=%d err=%v",
op, attempt, maxAttempts, wait.Milliseconds(), err)
time.Sleep(wait)
}
return lastErr
}
func isTransientMSSQLNetworkErr(err error) bool {
if err == nil {
return false
}
msg := strings.ToLower(strings.TrimSpace(err.Error()))
needles := []string{
"wsarecv",
"read tcp",
"connection reset",
"connection refused",
"broken pipe",
"i/o timeout",
"timeout",
}
for _, needle := range needles {
if strings.Contains(msg, needle) {
return true
}
}
return false
}
func ensureTxAlive(tx *sql.Tx, where string) error {
if tx == nil {
return fmt.Errorf("tx is nil at %s", where)
}
var tranCount int
if err := tx.QueryRow(`SELECT @@TRANCOUNT`).Scan(&tranCount); err != nil {
return fmt.Errorf("tx state query failed at %s: %w", where, err)
}
if tranCount <= 0 {
return fmt.Errorf("transaction no longer active at %s (trancount=%d)", where, tranCount)
}
return nil
}
func validateUpdateLines(lines []models.OrderProductionUpdateLine) error {
for _, line := range lines {
if strings.TrimSpace(line.OrderLineID) == "" {
return errors.New("OrderLineID zorunlu")
}
code := strings.ToUpper(strings.TrimSpace(line.NewItemCode))
if code == "" {
return errors.New("Yeni urun kodu zorunlu")
}
if !baggiModelCodeRegex.MatchString(code) {
return errors.New("Girdiginiz kod BAGGI kod sistemine uyumlu degil. Format: X999-XXX99999")
}
}
return nil
}
func splitLinesByCdItemDraft(lines []models.OrderProductionUpdateLine, cdItems []models.OrderProductionCdItemDraft) ([]models.OrderProductionUpdateLine, []models.OrderProductionUpdateLine) {
if len(lines) == 0 {
return nil, nil
}
newCodeSet := make(map[string]struct{}, len(cdItems))
for _, item := range cdItems {
code := strings.ToUpper(strings.TrimSpace(item.ItemCode))
if code == "" {
continue
}
newCodeSet[code] = struct{}{}
}
if len(newCodeSet) == 0 {
existingLines := make([]models.OrderProductionUpdateLine, 0, len(lines))
existingLines = append(existingLines, lines...)
return nil, existingLines
}
newLines := make([]models.OrderProductionUpdateLine, 0, len(lines))
existingLines := make([]models.OrderProductionUpdateLine, 0, len(lines))
for _, line := range lines {
code := strings.ToUpper(strings.TrimSpace(line.NewItemCode))
if _, ok := newCodeSet[code]; ok {
newLines = append(newLines, line)
continue
}
existingLines = append(existingLines, line)
}
return newLines, existingLines
}
func uniqueCodesFromLines(lines []models.OrderProductionUpdateLine) []string {
set := make(map[string]struct{}, len(lines))
out := make([]string, 0, len(lines))
for _, line := range lines {
code := strings.ToUpper(strings.TrimSpace(line.NewItemCode))
if code == "" {
continue
}
if _, ok := set[code]; ok {
continue
}
set[code] = struct{}{}
out = append(out, code)
}
return out
}
func writeDBError(w http.ResponseWriter, status int, step string, orderHeaderID string, username string, lineCount int, err error) {
var sqlErr mssql.Error
if errors.As(err, &sqlErr) {
log.Printf(
"❌ SQL error step=%s orderHeaderID=%s user=%s lineCount=%d number=%d state=%d class=%d server=%s proc=%s line=%d message=%s",
step, orderHeaderID, username, lineCount,
sqlErr.Number, sqlErr.State, sqlErr.Class, sqlErr.ServerName, sqlErr.ProcName, sqlErr.LineNo, sqlErr.Message,
)
} else {
log.Printf(
"❌ DB error step=%s orderHeaderID=%s user=%s lineCount=%d err=%v",
step, orderHeaderID, username, lineCount, err,
)
}
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{
"message": "Veritabani hatasi",
"step": step,
"detail": err.Error(),
})
}
func sendProductionUpdateMails(db *sql.DB, ml *mailer.GraphMailer, orderHeaderID string, actor string, lines []models.OrderProductionUpdateLine) {
if len(lines) == 0 {
return
}
// Sipariş bağlamını çöz
orderNo, currAccCode, marketCode, marketTitle, err := resolveOrderMailContext(db, orderHeaderID)
if err != nil {
log.Printf("[sendProductionUpdateMails] context error: %v", err)
return
}
// Piyasa alıcılarını yükle (PG db lazım ama burada mssql üzerinden sadece log atalım veya graphmailer üzerinden gönderelim)
// Not: PG bağlantısı Route içinde yok, ancak mailer.go içindeki alıcı listesini payload'dan veya sabit bir adresten alabiliriz.
// Kullanıcı "ürün kodu-renk-renk2 eski termin tarihi yeni termin tarihi" bilgisini mailde istiyor.
subject := fmt.Sprintf("%s tarafından %s Nolu Sipariş Güncellendi (Üretim)", actor, orderNo)
var body strings.Builder
body.WriteString("<html><head><meta charset='utf-8'></head><body>")
body.WriteString(fmt.Sprintf("<p><b>Sipariş No:</b> %s</p>", orderNo))
body.WriteString(fmt.Sprintf("<p><b>Cari:</b> %s</p>", currAccCode))
body.WriteString(fmt.Sprintf("<p><b>Piyasa:</b> %s (%s)</p>", marketTitle, marketCode))
body.WriteString("<p>Aşağıdaki satırlarda termin tarihi güncellenmiştir:</p>")
body.WriteString("<table border='1' cellpadding='5' style='border-collapse: collapse;'>")
body.WriteString("<tr style='background-color: #f2f2f2;'><th>Ürün Kodu</th><th>Renk</th><th>2. Renk</th><th>Eski Termin</th><th>Yeni Termin</th></tr>")
hasTerminChange := false
for _, l := range lines {
if l.OldDueDate != l.NewDueDate && l.NewDueDate != "" {
hasTerminChange = true
body.WriteString("<tr>")
body.WriteString(fmt.Sprintf("<td>%s</td>", l.NewItemCode))
body.WriteString(fmt.Sprintf("<td>%s</td>", l.NewColor))
body.WriteString(fmt.Sprintf("<td>%s</td>", l.NewDim2))
body.WriteString(fmt.Sprintf("<td>%s</td>", l.OldDueDate))
body.WriteString(fmt.Sprintf("<td style='color: red; font-weight: bold;'>%s</td>", l.NewDueDate))
body.WriteString("</tr>")
}
}
body.WriteString("</table>")
body.WriteString("<p><i>Bu mail sistem tarafından otomatik oluşturulmuştur.</i></p>")
body.WriteString("</body></html>")
if !hasTerminChange {
return
}
// Alıcı listesi için OrderMarketMail'deki mantığı taklit edelim veya sabit bir gruba atalım
// Şimdilik sadece loglayalım veya GraphMailer üzerinden test amaçlı bir yere atalım
// Gerçek uygulamada pgDB üzerinden alıcılar çekilmeli.
recipients := []string{"urun@baggi.com.tr"} // Varsayılan alıcı
msg := mailer.Message{
To: recipients,
Subject: subject,
BodyHTML: body.String(),
}
if err := ml.Send(context.Background(), msg); err != nil {
log.Printf("[sendProductionUpdateMails] send error: %v", err)
} else {
log.Printf("[sendProductionUpdateMails] mail sent to %v", recipients)
}
}