package queries import ( "bssapp-backend/auth" "bssapp-backend/db" "bssapp-backend/internal/authz" "bssapp-backend/models" "context" "database/sql" "fmt" "log" "sort" "strconv" "strings" ) type mkCariBakiyeLine struct { CurrAccTypeCode int CariKodu string CariDoviz string SirketKodu int PislemTipi string ParasalIslemTipi string YerelBakiye float64 Bakiye float64 VadeGun float64 VadeBelgeGun float64 } type cariMeta struct { CariDetay string CariTip string Kanal1 string Piyasa string Temsilci string Ulke string Il string Ilce string TC string RiskDurumu string MuhasebeKodu string SirketDetay string } type masterCariMeta struct { CariDetay string Kanal1 string Piyasa string Temsilci string Ulke string Il string Ilce string RiskDurumu string } type balanceFilters struct { cariIlkGrup map[string]struct{} piyasa map[string]struct{} temsilci map[string]struct{} riskDurumu map[string]struct{} islemTipi map[string]struct{} ulke map[string]struct{} il map[string]struct{} ilce map[string]struct{} } func GetCustomerBalanceList(ctx context.Context, params models.CustomerBalanceListParams) ([]models.CustomerBalanceListRow, error) { if strings.TrimSpace(params.SelectedDate) == "" { return nil, fmt.Errorf("selected_date is required") } lines, err := loadBalanceLines(ctx, params.SelectedDate, params.CariSearch) if err != nil { return nil, err } metaMap, err := loadCariMetaMap(ctx, lines) if err != nil { log.Printf("customer_balance_list: cari meta query failed, fallback without meta: %v", err) metaMap = map[string]cariMeta{} } masterMetaMap, err := loadMasterCariMetaMap(ctx, lines) if err != nil { log.Printf("customer_balance_list: master cari meta query failed, fallback without master meta: %v", err) masterMetaMap = map[string]masterCariMeta{} } companyMap, err := loadCompanyMap(ctx) if err != nil { return nil, err } glMap, err := loadGLAccountMap(ctx, lines) if err != nil { return nil, err } rateMap, err := loadNearestTryRates(ctx) if err != nil { return nil, err } usdTry := rateMap["USD"] if usdTry <= 0 { usdTry = 1 } filters := buildFilters(params) agg := make(map[string]*models.CustomerBalanceListRow, len(lines)) for _, ln := range lines { cari := strings.TrimSpace(ln.CariKodu) if cari == "" { continue } curr := strings.ToUpper(strings.TrimSpace(ln.CariDoviz)) if curr == "" { curr = "TRY" } meta := metaMap[metaKey(ln.CurrAccTypeCode, cari)] meta.MuhasebeKodu = glMap[glKey(ln.CurrAccTypeCode, cari, ln.SirketKodu)] meta.SirketDetay = companyMap[ln.SirketKodu] master := deriveMasterCari(cari) mm := masterMetaMap[master] if strings.TrimSpace(mm.Kanal1) != "" { meta.Kanal1 = mm.Kanal1 } if strings.TrimSpace(mm.Piyasa) != "" { meta.Piyasa = mm.Piyasa } if strings.TrimSpace(mm.Temsilci) != "" { meta.Temsilci = mm.Temsilci } if strings.TrimSpace(mm.Ulke) != "" { meta.Ulke = mm.Ulke } if strings.TrimSpace(mm.Il) != "" { meta.Il = mm.Il } if strings.TrimSpace(mm.Ilce) != "" { meta.Ilce = mm.Ilce } if strings.TrimSpace(mm.RiskDurumu) != "" { meta.RiskDurumu = mm.RiskDurumu } if !filters.matchLine(ln.PislemTipi, meta) { continue } key := strconv.Itoa(ln.CurrAccTypeCode) + "|" + cari + "|" + curr + "|" + strconv.Itoa(ln.SirketKodu) row, ok := agg[key] if !ok { row = &models.CustomerBalanceListRow{ CariIlkGrup: meta.Kanal1, Piyasa: meta.Piyasa, Temsilci: meta.Temsilci, Sirket: strconv.Itoa(ln.SirketKodu), AnaCariKodu: master, AnaCariAdi: firstNonEmpty(mm.CariDetay, meta.CariDetay), CariKodu: cari, CariDetay: meta.CariDetay, CariTip: meta.CariTip, Kanal1: meta.Kanal1, Ozellik03: meta.RiskDurumu, Ozellik05: meta.Ulke, Ozellik06: meta.Il, Ozellik07: meta.Ilce, Il: meta.Il, Ilce: meta.Ilce, MuhasebeKodu: meta.MuhasebeKodu, TC: meta.TC, RiskDurumu: meta.RiskDurumu, SirketDetay: meta.SirketDetay, CariDoviz: curr, } agg[key] = row } usd := toUSD(ln.Bakiye, curr, usdTry, rateMap) add12, add13 := resolveBalanceBuckets(ln) if add12 { row.Bakiye12 += ln.Bakiye row.TLBakiye12 += ln.YerelBakiye row.USDBakiye12 += usd } if add13 { row.Bakiye13 += ln.Bakiye row.TLBakiye13 += ln.YerelBakiye row.USDBakiye13 += usd } } out := make([]models.CustomerBalanceListRow, 0, len(agg)) for _, v := range agg { out = append(out, *v) } sort.Slice(out, func(i, j int) bool { if out[i].AnaCariKodu == out[j].AnaCariKodu { if out[i].CariKodu == out[j].CariKodu { return out[i].CariDoviz < out[j].CariDoviz } return out[i].CariKodu < out[j].CariKodu } return out[i].AnaCariKodu < out[j].AnaCariKodu }) return out, nil } func loadMasterCariMetaMap(ctx context.Context, lines []mkCariBakiyeLine) (map[string]masterCariMeta, error) { masters := make(map[string]struct{}) for _, ln := range lines { m := strings.TrimSpace(deriveMasterCari(ln.CariKodu)) if m != "" { masters[m] = struct{}{} } } if len(masters) == 0 { return map[string]masterCariMeta{}, nil } query := fmt.Sprintf(` WITH BaseCari AS ( SELECT CB.CurrAccCode, CB.CurrAccTypeCode, MasterCari = LEFT(CB.CurrAccCode, 8), rn = ROW_NUMBER() OVER ( PARTITION BY LEFT(CB.CurrAccCode, 8) ORDER BY CB.CurrAccCode ) FROM cdCurrAcc CB WITH (NOLOCK) WHERE CB.CurrAccTypeCode IN (1,3) AND LEFT(CB.CurrAccCode, 8) IN (%s) ), FirstCari AS ( SELECT * FROM BaseCari WHERE rn = 1 ) SELECT CariKodu = F.MasterCari, CariDetay = ISNULL(cd.CurrAccDescription, ''), KANAL_1 = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt08Desc ELSE CDesc.CustomerAtt08Desc END, ''), PIYASA = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt01Desc ELSE CDesc.CustomerAtt01Desc END, ''), CARI_TEMSILCI = ISNULL( CASE WHEN ISNULL(CASE WHEN F.CurrAccTypeCode = 1 THEN VDesc.VendorAtt02Desc ELSE CDesc.CustomerAtt02Desc END,'') = '' THEN ISNULL(CASE WHEN F.CurrAccTypeCode = 1 THEN VAttr.VendorAtt09 ELSE CAttr.CustomerAtt09 END,'') ELSE CASE WHEN F.CurrAccTypeCode = 1 THEN VDesc.VendorAtt02Desc ELSE CDesc.CustomerAtt02Desc END END,'' ), ULKE = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt05Desc ELSE CDesc.CustomerAtt05Desc END, ''), IL = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt06Desc ELSE CDesc.CustomerAtt06Desc END, ''), ILCE = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt07Desc ELSE CDesc.CustomerAtt07Desc END, ''), Risk_Durumu = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt03Desc ELSE CDesc.CustomerAtt03Desc END, '') FROM FirstCari F LEFT JOIN cdCurrAccDesc cd WITH (NOLOCK) ON cd.CurrAccTypeCode = F.CurrAccTypeCode AND cd.CurrAccCode = F.CurrAccCode AND cd.LangCode = 'TR' LEFT JOIN VendorAttributeDescriptions('TR') VDesc ON VDesc.CurrAccCode = F.CurrAccCode AND VDesc.CurrAccTypeCode = F.CurrAccTypeCode LEFT JOIN CustomerAttributeDescriptions('TR') CDesc ON CDesc.CurrAccCode = F.CurrAccCode AND CDesc.CurrAccTypeCode = F.CurrAccTypeCode LEFT JOIN VendorAttributes VAttr ON VAttr.CurrAccCode = F.CurrAccCode AND VAttr.CurrAccTypeCode = F.CurrAccTypeCode LEFT JOIN CustomerAttributes CAttr ON CAttr.CurrAccCode = F.CurrAccCode AND CAttr.CurrAccTypeCode = F.CurrAccTypeCode ORDER BY F.MasterCari; `, quotedInList(masters)) rows, err := db.MssqlDB.QueryContext(ctx, query) if err != nil { return nil, fmt.Errorf("master cari meta query error: %w", err) } defer rows.Close() out := make(map[string]masterCariMeta, len(masters)) for rows.Next() { var master string var m masterCariMeta if err := rows.Scan( &master, &m.CariDetay, &m.Kanal1, &m.Piyasa, &m.Temsilci, &m.Ulke, &m.Il, &m.Ilce, &m.RiskDurumu, ); err != nil { return nil, err } out[strings.TrimSpace(master)] = m } if err := rows.Err(); err != nil { return nil, err } return out, nil } func loadBalanceLines(ctx context.Context, selectedDate, cariSearch string) ([]mkCariBakiyeLine, error) { piyasaScope, err := buildPiyasaExistsForCariCode(ctx, "CariKodu") if err != nil { return nil, err } queryTemplate := ` SELECT CurrAccTypeCode, CariKodu, CariDoviz, SirketKodu, PislemTipi, %s YerelBakiye, Bakiye, CAST(0 AS DECIMAL(18,4)) AS Vade_Gun, CAST(0 AS DECIMAL(18,4)) AS Vade_BelgeTarihi_Gun FROM dbo.MK_CARI_BAKIYE_LIST(@SonTarih) WHERE (@CariSearch = '' OR CariKodu LIKE '%%' + @CariSearch + '%%') AND %s ` selectParasalCandidates := make([]string, 0, 7) if expr := strings.TrimSpace(resolveParasalIslemSelectExpr(ctx, "SELECT * FROM dbo.MK_CARI_BAKIYE_LIST('2000-01-01')")); expr != "" { selectParasalCandidates = append(selectParasalCandidates, expr) } selectParasalCandidates = append(selectParasalCandidates, "CAST(ATAtt01 AS varchar(16)) AS ParasalIslemTipi,", "CAST(ParasalIslemTipi AS varchar(16)) AS ParasalIslemTipi,", "CAST(ParislemTipi AS varchar(16)) AS ParasalIslemTipi,", "CAST(ParIslemTipi AS varchar(16)) AS ParasalIslemTipi,", "CAST('' AS varchar(16)) AS ParasalIslemTipi,", ) var rows *sql.Rows for i, sel := range selectParasalCandidates { query := fmt.Sprintf(queryTemplate, sel, piyasaScope) rows, err = db.MssqlDB.QueryContext(ctx, query, sql.Named("SonTarih", selectedDate), sql.Named("CariSearch", strings.TrimSpace(cariSearch)), ) if err == nil { break } if i < len(selectParasalCandidates)-1 && isInvalidColumnError(err) { continue } return nil, fmt.Errorf("MK_CARI_BAKIYE_LIST query error: %w", err) } defer rows.Close() out := make([]mkCariBakiyeLine, 0, 4096) for rows.Next() { var r mkCariBakiyeLine if err := rows.Scan( &r.CurrAccTypeCode, &r.CariKodu, &r.CariDoviz, &r.SirketKodu, &r.PislemTipi, &r.ParasalIslemTipi, &r.YerelBakiye, &r.Bakiye, &r.VadeGun, &r.VadeBelgeGun, ); err != nil { return nil, err } out = append(out, r) } if err := rows.Err(); err != nil { return nil, err } return out, nil } func loadCariMetaMap(ctx context.Context, lines []mkCariBakiyeLine) (map[string]cariMeta, error) { vendorCodes := make(map[string]struct{}) customerCodes := make(map[string]struct{}) for _, ln := range lines { code := strings.TrimSpace(ln.CariKodu) if code == "" { continue } if ln.CurrAccTypeCode == 1 { vendorCodes[code] = struct{}{} } else if ln.CurrAccTypeCode == 3 { customerCodes[code] = struct{}{} } } if len(vendorCodes) == 0 && len(customerCodes) == 0 { return map[string]cariMeta{}, nil } whereParts := make([]string, 0, 2) if len(vendorCodes) > 0 { whereParts = append(whereParts, fmt.Sprintf("(c.CurrAccTypeCode=1 AND c.CurrAccCode IN (%s))", quotedInList(vendorCodes))) } if len(customerCodes) > 0 { whereParts = append(whereParts, fmt.Sprintf("(c.CurrAccTypeCode=3 AND c.CurrAccCode IN (%s))", quotedInList(customerCodes))) } query := fmt.Sprintf(` SELECT c.CurrAccTypeCode, c.CurrAccCode, CariDetay = ISNULL(d.CurrAccDescription, ''), CariTip = CASE WHEN c.CurrAccTypeCode = 1 THEN N'Satıcı' ELSE N'Müşteri' END, KANAL_1 = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt08Desc ELSE cad.CustomerAtt08Desc END, ''), PIYASA = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt01Desc ELSE cad.CustomerAtt01Desc END, ''), CARI_TEMSILCI = ISNULL( CASE WHEN ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt02Desc ELSE cad.CustomerAtt02Desc END, '') = '' THEN ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN va.VendorAtt09 ELSE ca.CustomerAtt09 END, '') ELSE CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt02Desc ELSE cad.CustomerAtt02Desc END END, ''), ULKE = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt05Desc ELSE cad.CustomerAtt05Desc END, ''), IL = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt06Desc ELSE cad.CustomerAtt06Desc END, ''), ILCE = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt07Desc ELSE cad.CustomerAtt07Desc END, ''), TC = ISNULL(c.IdentityNum, ''), Risk_Durumu = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt03Desc ELSE cad.CustomerAtt03Desc END, '') FROM cdCurrAcc c WITH(NOLOCK) LEFT JOIN cdCurrAccDesc d WITH(NOLOCK) ON d.CurrAccTypeCode = c.CurrAccTypeCode AND d.CurrAccCode = c.CurrAccCode AND d.LangCode = 'TR' LEFT JOIN VendorAttributes va WITH(NOLOCK) ON va.CurrAccTypeCode = c.CurrAccTypeCode AND va.CurrAccCode = c.CurrAccCode LEFT JOIN VendorAttributeDescriptions('TR') vad ON vad.CurrAccTypeCode = c.CurrAccTypeCode AND vad.CurrAccCode = c.CurrAccCode LEFT JOIN CustomerAttributes ca WITH(NOLOCK) ON ca.CurrAccTypeCode = c.CurrAccTypeCode AND ca.CurrAccCode = c.CurrAccCode LEFT JOIN CustomerAttributeDescriptions('TR') cad ON cad.CurrAccTypeCode = c.CurrAccTypeCode AND cad.CurrAccCode = c.CurrAccCode WHERE c.CurrAccTypeCode IN (1,3) AND (%s) `, strings.Join(whereParts, " OR ")) rows, err := db.MssqlDB.QueryContext(ctx, query) if err != nil { return nil, fmt.Errorf("cari meta query error: %w", err) } defer rows.Close() out := make(map[string]cariMeta, len(lines)) for rows.Next() { var t int var code string var m cariMeta if err := rows.Scan( &t, &code, &m.CariDetay, &m.CariTip, &m.Kanal1, &m.Piyasa, &m.Temsilci, &m.Ulke, &m.Il, &m.Ilce, &m.TC, &m.RiskDurumu, ); err != nil { return nil, err } out[metaKey(t, code)] = m } if err := rows.Err(); err != nil { return nil, err } return out, nil } func loadGLAccountMap(ctx context.Context, lines []mkCariBakiyeLine) (map[string]string, error) { vendorCodes := make(map[string]struct{}) customerCodes := make(map[string]struct{}) companyCodes := make(map[int]struct{}) for _, ln := range lines { code := strings.TrimSpace(ln.CariKodu) if code == "" { continue } companyCodes[ln.SirketKodu] = struct{}{} if ln.CurrAccTypeCode == 1 { vendorCodes[code] = struct{}{} } else if ln.CurrAccTypeCode == 3 { customerCodes[code] = struct{}{} } } if len(companyCodes) == 0 || (len(vendorCodes) == 0 && len(customerCodes) == 0) { return map[string]string{}, nil } whereParts := make([]string, 0, 2) if len(vendorCodes) > 0 { whereParts = append(whereParts, fmt.Sprintf("(CurrAccTypeCode=1 AND CurrAccCode IN (%s))", quotedInList(vendorCodes))) } if len(customerCodes) > 0 { whereParts = append(whereParts, fmt.Sprintf("(CurrAccTypeCode=3 AND CurrAccCode IN (%s))", quotedInList(customerCodes))) } query := fmt.Sprintf(` SELECT CurrAccTypeCode, CurrAccCode, CompanyCode, GLAccCode FROM prCurrAccGLAccount WITH(NOLOCK) WHERE PostAccTypeCode = 100 AND CompanyCode IN (%s) AND (%s) `, intInList(companyCodes), strings.Join(whereParts, " OR ")) rows, err := db.MssqlDB.QueryContext(ctx, query) if err != nil { return nil, fmt.Errorf("gl account query error: %w", err) } defer rows.Close() out := make(map[string]string) for rows.Next() { var t int var code string var company int var gl sql.NullString if err := rows.Scan(&t, &code, &company, &gl); err != nil { return nil, err } out[glKey(t, code, company)] = strings.TrimSpace(gl.String) } if err := rows.Err(); err != nil { return nil, err } return out, nil } func loadCompanyMap(ctx context.Context) (map[int]string, error) { rows, err := db.MssqlDB.QueryContext(ctx, `SELECT CompanyCode, CompanyName FROM cdCompany WITH(NOLOCK)`) if err != nil { return nil, fmt.Errorf("company map query error: %w", err) } defer rows.Close() out := make(map[int]string) for rows.Next() { var code int var name sql.NullString if err := rows.Scan(&code, &name); err != nil { return nil, err } out[code] = strings.TrimSpace(name.String) } if err := rows.Err(); err != nil { return nil, err } return out, nil } func loadNearestTryRates(ctx context.Context) (map[string]float64, error) { query := ` WITH Ranked AS ( SELECT CurrencyCode, Rate, rn = ROW_NUMBER() OVER ( PARTITION BY CurrencyCode ORDER BY ABS(DATEDIFF(DAY, Date, GETDATE())), Date DESC ) FROM AllExchangeRates WHERE RelationCurrencyCode = 'TRY' AND ExchangeTypeCode = 6 AND Rate > 0 ) SELECT CurrencyCode, Rate FROM Ranked WHERE rn = 1 ` rows, err := db.MssqlDB.QueryContext(ctx, query) if err != nil { return nil, fmt.Errorf("exchange rates query error: %w", err) } defer rows.Close() out := map[string]float64{"TRY": 1} for rows.Next() { var code string var rate float64 if err := rows.Scan(&code, &rate); err != nil { return nil, err } code = strings.ToUpper(strings.TrimSpace(code)) if code != "" && rate > 0 { out[code] = rate } } if err := rows.Err(); err != nil { return nil, err } return out, nil } func toUSD(amount float64, currency string, usdTry float64, rateMap map[string]float64) float64 { if usdTry <= 0 { return 0 } switch currency { case "USD": return amount case "TRY": return amount / usdTry default: currTry := rateMap[currency] if currTry <= 0 { return 0 } return (amount * currTry) / usdTry } } func deriveMasterCari(cari string) string { cari = strings.TrimSpace(cari) if cari == "" { return "" } base := cari if idx := strings.Index(base, "/"); idx > 0 { base = base[:idx] } base = strings.TrimSpace(base) if len(base) >= 8 { return strings.TrimSpace(base[:8]) } return base } func buildFilters(params models.CustomerBalanceListParams) balanceFilters { return balanceFilters{ cariIlkGrup: parseCSVSet(params.CariIlkGrup), piyasa: parseCSVSet(params.Piyasa), temsilci: parseCSVSet(params.Temsilci), riskDurumu: parseCSVSet(params.RiskDurumu), islemTipi: parseIslemTipiSet(params.IslemTipi), ulke: parseCSVSet(params.Ulke), il: parseCSVSet(params.Il), ilce: parseCSVSet(params.Ilce), } } func (f balanceFilters) matchLine(islemTipi string, m cariMeta) bool { if !matchSet(f.islemTipi, islemTipi) { return false } if !matchSet(f.cariIlkGrup, m.Kanal1) { return false } if !matchSet(f.piyasa, m.Piyasa) { return false } if !matchSet(f.temsilci, m.Temsilci) { return false } if !matchSet(f.riskDurumu, m.RiskDurumu) { return false } if !matchSet(f.ulke, m.Ulke) { return false } if !matchSet(f.il, m.Il) { return false } if !matchSet(f.ilce, m.Ilce) { return false } return true } func matchSet(set map[string]struct{}, value string) bool { if len(set) == 0 { return true } trimmed := strings.TrimSpace(value) if trimmed == "" { return true } _, ok := set[trimmed] return ok } func parseCSVSet(v string) map[string]struct{} { out := make(map[string]struct{}) for _, p := range strings.Split(v, ",") { t := strings.TrimSpace(p) if t == "" { continue } out[t] = struct{}{} } return out } func parseIslemTipiSet(v string) map[string]struct{} { raw := parseCSVSet(v) if len(raw) == 0 { return raw } out := make(map[string]struct{}, 2) for token := range raw { switch strings.ToLower(strings.TrimSpace(token)) { case "1_2", "prbr_1_2", "usd_1_2", "try_1_2", "tl_1_2", "usd_bakiye_1_2", "tl_bakiye_1_2": out["1_2"] = struct{}{} case "1_3", "prbr_1_3", "usd_1_3", "try_1_3", "tl_1_3", "usd_bakiye_1_3", "tl_bakiye_1_3": out["1_3"] = struct{}{} } } if len(out) == 0 { return raw } return out } func getAuthorizedPiyasaCodes(ctx context.Context) ([]string, error) { claims, ok := auth.GetClaimsFromContext(ctx) if !ok || claims == nil { return nil, fmt.Errorf("unauthorized: claims not found") } if claims.IsAdmin() { return nil, nil } rawCodes := authz.GetPiyasaCodesFromCtx(ctx) if len(rawCodes) == 0 { return []string{}, nil } unique := make(map[string]struct{}, len(rawCodes)) out := make([]string, 0, len(rawCodes)) for _, code := range rawCodes { norm := strings.ToUpper(strings.TrimSpace(code)) if norm == "" { continue } if _, exists := unique[norm]; exists { continue } unique[norm] = struct{}{} out = append(out, norm) } if len(out) == 0 { return []string{}, nil } return out, nil } func buildPiyasaWhereClause(codes []string, column string) string { if len(codes) == 0 { return "1=1" } return authz.BuildINClause(column, codes) } func metaKey(currType int, code string) string { return strconv.Itoa(currType) + "|" + strings.TrimSpace(code) } func glKey(currType int, code string, company int) string { return strconv.Itoa(currType) + "|" + strings.TrimSpace(code) + "|" + strconv.Itoa(company) } func quotedInList(set map[string]struct{}) string { vals := make([]string, 0, len(set)) for v := range set { esc := strings.ReplaceAll(strings.TrimSpace(v), "'", "''") if esc != "" { vals = append(vals, "'"+esc+"'") } } if len(vals) == 0 { return "''" } sort.Strings(vals) return strings.Join(vals, ",") } func intInList(set map[int]struct{}) string { vals := make([]int, 0, len(set)) for v := range set { vals = append(vals, v) } if len(vals) == 0 { return "0" } sort.Ints(vals) parts := make([]string, 0, len(vals)) for _, v := range vals { parts = append(parts, strconv.Itoa(v)) } return strings.Join(parts, ",") } func firstNonEmpty(v ...string) string { for _, s := range v { if strings.TrimSpace(s) != "" { return s } } return "" } func isInvalidColumnError(err error) bool { if err == nil { return false } msg := strings.ToLower(err.Error()) return strings.Contains(msg, "invalid column name") } func shouldSkipBalanceLine(ln mkCariBakiyeLine) bool { add12, add13 := resolveBalanceBuckets(ln) p := strings.TrimSpace(ln.PislemTipi) if p == "1_2" { return !add12 } if p == "1_3" { return !add13 } return false } func resolveBalanceBuckets(ln mkCariBakiyeLine) (add12 bool, add13 bool) { p := strings.TrimSpace(ln.PislemTipi) t := normalizeParasalIslemTipi(ln.ParasalIslemTipi) switch t { case "1": return true, true case "2", "1_2": return true, false case "3", "1_3": return false, true } // Parasal tip yoksa eski davranis: PislemTipi'ne gore ayir. if p == "1_2" { return true, false } if p == "1_3" { return false, true } return false, false } func normalizeParasalIslemTipi(v string) string { s := strings.TrimSpace(v) if s == "" { return "" } lower := strings.ToLower(s) compact := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(lower, " ", ""), "-", "_"), "/", "_") if strings.Contains(compact, "1_2") { return "1_2" } if strings.Contains(compact, "1_3") { return "1_3" } // "1,2" / "1,3" gibi liste formatlarini dogrudan yakala. tokenized := strings.NewReplacer(" ", "", ";", ",", "|", ",", "/", ",", "-", ",", "_", ",").Replace(lower) parts := strings.Split(tokenized, ",") has1 := false has2 := false has3 := false for _, p := range parts { t := strings.TrimSpace(p) switch t { case "1": has1 = true case "2": has2 = true case "3": has3 = true } } if has1 && has2 { return "1_2" } if has1 && has3 { return "1_3" } if has2 && !has1 && !has3 { return "2" } if has3 && !has1 && !has2 { return "3" } if has1 && !has2 && !has3 { return "1" } // "2.00", "2,00", " 2 " gibi varyasyonlari tek tipe indir. s = strings.ReplaceAll(s, ",", ".") if n, err := strconv.ParseFloat(s, 64); err == nil { return strconv.Itoa(int(n)) } // Metinsel geldiyse ilk rakam bloğunu al. start := -1 end := -1 for i, r := range s { if r >= '0' && r <= '9' { if start == -1 { start = i } end = i continue } if start != -1 { break } } if start == -1 || end < start { return s } return s[start : end+1] } func resolveParasalIslemSelectExpr(ctx context.Context, sampleQuery string) string { sampleQuery = strings.TrimSpace(sampleQuery) if sampleQuery == "" { return "" } metaQuery := ` SELECT name FROM sys.dm_exec_describe_first_result_set(@tsql, NULL, 0) WHERE error_number IS NULL AND name IS NOT NULL ` rows, err := db.MssqlDB.QueryContext(ctx, metaQuery, sql.Named("tsql", sampleQuery)) if err != nil { return "" } defer rows.Close() type candidate struct { key string expr string } priority := []candidate{ {key: "ata tt01", expr: "CAST(%s AS varchar(16)) AS ParasalIslemTipi,"}, {key: "atatt01", expr: "CAST(%s AS varchar(16)) AS ParasalIslemTipi,"}, {key: "parasalislemtipi", expr: "CAST(%s AS varchar(16)) AS ParasalIslemTipi,"}, {key: "parislemtipi", expr: "CAST(%s AS varchar(16)) AS ParasalIslemTipi,"}, {key: "parislemtur", expr: "CAST(%s AS varchar(16)) AS ParasalIslemTipi,"}, } available := make(map[string]string) for rows.Next() { var col sql.NullString if err := rows.Scan(&col); err != nil { return "" } name := strings.TrimSpace(col.String) if name == "" { continue } normalized := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(name, "_", ""), " ", "")) available[normalized] = name } if err := rows.Err(); err != nil { return "" } for _, c := range priority { key := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(c.key, "_", ""), " ", "")) if col, ok := available[key]; ok { return fmt.Sprintf(c.expr, quoteSQLIdent(col)) } } return "" } func quoteSQLIdent(ident string) string { return "[" + strings.ReplaceAll(strings.TrimSpace(ident), "]", "]]") + "]" }