feat(admin): manual full-order refund + ledger CSV export
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 22s
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m53s

Closes the admin / reports / catalog work. Each fund row on the /_gm finance
panel gains a Refund action (payments.RefundOrderFull): a full-order refund the
operator records after refunding on the rail — a refund ledger row + a floor-0
chip revoke (never negative, D27), idempotent (a second refund reports
already-refunded). A ledger CSV export (/_gm/ledger.csv, payments.LedgerExport)
streams the whole append-only ledger for tax + reconciliation.

Tests: refund an order in full (chips revoked, a refund row), an idempotent
second refund, the CSV export shape; CSRF-guarded.
This commit is contained in:
Ilia Denisov
2026-07-10 06:46:18 +02:00
parent ec5c6afa23
commit 1bf612a087
8 changed files with 242 additions and 13 deletions
@@ -77,6 +77,36 @@ func (s *Store) accountStatement(ctx context.Context, accountID uuid.UUID) (Stat
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 {