1bf612a087
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.
78 lines
2.9 KiB
Go
78 lines
2.9 KiB
Go
//go:build integration
|
|
|
|
package inttest
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"scrabble/backend/internal/payments"
|
|
)
|
|
|
|
// TestConsoleRefundAndExport drives the manual refund and the ledger CSV export: a funded order is
|
|
// refunded in full (chips revoked, a refund row), the refund is idempotent, and the export carries
|
|
// the fund + refund rows; the refund POST is CSRF-guarded.
|
|
func TestConsoleRefundAndExport(t *testing.T) {
|
|
ctx := context.Background()
|
|
srv, _, pay := bannerServer(t)
|
|
h := srv.Handler()
|
|
id := provisionAccount(t)
|
|
const origin = "http://admin.test"
|
|
base := "http://admin.test/_gm/users/" + id.String()
|
|
|
|
// Fund a pack: 100 chips + a fund ledger row.
|
|
prod := seedPackProduct(t, 100, methodPrice{method: "direct", currency: "RUB", amount: 14900})
|
|
res, err := pay.CreateOrder(ctx, id, payments.NewContext("direct", "web"), []payments.Source{payments.SourceDirect}, prod, "robokassa")
|
|
if err != nil {
|
|
t.Fatalf("order: %v", err)
|
|
}
|
|
paid, _ := payments.MoneyFromMinor(14900, payments.CurrencyRUB)
|
|
if _, err := pay.Fund(ctx, res.OrderID, "robokassa", res.OrderID.String(), paid); err != nil {
|
|
t.Fatalf("fund: %v", err)
|
|
}
|
|
|
|
// CSRF: a refund without the origin header is refused.
|
|
if code, _ := consoleDo(h, http.MethodPost, base+"/refund", "order_id="+res.OrderID.String(), ""); code != http.StatusForbidden {
|
|
t.Fatalf("refund without origin = %d, want 403", code)
|
|
}
|
|
|
|
// Refund the order in full → 100 chips revoked (none spent).
|
|
if code, body := consoleDo(h, http.MethodPost, base+"/refund", "order_id="+res.OrderID.String(), origin); code != http.StatusOK || !strings.Contains(body, "revoked 100 chips") {
|
|
t.Fatalf("refund = %d, has 'revoked 100 chips' = %v", code, strings.Contains(body, "revoked 100 chips"))
|
|
}
|
|
stmt, err := pay.AccountStatement(ctx, id)
|
|
if err != nil {
|
|
t.Fatalf("statement: %v", err)
|
|
}
|
|
for _, sg := range stmt.Segments {
|
|
if sg.Source == payments.SourceDirect && sg.Chips != 0 {
|
|
t.Errorf("after refund: direct chips = %d, want 0", sg.Chips)
|
|
}
|
|
}
|
|
kinds := map[string]int{}
|
|
for _, e := range stmt.Ledger {
|
|
kinds[e.Kind]++
|
|
}
|
|
if kinds["fund"] != 1 || kinds["refund"] != 1 {
|
|
t.Errorf("ledger kinds = %v, want one fund + one refund", kinds)
|
|
}
|
|
|
|
// A second refund of the same order is idempotent.
|
|
if code, body := consoleDo(h, http.MethodPost, base+"/refund", "order_id="+res.OrderID.String(), origin); code != http.StatusOK || !strings.Contains(body, "Already refunded") {
|
|
t.Fatalf("second refund = %d, has 'Already refunded' = %v", code, strings.Contains(body, "Already refunded"))
|
|
}
|
|
|
|
// Export the whole ledger as CSV: the header, this account, and its fund + refund rows.
|
|
code, body := consoleDo(h, http.MethodGet, "http://admin.test/_gm/ledger.csv", "", "")
|
|
if code != http.StatusOK {
|
|
t.Fatalf("export = %d, want 200", code)
|
|
}
|
|
for _, want := range []string{"created_at,account_id,kind", id.String(), ",fund,", ",refund,"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("ledger CSV missing %q", want)
|
|
}
|
|
}
|
|
}
|