package queries import ( "bssapp-backend/db" "bssapp-backend/models" "context" "database/sql" "fmt" "math" "sort" "strconv" "strings" "time" ) func GetStatementAging(params models.StatementAgingParams) ([]map[string]interface{}, error) { accountCode := normalizeMasterAccountCode(params.AccountCode) if strings.TrimSpace(accountCode) == "" { return nil, fmt.Errorf("accountcode is required") } if strings.TrimSpace(params.EndDate) == "" { return nil, fmt.Errorf("enddate is required") } useType2, useType3 := resolveUseTypes(params.Parislemler) endDate, _ := time.Parse("2006-01-02", strings.TrimSpace(params.EndDate)) rows, err := db.MssqlDB.Query(` EXEC dbo.SP_FIFO_MATCH_FINAL @Cari8 = @Cari8, @SonTarih = @SonTarih, @UseType2 = @UseType2, @UseType3 = @UseType3; `, sql.Named("Cari8", accountCode), sql.Named("SonTarih", params.EndDate), sql.Named("UseType2", useType2), sql.Named("UseType3", useType3), ) if err != nil { return nil, fmt.Errorf("SP_FIFO_MATCH_FINAL query error: %w", err) } defer rows.Close() columns, err := rows.Columns() if err != nil { return nil, fmt.Errorf("columns read error: %w", err) } result := make([]map[string]interface{}, 0, 2048) cari8Set := make(map[string]struct{}) currencySet := make(map[string]struct{}) for rows.Next() { values := make([]interface{}, len(columns)) scanArgs := make([]interface{}, len(columns)) for i := range values { scanArgs[i] = &values[i] } if err := rows.Scan(scanArgs...); err != nil { return nil, fmt.Errorf("row scan error: %w", err) } row := make(map[string]interface{}, len(columns)) for i, col := range columns { switch v := values[i].(type) { case nil: row[col] = nil case []byte: row[col] = string(v) case time.Time: row[col] = v.Format("2006-01-02") default: row[col] = v } } cari8 := strings.TrimSpace(asString(row["Cari8"])) if cari8 != "" { cari8Set[cari8] = struct{}{} } curr := strings.ToUpper(strings.TrimSpace(asString(row["DocCurrencyCode"]))) if curr != "" && curr != "TRY" { currencySet[curr] = struct{}{} } currencySet["USD"] = struct{}{} result = append(result, row) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("rows error: %w", err) } cariDetailMap, err := loadAgingMasterCariDetailMap(context.Background(), cari8Set) if err != nil { return nil, err } rateSeriesByCurr, err := loadTryRateSeriesByCurrency(context.Background(), currencySet) if err != nil { return nil, err } for i := range result { row := result[i] cari8 := strings.TrimSpace(asString(row["Cari8"])) curr := strings.ToUpper(strings.TrimSpace(asString(row["DocCurrencyCode"]))) if curr == "" { curr = "TRY" } aciklama := strings.ToUpper(strings.TrimSpace(asString(row["Aciklama"]))) targetDate := endDate if aciklama != "ACIKKALEM" { if odemeTarihi, ok := parseDateOnly(asString(row["OdemeTarihi"])); ok { targetDate = odemeTarihi } } tutar := asFloat64(row["EslesenTutar"]) currTry := resolveTryRate(curr, targetDate, rateSeriesByCurr) usdTry := resolveTryRate("USD", targetDate, rateSeriesByCurr) tryTutar := toTRYByRate(tutar, curr, currTry) usdTutar := toUSDByRates(tutar, curr, currTry, usdTry) gunKur := usdRateInCurrency(curr, currTry, usdTry) row["CariDetay"] = cariDetailMap[cari8] row["UsdTutar"] = round2(usdTutar) row["TryTutar"] = round2(tryTutar) row["GunKur"] = round6(gunKur) } return result, nil } func resolveUseTypes(parislemler []string) (int, int) { if len(parislemler) == 0 { return 1, 0 } useType2 := 0 useType3 := 0 for _, v := range parislemler { switch strings.TrimSpace(v) { case "2": useType2 = 1 case "3": useType3 = 1 } } if useType2 == 0 && useType3 == 0 { return 1, 0 } return useType2, useType3 } func loadAgingMasterCariDetailMap(ctx context.Context, cari8Set map[string]struct{}) (map[string]string, error) { if len(cari8Set) == 0 { return map[string]string{}, nil } query := fmt.Sprintf(` WITH BaseCari AS ( SELECT CurrAccCode, CurrAccTypeCode, MasterCari = LEFT(CurrAccCode, 8), rn = ROW_NUMBER() OVER ( PARTITION BY LEFT(CurrAccCode, 8) ORDER BY CurrAccCode ) FROM cdCurrAcc WITH (NOLOCK) WHERE CurrAccTypeCode IN (1,3) AND LEFT(CurrAccCode, 8) IN (%s) ) SELECT b.MasterCari, CariDetay = ISNULL(d.CurrAccDescription, '') FROM BaseCari b LEFT JOIN cdCurrAccDesc d WITH (NOLOCK) ON d.CurrAccTypeCode = b.CurrAccTypeCode AND d.CurrAccCode = b.CurrAccCode AND d.LangCode = 'TR' WHERE b.rn = 1; `, quotedInList(cari8Set)) rows, err := db.MssqlDB.QueryContext(ctx, query) if err != nil { return nil, fmt.Errorf("aging cari detail query error: %w", err) } defer rows.Close() out := make(map[string]string, len(cari8Set)) for rows.Next() { var cari8 string var detail sql.NullString if err := rows.Scan(&cari8, &detail); err != nil { return nil, err } out[strings.TrimSpace(cari8)] = strings.TrimSpace(detail.String) } if err := rows.Err(); err != nil { return nil, err } return out, nil } func asString(v interface{}) string { switch x := v.(type) { case nil: return "" case string: return x case []byte: return string(x) default: return fmt.Sprint(x) } } func asFloat64(v interface{}) float64 { switch x := v.(type) { case nil: return 0 case float64: return x case float32: return float64(x) case int64: return float64(x) case int32: return float64(x) case int: return float64(x) case string: return parseNumberString(x) case []byte: return parseNumberString(string(x)) default: return parseNumberString(fmt.Sprint(x)) } } func parseNumberString(s string) float64 { s = strings.TrimSpace(s) if s == "" { return 0 } hasComma := strings.Contains(s, ",") hasDot := strings.Contains(s, ".") if hasComma && hasDot { if strings.LastIndex(s, ",") > strings.LastIndex(s, ".") { s = strings.ReplaceAll(s, ".", "") s = strings.Replace(s, ",", ".", 1) } else { s = strings.ReplaceAll(s, ",", "") } } else if hasComma { s = strings.ReplaceAll(s, ".", "") s = strings.Replace(s, ",", ".", 1) } n, err := strconv.ParseFloat(s, 64) if err != nil { return 0 } return n } func round2(v float64) float64 { return math.Round(v*100) / 100 } func round6(v float64) float64 { return math.Round(v*1_000_000) / 1_000_000 } type ratePoint struct { date time.Time rate float64 } func loadTryRateSeriesByCurrency(ctx context.Context, currencies map[string]struct{}) (map[string][]ratePoint, error) { if len(currencies) == 0 { return map[string][]ratePoint{}, nil } query := fmt.Sprintf(` SELECT CurrencyCode, Rate, CAST([Date] AS date) AS RateDate FROM AllExchangeRates WHERE RelationCurrencyCode = 'TRY' AND ExchangeTypeCode = 6 AND Rate > 0 AND CurrencyCode IN (%s) `, quotedInList(currencies)) rows, err := db.MssqlDB.QueryContext(ctx, query) if err != nil { return nil, fmt.Errorf("aging currency series query error: %w", err) } defer rows.Close() out := make(map[string][]ratePoint, len(currencies)) for rows.Next() { var code string var rate float64 var dt time.Time if err := rows.Scan(&code, &rate, &dt); err != nil { return nil, err } code = strings.ToUpper(strings.TrimSpace(code)) out[code] = append(out[code], ratePoint{date: dt, rate: rate}) } if err := rows.Err(); err != nil { return nil, err } for c := range out { sort.Slice(out[c], func(i, j int) bool { return out[c][i].date.Before(out[c][j].date) }) } return out, nil } func resolveTryRate(currency string, target time.Time, series map[string][]ratePoint) float64 { currency = strings.ToUpper(strings.TrimSpace(currency)) if currency == "" || currency == "TRY" { return 1 } points := series[currency] if len(points) == 0 { return 0 } best := points[0] bestDiff := absDurationDays(points[0].date.Sub(target)) for i := 1; i < len(points); i++ { diff := absDurationDays(points[i].date.Sub(target)) if diff < bestDiff || (diff == bestDiff && points[i].date.After(best.date)) { best = points[i] bestDiff = diff } } return best.rate } func absDurationDays(d time.Duration) int64 { if d < 0 { d = -d } return int64(d.Hours() / 24) } func parseDateOnly(v string) (time.Time, bool) { v = strings.TrimSpace(v) if v == "" { return time.Time{}, false } t, err := time.Parse("2006-01-02", v) if err != nil { return time.Time{}, false } return t, true } func toTRYByRate(amount float64, currency string, currTry float64) float64 { currency = strings.ToUpper(strings.TrimSpace(currency)) if currency == "" || currency == "TRY" { return amount } if currTry <= 0 { return 0 } return amount * currTry } func toUSDByRates(amount float64, currency string, currTry, usdTry float64) float64 { currency = strings.ToUpper(strings.TrimSpace(currency)) switch currency { case "USD": return amount case "", "TRY": if usdTry <= 0 { return 0 } return amount / usdTry default: if currTry <= 0 || usdTry <= 0 { return 0 } return (amount * currTry) / usdTry } } // Returns X for "1 USD = X ". func usdRateInCurrency(currency string, currTry, usdTry float64) float64 { currency = strings.ToUpper(strings.TrimSpace(currency)) switch currency { case "", "USD": return 1 case "TRY": if usdTry <= 0 { return 0 } return usdTry default: if currTry <= 0 || usdTry <= 0 { return 0 } return usdTry / currTry } }