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 YerelBakiye float64 Bakiye 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) switch strings.TrimSpace(ln.PislemTipi) { case "1_2": row.Bakiye12 += ln.Bakiye row.TLBakiye12 += ln.YerelBakiye row.USDBakiye12 += usd case "1_3": 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) { query := ` SELECT CurrAccTypeCode, CariKodu, CariDoviz, SirketKodu, PislemTipi, YerelBakiye, Bakiye FROM dbo.MK_CARI_BAKIYE_LIST(@SonTarih) WHERE (@CariSearch = '' OR CariKodu LIKE '%' + @CariSearch + '%') ` rows, err := db.MssqlDB.QueryContext(ctx, query, sql.Named("SonTarih", selectedDate), sql.Named("CariSearch", strings.TrimSpace(cariSearch)), ) if err != nil { 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.YerelBakiye, &r.Bakiye, ); 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: parseCSVSet(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 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 "" }