package membership import ( "fmt" "fuj-management/go/internal/domain/reconcile" "io" "sort" "strings" "time" ) // printReconcileReport writes the full balance report to w. // Mirrors scripts/match_payments.py print_report(). // // Verify with: // // PYTHONPATH=scripts:. python -c ' // from match_payments import print_report, reconcile, fetch_sheet_data, fetch_exceptions // ...' func printReconcileReport(w io.Writer, result reconcile.Result, sortedMonths []string) { monthLabel := func(m string) string { t, err := time.Parse("2006-01", m) if err != nil { return m } return t.Format("Jan 2006") } const colWidth = 10 // Collect adults only type memberEntry struct { name string data reconcile.MemberResult } var adults []memberEntry for name, data := range result.Members { if data.Tier == "A" { adults = append(adults, memberEntry{name: name, data: data}) } } sort.Slice(adults, func(i, j int) bool { return adults[i].name < adults[j].name }) // Header banner fmt.Fprintln(w, strings.Repeat("=", 80)) fmt.Fprintln(w, "PAYMENT RECONCILIATION REPORT") fmt.Fprintln(w, strings.Repeat("=", 80)) // Name column width nameWidth := 20 for _, e := range adults { if len(e.name) > nameWidth { nameWidth = len(e.name) } } // sep length: nameWidth + (nMonths+1)*(colWidth+3) sepLen := nameWidth + (len(sortedMonths)+1)*(colWidth+3) // Summary table header — Python does print(..., end="") then print(f" | {'Balance':>10}") fmt.Fprintf(w, "\n%-*s", nameWidth, "Member") for _, m := range sortedMonths { fmt.Fprintf(w, " | %*s", colWidth, monthLabel(m)) } fmt.Fprintf(w, " | %*s\n", colWidth, "Balance") fmt.Fprintln(w, strings.Repeat("-", sepLen)) var totalExpected, totalPaid int for _, e := range adults { fmt.Fprintf(w, "%-*s", nameWidth, e.name) memberBalance := 0 for _, m := range sortedMonths { md := e.data.Months[m] expected := md.Expected paid := int(md.Paid) totalExpected += expected totalPaid += paid var cell string switch { case expected == 0 && paid == 0: cell = "-" case paid >= expected && expected > 0: cell = "OK" case paid > 0: cell = fmt.Sprintf("%d/%d", paid, expected) default: cell = fmt.Sprintf("UNPAID %d", expected) } memberBalance += paid - expected fmt.Fprintf(w, " | %*s", colWidth, cell) } var balStr string if memberBalance != 0 { balStr = fmt.Sprintf("%+d", memberBalance) } else { balStr = "0" } fmt.Fprintf(w, " | %*s\n", colWidth, balStr) } // TOTAL footer fmt.Fprintln(w, strings.Repeat("-", sepLen)) fmt.Fprintf(w, "%-*s", nameWidth, "TOTAL") for range sortedMonths { fmt.Fprintf(w, " | %*s", colWidth, "") } balance := totalPaid - totalExpected fmt.Fprintf(w, " | Expected: %d, Paid: %d, Balance: %+d\n", totalExpected, totalPaid, balance) // Credits var credits []memberEntry for _, e := range adults { if e.data.TotalBalance > 0 { credits = append(credits, e) } } // also non-adult members with positive balance for name, data := range result.Members { if data.Tier != "A" && data.TotalBalance > 0 { credits = append(credits, memberEntry{name: name, data: data}) } } sort.Slice(credits, func(i, j int) bool { return credits[i].name < credits[j].name }) if len(credits) > 0 { fmt.Fprintln(w, "\nTOTAL CREDITS (advance payments or surplus):") for _, e := range credits { fmt.Fprintf(w, " %s: %d CZK\n", e.name, e.data.TotalBalance) } } // Debts var debts []memberEntry for _, e := range adults { if e.data.TotalBalance < 0 { debts = append(debts, e) } } for name, data := range result.Members { if data.Tier != "A" && data.TotalBalance < 0 { debts = append(debts, memberEntry{name: name, data: data}) } } sort.Slice(debts, func(i, j int) bool { return debts[i].name < debts[j].name }) if len(debts) > 0 { fmt.Fprintln(w, "\nTOTAL DEBTS (missing payments):") for _, e := range debts { fmt.Fprintf(w, " %s: %d CZK\n", e.name, -e.data.TotalBalance) } } // Unmatched transactions if len(result.Unmatched) > 0 { fmt.Fprintln(w, "\nUNMATCHED TRANSACTIONS (need manual review)") fmt.Fprintf(w, " %-12s %10s %-30s %s\n", "Date", "Amount", "Sender", "Message") fmt.Fprintf(w, " %-12s %10s %-30s %-30s\n", strings.Repeat("-", 12), strings.Repeat("-", 10), strings.Repeat("-", 30), strings.Repeat("-", 30)) for _, tx := range result.Unmatched { fmt.Fprintf(w, " %-12s %10.0f %-30s %s\n", tx.Date, tx.Amount, tx.Sender, tx.Message) } } // Matched transaction details fmt.Fprintln(w, "\nMATCHED TRANSACTION DETAILS") for _, e := range adults { hasPayments := false for _, m := range sortedMonths { if len(e.data.Months[m].Transactions) > 0 { hasPayments = true break } } if !hasPayments { continue } fmt.Fprintf(w, "\n %s:\n", e.name) for _, m := range sortedMonths { for _, tx := range e.data.Months[m].Transactions { conf := "" if tx.Confidence == "review" { conf = " [REVIEW]" } fmt.Fprintf(w, " %s: %.0f CZK from %s — \"%s\"%s\n", monthLabel(m), tx.Amount, tx.Sender, tx.Message, conf) } } } }