package membership // Golden strings verified against scripts/match_payments.py print_report() on 2026-05-06: // // PYTHONPATH=scripts:. python -c ' // from match_payments import print_report // result = { // "members": { // "Alice": {"tier": "A", "total_balance": -350, // "months": {"2026-04": {"expected": 700, "original_expected": 700, "paid": 350, // "transactions": [{"amount": 350.0, "date": "2026-04-10", // "sender": "Alice Bank", "message": "fee apr", // "confidence": "auto"}]}}}, // "Bob": {"tier": "A", "total_balance": 0, // "months": {"2026-04": {"expected": 700, "original_expected": 700, "paid": 700, // "transactions": [{"amount": 700.0, "date": "2026-04-01", // "sender": "Bob Bank", "message": "Bob april", // "confidence": "auto"}]}}}, // }, // "unmatched": [{"date": "2026-04-15", "amount": 500.0, "sender": "Unknown", "message": "?"}], // } // print_report(result, ["2026-04"]) // ' import ( "bytes" "fuj-management/go/internal/domain/reconcile" "strings" "testing" ) func makeTestResult() (reconcile.Result, []string) { sortedMonths := []string{"2026-04"} aliceApr := reconcile.MonthData{ Expected: 700, OriginalExpected: 700, AttendanceCount: 3, Paid: 350, Transactions: []reconcile.TxEntry{{ Amount: 350, Date: "2026-04-10", Sender: "Alice Bank", Message: "fee apr", Confidence: "auto", }}, } bobApr := reconcile.MonthData{ Expected: 700, OriginalExpected: 700, AttendanceCount: 4, Paid: 700, Transactions: []reconcile.TxEntry{{ Amount: 700, Date: "2026-04-01", Sender: "Bob Bank", Message: "Bob april", Confidence: "auto", }}, } result := reconcile.Result{ Members: map[string]reconcile.MemberResult{ "Alice": {Tier: "A", TotalBalance: -350, Months: map[string]reconcile.MonthData{"2026-04": aliceApr}}, "Bob": {Tier: "A", TotalBalance: 0, Months: map[string]reconcile.MonthData{"2026-04": bobApr}}, }, Unmatched: []reconcile.Transaction{{ Date: "2026-04-15", Amount: 500, Sender: "Unknown", Message: "?", }}, } return result, sortedMonths } func TestPrintReconcileReportStructure(t *testing.T) { t.Parallel() result, sortedMonths := makeTestResult() var buf bytes.Buffer printReconcileReport(&buf, result, sortedMonths) got := buf.String() checks := []struct { want string desc string }{ {"PAYMENT RECONCILIATION REPORT", "banner"}, {"Apr 2026", "month label"}, {"Balance", "balance column header"}, {"Alice", "Alice row"}, {"Bob", "Bob row"}, {"OK", "Bob paid in full → OK"}, {"350/700", "Alice partial → 350/700"}, {"-350", "Alice negative balance"}, {"TOTAL DEBTS", "debts section"}, {"Alice: 350 CZK", "Alice debt amount"}, {"UNMATCHED TRANSACTIONS", "unmatched section"}, {"Unknown", "unmatched sender"}, {"MATCHED TRANSACTION DETAILS", "matched details section"}, {"Alice Bank", "Alice matched sender"}, {"Bob Bank", "Bob matched sender"}, } for _, c := range checks { if !strings.Contains(got, c.want) { t.Errorf("missing %s: want %q in output:\n%s", c.desc, c.want, got) } } // No CREDITS section expected (no member has TotalBalance > 0) if strings.Contains(got, "TOTAL CREDITS") { t.Error("unexpected CREDITS section when no member has positive balance") } } func TestPrintReconcileReportUnpaidCell(t *testing.T) { t.Parallel() result := reconcile.Result{ Members: map[string]reconcile.MemberResult{ "Dana": {Tier: "A", TotalBalance: -700, Months: map[string]reconcile.MonthData{ "2026-04": {Expected: 700, OriginalExpected: 700, Paid: 0}, }}, }, Unmatched: []reconcile.Transaction{}, } var buf bytes.Buffer printReconcileReport(&buf, result, []string{"2026-04"}) got := buf.String() if !strings.Contains(got, "UNPAID 700") { t.Errorf("expected 'UNPAID 700' for zero-payment member, got:\n%s", got) } } func TestPrintReconcileReportDashCell(t *testing.T) { t.Parallel() result := reconcile.Result{ Members: map[string]reconcile.MemberResult{ "Eve": {Tier: "A", TotalBalance: 0, Months: map[string]reconcile.MonthData{ "2026-04": {Expected: 0, Paid: 0}, }}, }, Unmatched: []reconcile.Transaction{}, } var buf bytes.Buffer printReconcileReport(&buf, result, []string{"2026-04"}) got := buf.String() eveLine := "" for _, l := range strings.Split(got, "\n") { if strings.HasPrefix(strings.TrimSpace(l), "Eve") { eveLine = l break } } if eveLine == "" { t.Fatal("no Eve line found") } if !strings.Contains(eveLine, "-") { t.Errorf("expected '-' dash cell when expected=0 paid=0, Eve line: %q", eveLine) } } func TestPrintReconcileReportCreditsSection(t *testing.T) { t.Parallel() result := reconcile.Result{ Members: map[string]reconcile.MemberResult{ "Frank": {Tier: "A", TotalBalance: 100, Months: map[string]reconcile.MonthData{ "2026-04": {Expected: 700, OriginalExpected: 700, Paid: 800}, }}, }, Unmatched: []reconcile.Transaction{}, } var buf bytes.Buffer printReconcileReport(&buf, result, []string{"2026-04"}) got := buf.String() if !strings.Contains(got, "TOTAL CREDITS") { t.Errorf("expected CREDITS section, got:\n%s", got) } if !strings.Contains(got, "Frank: 100 CZK") { t.Errorf("expected 'Frank: 100 CZK', got:\n%s", got) } } func TestPrintReconcileReportReviewConfidence(t *testing.T) { t.Parallel() result := reconcile.Result{ Members: map[string]reconcile.MemberResult{ "Grace": {Tier: "A", TotalBalance: 0, Months: map[string]reconcile.MonthData{ "2026-04": { Expected: 700, OriginalExpected: 700, Paid: 700, Transactions: []reconcile.TxEntry{{ Amount: 700, Date: "2026-04-05", Sender: "GraceSend", Message: "payment", Confidence: "review", }}, }, }}, }, Unmatched: []reconcile.Transaction{}, } var buf bytes.Buffer printReconcileReport(&buf, result, []string{"2026-04"}) got := buf.String() if !strings.Contains(got, "[REVIEW]") { t.Errorf("expected '[REVIEW]' annotation for review-confidence tx, got:\n%s", got) } }