From a422569c87a5e43a58af6d063e1f525313526cd8 Mon Sep 17 00:00:00 2001 From: MEHMETKECECI Date: Fri, 13 Feb 2026 21:00:39 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- svc/queries/order_bulk_close.go | 344 ++++++++++++++++++++++++++++++++ svc/routes/order_bulk_close.go | 115 +++++++++++ 2 files changed, 459 insertions(+) create mode 100644 svc/queries/order_bulk_close.go create mode 100644 svc/routes/order_bulk_close.go diff --git a/svc/queries/order_bulk_close.go b/svc/queries/order_bulk_close.go new file mode 100644 index 0000000..e6fd5fb --- /dev/null +++ b/svc/queries/order_bulk_close.go @@ -0,0 +1,344 @@ +package queries + +import ( + "bssapp-backend/auth" + "bssapp-backend/internal/authz" + "context" + "database/sql" + "fmt" + "strings" +) + +func resolvePiyasaWhere(ctx context.Context, pg *sql.DB) (string, error) { + claims, ok := auth.GetClaimsFromContext(ctx) + if !ok || claims == nil { + return "", fmt.Errorf("unauthorized: claims not found") + } + + if claims.IsAdmin() { + return "1=1", nil + } + + codes, err := authz.GetUserPiyasaCodes(pg, int(claims.ID)) + if err != nil { + return "", fmt.Errorf("piyasa codes load error: %w", err) + } + + if len(codes) == 0 { + return "1=0", nil + } + + return authz.BuildINClause("UPPER(f2.CustomerAtt01)", codes), nil +} + +func GetOrderListCloseReady( + ctx context.Context, + mssql *sql.DB, + pg *sql.DB, + search string, +) (*sql.Rows, error) { + + piyasaWhere, err := resolvePiyasaWhere(ctx, pg) + if err != nil { + return nil, err + } + + baseQuery := fmt.Sprintf(` +SELECT + CAST(h.OrderHeaderID AS NVARCHAR(50)) AS OrderHeaderID, + ISNULL(h.OrderNumber, '') AS OrderNumber, + CONVERT(varchar, h.OrderDate, 23) AS OrderDate, + + ISNULL(h.CurrAccCode, '') AS CurrAccCode, + ISNULL(ca.CurrAccDescription, '') AS CurrAccDescription, + + ISNULL(mt.AttributeDescription, '') AS MusteriTemsilcisi, + ISNULL(py.AttributeDescription, '') AS Piyasa, + + CONVERT(varchar, h.CreditableConfirmedDate,23) AS CreditableConfirmedDate, + ISNULL(h.DocCurrencyCode,'TRY') AS DocCurrencyCode, + + ISNULL(l.TotalAmount,0) AS TotalAmount, + + CASE + WHEN h.DocCurrencyCode = 'USD' + THEN ISNULL(l.TotalAmount,0) + WHEN h.DocCurrencyCode = 'TRY' + AND usd.Rate > 0 + THEN ISNULL(l.TotalAmount,0) / usd.Rate + WHEN h.DocCurrencyCode IN ('EUR','GBP') + AND cur.Rate > 0 + AND usd.Rate > 0 + THEN (ISNULL(l.TotalAmount,0) * cur.Rate) / usd.Rate + ELSE 0 + END AS TotalAmountUSD, + + ISNULL(l.PackedAmount,0) AS PackedAmount, + + CASE + WHEN h.DocCurrencyCode = 'USD' + THEN ISNULL(l.PackedAmount,0) + WHEN h.DocCurrencyCode = 'TRY' + AND usd.Rate > 0 + THEN ISNULL(l.PackedTRY,0) / usd.Rate + WHEN cur.Rate > 0 + AND usd.Rate > 0 + THEN (ISNULL(l.PackedAmount,0) * cur.Rate) / usd.Rate + ELSE 0 + END AS PackedUSD, + + CASE + WHEN ISNULL(l.TotalAmount,0) > 0 + THEN (ISNULL(l.PackedAmount,0) * 100.0) / NULLIF(l.TotalAmount,0) + ELSE 0 + END AS PackedRatePct, + + ISNULL(h.IsCreditableConfirmed,0) AS IsCreditableConfirmed, + ISNULL(h.Description,'') AS Description, + + usd.Rate AS ExchangeRateUSD + +FROM dbo.trOrderHeader h + +JOIN ( + SELECT + l.OrderHeaderID, + SUM(ISNULL(c.NetAmount,0)) AS TotalAmount, + SUM( + CASE + WHEN ISNULL(c.CurrencyCode,'') = 'TRY' + THEN ISNULL(c.NetAmount,0) + ELSE 0 + END + ) AS TotalTRY, + SUM( + CASE + WHEN ISNULL(l.IsClosed,0) = 1 + THEN ISNULL(c.NetAmount,0) + ELSE 0 + END + ) AS PackedAmount, + SUM( + CASE + WHEN ISNULL(l.IsClosed,0) = 1 + AND ISNULL(c.CurrencyCode,'') = 'TRY' + THEN ISNULL(c.NetAmount,0) + ELSE 0 + END + ) AS PackedTRY + FROM dbo.trOrderLine l + JOIN dbo.trOrderHeader h2 + ON h2.OrderHeaderID = l.OrderHeaderID + LEFT JOIN dbo.trOrderLineCurrency c + ON c.OrderLineID = l.OrderLineID + AND c.CurrencyCode = ISNULL(h2.DocCurrencyCode,'TRY') + GROUP BY l.OrderHeaderID +) l + ON l.OrderHeaderID = h.OrderHeaderID + +LEFT JOIN dbo.cdCurrAccDesc ca + ON ca.CurrAccCode = h.CurrAccCode + AND ca.LangCode = 'TR' + +LEFT JOIN dbo.CustomerAttributesFilter f + ON f.CurrAccCode = h.CurrAccCode + +LEFT JOIN dbo.cdCurrAccAttributeDesc mt + ON mt.CurrAccTypeCode = 3 + AND mt.AttributeTypeCode = 2 + AND mt.AttributeCode = f.CustomerAtt02 + AND mt.LangCode = 'TR' + +LEFT JOIN dbo.cdCurrAccAttributeDesc py + ON py.CurrAccTypeCode = 3 + AND py.AttributeTypeCode = 1 + AND py.AttributeCode = f.CustomerAtt01 + AND py.LangCode = 'TR' + +OUTER APPLY ( + SELECT TOP 1 Rate + FROM dbo.AllExchangeRates + WHERE CurrencyCode = 'USD' + AND RelationCurrencyCode = 'TRY' + AND ExchangeTypeCode = 6 + AND Rate > 0 + AND Date <= CAST(GETDATE() AS date) + ORDER BY Date DESC +) usd + +OUTER APPLY ( + SELECT TOP 1 Rate + FROM dbo.AllExchangeRates + WHERE CurrencyCode = h.DocCurrencyCode + AND RelationCurrencyCode = 'TRY' + AND ExchangeTypeCode = 6 + AND Rate > 0 + AND Date <= CAST(GETDATE() AS date) + ORDER BY Date DESC +) cur + +WHERE + ISNULL(h.IsCancelOrder,0) = 0 + AND h.OrderTypeCode = 1 + AND h.ProcessCode = 'WS' + AND h.IsClosed = 0 + AND ISNULL(l.TotalAmount,0) > 0 + AND (ISNULL(l.PackedAmount,0) * 100.0) / NULLIF(l.TotalAmount,0) >= 100 + + AND EXISTS ( + SELECT 1 + FROM dbo.CustomerAttributesFilter f2 + WHERE f2.CurrAccCode = h.CurrAccCode + AND %s + ) +`, piyasaWhere) + + if search != "" { + baseQuery += ` +AND EXISTS ( + SELECT 1 + FROM dbo.trOrderHeader h2 + LEFT JOIN dbo.cdCurrAccDesc ca2 + ON ca2.CurrAccCode = h2.CurrAccCode + AND ca2.LangCode = 'TR' + WHERE h2.OrderHeaderID = h.OrderHeaderID + AND ( + LOWER(REPLACE(REPLACE(h2.OrderNumber,'İ','I'),'ı','i')) + COLLATE Latin1_General_CI_AI LIKE LOWER(@p1) + + OR LOWER(REPLACE(REPLACE(h2.CurrAccCode,'İ','I'),'ı','i')) + COLLATE Latin1_General_CI_AI LIKE LOWER(@p1) + + OR LOWER(REPLACE(REPLACE(ca2.CurrAccDescription,'İ','I'),'ı','i')) + COLLATE Latin1_General_CI_AI LIKE LOWER(@p1) + + OR LOWER(REPLACE(REPLACE(h2.Description,'İ','I'),'ı','i')) + COLLATE Latin1_General_CI_AI LIKE LOWER(@p1) + ) +) +` + } + + baseQuery += ` +ORDER BY h.CreatedDate DESC +` + + if search != "" { + searchLike := fmt.Sprintf("%%%s%%", search) + return mssql.Query(baseQuery, searchLike) + } + + return mssql.Query(baseQuery) +} + +func BulkCloseOrders( + ctx context.Context, + mssql *sql.DB, + pg *sql.DB, + orderNumbers []string, + updatedBy string, +) (int64, error) { + + piyasaWhere, err := resolvePiyasaWhere(ctx, pg) + if err != nil { + return 0, err + } + + clean := make([]string, 0, len(orderNumbers)) + seen := make(map[string]struct{}) + + for _, n := range orderNumbers { + n = strings.TrimSpace(n) + if n == "" { + continue + } + key := strings.ToUpper(n) + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + clean = append(clean, n) + } + + if len(clean) == 0 { + return 0, fmt.Errorf("order_numbers is empty") + } + + if strings.TrimSpace(updatedBy) == "" { + updatedBy = "SYSTEM" + } + + placeholders := make([]string, len(clean)) + args := make([]any, 0, len(clean)+1) + args = append(args, updatedBy) // @p1 + + for i, no := range clean { + placeholders[i] = fmt.Sprintf("@p%d", i+2) + args = append(args, no) + } + + tx, err := mssql.BeginTx(ctx, nil) + if err != nil { + return 0, fmt.Errorf("begin tx failed: %w", err) + } + defer tx.Rollback() + + q := fmt.Sprintf(` +UPDATE h +SET + h.IsClosed = 1, + h.LastUpdatedDate = GETDATE(), + h.LastUpdatedUserName = @p1 +FROM dbo.trOrderHeader h +WHERE + h.IsClosed = 0 + AND h.OrderNumber IN (%s) + AND EXISTS ( + SELECT 1 + FROM dbo.CustomerAttributesFilter f2 + WHERE f2.CurrAccCode = h.CurrAccCode + AND %s + ) + AND EXISTS ( + SELECT 1 + FROM ( + SELECT + l.OrderHeaderID, + SUM(ISNULL(c.NetAmount,0)) AS TotalAmount, + SUM( + CASE + WHEN ISNULL(l.IsClosed,0) = 1 + THEN ISNULL(c.NetAmount,0) + ELSE 0 + END + ) AS PackedAmount + FROM dbo.trOrderLine l + JOIN dbo.trOrderHeader h2 + ON h2.OrderHeaderID = l.OrderHeaderID + LEFT JOIN dbo.trOrderLineCurrency c + ON c.OrderLineID = l.OrderLineID + AND c.CurrencyCode = ISNULL(h2.DocCurrencyCode,'TRY') + GROUP BY l.OrderHeaderID + ) t + WHERE t.OrderHeaderID = h.OrderHeaderID + AND t.TotalAmount > 0 + AND (t.PackedAmount * 100.0) / NULLIF(t.TotalAmount,0) >= 100 + ); +`, strings.Join(placeholders, ","), piyasaWhere) + + res, err := tx.ExecContext(ctx, q, args...) + if err != nil { + return 0, fmt.Errorf("bulk close update failed: %w", err) + } + + affected, err := res.RowsAffected() + if err != nil { + return 0, fmt.Errorf("rows affected read failed: %w", err) + } + + if err := tx.Commit(); err != nil { + return 0, fmt.Errorf("commit failed: %w", err) + } + + return affected, nil +} diff --git a/svc/routes/order_bulk_close.go b/svc/routes/order_bulk_close.go new file mode 100644 index 0000000..1dd281c --- /dev/null +++ b/svc/routes/order_bulk_close.go @@ -0,0 +1,115 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/db" + "bssapp-backend/models" + "bssapp-backend/queries" + "database/sql" + "encoding/json" + "log" + "net/http" + "strings" +) + +type bulkCloseOrdersRequest struct { + OrderNumbers []string `json:"order_numbers"` +} + +func OrderCloseReadyListRoute(mssqlDB *sql.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + raw := r.URL.Query().Get("search") + search := strings.TrimSpace(raw) + + rows, err := queries.GetOrderListCloseReady( + r.Context(), + mssqlDB, + db.PgDB, + search, + ) + if err != nil { + log.Printf("❌ close-ready list query error: %v", err) + http.Error(w, "Veritabanı hatası", http.StatusInternalServerError) + return + } + defer rows.Close() + + list := make([]models.OrderList, 0, 100) + for rows.Next() { + var o models.OrderList + if err := rows.Scan( + &o.OrderHeaderID, + &o.OrderNumber, + &o.OrderDate, + &o.CurrAccCode, + &o.CurrAccDescription, + &o.MusteriTemsilcisi, + &o.Piyasa, + &o.CreditableConfirmedDate, + &o.DocCurrencyCode, + &o.TotalAmount, + &o.TotalAmountUSD, + &o.PackedAmount, + &o.PackedUSD, + &o.PackedRatePct, + &o.IsCreditableConfirmed, + &o.Description, + &o.ExchangeRateUSD, + ); err != nil { + log.Printf("⚠️ close-ready scan error: %v", err) + continue + } + list = append(list, o) + } + + if err := rows.Err(); err != nil { + log.Printf("⚠️ close-ready rows err: %v", err) + } + + _ = json.NewEncoder(w).Encode(list) + }) +} + +func OrderBulkCloseRoute(mssqlDB *sql.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + var req bulkCloseOrdersRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Geçersiz JSON", http.StatusBadRequest) + return + } + + if len(req.OrderNumbers) == 0 { + http.Error(w, "order_numbers zorunludur", http.StatusBadRequest) + return + } + + updatedBy := "SYSTEM" + if claims, ok := auth.GetClaimsFromContext(r.Context()); ok && claims != nil { + if u := strings.TrimSpace(claims.Username); u != "" { + updatedBy = u + } + } + + affected, err := queries.BulkCloseOrders( + r.Context(), + mssqlDB, + db.PgDB, + req.OrderNumbers, + updatedBy, + ) + if err != nil { + log.Printf("❌ bulk close failed: %v", err) + http.Error(w, "Toplu kapatma sırasında hata oluştu", http.StatusInternalServerError) + return + } + + _ = json.NewEncoder(w).Encode(map[string]any{ + "success": true, + "affected": affected, + }) + }) +}