package queries import ( "bssapp-backend/db" "bssapp-backend/models" "context" "database/sql" "fmt" "math" "strconv" "strings" "time" ) func GetStatementAging(params models.StatementAgingParams) ([]map[string]interface{}, error) { accountCode := normalizeMasterAccountCode(params.AccountCode) if strings.TrimSpace(params.EndDate) == "" { return nil, fmt.Errorf("enddate is required") } useType2, useType3 := resolveUseTypes(params.Parislemler) rateMap, err := loadNearestTryRates(context.Background()) if err != nil { return nil, err } usdTry := rateMap["USD"] if usdTry <= 0 { usdTry = 1 } 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{}) 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{}{} } 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 } 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" } tutar := asFloat64(row["EslesenTutar"]) usdTutar := toUSD(tutar, curr, usdTry, rateMap) currTry := rateMap[curr] usdToCurr := 0.0 if currTry > 0 && usdTry > 0 { usdToCurr = usdTry / currTry } row["CariDetay"] = cariDetailMap[cari8] row["UsdTutar"] = round2(usdTutar) row["CurrencyTryRate"] = round6(currTry) row["UsdTryRate"] = round6(usdTry) row["CurrencyUsdRate"] = round6(usdToCurr) } 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 }