package payments import ( "context" "errors" "fmt" "time" "github.com/go-jet/jet/v2/postgres" "github.com/go-jet/jet/v2/qrm" "github.com/google/uuid" "scrabble/backend/internal/postgres/jet/payments/model" "scrabble/backend/internal/postgres/jet/payments/table" ) // statementOrder is the fixed segment/origin ordering the report renders, so the panel is stable // (the balance/benefit maps iterate randomly). var statementOrder = []Source{SourceDirect, SourceVK, SourceTelegram} // accountStatement reads the account's balances, benefits, refund risk and full ledger history for // the admin report. Uncached and outside any transaction — an operator view, not a hot path. func (s *Store) accountStatement(ctx context.Context, accountID uuid.UUID) (Statement, error) { st, err := s.loadState(ctx, accountID) if err != nil { return Statement{}, err } var out Statement for _, src := range statementOrder { if chips, ok := st.chips[src]; ok { out.Segments = append(out.Segments, SegmentChips{Source: src, Chips: chips}) } if b, ok := st.benefits[src]; ok { out.Benefits = append(out.Benefits, OriginBenefit{ Origin: src, Hints: b.hints, AdsPaidUntil: derefTime(b.adsPaidUntil), AdsForever: b.adsForever, }) } } var risk model.AccountRisk err = postgres.SELECT(table.AccountRisk.AllColumns). FROM(table.AccountRisk). WHERE(table.AccountRisk.AccountID.EQ(postgres.UUID(accountID))). LIMIT(1). QueryContext(ctx, s.db, &risk) switch { case err == nil: out.Risk = RiskInfo{Present: true, Abuse: risk.Abuse, LossChips: int(risk.LossChips)} case errors.Is(err, qrm.ErrNoRows): // no risk row — a clean account default: return Statement{}, fmt.Errorf("payments: load risk %s: %w", accountID, err) } var rows []model.Ledger if err := postgres.SELECT(table.Ledger.AllColumns). FROM(table.Ledger). WHERE(table.Ledger.AccountID.EQ(postgres.UUID(accountID))). ORDER_BY(table.Ledger.CreatedAt.DESC()). QueryContext(ctx, s.db, &rows); err != nil { return Statement{}, fmt.Errorf("payments: load ledger %s: %w", accountID, err) } for _, r := range rows { out.Ledger = append(out.Ledger, LedgerEntry{ Kind: r.Kind, Source: derefStr(r.Source), Origin: derefStr(r.Origin), ChipsDelta: int(r.ChipsDelta), ProductID: derefUUID(r.ProductID), OrderID: derefUUID(r.OrderID), Provider: derefStr(r.Provider), ProviderPaymentID: derefStr(r.ProviderPaymentID), Snapshot: derefStr(r.Snapshot), CreatedAt: r.CreatedAt, }) } return out, nil } // allLedger reads the entire append-only ledger (all accounts, newest first) for the admin export. func (s *Store) allLedger(ctx context.Context) ([]LedgerExportRow, error) { var rows []model.Ledger if err := postgres.SELECT(table.Ledger.AllColumns). FROM(table.Ledger). ORDER_BY(table.Ledger.CreatedAt.DESC()). QueryContext(ctx, s.db, &rows); err != nil { return nil, fmt.Errorf("payments: export ledger: %w", err) } out := make([]LedgerExportRow, 0, len(rows)) for _, r := range rows { out = append(out, LedgerExportRow{ AccountID: r.AccountID.String(), LedgerEntry: LedgerEntry{ Kind: r.Kind, Source: derefStr(r.Source), Origin: derefStr(r.Origin), ChipsDelta: int(r.ChipsDelta), ProductID: derefUUID(r.ProductID), OrderID: derefUUID(r.OrderID), Provider: derefStr(r.Provider), ProviderPaymentID: derefStr(r.ProviderPaymentID), Snapshot: derefStr(r.Snapshot), CreatedAt: r.CreatedAt, }, }) } return out, nil } // derefStr returns the pointed-to string, or "" when the pointer is nil (a NULL column). func derefStr(p *string) string { if p == nil { return "" } return *p } // derefUUID renders the pointed-to UUID as a string, or "" when the pointer is nil. func derefUUID(p *uuid.UUID) string { if p == nil { return "" } return p.String() } // derefTime returns the pointed-to time, or the zero time when the pointer is nil (a NULL column). func derefTime(p *time.Time) time.Time { if p == nil { return time.Time{} } return *p }