- Add canonical_member_key() in match_payments.py to normalize names via NFKD + lowercase + whitespace-collapse before ledger lookup; resolves payments attributed to e.g. "Maria Maco" to canonical "Mária Maco". Emits logger.info when a non-canonical cell is rescued so sheet typos are visible in logs without losing the payment allocation. - Extend group_payments_by_person() in app.py to accept member_names and re-key raw-payment groups under the canonical attendance-sheet name so the modal's Raw Payments debug section also finds the row correctly. - Add raw payments collapsible section to member detail modal in adults.html and juniors.html for debugging payment attribution issues. - Remove 4 obsolete tests targeting routes /fees, /fees-juniors, /reconcile, /reconcile-juniors that no longer exist; add test_match_payments.py covering canonical key equivalence and reconcile() tolerance end-to-end. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
6.5 KiB
Member modal — raw payments debug list
Context
When a payer's bank message doesn't follow our convention, infer_payments.py may map the transfer to the wrong period (or none), and today the member detail modal hides this — it only shows the post-allocation, per-month splits produced by reconcile(). To diagnose these cases the user needs to see the original sheet rows that were attributed to a member: full Amount, Inferred Amount, Person, Purpose, Sender, Message, Bank ID, manual fix. The list should be hidden by default and revealed by a small toggle, since it is only relevant during debugging.
Approach
Reuse the grouping logic that already exists in the /payments route: group raw tx dicts by parsed Person, expose that mapping to the modal, and render it on demand under a new collapsible section.
1. Backend — group raw txs by member
In app.py:
-
Factor the existing per-person grouping in
payments()into a small helper near the top of the file:def group_payments_by_person(transactions): grouped = {} for tx in transactions: person = str(tx.get("person", "")).strip() if not person: continue # unmatched rows are not tied to a member for p in person.split(","): p = re.sub(r"\[\?\]\s*", "", p).strip() if not p: continue grouped.setdefault(p, []).append(tx) for rows in grouped.values(): rows.sort(key=lambda t: str(t.get("date", "")), reverse=True) return groupedCall it from
payments(),adults_view()andjuniors_view()— the existingpayments()body collapses to one line. -
In
adults_view()andjuniors_view(), aftertransactions = get_cached_data(...), buildraw_payments_by_person = group_payments_by_person(transactions)and pass it torender_templateasraw_payments_json=json.dumps(raw_payments_by_person). -
Note: rows where
Personis empty are skipped on purpose — those have no member to attach to and are already shown by the dashboard'sUnmatchedblock.
2. Templates — add a collapsible raw section to the modal
In templates/adults.html and templates/juniors.html, make the same structural and JS changes (the modal markup is mirrored in both files — adults.html:677-682 and juniors.html:658-663).
-
Inject the new dataset alongside the existing
memberData:const rawPaymentsByPerson = {{ raw_payments_json| safe }};(next to
adults.html:696). -
Add a new section directly after the Payment History block:
<div class="modal-section"> <div class="modal-section-title"> Raw Payments <a href="#" id="rawPaymentsToggle" class="raw-toggle" onclick="toggleRawPayments(event)">[show]</a> </div> <div id="modalRawList" class="tx-list" style="display: none;"> <!-- Filled by JS --> </div> </div>Add a small CSS rule for
.raw-toggle(muted color, smaller font,margin-left: 8px) — a few lines next to the existing.modal-section-titlestyle. Don't restyle the whole modal. -
In
showMemberDetails(name):- Reset the toggle to
[show]and the#modalRawListtodisplay: noneon every open (so the state doesn't leak between members). - Populate
#modalRawListfromrawPaymentsByPerson[name] || []. For each row render:Date | Purposeon the meta line,Amount CZK(withInferred: X CZKannotation wheninferred_amountdiffers fromamount),Sender,Person(full string — useful when split between multiple people),Message, and a small footer withBank IDand a[manual fix]marker ifmanual_fixis truthy. Reuse the existingtx-item/tx-meta/tx-main/tx-msgstyles to match the rest of the modal. - When the list is empty, render
<div style="color: #444; font-style: italic; padding: 10px 0;">No raw payments tied to this member.</div>(same idiom used atadults.html:813).
- Reset the toggle to
-
Add the toggle handler near
closeModal:function toggleRawPayments(ev) { ev.preventDefault(); const list = document.getElementById('modalRawList'); const link = document.getElementById('rawPaymentsToggle'); const hidden = list.style.display === 'none'; list.style.display = hidden ? 'block' : 'none'; link.textContent = hidden ? '[hide]' : '[show]'; }
3. Why not extend reconcile() instead
reconcile() already collapses each row into per-month allocated shares and drops purpose, inferred_amount, bank_id, manual_fix, and the gross amount (trace). Carrying the raw tx through reconcile() would inflate the contract for every consumer when only the modal needs it. Grouping the already-fetched transactions list at the route level is one extra dict per request and reuses the cached payments data — no new sheet reads.
Critical files
- app.py — add
group_payments_by_person()helper; call it inadults_view(),juniors_view(), andpayments(); passraw_payments_jsonto the two dashboard templates. - templates/adults.html — modal section + JS + tiny CSS for the toggle link.
- templates/juniors.html — same changes as adults.html.
Verification
make web-debugand openhttp://localhost:5001/adults.- Pick a member known to have multiple payments (use the existing
/paymentspage as a cross-reference). - Click
[i]→ modal opens, raw list is hidden, link shows[show]. Click the link → list appears with the raw rows; click again → hides, link returns to[show]. - Switch to another member via keyboard (ArrowDown) — the toggle resets to hidden and the list updates to the new member's rows (no leaking).
- Compare the raw rows in the modal against the
/paymentspage grouping for the same person — same set of rows, sameDate/Amount/Message. - Pick a row with a non-conformant message (e.g. one where
Personwas inferred to multiple people) — confirmPersonshows the full comma-separated string andInferred Amountis visible when it differs fromAmount. - Repeat the click-through on
/juniorsto confirm parity. make test— no backend behavior change is expected, but run to catch template/route smoke breakage.