fix: Tolerate diacritic/case/whitespace mismatches in Person column matching
Some checks failed
Deploy to K8s / deploy (push) Successful in 11s
Build and Push / build (push) Successful in 6s
Build and Push / build-go (push) Failing after 6s

- 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>
This commit is contained in:
2026-05-05 17:22:54 +02:00
parent 81b36878b3
commit 394da2e6b8
8 changed files with 498 additions and 120 deletions

View File

@@ -365,6 +365,19 @@
border-bottom: 1px dashed #222;
}
.raw-toggle {
color: #333;
font-size: 9px;
text-transform: lowercase;
margin-left: 8px;
text-decoration: none;
letter-spacing: 0;
}
.raw-toggle:hover {
color: #666;
}
.modal-table {
width: 100%;
border-collapse: collapse;
@@ -661,6 +674,16 @@
<!-- Filled by JS -->
</div>
</div>
<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>
</div>
</div>
@@ -677,6 +700,7 @@
const memberData = {{ member_data| safe }};
const sortedMonths = {{ raw_months| tojson }};
const monthLabels = {{ month_labels_json| safe }};
const rawPaymentsByPerson = {{ raw_payments_json| safe }};
let currentMemberName = null;
function showMemberDetails(name) {
@@ -809,9 +833,49 @@
});
}
// Raw payments (debug) — hidden by default, reset toggle on each open
const rawList = document.getElementById('modalRawList');
const rawToggle = document.getElementById('rawPaymentsToggle');
rawList.style.display = 'none';
rawToggle.textContent = '[show]';
rawList.innerHTML = '';
const rawRows = rawPaymentsByPerson[name] || [];
if (rawRows.length === 0) {
rawList.innerHTML = '<div style="color: #444; font-style: italic; padding: 10px 0;">No raw payments tied to this member.</div>';
} else {
rawRows.forEach(tx => {
const inferredNote = tx.inferred_amount && tx.inferred_amount !== '' && tx.inferred_amount != tx.amount
? ` <span style="color:#888;">(inferred: ${tx.inferred_amount})</span>`
: '';
const manualNote = tx.manual_fix ? ' <span style="color:#ffaa00;">[manual fix]</span>' : '';
const bankIdNote = tx.bank_id ? `<span style="color:#444;"> · bank_id: ${tx.bank_id}</span>` : '';
const item = document.createElement('div');
item.className = 'tx-item';
item.innerHTML = `
<div class="tx-meta">${tx.date} | purpose: ${tx.purpose || '—'}${manualNote}</div>
<div class="tx-main">
<span class="tx-amount">${tx.amount} CZK${inferredNote}</span>
<span class="tx-sender">${tx.sender || ''}</span>
</div>
<div class="tx-msg">${tx.message || ''}</div>
<div class="tx-meta">${tx.person || ''}${bankIdNote}</div>
`;
rawList.appendChild(item);
});
}
document.getElementById('memberModal').classList.add('active');
}
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]';
}
function closeModal(id) {
if (id) {
document.getElementById(id).style.display = 'none';