Files
bssapp/svc/queries/order_write.go

1502 lines
39 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.
// ===================== PART 1 (Satır 1-300) =====================
// =======================================================
// order_write.go — v4.3 FINAL (Insert + Update + Delete)
// - PCTCode her zaman "%0" olarak yazılır (cdPCT FK uyumlu)
// - INSERT/UPDATE öncesi ItemVariant Guard + Duplicate Guard (payload içi)
// - UpdateOrder: DELETE öncesi child tablolar (trOrderLineCurrency) temizlenir
// =======================================================
package queries
import (
"bssapp-backend/db"
"bssapp-backend/models"
"database/sql"
"fmt"
"strings"
"time"
"github.com/google/uuid"
)
func nf0(v models.NullFloat64) float64 {
if !v.Valid {
return 0
}
return v.Float64
}
// =======================================================
// ✅ trOrderLineCurrency UPSERT (Doc Price / Amount yaz)
// =======================================================
func upsertLineCurrency(
tx *sql.Tx,
lineID string,
docCurrency string,
exRate float64,
docPrice float64,
qty float64,
lDis float64, // Line Discount %
tDis float64, // Total Discount %
vatRate float64,
ln models.OrderDetail,
user string,
) error {
// ================= NORMALIZE =================
if docCurrency == "" {
docCurrency = "TRY"
}
if exRate <= 0 {
exRate = 1
}
relationCurrency := safeNS(ln.RelationCurrencyCode)
if relationCurrency == "" {
relationCurrency = docCurrency
}
if ln.LineDiscount.Valid {
lDis = ln.LineDiscount.Float64
}
if ln.TotalDiscount.Valid {
tDis = ln.TotalDiscount.Float64
}
basePrice := docPrice // Price (KDV hariç)
if docCurrency == "TRY" && ln.LocalPrice.Valid && ln.LocalPrice.Float64 > 0 {
basePrice = ln.LocalPrice.Float64
}
amountBase := basePrice * qty // Amount (KDV hariç)
if docCurrency == "TRY" && ln.LocalAmount.Valid && ln.LocalAmount.Float64 > 0 {
amountBase = ln.LocalAmount.Float64
}
priceVI := basePrice // PriceVI (KDV dahil)
if ln.DocPrice.Valid && ln.DocPrice.Float64 > 0 {
priceVI = ln.DocPrice.Float64
}
amountVI := priceVI * qty // AmountVI (KDV dahil)
if ln.DocAmount.Valid && ln.DocAmount.Float64 > 0 {
amountVI = ln.DocAmount.Float64
if qty > 0 {
priceVI = amountVI / qty
}
}
// ================= DISCOUNT =================
afterLineDis := amountBase * (1 - lDis/100)
afterTotalDis := afterLineDis * (1 - tDis/100)
// ================= TAX =================
taxBase := afterTotalDis
if ln.TaxBase.Valid {
taxBase = ln.TaxBase.Float64
}
vat := taxBase * vatRate / 100
if ln.VatAmount.Valid {
vat = ln.VatAmount.Float64
}
vatDeducation := 0.0
if ln.VatDeducation.Valid {
vatDeducation = ln.VatDeducation.Float64
}
net := taxBase + vat - vatDeducation
if ln.NetAmount.Valid {
net = ln.NetAmount.Float64
}
pct := 0.0
if ln.Pct.Valid {
pct = ln.Pct.Float64
}
// payload yoksa doc tutarı net ile hizala
if !ln.DocAmount.Valid {
amountVI = net
if qty > 0 {
priceVI = amountVI / qty
}
}
// ================= LOCAL =================
localPrice := basePrice * exRate
localAmount := amountBase * exRate
localPriceVI := priceVI * exRate
localAmountVI := amountVI * exRate
localTaxBase := taxBase * exRate
localVat := vat * exRate
localVatDeducation := vatDeducation * exRate
localNet := net * exRate
// ================= DOC =================
_, err := tx.Exec(`
MERGE BAGGI_V3.dbo.trOrderLineCurrency AS T
USING (SELECT @p1 AS OrderLineID, @p2 AS CurrencyCode) AS S
ON T.OrderLineID=S.OrderLineID AND T.CurrencyCode=S.CurrencyCode
WHEN MATCHED THEN UPDATE SET
RelationCurrencyCode=@p3,
ExchangeRate=@p4,
PriceVI=@p5,
AmountVI=@p6,
Price=@p7,
Amount=@p8,
LDiscount1=@p9,
TDiscount1=@p10,
TaxBase=@p11,
Pct=@p12,
Vat=@p13,
VatDeducation=@p14,
NetAmount=@p15,
LastUpdatedUserName=@p16,
LastUpdatedDate=GETDATE()
WHEN NOT MATCHED THEN INSERT (
OrderLineID,
CurrencyCode,
RelationCurrencyCode,
ExchangeRate,
PriceVI,
AmountVI,
Price,
Amount,
LDiscount1,
TDiscount1,
TaxBase,
Pct,
Vat,
VatDeducation,
NetAmount,
CreatedUserName,
CreatedDate
)
VALUES (
@p1,@p2,@p3,
@p4,
@p5,@p6,
@p7,@p8,
@p9,@p10,
@p11,@p12,@p13,@p14,@p15,
@p16,GETDATE()
);`,
lineID,
docCurrency,
relationCurrency,
exRate,
priceVI,
amountVI,
basePrice,
amountBase,
lDis,
tDis,
taxBase,
pct,
vat,
vatDeducation,
net,
user,
)
if err != nil {
return err
}
if docCurrency != "TRY" {
_, err = tx.Exec(`
MERGE BAGGI_V3.dbo.trOrderLineCurrency AS T
USING (SELECT @p1 AS OrderLineID,'TRY' AS CurrencyCode) AS S
ON T.OrderLineID=S.OrderLineID AND T.CurrencyCode=S.CurrencyCode
WHEN MATCHED THEN UPDATE SET
RelationCurrencyCode=@p2,
ExchangeRate=@p3,
PriceVI=@p4,
AmountVI=@p5,
Price=@p6,
Amount=@p7,
LDiscount1=@p8,
TDiscount1=@p9,
TaxBase=@p10,
Pct=@p11,
Vat=@p12,
VatDeducation=@p13,
NetAmount=@p14,
LastUpdatedUserName=@p15,
LastUpdatedDate=GETDATE()
WHEN NOT MATCHED THEN INSERT (
OrderLineID,
CurrencyCode,
RelationCurrencyCode,
ExchangeRate,
PriceVI,
AmountVI,
Price,
Amount,
LDiscount1,
TDiscount1,
TaxBase,
Pct,
Vat,
VatDeducation,
NetAmount,
CreatedUserName,
CreatedDate
)
VALUES (
@p1,'TRY',@p2,
@p3,
@p4,@p5,
@p6,@p7,
@p8,@p9,
@p10,@p11,@p12,@p13,@p14,
@p15,GETDATE()
);`,
lineID,
docCurrency,
exRate,
localPriceVI,
localAmountVI,
localPrice,
localAmount,
lDis,
tDis,
localTaxBase,
pct,
localVat,
localVatDeducation,
localNet,
user,
)
if err != nil {
return err
}
}
return nil
}
// =======================================================
// COMBO KEY & STRING HELPERS
// =======================================================
func normalizeComboKey(s string) string {
return strings.ToUpper(strings.TrimSpace(s))
}
// makeComboKey: frontend tarafında NullString olmayan structlar için
func makeComboKey(ln models.OrderDetail) string {
model := safeNS(ln.ItemCode)
renk := safeNS(ln.ColorCode)
renk2 := safeNS(ln.ItemDim2Code)
beden := safeNS(ln.ItemDim1Code)
return normalizeComboKey(
fmt.Sprintf("%s||%s||%s||%s", model, renk, beden, renk2),
)
}
// qtyValue → NullFloat64 güvenli float64
func qtyValue(q models.NullFloat64) float64 {
if !q.Valid {
return 0
}
return q.Float64
}
func buildV3AuditUser(user *models.User) string {
if user == nil {
return "V3-0"
}
v3Name := strings.ToUpper(strings.TrimSpace(user.V3Username))
v3Group := user.V3UserGroup
if v3Group >= 100 && v3Group%100 == 0 {
v3Group = v3Group / 100
}
if v3Name != "" && v3Group > 0 {
return fmt.Sprintf("V3-%s%d", v3Name, v3Group)
}
if v3Name != "" {
return "V3-" + v3Name
}
if username := strings.TrimSpace(user.Username); username != "" {
return username
}
if v3Group > 0 {
return fmt.Sprintf("V3-%d", v3Group)
}
return "V3-0"
}
// VatCode: NullString → string
// - NULL → ""
// - "0" → "" (FK patlamasın, sadece anlamlı kodlar gönderiyoruz)
// - "%0", "%10" vb → trimlenmiş hali
func normalizeVatCode(ns models.NullString) string {
if !ns.Valid {
return ""
}
s := strings.TrimSpace(ns.String)
if s == "0" {
return ""
}
return s
}
// PCTCode: NullString → string
// - Artık her durumda "%0" döndürür (tek PCT tipi kullanımı)
// NOT: SQL tarafında da "%0" cdPCT içinde tanımlı olmalı
func normalizePCTCode(ns models.NullString) string {
return "%0"
}
// models.NullString → trimlenmiş string (NULL ise "")
func safeNS(ns models.NullString) string {
if !ns.Valid {
return ""
}
return strings.TrimSpace(ns.String)
}
// =======================================================
// COMBO KEY HELPERS (DB alanlarından)
// =======================================================
// makeComboKeyParts: düz string alanlardan comboKey üret
// comboKey = model || renk || beden || renk2
func makeComboKeyParts(item, color, dim1, dim2 string) string {
item = strings.TrimSpace(item)
color = strings.TrimSpace(color)
dim1 = strings.TrimSpace(dim1)
dim2 = strings.TrimSpace(dim2)
if item == "" && color == "" && dim1 == "" && dim2 == "" {
return ""
}
return normalizeComboKey(item + "||" + color + "||" + dim1 + "||" + dim2)
}
// comboFromNulls: NullString alanlardan comboKey üret
func comboFromNulls(item, color, dim1, dim2 models.NullString) string {
return makeComboKeyParts(
safeNS(item),
safeNS(color),
safeNS(dim1),
safeNS(dim2),
)
}
// =======================================================
// ✅ ItemVariant Guard — INSERT / UPDATE öncesi
// =======================================================
// normalizeKeyPart: NullString → trim + UPPER
func normalizeKeyPart(ns models.NullString) string {
s := strings.TrimSpace(safeNS(ns))
return strings.ToUpper(s)
}
// =======================================================
// AKSBIR DETECTION
// =======================================================
// =======================================================
// COMBO KEY BUILDER (AKSBIR AWARE)
// =======================================================
// Variant check: ItemCode + ColorCode + Dim1 + Dim2
func ValidateItemVariant(tx *sql.Tx, ln models.OrderDetail) error {
fmt.Printf(
"🧪 VARIANT GUARD INPUT | ClientKey=%s Item=%q Color=%q Dim1=%q Dim2=%q Dim3=%q Qty1=%v\n",
safeNS(ln.ClientKey),
safeNS(ln.ItemCode),
safeNS(ln.ColorCode),
safeNS(ln.ItemDim1Code),
safeNS(ln.ItemDim2Code),
safeNS(ln.ItemDim3Code),
nf0(ln.Qty1),
)
item := normalizeKeyPart(ln.ItemCode)
color := normalizeKeyPart(ln.ColorCode)
dim1 := normalizeKeyPart(ln.ItemDim1Code)
dim2 := normalizeKeyPart(ln.ItemDim2Code)
// ✅ Placeholder/boş standardizasyon (SENDE "_" geliyor)
normalizeEmpty := func(s string) string {
s = strings.TrimSpace(strings.ToUpper(s))
if s == "_" || s == "-" {
return ""
}
return s
}
item = normalizeEmpty(item)
color = normalizeEmpty(color)
dim1 = normalizeEmpty(dim1)
dim2 = normalizeEmpty(dim2)
if item == "" {
return fmt.Errorf(
"ItemCode boş olamaz (ClientKey=%s)",
safeNS(ln.ClientKey),
)
fmt.Printf(
"🧪 VARIANT NORMALIZED | Item=%q Color=%q Dim1=%q Dim2=%q\n",
item, color, dim1, dim2,
)
}
var exists int
err := tx.QueryRow(`
SELECT CASE WHEN EXISTS (
SELECT 1
FROM BAGGI_V3.dbo.prItemVariant V WITH (NOLOCK)
WHERE ISNULL(LTRIM(RTRIM(V.ItemCode)),'') = @p1
AND ISNULL(LTRIM(RTRIM(V.ColorCode)),'') = @p2
AND ISNULL(LTRIM(RTRIM(V.ItemDim1Code)),'') = @p3
AND ISNULL(LTRIM(RTRIM(V.ItemDim2Code)),'') = @p4
) THEN 1 ELSE 0 END
`, item, color, dim1, dim2).Scan(&exists)
if err != nil {
return fmt.Errorf("ItemVariant kontrol query hatası: %w", err)
}
if exists != 1 {
return &models.ValidationError{
Code: "INVALID_ITEM_VARIANT",
Message: "Tanımsız ürün kombinasyonu",
ClientKey: safeNS(ln.ClientKey),
ItemCode: item,
ColorCode: color,
Dim1: dim1,
Dim2: dim2,
}
}
return nil
}
// ValidateOrderVariants: save/update öncesi payload satırlarını prItemVariant'a göre doğrular.
// invalid döner; error sadece DB/prepare/query hatalarında döner.
func ValidateOrderVariants(db *sql.DB, lines []models.OrderDetail) ([]models.InvalidVariant, error) {
normalizeEmpty := func(s string) string {
s = strings.TrimSpace(strings.ToUpper(s))
if s == "_" || s == "-" {
return ""
}
return s
}
stmt, err := db.Prepare(`
SELECT CASE WHEN EXISTS (
SELECT 1
FROM BAGGI_V3.dbo.prItemVariant V WITH (NOLOCK)
WHERE ISNULL(LTRIM(RTRIM(V.ItemCode)),'') = @p1
AND ISNULL(LTRIM(RTRIM(V.ColorCode)),'') = @p2
AND ISNULL(LTRIM(RTRIM(V.ItemDim1Code)),'') = @p3
AND ISNULL(LTRIM(RTRIM(V.ItemDim2Code)),'') = @p4
) THEN 1 ELSE 0 END
`)
if err != nil {
return nil, fmt.Errorf("validate prepare hatası: %w", err)
}
defer stmt.Close()
invalid := make([]models.InvalidVariant, 0)
for i, ln := range lines {
qty := qtyValue(ln.Qty1)
if qty <= 0 {
continue
}
item := normalizeEmpty(normalizeKeyPart(ln.ItemCode))
color := normalizeEmpty(normalizeKeyPart(ln.ColorCode))
dim1 := normalizeEmpty(normalizeKeyPart(ln.ItemDim1Code))
dim2 := normalizeEmpty(normalizeKeyPart(ln.ItemDim2Code))
// ItemCode boş ise invalid
if strings.TrimSpace(item) == "" {
invalid = append(invalid, models.InvalidVariant{
Index: i,
ClientKey: safeNS(ln.ClientKey),
ItemCode: item,
ColorCode: color,
Dim1: dim1,
Dim2: dim2,
Qty1: qty,
ComboKey: safeNS(ln.ComboKey),
Reason: "ItemCode boş",
})
continue
}
var exists int
if err := stmt.QueryRow(item, color, dim1, dim2).Scan(&exists); err != nil {
return nil, fmt.Errorf("validate query hatası (i=%d): %w", i, err)
}
if exists != 1 {
invalid = append(invalid, models.InvalidVariant{
Index: i,
ClientKey: safeNS(ln.ClientKey),
ItemCode: item,
ColorCode: color,
Dim1: dim1,
Dim2: dim2,
Qty1: qty,
ComboKey: safeNS(ln.ComboKey),
Reason: "prItemVariantta yok",
})
}
}
return invalid, nil
}
// =======================================================
// LineResult → frontend senkronu için
// =======================================================
type OrderLineResult struct {
ClientKey string `json:"clientKey"`
OrderLineID string `json:"orderLineID"`
}
// =======================================================
// PART 1 — InsertOrder (header + lines insert) — FINAL v5.1
// =======================================================
func InsertOrder(header models.OrderHeader, lines []models.OrderDetail, user *models.User) (string, []OrderLineResult, error) {
conn := db.GetDB()
fmt.Println("🟦 InsertOrder() BAŞLADI -----------------------------")
tx, err := conn.Begin()
if err != nil {
return "", nil, fmt.Errorf("tx baslatilamadi: %w", err)
}
defer tx.Rollback()
now := time.Now()
v3User := buildV3AuditUser(user)
// =======================================================
// 1) BACKEND — OrderHeaderID üretimi (HER ZAMAN)
// =======================================================
realHeaderID := uuid.New().String()
fmt.Println("🟩 Backend yeni OrderHeaderID üretti:", realHeaderID)
header.OrderHeaderID = realHeaderID
// =======================================================
// 2) OrderNumber üretimi (LOCAL-* ise gerçek WS numarası)
// =======================================================
if !header.OrderNumber.Valid ||
strings.HasPrefix(header.OrderNumber.String, "LOCAL-") ||
len(strings.TrimSpace(header.OrderNumber.String)) == 0 {
fmt.Println("🟨 LOCAL numara geldi → gerçek WS numarası üretilecek...")
var realNumber string
err := tx.QueryRow(`
SELECT CONCAT(
'1-WS-3-',
RIGHT('00000' + CAST(NEXT VALUE FOR BAGGI_V3.dbo.Seq_OrderNumber_WS AS VARCHAR(10)), 5)
)
`).Scan(&realNumber)
if err != nil {
return "", nil, fmt.Errorf("Gerçek sipariş numarası üretilemedi: %w", err)
}
fmt.Println("🟩 Üretilen gerçek WS numarası:", realNumber)
header.OrderNumber.String = realNumber
header.OrderNumber.Valid = true
}
newID := realHeaderID // artık DBye bu yazılacak
// =======================================================
// 3) Döviz kuru çözümü
// =======================================================
exRate := 1.0
if header.DocCurrencyCode.Valid && header.DocCurrencyCode.String != "TRY" {
if c, err := GetTodayCurrencyV3(conn, header.DocCurrencyCode.String); err == nil {
if c.Rate > 0 {
exRate = c.Rate
}
}
}
// =======================================================
// 4) HEADER INSERT
// =======================================================
queryHeader := `
INSERT INTO BAGGI_V3.dbo.trOrderHeader (
OrderHeaderID, OrderTypeCode, ProcessCode, OrderNumber, IsCancelOrder,
OrderDate, OrderTime, DocumentNumber, PaymentTerm,
AverageDueDate, Description, InternalDescription,
CurrAccTypeCode, CurrAccCode, SubCurrAccID, ContactID,
ShipmentMethodCode, ShippingPostalAddressID, BillingPostalAddressID,
GuarantorContactID, GuarantorContactID2, RoundsmanCode,
DeliveryCompanyCode, TaxTypeCode, WithHoldingTaxTypeCode, DOVCode,
TaxExemptionCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode,
POSTerminalID, WarehouseCode, ToWarehouseCode,
OrdererCompanyCode, OrdererOfficeCode, OrdererStoreCode,
GLTypeCode, DocCurrencyCode, LocalCurrencyCode, ExchangeRate,
TDisRate1, TDisRate2, TDisRate3, TDisRate4, TDisRate5,
DiscountReasonCode, SurplusOrderQtyToleranceRate,
ImportFileNumber, ExportFileNumber,
IncotermCode1, IncotermCode2, LettersOfCreditNumber,
PaymentMethodCode, IsInclutedVat, IsCreditSale, IsCreditableConfirmed,
CreditableConfirmedUser, CreditableConfirmedDate,
IsSalesViaInternet, IsSuspended, IsCompleted, IsPrinted,
IsLocked, UserLocked, IsClosed,
ApplicationCode, ApplicationID,
CreatedUserName, CreatedDate, LastUpdatedUserName, LastUpdatedDate,
IsProposalBased
)
VALUES (
@p1,@p2,@p3,@p4,
@p5,@p6,@p7,@p8,
@p9,@p10,@p11,
@p12,@p13,@p14,@p15,
@p16,@p17,@p18,
@p19,@p20,@p21,
@p22,@p23,@p24,@p25,
@p26,@p27,@p28,@p29,@p30,
@p31,@p32,@p33,
@p34,@p35,@p36,
@p37,@p38,@p39,@p40,
@p41,@p42,@p43,@p44,@p45,
@p46,@p47,
@p48,@p49,
@p50,@p51,@p52,
@p53,@p54,@p55,@p56,@p57,
@p58,@p59,@p60,@p61,@p62,
@p63,@p64,@p65,
@p66,@p67,@p68,@p69,@p70,@p71,@p72,@p73
);
`
fmt.Println("🟪 HEADER INSERT ÇALIŞIYOR...")
// ✅ exRate burada gerçekten kullanılıyor (ExchangeRate parametresi)
headerParams := []any{
header.OrderHeaderID,
nullableInt16(header.OrderTypeCode, 1),
nullableString(header.ProcessCode, "WS"),
nullableString(header.OrderNumber, ""),
nullableBool(header.IsCancelOrder, false),
nullableDateString(header.OrderDate),
nullableTimeString(header.OrderTime),
nullableString(header.DocumentNumber, ""),
nullableInt16(header.PaymentTerm, 0),
nullableDateString(header.AverageDueDate),
nullableString(header.Description, ""),
nullableString(header.InternalDescription, ""),
nullableInt16(header.CurrAccTypeCode, 0),
nullableString(header.CurrAccCode, ""),
nullableUUID(header.SubCurrAccID),
nullableUUID(header.ContactID),
nullableString(header.ShipmentMethodCode, ""),
nullableUUID(header.ShippingPostalAddressID),
nullableUUID(header.BillingPostalAddressID),
nullableUUID(header.GuarantorContactID),
nullableUUID(header.GuarantorContactID2),
nullableString(header.RoundsmanCode, ""),
nullableString(header.DeliveryCompanyCode, ""),
nullableInt16(header.TaxTypeCode, 0),
nullableString(header.WithHoldingTaxTypeCode, ""),
nullableString(header.DOVCode, ""),
nullableInt16(header.TaxExemptionCode, 0),
nullableInt32ToInt16(header.CompanyCode, 1),
nullableString(header.OfficeCode, "101"),
nullableInt16(header.StoreTypeCode, 5),
nullableString(header.StoreCode, ""),
nullableInt16(header.POSTerminalID, 0),
nullableString(header.WarehouseCode, "1-0-12"),
nullableString(header.ToWarehouseCode, ""),
nullableInt32ToInt16(header.OrdererCompanyCode, 1),
nullableString(header.OrdererOfficeCode, "101"),
nullableString(header.OrdererStoreCode, ""),
nullableString(header.GLTypeCode, ""),
nullableString(header.DocCurrencyCode, "TRY"),
nullableString(header.LocalCurrencyCode, "TRY"),
nullableFloat64(header.ExchangeRate, exRate), // ✅ exRate kullanıldı
nullableFloat64(header.TDisRate1, 0),
nullableFloat64(header.TDisRate2, 0),
nullableFloat64(header.TDisRate3, 0),
nullableFloat64(header.TDisRate4, 0),
nullableFloat64(header.TDisRate5, 0),
nullableInt16(header.DiscountReasonCode, 0),
nullableFloat64(header.SurplusOrderQtyToleranceRate, 0),
nullableString(header.ImportFileNumber, ""),
nullableString(header.ExportFileNumber, ""),
nullableString(header.IncotermCode1, ""),
nullableString(header.IncotermCode2, ""),
nullableString(header.LettersOfCreditNumber, ""),
nullableString(header.PaymentMethodCode, ""),
nullableBool(header.IsInclutedVat, false),
nullableBool(header.IsCreditSale, true),
nullableBool(header.IsCreditableConfirmed, true),
nullableString(header.CreditableConfirmedUser, v3User),
nullableDateTime(header.CreditableConfirmedDate, now),
nullableBool(header.IsSalesViaInternet, false),
nullableBool(header.IsSuspended, false),
nullableBool(header.IsCompleted, false),
nullableBool(header.IsPrinted, false),
nullableBool(header.IsLocked, false),
nullableBool(header.UserLocked, false),
nullableBool(header.IsClosed, false),
nullableString(header.ApplicationCode, "Order"),
nullableUUID(header.ApplicationID),
nullableString(header.CreatedUserName, v3User),
nullableDateTimeString(header.CreatedDate, now),
nullableString(header.LastUpdatedUserName, v3User),
nullableDateTimeString(header.LastUpdatedDate, now),
nullableBool(header.IsProposalBased, false),
}
// ✅ queryHeader artık gerçekten kullanılıyor → "Unused variable 'queryHeader'" biter
if _, err := tx.Exec(queryHeader, headerParams...); err != nil {
fmt.Println("❌ HEADER INSERT ERROR:", err)
return "", nil, fmt.Errorf("header insert hatasi: %w", err)
}
fmt.Println("🟩 HEADER INSERT OK — ID:", newID)
// headerParams ... (senin mevcut hali aynen)
// =======================================================
// 5) LINE INSERT
// =======================================================
insStmt, err := tx.Prepare(`
INSERT INTO BAGGI_V3.dbo.trOrderLine (
OrderLineID,
SortOrder, ItemTypeCode, ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code,
Qty1, Qty2,
CancelQty1, CancelQty2,
OrderCancelReasonCode,
DeliveryDate, PlannedDateOfLading,
LineDescription,
UsedBarcode, CostCenterCode,
VatCode, VatRate, PCTCode, PCTRate,
LDisRate1, LDisRate2, LDisRate3, LDisRate4, LDisRate5,
DocCurrencyCode, PriceCurrencyCode, PriceExchangeRate,
Price,
BaseProcessCode, BaseOrderNumber,
BaseCustomerTypeCode, BaseCustomerCode,
BaseSubCurrAccID, BaseStoreCode,
OrderHeaderID,
CreatedUserName, CreatedDate,
LastUpdatedUserName, LastUpdatedDate,
SurplusOrderQtyToleranceRate,
WithHoldingTaxTypeCode,
DOVCode
)
VALUES (
@p1,@p2,@p3,@p4,
@p5,@p6,@p7,@p8,
@p9,@p10,
@p11,@p12,
@p13,
@p14,@p15,
@p16,
@p17,@p18,
@p19,@p20,@p21,@p22,
@p23,@p24,@p25,@p26,@p27,
@p28,@p29,@p30,
@p31,
@p32,@p33,
@p34,@p35,
@p36,@p37,
@p38,
@p39,@p40,
@p41,@p42,
@p43,
@p44,
@p45
)`)
if err != nil {
return "", nil, fmt.Errorf("line insert stmt hazirlanamadi: %w", err)
}
defer insStmt.Close()
lineResults := make([]OrderLineResult, 0, len(lines))
// ✅ Duplicate Guard (payload içi)
seenCombo := make(map[string]bool)
for i, ln := range lines {
fmt.Println("────────────────────────────────────")
fmt.Printf("🟨 [INSERT] LINE %d — gelen OrderLineID=%s\n", i+1, ln.OrderLineID)
// Her satır için yeni GUID
if ln.OrderLineID == "" {
newLineID := uuid.New().String()
fmt.Println("🆕 Yeni LineID üretildi:", newLineID)
ln.OrderLineID = newLineID
}
// ✅ Duplicate Guard (comboKey)
comboKey := normalizeComboKey(safeNS(ln.ComboKey))
if comboKey == "" {
comboKey = makeComboKey(ln)
}
if qtyValue(ln.Qty1) > 0 && comboKey != "" {
if seenCombo[comboKey] {
return "", nil, fmt.Errorf(
"Duplicate satır (comboKey=%s, ClientKey=%s)",
comboKey,
safeNS(ln.ClientKey),
)
}
seenCombo[comboKey] = true
}
// V2 Logic → %0 PCT
vatCode := normalizeVatCode(ln.VatCode)
pctCode := normalizePCTCode(ln.PCTCode)
var pctParam any
if pctCode == "" {
pctParam = nil
} else {
pctParam = pctCode
}
planned := nullableDateString(ln.PlannedDateOfLading)
// ✅ INSERT ÖNCESİ ItemVariant GUARD
if qtyValue(ln.Qty1) > 0 {
if err := ValidateItemVariant(tx, ln); err != nil {
fmt.Println("❌ VARIANT GUARD (INSERT):", err)
return "", nil, err
}
}
fmt.Printf(
"🚨 INSERT LINE[%d] | LineID=%s ClientKey=%s Item=%q Color=%q Dim1=%q Dim2=%q Dim3=%q Qty1=%v\n",
i+1,
ln.OrderLineID,
safeNS(ln.ClientKey),
safeNS(ln.ItemCode),
safeNS(ln.ColorCode),
safeNS(ln.ItemDim1Code),
safeNS(ln.ItemDim2Code),
safeNS(ln.ItemDim3Code),
nf0(ln.Qty1),
)
_, err := insStmt.Exec(
ln.OrderLineID,
ln.SortOrder,
ln.ItemTypeCode,
nullableString(ln.ItemCode, ""),
safeNS(ln.ColorCode),
safeNS(ln.ItemDim1Code),
safeNS(ln.ItemDim2Code),
nullableString(ln.ItemDim3Code, ""),
ln.Qty1, ln.Qty2,
ln.CancelQty1, ln.CancelQty2,
nullableString(ln.OrderCancelReasonCode, ""),
nullableTime(ln.DeliveryDate, now),
planned,
nullableString(ln.LineDescription, ""),
nullableString(ln.UsedBarcode, ""),
nullableString(ln.CostCenterCode, ""),
vatCode, nf0(ln.VatRate),
pctParam, nf0(ln.PCTRate),
nf0(ln.LDisRate1),
nf0(ln.LDisRate2),
nf0(ln.LDisRate3),
nf0(ln.LDisRate4),
nf0(ln.LDisRate5),
nullableString(ln.DocCurrencyCode, "TRY"),
nullableString(ln.PriceCurrencyCode, "TRY"),
nf0(ln.PriceExchangeRate),
nf0(ln.Price),
nullableString(ln.BaseProcessCode, ""),
nullableString(ln.BaseOrderNumber, ""),
ln.BaseCustomerTypeCode,
nullableString(ln.BaseCustomerCode, ""),
nullableUUID(ln.BaseSubCurrAccID),
nullableString(ln.BaseStoreCode, ""),
header.OrderHeaderID,
v3User, now,
v3User, now,
nf0(ln.SurplusOrderQtyToleranceRate),
nullableString(ln.WithHoldingTaxTypeCode, ""),
nullableString(ln.DOVCode, ""),
)
if err != nil {
fmt.Println("❌ INSERT LINE ERROR")
fmt.Printf(
"💥 FAILED LINE | LineID=%s ClientKey=%s Item=%q Color=%q Dim1=%q Dim2=%q Dim3=%q\n",
ln.OrderLineID,
safeNS(ln.ClientKey),
safeNS(ln.ItemCode),
safeNS(ln.ColorCode),
safeNS(ln.ItemDim1Code),
safeNS(ln.ItemDim2Code),
safeNS(ln.ItemDim3Code),
)
fmt.Println("SQL ERROR:", err)
return "", nil, fmt.Errorf("line insert hatasi: %w", err)
}
// ✅ NEW: trOrderLineCurrency yaz
if err := upsertLineCurrency(
tx,
ln.OrderLineID,
safeNS(ln.DocCurrencyCode),
nf0(ln.PriceExchangeRate),
nf0(ln.Price),
nf0(ln.Qty1),
nf0(ln.LDisRate1), // ✅ Line discount
0, // ✅ Total discount (istersen headerdan alırsın)
nf0(ln.VatRate), // ✅ Vat rate
ln,
v3User,
); err != nil {
return "", nil, fmt.Errorf("currency insert hatası: %w", err)
}
if ln.ClientKey.Valid && ln.ClientKey.String != "" {
lineResults = append(lineResults, OrderLineResult{
ClientKey: ln.ClientKey.String,
OrderLineID: ln.OrderLineID,
})
}
}
// =======================================================
// 6) COMMIT
// =======================================================
fmt.Println("🟨 COMMIT EDİLİYOR...")
if err := tx.Commit(); err != nil {
fmt.Println("❌ COMMIT ERROR:", err)
return "", nil, err
}
fmt.Println("✅ COMMIT OK — INSERT ORDER BİTTİ")
fmt.Println("────────────────────────────────────")
return newID, lineResults, nil
}
// =======================================================
// PART 2 — UpdateOrder FULL DEBUG (v4.3)
// =======================================================
func UpdateOrder(header models.OrderHeader, lines []models.OrderDetail, user *models.User) ([]OrderLineResult, error) {
conn := db.GetDB()
fmt.Println("══════════════════════════════════════")
fmt.Println("🔍 [DEBUG] UpdateOrder çağrıldı")
fmt.Printf("🔍 HeaderID: %v\n", header.OrderHeaderID)
fmt.Printf("🔍 Line sayısı: %v\n", len(lines))
fmt.Printf("🔍 User: %v (V3: %s/%d)\n", user.Username, user.V3Username, user.V3UserGroup)
fmt.Println("══════════════════════════════════════")
tx, err := conn.Begin()
if err != nil {
return nil, fmt.Errorf("tx baslatilamadi: %w", err)
}
defer tx.Rollback()
now := time.Now()
v3User := buildV3AuditUser(user)
// Döviz kuru (Header ExchangeRate fallback)
exRate := 1.0
if header.DocCurrencyCode.Valid && header.DocCurrencyCode.String != "TRY" {
if c, err := GetTodayCurrencyV3(conn, header.DocCurrencyCode.String); err == nil && c.Rate > 0 {
exRate = c.Rate
}
}
// =======================================================
// 0) Mevcut satırları oku (GUID STRING olarak!)
// =======================================================
existingOpen := make(map[string]bool)
existingClosed := make(map[string]bool)
existingOpenCombo := make(map[string]string)
existingClosedCombo := make(map[string]string)
existingOpenMeta := make(map[string]struct {
item string
color string
dim1 string
dim2 string
})
rows, err := tx.Query(`
SELECT
CONVERT(varchar(36), OrderLineID),
ISNULL(IsClosed, 0),
ISNULL(ItemCode,''),
ISNULL(ColorCode,''),
ISNULL(ItemDim1Code,''),
ISNULL(ItemDim2Code,'')
FROM BAGGI_V3.dbo.trOrderLine
WHERE OrderHeaderID=@p1
`, header.OrderHeaderID)
if err != nil {
return nil, fmt.Errorf("mevcut satirlar okunamadi: %w", err)
}
defer rows.Close()
for rows.Next() {
var id, item, color, dim1, dim2 string
var closed bool
if err := rows.Scan(&id, &closed, &item, &color, &dim1, &dim2); err != nil {
return nil, err
}
combo := makeComboKeyParts(item, color, dim1, dim2)
if closed {
existingClosed[id] = true
if combo != "" {
existingClosedCombo[combo] = id
}
} else {
existingOpen[id] = true
existingOpenMeta[id] = struct {
item string
color string
dim1 string
dim2 string
}{
item: strings.TrimSpace(item),
color: strings.TrimSpace(color),
dim1: strings.TrimSpace(dim1),
dim2: strings.TrimSpace(dim2),
}
if combo != "" {
existingOpenCombo[combo] = id
}
}
}
isLineInvoiced := func(lineID string) (bool, error) {
var exists int
err := tx.QueryRow(`
SELECT CASE WHEN EXISTS (
SELECT 1
FROM BAGGI_V3.dbo.trInvoiceLine WITH (NOLOCK)
WHERE OrderLineID=@p1
) THEN 1 ELSE 0 END
`, lineID).Scan(&exists)
if err != nil {
return false, fmt.Errorf("invoice line kontrol query hatasi: %w", err)
}
return exists == 1, nil
}
// ======================================================
// HEADER UPDATE
// ======================================================
_, err = tx.Exec(`
UPDATE BAGGI_V3.dbo.trOrderHeader SET
OrderDate=@p1,
OrderTime=@p2,
AverageDueDate=@p3,
Description=@p4,
InternalDescription=@p5,
DocCurrencyCode=@p6,
LocalCurrencyCode=@p7,
ExchangeRate=@p8,
LastUpdatedUserName=@p9,
LastUpdatedDate=@p10
WHERE OrderHeaderID=@p11
`,
nullableDateString(header.OrderDate),
nullableTimeString(header.OrderTime),
nullableDateString(header.AverageDueDate),
nullableString(header.Description, ""),
nullableString(header.InternalDescription, ""),
nullableString(header.DocCurrencyCode, "TRY"),
nullableString(header.LocalCurrencyCode, "TRY"),
nullableFloat64(header.ExchangeRate, exRate),
v3User,
now,
header.OrderHeaderID,
)
if err != nil {
return nil, err
}
// ======================================================
// PREPARE STATEMENTS
// ======================================================
insStmt, err := tx.Prepare(`
INSERT INTO BAGGI_V3.dbo.trOrderLine (
OrderLineID, SortOrder, ItemTypeCode, ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code,
Qty1, Qty2, CancelQty1, CancelQty2, OrderCancelReasonCode,
DeliveryDate, PlannedDateOfLading, LineDescription,
UsedBarcode, CostCenterCode,
VatCode, VatRate, PCTCode, PCTRate,
LDisRate1, LDisRate2, LDisRate3, LDisRate4, LDisRate5,
DocCurrencyCode, PriceCurrencyCode, PriceExchangeRate,
Price, BaseProcessCode, BaseOrderNumber,
BaseCustomerTypeCode, BaseCustomerCode,
BaseSubCurrAccID, BaseStoreCode,
OrderHeaderID, CreatedUserName, CreatedDate,
LastUpdatedUserName, LastUpdatedDate,
SurplusOrderQtyToleranceRate,
WithHoldingTaxTypeCode, DOVCode
)
VALUES (
@p1,@p2,@p3,@p4,@p5,@p6,@p7,@p8,@p9,@p10,
@p11,@p12,@p13,@p14,@p15,@p16,@p17,@p18,
@p19,@p20,@p21,@p22,@p23,@p24,@p25,@p26,@p27,
@p28,@p29,@p30,@p31,@p32,@p33,@p34,@p35,
@p36,@p37,@p38,@p39,@p40,@p41,@p42,@p43,
@p44,@p45
)`)
if err != nil {
return nil, err
}
defer insStmt.Close()
updStmt, err := tx.Prepare(`
UPDATE BAGGI_V3.dbo.trOrderLine SET
SortOrder=@p1, ItemTypeCode=@p2, ItemCode=@p3, ColorCode=@p4,
ItemDim1Code=@p5, ItemDim2Code=@p6, ItemDim3Code=@p7,
Qty1=@p8, Qty2=@p9, CancelQty1=@p10, CancelQty2=@p11,
OrderCancelReasonCode=@p12,
DeliveryDate=@p13, PlannedDateOfLading=@p14,
LineDescription=@p15, UsedBarcode=@p16, CostCenterCode=@p17,
VatCode=@p18, VatRate=@p19, PCTCode=@p20, PCTRate=@p21,
LDisRate1=@p22, LDisRate2=@p23, LDisRate3=@p24,
LDisRate4=@p25, LDisRate5=@p26,
DocCurrencyCode=@p27, PriceCurrencyCode=@p28,
PriceExchangeRate=@p29, Price=@p30,
BaseProcessCode=@p31, BaseOrderNumber=@p32,
BaseCustomerTypeCode=@p33, BaseCustomerCode=@p34,
BaseSubCurrAccID=@p35, BaseStoreCode=@p36,
LastUpdatedUserName=@p37, LastUpdatedDate=@p38,
SurplusOrderQtyToleranceRate=@p39,
WithHoldingTaxTypeCode=@p40, DOVCode=@p41
WHERE OrderLineID=@p42 AND ISNULL(IsClosed,0)=0`)
if err != nil {
return nil, err
}
defer updStmt.Close()
// ======================================================
// LOOP
// ======================================================
lineResults := make([]OrderLineResult, 0)
seenCombo := make(map[string]bool)
for _, ln := range lines {
comboKey := normalizeComboKey(safeNS(ln.ComboKey))
if comboKey == "" {
comboKey = makeComboKey(ln)
}
// Duplicate guard (SADECE aktif)
if qtyValue(ln.Qty1) > 0 && comboKey != "" {
if seenCombo[comboKey] {
return nil, fmt.Errorf("Duplicate satır: %s", comboKey)
}
seenCombo[comboKey] = true
}
// Kapalı satır guard
if ln.OrderLineID != "" && existingClosed[ln.OrderLineID] {
continue
}
if comboKey != "" {
if _, ok := existingClosedCombo[comboKey]; ok {
continue
}
}
// DELETE SIGNAL
if ln.OrderLineID != "" && qtyValue(ln.Qty1) <= 0 {
invoiced, err := isLineInvoiced(ln.OrderLineID)
if err != nil {
return nil, err
}
if invoiced {
return nil, &models.ValidationError{
Code: "ORDER_LINE_INVOICED",
Message: fmt.Sprintf("Faturalanmis satir silinemez (OrderLineID=%s)", ln.OrderLineID),
ClientKey: safeNS(ln.ClientKey),
ItemCode: strings.TrimSpace(safeNS(ln.ItemCode)),
ColorCode: strings.TrimSpace(safeNS(ln.ColorCode)),
Dim1: strings.TrimSpace(safeNS(ln.ItemDim1Code)),
Dim2: strings.TrimSpace(safeNS(ln.ItemDim2Code)),
}
}
if _, err := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLineCurrency WHERE OrderLineID=@p1`, ln.OrderLineID); err != nil {
return nil, err
}
if _, err := tx.Exec(`
DELETE FROM BAGGI_V3.dbo.trOrderLine
WHERE OrderHeaderID=@p1 AND OrderLineID=@p2 AND ISNULL(IsClosed,0)=0
`, header.OrderHeaderID, ln.OrderLineID); err != nil {
return nil, err
}
delete(existingOpen, ln.OrderLineID)
delete(existingOpenCombo, comboKey)
continue
}
isNew := false
// ID resolve: boşsa combo'dan yakala, yoksa yeni üret
if ln.OrderLineID == "" {
if dbID, ok := existingOpenCombo[comboKey]; ok {
ln.OrderLineID = dbID
} else {
ln.OrderLineID = uuid.New().String()
isNew = true
}
}
// Variant guard
if qtyValue(ln.Qty1) > 0 {
if err := ValidateItemVariant(tx, ln); err != nil {
return nil, err
}
}
if isNew {
_, err := insStmt.Exec(
ln.OrderLineID, ln.SortOrder, ln.ItemTypeCode,
nullableString(ln.ItemCode, ""), safeNS(ln.ColorCode),
safeNS(ln.ItemDim1Code), safeNS(ln.ItemDim2Code),
nullableString(ln.ItemDim3Code, ""),
ln.Qty1, ln.Qty2, ln.CancelQty1, ln.CancelQty2,
nullableString(ln.OrderCancelReasonCode, ""),
nullableTime(ln.DeliveryDate, now),
nullableDateString(ln.PlannedDateOfLading),
nullableString(ln.LineDescription, ""),
nullableString(ln.UsedBarcode, ""),
nullableString(ln.CostCenterCode, ""),
normalizeVatCode(ln.VatCode), nf0(ln.VatRate),
normalizePCTCode(ln.PCTCode), nf0(ln.PCTRate),
nf0(ln.LDisRate1), nf0(ln.LDisRate2),
nf0(ln.LDisRate3), nf0(ln.LDisRate4), nf0(ln.LDisRate5),
nullableString(ln.DocCurrencyCode, "TRY"),
nullableString(ln.PriceCurrencyCode, "TRY"),
nf0(ln.PriceExchangeRate), nf0(ln.Price),
nullableString(ln.BaseProcessCode, ""),
nullableString(ln.BaseOrderNumber, ""),
ln.BaseCustomerTypeCode,
nullableString(ln.BaseCustomerCode, ""),
nullableUUID(ln.BaseSubCurrAccID),
nullableString(ln.BaseStoreCode, ""),
header.OrderHeaderID,
v3User, now, v3User, now,
nf0(ln.SurplusOrderQtyToleranceRate),
nullableString(ln.WithHoldingTaxTypeCode, ""),
nullableString(ln.DOVCode, ""),
)
if err != nil {
return nil, err
}
} else {
_, err := updStmt.Exec(
ln.SortOrder, ln.ItemTypeCode,
nullableString(ln.ItemCode, ""),
safeNS(ln.ColorCode),
safeNS(ln.ItemDim1Code),
safeNS(ln.ItemDim2Code),
nullableString(ln.ItemDim3Code, ""),
nf0(ln.Qty1), nf0(ln.Qty2),
nf0(ln.CancelQty1), nf0(ln.CancelQty2),
nullableString(ln.OrderCancelReasonCode, ""),
nullableTime(ln.DeliveryDate, now),
nullableDateString(ln.PlannedDateOfLading),
nullableString(ln.LineDescription, ""),
nullableString(ln.UsedBarcode, ""),
nullableString(ln.CostCenterCode, ""),
normalizeVatCode(ln.VatCode), nf0(ln.VatRate),
normalizePCTCode(ln.PCTCode), nf0(ln.PCTRate),
nf0(ln.LDisRate1), nf0(ln.LDisRate2),
nf0(ln.LDisRate3), nf0(ln.LDisRate4), nf0(ln.LDisRate5),
nullableString(ln.DocCurrencyCode, "TRY"),
nullableString(ln.PriceCurrencyCode, "TRY"),
nf0(ln.PriceExchangeRate), nf0(ln.Price),
nullableString(ln.BaseProcessCode, ""),
nullableString(ln.BaseOrderNumber, ""),
ln.BaseCustomerTypeCode,
nullableString(ln.BaseCustomerCode, ""),
nullableUUID(ln.BaseSubCurrAccID),
nullableString(ln.BaseStoreCode, ""),
v3User, now,
nf0(ln.SurplusOrderQtyToleranceRate),
nullableString(ln.WithHoldingTaxTypeCode, ""),
nullableString(ln.DOVCode, ""),
ln.OrderLineID,
)
if err != nil {
return nil, err
}
}
// ✅ Currency UPSERT (insert/update sonrası ortak)
if err := upsertLineCurrency(
tx,
ln.OrderLineID,
safeNS(ln.DocCurrencyCode),
nf0(ln.PriceExchangeRate),
nf0(ln.Price),
nf0(ln.Qty1),
nf0(ln.LDisRate1),
0, // TODO: header TDisRate toplamını istersen buraya bağlarız
nf0(ln.VatRate), // satır vat oranı
ln,
v3User,
); err != nil {
return nil, err
}
// Bu satır işlendi -> existingOpen setinden düş
delete(existingOpen, ln.OrderLineID)
delete(existingOpenCombo, comboKey)
// Sonuç mapping
if ln.ClientKey.Valid {
lineResults = append(lineResults, OrderLineResult{
ClientKey: ln.ClientKey.String,
OrderLineID: ln.OrderLineID,
})
}
}
// =======================================================
// Grid dışı kalan açık satırlar (payload'da yok -> sil)
// =======================================================
for id := range existingOpen {
invoiced, err := isLineInvoiced(id)
if err != nil {
return nil, err
}
if invoiced {
meta := existingOpenMeta[id]
fmt.Printf("[ORDER_UPDATE] skip delete invoiced line id=%s item=%s color=%s dim1=%s dim2=%s\n",
id, meta.item, meta.color, meta.dim1, meta.dim2)
continue
}
if _, err := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLineCurrency WHERE OrderLineID=@p1`, id); err != nil {
return nil, err
}
if _, err := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLine WHERE OrderLineID=@p1 AND ISNULL(IsClosed,0)=0`, id); err != nil {
return nil, err
}
}
// =======================================================
// COMMIT + RETURN
// =======================================================
if err := tx.Commit(); err != nil {
return nil, err
}
return lineResults, nil
}