//go:build integration package inttest import ( "context" "net/http" "strings" "testing" "github.com/google/uuid" "scrabble/backend/internal/payments" ) // TestAccountStatement checks the admin financial statement: a funded pack shows as a chip segment // + a fund ledger row, an admin grant as a benefit + an admin_grant row, the ledger is newest-first, // and a clean account carries no refund risk. func TestAccountStatement(t *testing.T) { ctx := context.Background() svc := newPaymentsService() acc := uuid.New() // A funded pack: 100 chips to the direct segment + a fund ledger row. prod := seedPackProduct(t, 100, methodPrice{method: "direct", currency: "RUB", amount: 14900}) res, err := svc.CreateOrder(ctx, acc, payments.NewContext("direct", "web"), []payments.Source{payments.SourceDirect}, prod, "robokassa") if err != nil { t.Fatalf("create order: %v", err) } paid, _ := payments.MoneyFromMinor(14900, payments.CurrencyRUB) if _, err := svc.Fund(ctx, res.OrderID, "robokassa", res.OrderID.String(), paid); err != nil { t.Fatalf("fund: %v", err) } // An admin grant: 5 hints to the direct origin + an admin_grant ledger row. if err := svc.Grant(ctx, acc, payments.SourceDirect, 5, 0, false); err != nil { t.Fatalf("grant: %v", err) } stmt, err := svc.AccountStatement(ctx, acc) if err != nil { t.Fatalf("statement: %v", err) } if len(stmt.Segments) != 1 || stmt.Segments[0].Source != payments.SourceDirect || stmt.Segments[0].Chips != 100 { t.Fatalf("segments = %+v, want 100 chips on direct", stmt.Segments) } if len(stmt.Benefits) != 1 || stmt.Benefits[0].Origin != payments.SourceDirect || stmt.Benefits[0].Hints != 5 { t.Fatalf("benefits = %+v, want 5 hints on direct", stmt.Benefits) } if len(stmt.Ledger) != 2 { t.Fatalf("ledger rows = %d, want 2 (fund + admin_grant)", len(stmt.Ledger)) } // Newest-first ordering (the grant is the later write). if stmt.Ledger[0].CreatedAt.Before(stmt.Ledger[1].CreatedAt) { t.Error("ledger is not newest-first") } kinds := map[string]int{} for _, e := range stmt.Ledger { kinds[e.Kind]++ if e.Kind == "fund" && e.ChipsDelta != 100 { t.Errorf("fund chips delta = %d, want 100", e.ChipsDelta) } } if kinds["fund"] != 1 || kinds["admin_grant"] != 1 { t.Fatalf("ledger kinds = %v, want one fund + one admin_grant", kinds) } if stmt.Risk.Present { t.Errorf("risk = %+v, want none for a clean account", stmt.Risk) } } // TestConsoleFinancePanel checks the user card renders the finance panel: a funded pack + an admin // grant surface as the segment balance, the benefit and the ledger rows. func TestConsoleFinancePanel(t *testing.T) { ctx := context.Background() srv, _, pay := bannerServer(t) id := provisionAccount(t) 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("create 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) } if err := pay.Grant(ctx, id, payments.SourceDirect, 5, 0, false); err != nil { t.Fatalf("grant: %v", err) } code, body := consoleDo(srv.Handler(), http.MethodGet, "http://admin.test/_gm/users/"+id.String(), "", "") if code != http.StatusOK { t.Fatalf("user card = %d, want 200", code) } for _, want := range []string{"Finance", "Chips (direct)", "Benefits (direct)", "5 hints", "admin_grant", "fund"} { if !strings.Contains(body, want) { t.Errorf("finance panel missing %q", want) } } }