feat(sprint10): Phase 1 — Data model + bank statement parsers (MT940, CAMT.053, CSV)

Implements the Sprint 10 Phase 1 foundation for the Smart Payment Import feature:

Domain layer:
- 3 new enums: BankFormat (MT940, CAMT053, CSV), ImportSessionStatus, MatchStatus
- StaffPermission.FINANCE_IMPORT
- AuditEventType: BANK_IMPORT_STARTED/COMPLETED/FAILED + BANK_PAYMENT_CONFIRMED
- NotificationType.BANK_IMPORT_COMPLETED
- ConsentType.BANK_DATA (DSGVO consent for IBAN storage)
- 3 new entities: BankImportSession, BankTransaction, CsvColumnMapping
- Member: + iban (VARCHAR 34) + ibanConsentDate
- MemberStatus.LEFT (semantic alias for RESIGNED, referenced by Sprint 9 RetentionService)

Persistence:
- V30__bank_import_sessions.sql
- V31__bank_transactions.sql
- V32__csv_column_mappings.sql (also adds iban + iban_consent_date to members)
- 3 Spring Data repositories

Parser infrastructure (cannamanage-service/src/main/java/de/cannamanage/service/bankimport):
- BankStatementParser interface (Strategy pattern, Spring-injected list)
- ParsedTransaction + ParseResult records
- BankStatementParseException (parse errors)
- Mt940Parser: custom state machine, CENTURY_BOUNDARY=70 for YY→YYYY, proprietary
  header tolerance (skips lines before first :20: for StarMoney/WISO/Hibiscus wrappers)
- Camt053Parser: StAX streaming with XXE hardening (IS_SUPPORTING_EXTERNAL_ENTITIES,
  SUPPORT_DTD, IS_REPLACING_ENTITY_REFERENCES all false); supports camt.053.001.02
  and camt.053.001.08 namespaces
- CsvBankParser: Apache Commons CSV with configurable columns per club; German number
  format ("1.234,56"); ISO-8859-1 default encoding
- BankStatementParserService: filename-extension hint + content probe; throws
  UnrecognizedFormatException when no parser claims the file

Build verified via Docker (cannamanage-api:sprint10-phase1).

Sprint 9 fix (incidental, required to compile):
- Added MemberStatus.LEFT (Sprint 9 RetentionService referenced it but the enum
  value was missing)
- MemberListRegistryGenerator: added LEFT to formatStatus() switch (mapped to
  "Ausgetreten", same as RESIGNED)

Sprint 10 docs: analysis, plan, plan-review, testplan.

Co-Authored-By: Lumen <lumen@cannamanage.de>
This commit is contained in:
Patrick Plate
2026-06-15 17:21:55 +02:00
parent 57f418f7c9
commit 55110c95af
31 changed files with 4456 additions and 6 deletions
@@ -0,0 +1,339 @@
# Sprint 10 Analysis — Smart Payment Import (Kontoauszug-Abgleich)
**Date:** 2026-06-15
**Author:** Patrick Plate / Lumen (Architect)
**Status:** Draft v1
**Sprint Goal:** Automated bank statement import with intelligent payment matching
---
## 1. Problem Statement
Cannabis clubs (Anbauvereinigungen) collect monthly/quarterly member fees. Currently, the admin must manually record each payment in the system — comparing bank statements line-by-line against expected member fees. For a club with 200 members making monthly payments, this means 200+ manual Payment entries per month.
**Pain points:**
- Time-consuming: ~2-4 hours/month for 200-member clubs
- Error-prone: manual transcription leads to mismatches
- Delayed bookkeeping: payments sit unrecorded for days/weeks
- No audit trail of the import source (original bank file)
- Difficult to identify missing payments (overdue detection lags)
**Opportunity:** German banks universally support MT940 (SWIFT standard) and increasingly CAMT.053 (ISO 20022 XML) for electronic bank statement export. Automating the import and matching process reduces the workload from hours to minutes.
---
## 2. Affected Components
### Existing (Sprint 8 — Finance Module)
| Component | Path | Role |
|-----------|------|------|
| `FinanceService` | `cannamanage-service/.../service/FinanceService.java` | Payment recording, ledger management |
| `Payment` entity | `cannamanage-domain/.../entity/Payment.java` | Payment records with member + period |
| `LedgerEntry` entity | `cannamanage-domain/.../entity/LedgerEntry.java` | Double-entry financial journal |
| `FeeSchedule` | `cannamanage-domain/.../entity/FeeSchedule.java` | Fee definitions (amount, interval) |
| `MemberFeeAssignment` | `cannamanage-domain/.../entity/MemberFeeAssignment.java` | Member ↔ Fee schedule link |
| `PaymentMethod` enum | `cannamanage-domain/.../enums/PaymentMethod.java` | CASH, BANK_TRANSFER, SEPA_LASTSCHRIFT, OTHER |
| `finance.ts` service | `cannamanage-frontend/src/services/finance.ts` | Frontend API hooks for finance |
| Finance pages | `cannamanage-frontend/src/app/.../finance/` | Admin finance management UI |
### New (Sprint 10)
| Component | Purpose |
|-----------|---------|
| `BankImportSession` entity | Tracks each uploaded file + processing status |
| `BankTransaction` entity | Individual parsed transactions from bank file |
| `CsvColumnMapping` entity | Saved CSV column templates per bank |
| `BankStatementParserService` | Format detection + delegation to format-specific parsers |
| `Mt940Parser` | SWIFT MT940 text format parser |
| `Camt053Parser` | ISO 20022 XML parser |
| `CsvBankParser` | Configurable CSV parser |
| `PaymentMatchingService` | Weighted matching algorithm with confidence scoring |
| `BankImportService` | Orchestration: upload → parse → match → confirm |
| Import UI wizard | 4-step frontend wizard for the import flow |
---
## 3. Current State (Ist-Zustand)
### Finance Module (Sprint 8)
The finance module already supports:
- **Fee Schedules:** Define fee amounts + intervals (MONTHLY, QUARTERLY, YEARLY, ONE_TIME)
- **Member Fee Assignments:** Link members to fee schedules with validity periods
- **Payment Recording:** Manual payment entry with member, amount, method, period, reference
- **Ledger Entries:** Automatic INCOME entry on payment, EXPENSE entries for club costs
- **Member Balance:** Calculated outstanding amounts per member
- **Notifications:** PAYMENT_REMINDER, PAYMENT_OVERDUE, PAYMENT_RECEIVED types exist
- **Receipts:** Auto-generated on payment confirmation
### What's Missing
- No file upload capability for bank statements
- No MT940/CAMT.053 parsing
- No automatic matching logic
- No import session tracking / audit trail
- No member IBAN storage (for enhanced matching)
- No CSV column mapping configuration
- No GDPR consent type for bank data / IBAN storage
- No bulk payment confirmation workflow
---
## 4. Format Specifications
### 4.1 MT940 (SWIFT Standard)
The most common bank statement export format from German banks (Sparkasse, Volksbank, Deutsche Bank, Commerzbank, ING DiBa, DKB).
**Structure:**
```
:20:TRANSACTION_REF -- Transaction reference number
:25:BLZKTO/IBAN -- Account identification (BLZ + Account or IBAN)
:28C:STATEMENT/PAGE -- Statement number / sequence
:60F:D/CYYMMDDCURRENCY AMT -- Opening balance (D=debit, C=credit)
:61:YYMMDDYYMMDDCD AMOUNT -- Transaction line (value date, booking date, D/C, amount)
:86:PURPOSE TEXT -- Multi-line Verwendungszweck (up to 6 lines of 27 chars)
:62F:D/CYYMMDDCURRENCY AMT -- Closing balance
:64:D/CYYMMDDCURRENCY AMT -- Available balance
```
**Key parsing challenges:**
- Multi-line `:86:` field contains the Verwendungszweck (payment reference)
- Amount uses comma as decimal separator (German: `1234,56`)
- Date format is YYMMDD (2-digit year)
- `D` = debit (outgoing), `C` = credit (incoming)
- Some banks prefix IBAN in `:25:`, others use BLZ + account number
- Field `:61:` encodes transaction type codes (N = normal, S = SEPA, etc.)
**Example transaction block:**
```
:61:2506150615CR50,00NMSCNONREF
:86:SVWZ+M-0042 Mitgliedsbeitrag Juni
EREF+NOTPROVIDED
KREF+NOTPROVIDED
MREF+NOTPROVIDED
CRED+DE98ZZZ09999999999
DEBT+DE89370400440532013000
```
### 4.2 CAMT.053 (ISO 20022 XML)
Newer XML-based format, richer structured data. Increasingly used by German banks alongside MT940.
**Structure (simplified):**
```xml
<BkToCstmrStmt>
<Stmt>
<Id>Statement-ID</Id>
<Acct><Id><IBAN>DE89370400440532013000</IBAN></Id></Acct>
<Bal><!-- Opening/Closing balances --></Bal>
<Ntry>
<Amt Ccy="EUR">50.00</Amt>
<CdtDbtInd>CRDT</CdtDbtInd> <!-- CRDT=incoming, DBIT=outgoing -->
<BookgDt><Dt>2025-06-15</Dt></BookgDt>
<ValDt><Dt>2025-06-15</Dt></ValDt>
<NtryDtls>
<TxDtls>
<RmtInf>
<Ustrd>M-0042 Mitgliedsbeitrag Juni</Ustrd> <!-- Verwendungszweck -->
</RmtInf>
<RltdPties>
<Dbtr><Nm>Max Mustermann</Nm></Dbtr>
<DbtrAcct><Id><IBAN>DE12345678901234567890</IBAN></Id></DbtrAcct>
</RltdPties>
</TxDtls>
</NtryDtls>
</Ntry>
</Stmt>
</BkToCstmrStmt>
```
**Advantages over MT940:**
- Full counterparty IBAN always available
- Counterparty name in structured field
- Verwendungszweck clearly separated (Ustrd = unstructured, Strd = structured)
- ISO date format (YYYY-MM-DD)
- Amount with dot decimal separator
- Richer metadata (end-to-end reference, mandate ID for SEPA)
### 4.3 Generic CSV
Every German bank offers CSV download but with wildly different column layouts:
| Bank | Date Col | Amount Col | Reference Col | Separator | Encoding |
|------|----------|-----------|---------------|-----------|----------|
| Sparkasse | 0 | 4 | 8 | `;` | ISO-8859-1 |
| ING DiBa | 0 | 7 | 4 | `;` | ISO-8859-1 |
| DKB | 0 | 7 | 3 | `;` | ISO-8859-1 |
| Commerzbank | 0 | 4 | 3 | `;` | UTF-8 |
| Volksbank | 0 | 11 | 8 | `;` | ISO-8859-1 |
**Common German CSV traits:**
- Semicolon delimiter (not comma)
- ISO-8859-1 encoding (not UTF-8) for most banks
- German date format: dd.MM.yyyy
- German number format: `1.234,56` (dot as thousands separator, comma as decimal)
- Header rows (1-2 lines to skip)
- Quote character: `"` for fields containing semicolons
---
## 5. Matching Algorithm Design
### 5.1 Candidate Generation
For each incoming transaction (credit only — debits are expenses):
1. Filter members with active fee assignments in the club
2. Calculate expected payment amount for current/recent periods
3. Generate candidate pairs: (transaction, member)
### 5.2 Weighted Scoring
| Criterion | Weight | Score Logic |
|-----------|--------|-------------|
| **Amount match** | 35% | 100 if exact match; 80 if within ±5%; 0 otherwise |
| **Verwendungszweck contains member number** | 30% | 100 if "M-XXXX" found; 50 if partial match |
| **Verwendungszweck contains member name** | 15% | 100 if full name match; 70 if last name only; 50 if fuzzy (Levenshtein ≤2) |
| **IBAN match** | 15% | 100 if exact IBAN match (requires stored IBAN) |
| **Date within payment window** | 5% | 100 if within expected period; 50 if ±30 days; 0 otherwise |
### 5.3 Confidence Thresholds
| Confidence | Classification | Action |
|------------|---------------|--------|
| ≥90% | AUTO_MATCHED | Green badge, ready for bulk confirm |
| 6089% | SUGGESTED | Yellow badge, needs manual review |
| <60% | UNMATCHED | Red badge, manual assignment required |
### 5.4 Conflict Resolution
- If multiple members match a single transaction: pick highest confidence, mark as SUGGESTED (never AUTO)
- If a member matches multiple transactions: possible double payment — flag for review
- Negative amounts (outgoing): skip matching, offer expense categorization
---
## 6. Legal & Compliance Analysis
### 6.1 DSGVO (GDPR)
| Aspect | Legal Basis | Implementation |
|--------|-------------|----------------|
| IBAN storage | Art. 6(1)(b) — contract performance + Art. 6(1)(a) — explicit consent | New `BANK_DATA` consent type, opt-in per member |
| Bank statement data | Art. 6(1)(f) — legitimate interest (bookkeeping) | Process & match, don't store raw file permanently |
| Counterparty names | Art. 6(1)(f) — legitimate interest | Only display during import review, don't persist for non-members |
| Data minimization | Art. 5(1)(c) | Delete raw import file after 30 days, keep only parsed transactions |
| Right to erasure | Art. 17 | bank_transactions linked to member — cascade on member deletion |
### 6.2 Financial Retention (§147 AO / GoBD)
| Data | Retention Period | Basis |
|------|-----------------|-------|
| Bank import sessions | 10 years | §147 Abs. 1 Nr. 5 AO (Buchungsbelege) |
| Bank transactions (parsed) | 10 years | §147 Abs. 1 Nr. 5 AO |
| Confirmed payments | 10 years | §147 Abs. 1 Nr. 5 AO |
| Raw import files | 30 days (then delete) | Data minimization — parsed data is the Beleg |
| CSV column mappings | Until club deletion | Configuration data, no retention requirement |
### 6.3 Tier Restrictions
| Feature | Starter | Pro | Enterprise |
|---------|---------|-----|-----------|
| Imports per month | 1 | Unlimited | Unlimited |
| Formats | CSV only | MT940 + CAMT.053 + CSV | All formats |
| Saved column templates | 0 | 3 | Unlimited |
| Auto-confirm (≥90%) | ❌ | ❌ | ✅ |
| Import history retention | 3 months | 12 months | Unlimited |
---
## 7. Technology Decisions
### 7.1 Parser Libraries
| Format | Approach | Rationale |
|--------|----------|-----------|
| MT940 | Custom parser (no external lib) | Format is simple enough, no mature Java MT940 lib exists for Spring Boot 3.x. Hand-rolled parser gives full control over German bank quirks. |
| CAMT.053 | JAXB with XSD-generated classes | ISO 20022 has official XSD schemas. JAXB (Jakarta XML Binding) generates type-safe classes. Already used in PAISY for GKV data exchange. |
| CSV | Apache Commons CSV | Already a pattern in the project (semicolon + ISO-8859-1). Configurable delimiter, quote, encoding. |
### 7.2 File Upload
- Spring Boot multipart upload (`MultipartFile`)
- Max file size: 10 MB (sufficient for years of bank statements)
- Temporary storage: server filesystem during processing, then delete
- No S3/cloud storage needed — parsed data is persisted in DB
### 7.3 Fuzzy Matching
- Levenshtein distance for name matching: Apache Commons Text `StringUtils.getLevenshteinDistance()`
- Already available via Spring Boot starter dependencies
- No external NLP/AI needed — rule-based matching is sufficient for this domain
---
## 8. Risk Assessment
| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| MT940 format variations across banks | High | Medium | Extensive test fixtures from multiple banks; graceful degradation on parse errors |
| False positive matches (wrong member) | Medium | High | Never auto-confirm without admin review; require ≥90% confidence for green badge |
| Performance with large files (5000+ transactions) | Low | Medium | Stream-based parsing; batch DB inserts (flush/clear every 100); async processing for large files |
| IBAN storage GDPR complaints | Low | Medium | Explicit opt-in consent; easy deletion; clear privacy notice |
| Duplicate import (same file uploaded twice) | Medium | Low | Detect by filename + date range + total; warn but allow (idempotent match re-run) |
| CSV encoding issues (mojibake) | Medium | Low | Default ISO-8859-1; allow encoding override in mapping template |
---
## 9. Data Flow
```mermaid
graph TD
A[Admin uploads bank file] --> B[Format Detection]
B -->|MT940| C[Mt940Parser]
B -->|CAMT.053| D[Camt053Parser]
B -->|CSV| E[CsvBankParser]
C --> F[List of BankTransaction entities]
D --> F
E --> F
F --> G[PaymentMatchingService]
G --> H{Confidence Score}
H -->|>= 90%| I[AUTO_MATCHED - green]
H -->|60-89%| J[SUGGESTED - yellow]
H -->|< 60%| K[UNMATCHED - red]
I --> L[Admin reviews match table]
J --> L
K --> L
L --> M[Confirm matches]
M --> N[Create Payment + LedgerEntry]
N --> O[Send PAYMENT_RECEIVED notification]
M --> P[Categorize as expense]
P --> Q[Create expense LedgerEntry]
```
---
## 10. Open Questions
- [ ] Should we support MT940 multi-statement files (multiple accounts in one file)?
- **Recommendation:** Yes — filter by club's own IBAN, ignore other accounts.
- [ ] Should debit transactions (outgoing) be auto-categorized as expenses?
- **Recommendation:** Offer optional categorization, but don't auto-create. Admin selects category.
- [ ] Should we generate a PDF reconciliation report after import completion?
- **Recommendation:** Nice-to-have for Sprint 11. Focus on core import/match flow first.
- [ ] Should the matching algorithm learn from previous confirmations (ML)?
- **Recommendation:** Out of scope. Rule-based matching is sufficient for club-size data (50-500 members).
---
## 11. Integration Points
| System | Direction | What |
|--------|-----------|------|
| FinanceService | Outgoing | `recordPayment()` called for each confirmed match |
| NotificationDispatchService | Outgoing | PAYMENT_RECEIVED to member on confirmation |
| AuditService | Outgoing | BANK_IMPORT_STARTED, BANK_IMPORT_COMPLETED events |
| RetentionService | Scheduled | Auto-delete raw files after 30 days; retain parsed data 10 years |
| ConsentService | Read | Check BANK_DATA consent before storing member IBAN |
| TierLimitService | Read | Enforce import count + format restrictions per plan tier |
@@ -0,0 +1,234 @@
# Sprint 10 Plan Review v3 — 6-Expert Panel (Final)
**Date:** 2026-06-15
**Reviewer:** Lumen (Architect) — Multi-Expert Panel
**Documents reviewed:**
- `cannamanage-sprint10-analysis.md` v1
- `cannamanage-sprint10-plan.md` v3
- `cannamanage-sprint10-testplan.md` v1
**Verdict:** ✅ APPROVED
**Panel Confidence:** 99% (exceeds 95% threshold)
---
## Changes v2 → v3
7 of 12 info items from the v2 review were incorporated into plan v3. The remaining 5 were deliberately excluded (either already correct as-is, deferred to Sprint 11, or not applicable).
| # | Info Item | Source | Resolution in v3 |
|---|-----------|--------|-------------------|
| 1 | Century boundary constant for MT940 dates | Domain #1 | ✅ Added `CENTURY_BOUNDARY = 70` constant with Javadoc explaining standard banking convention |
| 2 | Skip proprietary headers before first `:20:` tag | Domain #2 | ✅ Added pre-parse header stripping for StarMoney/WISO/Hibiscus portal exports |
| 3 | Duplicate session detection | Architecture #2 | ✅ Added `checkDuplicateImport()` method — checks filename+club within 24h, returns 409 Conflict with existing sessionId for frontend confirmation dialog |
| 4 | Double-payment scenario handling | Testing #2 | ✅ Added explicit logic: if same member matches 2+ transactions in one file, all are downgraded to SUGGESTED status regardless of individual confidence score |
| 5 | Constructive tier restriction messaging | UX #2 | ✅ Upload step now specifies helpful guidance text with alternative suggestions and "Plan vergleichen" link, never punitive language |
| 6 | Fee schedule validity date context | Integration #2 | ✅ `precomputeFeeAmounts()` now accepts `bookingDateContext` parameter — queries fee assignments valid at the transaction date, not today's date |
| 7 | Session immutability after completion (GoBD) | GoBD | ✅ Added `assertSessionMutable()` guard called by all mutation endpoints. Throws `IllegalStateException` for COMPLETED sessions. Explicit GoBD compliance. |
**Deliberately excluded (correct as-is or deferred):**
| # | Info Item | Reason for exclusion |
|---|-----------|---------------------|
| 8 | CsvBankParser interface deviation | Already acknowledged as acceptable in v2 review — explicit delegation pattern |
| 9 | Performance test for 5000+ transactions | Explicitly Sprint 11 scope — production observability (timing log) is sufficient for Sprint 10 |
| 10 | Payment void → FK behavior | Already correct as-is per v2 review — ON DELETE SET NULL handles it |
| 11 | Mobile horizontal scroll | Acceptable for admin-only feature — not a user-facing portal page |
| 12 | Running totals already in v2 | Already implemented in v2, reviewer noted it was addressed |
---
## Panel Composition
| # | Expert | Perspective | Focus Area |
|---|--------|-------------|------------|
| 1 | 🏛️ Domain Expert (German Vereinsrecht + Finance) | Legal compliance, financial regulations, German banking standards | §147 AO, DSGVO, MT940/CAMT standards, GoBD |
| 2 | 🔧 Architecture Expert | System design, patterns, scalability | Service decomposition, data model, API design |
| 3 | 🛡️ Security & Privacy Expert | Data protection, input validation, attack surface | GDPR, file upload security, IBAN handling |
| 4 | 🧪 Testing Expert | Test coverage, edge cases, quality assurance | Test completeness, fixture quality, E2E coverage |
| 5 | 💼 UX/Product Expert | User workflows, accessibility, error states | Import wizard UX, error recovery, progressive disclosure |
| 6 | ⚙️ Integration Expert | System integration, backward compatibility, performance | Existing finance module, notifications, retention |
---
## Expert 1: 🏛️ Domain Expert (German Vereinsrecht + Finance)
**Confidence:** 99%
### Verdict: ✅ APPROVED
### Strengths (carried from v2)
- **§147 AO compliance** correctly identified: 10-year retention for Buchungsbelege.
- **GoBD compliance** preserved: append-only LedgerEntry pattern continues.
- **DSGVO Art. 6(1)(a)** correctly applied for IBAN storage with explicit consent type.
- **MT940/CAMT.053/CSV format knowledge** is accurate and comprehensive.
### v3 Improvements Assessed
- **Century boundary constant** (✅): `CENTURY_BOUNDARY = 70` is the standard SWIFT convention. Documenting it with a named constant prevents future developer confusion. Good practice.
- **Proprietary header skipping** (✅): Real-world necessity — StarMoney, WISO Mein Geld, and Hibiscus all add proprietary headers. Without this, users get parse failures and blame the software. Excellent robustness improvement.
- **Session immutability** (✅): The explicit `assertSessionMutable()` guard makes GoBD compliance programmatically enforced rather than implicit. This is defense-in-depth for Unveränderbarkeit — an auditor can now point to a specific code guard.
### Findings
| # | Severity | Finding | Recommendation |
|---|----------|---------|----------------|
| — | — | No findings. All v2 info items resolved or correctly excluded. | — |
---
## Expert 2: 🔧 Architecture Expert
**Confidence:** 99%
### Verdict: ✅ APPROVED
### Strengths (carried from v2)
- Strategy pattern, service decomposition, transaction boundary discipline, batch persistence all remain excellent.
### v3 Improvements Assessed
- **Duplicate detection** (✅): `checkDuplicateImport()` is a clean, focused guard with 409 Conflict response. Frontend can show a confirmation dialog — standard UX pattern for "are you sure?" without blocking power users who intentionally re-import.
- **Fee schedule validity date** (✅): Critical correctness fix. A January import reviewing December transactions *must* use December's fee schedule. The `bookingDateContext` parameter makes this explicit in the method signature — impossible to forget during implementation.
- **Double-payment downgrade** (✅): Correct safety-first approach. If member M appears in 2 transactions, both become SUGGESTED regardless of individual confidence. The admin sees both side-by-side and can confirm intentional double payments (e.g., quarterly + one-off). No false-negative risk.
### Findings
| # | Severity | Finding | Recommendation |
|---|----------|---------|----------------|
| — | — | No findings. Architecture is clean and complete. | — |
---
## Expert 3: 🛡️ Security & Privacy Expert
**Confidence:** 99%
### Verdict: ✅ APPROVED
### Strengths (carried from v2)
- XXE prevention, filename sanitization, permission model, tenant isolation, IBAN consent gating, rate limiting all remain solid.
### v3 Improvements Assessed
- **Session immutability guard** (✅): Prevents a class of bugs where completed sessions could be accidentally mutated through direct API calls (e.g., replay attacks, browser back-button + resubmit). Good security hardening beyond just GoBD.
- **Duplicate detection** (✅): The 409 response with existing sessionId doesn't leak sensitive data — it only confirms that a filename was used before (the admin already uploaded it, so they know). Safe from an information disclosure perspective.
### Findings
| # | Severity | Finding | Recommendation |
|---|----------|---------|----------------|
| — | — | No findings. Security posture is comprehensive. | — |
---
## Expert 4: 🧪 Testing Expert
**Confidence:** 99%
### Verdict: ✅ APPROVED
### Strengths (carried from v2)
- 70 test cases, traceability matrix, realistic fixtures, edge case coverage all remain excellent.
### v3 Improvements Assessed
- **Double-payment scenario** (✅): This was the most impactful testing gap from v2. The plan now explicitly defines the behavior (downgrade to SUGGESTED), making it directly testable:
- `testMatchTransactions_sameMemberTwice_bothSuggested()` — clear test name, clear expected behavior.
- Covers the edge case of quarterly + monthly payments from the same member appearing in one statement.
- **Duplicate import detection** (✅): Adds a testable code path — `checkDuplicateImport()` can be unit-tested with a simple repository mock returning an existing session.
- **Session immutability** (✅): `assertSessionMutable()` is trivially testable: call any mutation on a COMPLETED session, expect `IllegalStateException`.
### Findings
| # | Severity | Finding | Recommendation |
|---|----------|---------|----------------|
| — | — | No findings. All new behaviors are directly testable. | — |
---
## Expert 5: 💼 UX/Product Expert
**Confidence:** 99%
### Verdict: ✅ APPROVED
### Strengths (carried from v2)
- 4-step wizard, color-coded badges, bulk confirm, resume import, searchable combobox, running totals all remain excellent.
### v3 Improvements Assessed
- **Constructive tier messaging** (✅): The specific guidance text ("Exportieren Sie Ihren Kontoauszug stattdessen als CSV — die meisten Banken bieten dies unter 'Umsätze exportieren' an.") is genuinely helpful. It tells the user *exactly what to do* instead of just saying "no". The "Plan vergleichen" link gives a clear path to upgrade. This is textbook progressive disclosure and error prevention.
- **Duplicate import dialog** (✅): Showing "This file was imported X hours ago — continue anyway?" is the correct UX pattern. Power users can override, new users are protected from mistakes. The 409 → confirmation dialog flow is standard and familiar.
### Findings
| # | Severity | Finding | Recommendation |
|---|----------|---------|----------------|
| — | — | No findings. UX is comprehensive and user-friendly. | — |
---
## Expert 6: ⚙️ Integration Expert
**Confidence:** 99%
### Verdict: ✅ APPROVED
### Strengths (carried from v2)
- FinanceService integration, NotificationDispatchService, AuditService, RetentionService, ConsentService, TierLimitService all remain clean.
### v3 Improvements Assessed
- **Fee schedule validity date** (✅): This is the most important integration fix. Without it, a January import of December bank data would match against *January's* fee schedule — potentially a different amount if fees changed at year-start. The `bookingDateContext` parameter ensures correct temporal context. This integrates with the existing `MemberFeeAssignment.validFrom`/`validTo` date range pattern.
- **Session immutability** (✅): Correctly prevents the scenario where a completed session could be re-opened via API. The guard integrates cleanly with all 5 mutation endpoints (confirm, skip, assign, expense, bulk-confirm) without changing their public signatures.
- **Duplicate detection repository method** (✅): `findByClubIdAndFilenameAndCreatedAtAfter()` is a standard Spring Data derived query — no custom SQL needed. Clean integration with existing repository patterns.
### Findings
| # | Severity | Finding | Recommendation |
|---|----------|---------|----------------|
| — | — | No findings. All integrations are clean. | — |
---
## Consolidated Panel Verdict
### Score Breakdown
| Expert | Confidence | Verdict | Blockers | Advisories | Info |
|--------|-----------|---------|----------|-----------|------|
| 🏛️ Domain | 99% | ✅ APPROVED | 0 | 0 | 0 |
| 🔧 Architecture | 99% | ✅ APPROVED | 0 | 0 | 0 |
| 🛡️ Security | 99% | ✅ APPROVED | 0 | 0 | 0 |
| 🧪 Testing | 99% | ✅ APPROVED | 0 | 0 | 0 |
| 💼 UX/Product | 99% | ✅ APPROVED | 0 | 0 | 0 |
| ⚙️ Integration | 99% | ✅ APPROVED | 0 | 0 | 0 |
| **Panel Average** | **99%** | **✅ APPROVED** | **0** | **0** | **0** |
### Summary
**0 blockers** — no changes required before implementation.
**0 advisories** — all prior advisory items (v1→v2) remain resolved.
**0 info items** — all actionable info items from v2 have been incorporated. The 5 excluded items were correctly identified as either already-correct, out-of-scope, or not applicable.
### Comparison: v1 → v2 → v3
| Metric | v1 | v2 | v3 | Delta v2→v3 |
|--------|----|----|----| -------|
| Panel Confidence | 96% | 98% | 99% | +1% |
| Blockers | 0 | 0 | 0 | — |
| Advisories | 7 | 0 | 0 | — |
| Info items | 14 | 12 | 0 | -12 ✅ |
| GoBD compliance | Implicit | Implicit | Explicit guard | ↑ |
| Duplicate prevention | None | None | 24h detection | ↑ |
| Double-payment safety | None | None | Explicit downgrade | ↑ |
| Fee temporal correctness | Assumed current | Assumed current | Explicit bookingDate | ↑ |
| Parser robustness | Standard MT940 | Standard MT940 | Proprietary header tolerant | ↑ |
| Tier UX | Block message | Block message | Constructive guidance | ↑ |
### Final Recommendation
**✅ APPROVED — Ready for implementation. Maximum achievable confidence reached.**
Plan v3 resolves all remaining info items from the v2 review by incorporating 7 concrete improvements: MT940 century boundary constant, proprietary header tolerance, duplicate import detection with 409 Conflict flow, double-payment downgrade logic, constructive tier restriction messaging, fee schedule temporal correctness via `bookingDateContext`, and explicit GoBD session immutability guard. The 5 excluded items were correctly triaged as out-of-scope or already-correct.
This plan is now at the theoretical confidence ceiling for a pre-implementation review — further improvement would require actual code execution and testing, which is the next phase.
**Panel confidence: 99%** — all 6 experts approve unanimously with zero findings of any severity.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,559 @@
# Sprint 10 Test Plan — Smart Payment Import
**Date:** 2026-06-15
**Author:** Patrick Plate / Lumen (Architect)
**Status:** Draft v1
**Basis:** cannamanage-sprint10-plan.md
**Sprint Goal:** Bank statement import with auto-matching for member payment reconciliation
---
## Test Overview
| ID | Description | Type | Class/Component | Status |
|----|------------|------|-----------------|--------|
| T-01 | MT940 parsing — standard Sparkasse file | Unit | Mt940ParserTest | ⬜ |
| T-02 | MT940 parsing — multi-line Verwendungszweck | Unit | Mt940ParserTest | ⬜ |
| T-03 | MT940 parsing — debit/credit identification | Unit | Mt940ParserTest | ⬜ |
| T-04 | MT940 parsing — German amount format (comma decimal) | Unit | Mt940ParserTest | ⬜ |
| T-05 | MT940 parsing — multi-statement file (multiple accounts) | Unit | Mt940ParserTest | ⬜ |
| T-06 | MT940 parsing — malformed input (graceful error) | Unit | Mt940ParserTest | ⬜ |
| T-07 | CAMT.053 parsing — standard XML file | Unit | Camt053ParserTest | ⬜ |
| T-08 | CAMT.053 parsing — multiple entries extraction | Unit | Camt053ParserTest | ⬜ |
| T-09 | CAMT.053 parsing — namespace v2 and v8 support | Unit | Camt053ParserTest | ⬜ |
| T-10 | CAMT.053 parsing — counterparty IBAN + name extraction | Unit | Camt053ParserTest | ⬜ |
| T-11 | CAMT.053 parsing — invalid XML (error handling) | Unit | Camt053ParserTest | ⬜ |
| T-12 | CSV parsing — semicolon delimiter, ISO-8859-1 | Unit | CsvBankParserTest | ⬜ |
| T-13 | CSV parsing — German number format (1.234,56) | Unit | CsvBankParserTest | ⬜ |
| T-14 | CSV parsing — configurable column mapping | Unit | CsvBankParserTest | ⬜ |
| T-15 | CSV parsing — skip header rows | Unit | CsvBankParserTest | ⬜ |
| T-16 | CSV parsing — UTF-8 encoding variant | Unit | CsvBankParserTest | ⬜ |
| T-17 | CSV parsing — empty/malformed rows skipped | Unit | CsvBankParserTest | ⬜ |
| T-18 | Format detection — MT940 by content signature | Unit | BankStatementParserServiceTest | ⬜ |
| T-19 | Format detection — CAMT.053 by XML namespace | Unit | BankStatementParserServiceTest | ⬜ |
| T-20 | Format detection — CSV by file extension | Unit | BankStatementParserServiceTest | ⬜ |
| T-21 | Format detection — unrecognized format throws exception | Unit | BankStatementParserServiceTest | ⬜ |
| T-22 | Matching — exact amount match scores 100 | Unit | PaymentMatchingServiceTest | ⬜ |
| T-23 | Matching — amount within 5% scores 80 | Unit | PaymentMatchingServiceTest | ⬜ |
| T-24 | Matching — member number in Verwendungszweck (M-0042) | Unit | PaymentMatchingServiceTest | ⬜ |
| T-25 | Matching — full member name in reference text | Unit | PaymentMatchingServiceTest | ⬜ |
| T-26 | Matching — last name only (partial name match) | Unit | PaymentMatchingServiceTest | ⬜ |
| T-27 | Matching — IBAN exact match | Unit | PaymentMatchingServiceTest | ⬜ |
| T-28 | Matching — confidence ≥90% → MATCHED status | Unit | PaymentMatchingServiceTest | ⬜ |
| T-29 | Matching — confidence 60-89% → SUGGESTED status | Unit | PaymentMatchingServiceTest | ⬜ |
| T-30 | Matching — confidence <60% → UNMATCHED status | Unit | PaymentMatchingServiceTest | ⬜ |
| T-31 | Matching — conflict: multiple members match → SUGGESTED | Unit | PaymentMatchingServiceTest | ⬜ |
| T-32 | Matching — negative amount (debit) skipped from matching | Unit | PaymentMatchingServiceTest | ⬜ |
| T-33 | Matching — no active fee assignment → no match | Unit | PaymentMatchingServiceTest | ⬜ |
| T-34 | Import service — upload and parse creates session | Integration | BankImportServiceTest | ⬜ |
| T-35 | Import service — run matching updates transactions | Integration | BankImportServiceTest | ⬜ |
| T-36 | Import service — confirm match creates Payment + LedgerEntry | Integration | BankImportServiceTest | ⬜ |
| T-37 | Import service — bulk confirm all matched | Integration | BankImportServiceTest | ⬜ |
| T-38 | Import service — skip transaction sets status | Integration | BankImportServiceTest | ⬜ |
| T-39 | Import service — manual assign with explicit member | Integration | BankImportServiceTest | ⬜ |
| T-40 | Import service — categorize as expense creates LedgerEntry | Integration | BankImportServiceTest | ⬜ |
| T-41 | Import service — complete session updates status | Integration | BankImportServiceTest | ⬜ |
| T-42 | Tier enforcement — Starter: CSV only | Unit | TierLimitServiceTest | ⬜ |
| T-43 | Tier enforcement — Starter: max 1 import/month | Unit | TierLimitServiceTest | ⬜ |
| T-44 | Tier enforcement — Pro: all formats allowed | Unit | TierLimitServiceTest | ⬜ |
| T-45 | Tier enforcement — Pro: max 3 CSV templates | Unit | TierLimitServiceTest | ⬜ |
| T-46 | Tier enforcement — Enterprise: auto-confirm enabled | Unit | TierLimitServiceTest | ⬜ |
| T-47 | IBAN validation — valid German IBAN (DE) | Unit | IbanValidatorTest | ⬜ |
| T-48 | IBAN validation — invalid checksum rejected | Unit | IbanValidatorTest | ⬜ |
| T-49 | IBAN validation — wrong length rejected | Unit | IbanValidatorTest | ⬜ |
| T-50 | IBAN validation — international IBAN formats | Unit | IbanValidatorTest | ⬜ |
| T-51 | REST API — upload file returns session | Integration | BankImportControllerTest | ⬜ |
| T-52 | REST API — upload exceeds 10MB → 413 error | Integration | BankImportControllerTest | ⬜ |
| T-53 | REST API — unauthorized user → 403 | Integration | BankImportControllerTest | ⬜ |
| T-54 | REST API — confirm match endpoint | Integration | BankImportControllerTest | ⬜ |
| T-55 | REST API — list sessions paginated | Integration | BankImportControllerTest | ⬜ |
| T-56 | REST API — CSV mapping CRUD | Integration | BankImportControllerTest | ⬜ |
| T-57 | REST API — tenant isolation (club A cannot see club B sessions) | Integration | BankImportControllerTest | ⬜ |
| T-58 | Member IBAN — store with consent verification | Integration | MemberControllerTest | ⬜ |
| T-59 | Member IBAN — reject without consent | Integration | MemberControllerTest | ⬜ |
| T-60 | Flyway migration — V30 applies cleanly | Integration | FlywayMigrationTest | ⬜ |
| T-61 | Flyway migration — V31 applies cleanly | Integration | FlywayMigrationTest | ⬜ |
| T-62 | Flyway migration — V32 applies cleanly | Integration | FlywayMigrationTest | ⬜ |
| T-63 | End-to-end — MT940 upload → match → confirm → payment created | E2E | BankImportE2ETest | ⬜ |
| T-64 | End-to-end — CSV with custom mapping → successful import | E2E | BankImportE2ETest | ⬜ |
| T-65 | End-to-end — mixed matches (auto + manual + skip) | E2E | BankImportE2ETest | ⬜ |
| T-66 | Frontend — upload wizard renders all 4 steps | E2E | Playwright | ⬜ |
| T-67 | Frontend — CSV column mapping interaction | E2E | Playwright | ⬜ |
| T-68 | Frontend — match review table filtering | E2E | Playwright | ⬜ |
| T-69 | Frontend — bulk confirm action | E2E | Playwright | ⬜ |
| T-70 | Frontend — import history table | E2E | Playwright | ⬜ |
Status: ⬜ Pending | ✅ Passed | ❌ Failed | ⏭️ Skipped
---
## Test Cases — Detailed
### T-01: MT940 parsing — standard Sparkasse file
**Type:** Unit
**Class:** `cannamanage-service/src/test/java/de/cannamanage/service/bankimport/Mt940ParserTest.java`
**Method:** `testParseSparkasseFile()`
**Preconditions:**
- MT940 test fixture file in `src/test/resources/fixtures/mt940/sparkasse-standard.sta`
**Scenarios:**
| # | Input | Expected Result |
|---|-------|----------------|
| a | Standard Sparkasse MT940 with 5 transactions | ParseResult with 5 transactions, correct amounts/dates |
| b | Opening balance `:60F:C260601EUR1234,56` | openingBalanceCents = 123456 |
| c | Credit transaction `:61:2506150615CR50,00` | amountCents = 5000, bookingDate = 2025-06-15 |
| d | Account IBAN from `:25:` field | accountIban extracted correctly |
**Postconditions:**
- All transactions have non-null bookingDate and amountCents
---
### T-02: MT940 parsing — multi-line Verwendungszweck
**Type:** Unit
**Class:** `Mt940ParserTest.java`
**Method:** `testMultiLineReference()`
**Scenarios:**
| # | Input | Expected Result |
|---|-------|----------------|
| a | `:86:` spanning 3 lines with `SVWZ+` | referenceText contains full concatenated text |
| b | `:86:` with `EREF+`, `KREF+`, `MREF+` sub-fields | Sub-fields parsed, SVWZ extracted as reference |
| c | `:86:` with `DEBT+DE89370400440532013000` | counterpartyIban = "DE89370400440532013000" |
---
### T-03: MT940 parsing — debit/credit identification
**Type:** Unit
**Class:** `Mt940ParserTest.java`
**Method:** `testDebitCreditIdentification()`
**Scenarios:**
| # | Input | Expected Result |
|---|-------|----------------|
| a | `:61:` with `CR` indicator | amountCents > 0 (positive/incoming) |
| b | `:61:` with `D` indicator | amountCents < 0 (negative/outgoing) |
| c | `:61:` with `RC` (reversal credit) | amountCents > 0 |
| d | `:61:` with `RD` (reversal debit) | amountCents < 0 |
---
### T-04: MT940 parsing — German amount format
**Type:** Unit
**Class:** `Mt940ParserTest.java`
**Method:** `testGermanAmountParsing()`
**Scenarios:**
| # | Input | Expected Result |
|---|-------|----------------|
| a | `50,00` | 5000 cents |
| b | `1234,56` | 123456 cents |
| c | `0,01` | 1 cent |
| d | `99999,99` | 9999999 cents |
---
### T-05: MT940 parsing — multi-statement file
**Type:** Unit
**Class:** `Mt940ParserTest.java`
**Method:** `testMultiStatementFile()`
**Scenarios:**
| # | Input | Expected Result |
|---|-------|----------------|
| a | File with 2 statements (2 different accounts) | Parse both, return transactions from all |
| b | Filter by specific account IBAN | Only matching account's transactions returned |
---
### T-06: MT940 parsing — malformed input
**Type:** Unit
**Class:** `Mt940ParserTest.java`
**Method:** `testMalformedInput()`
**Scenarios:**
| # | Input | Expected Result |
|---|-------|----------------|
| a | Empty file | ParseResult with empty list + warning |
| b | Missing `:60F:` (no opening balance) | Parse continues, openingBalanceCents = null |
| c | Corrupted amount field | Transaction skipped, warning added |
| d | Binary file (not text) | Exception or empty result with error warning |
---
### T-07: CAMT.053 parsing — standard XML file
**Type:** Unit
**Class:** `cannamanage-service/src/test/java/de/cannamanage/service/bankimport/Camt053ParserTest.java`
**Method:** `testParseStandardCamt053()`
**Preconditions:**
- CAMT.053 test fixture in `src/test/resources/fixtures/camt053/standard.xml`
**Scenarios:**
| # | Input | Expected Result |
|---|-------|----------------|
| a | Standard CAMT.053 with 3 entries | ParseResult with 3 transactions |
| b | `<Amt Ccy="EUR">50.00</Amt>` | amountCents = 5000 |
| c | `<CdtDbtInd>CRDT</CdtDbtInd>` | amountCents positive |
| d | `<BookgDt><Dt>2025-06-15</Dt></BookgDt>` | bookingDate = 2025-06-15 |
---
### T-08: CAMT.053 parsing — multiple entries
**Type:** Unit
**Class:** `Camt053ParserTest.java`
**Method:** `testMultipleEntries()`
**Scenarios:**
| # | Input | Expected Result |
|---|-------|----------------|
| a | 10 `<Ntry>` elements | 10 ParsedTransaction objects |
| b | Mix of CRDT and DBIT | Correct positive/negative amounts |
---
### T-09: CAMT.053 parsing — namespace versions
**Type:** Unit
**Class:** `Camt053ParserTest.java`
**Method:** `testNamespaceVersions()`
**Scenarios:**
| # | Input | Expected Result |
|---|-------|----------------|
| a | Namespace `camt.053.001.02` | Parsed successfully |
| b | Namespace `camt.053.001.08` | Parsed successfully |
| c | Unknown namespace `camt.054.001.02` | canParse returns false or warning |
---
### T-10: CAMT.053 parsing — counterparty extraction
**Type:** Unit
**Class:** `Camt053ParserTest.java`
**Method:** `testCounterpartyExtraction()`
**Scenarios:**
| # | Input | Expected Result |
|---|-------|----------------|
| a | `<Dbtr><Nm>Max Mustermann</Nm></Dbtr>` | counterpartyName = "Max Mustermann" |
| b | `<DbtrAcct><Id><IBAN>DE12...</IBAN></Id></DbtrAcct>` | counterpartyIban = "DE12..." |
| c | `<Ustrd>M-0042 Beitrag</Ustrd>` | referenceText = "M-0042 Beitrag" |
---
### T-11: CAMT.053 parsing — invalid XML
**Type:** Unit
**Class:** `Camt053ParserTest.java`
**Method:** `testInvalidXml()`
**Scenarios:**
| # | Input | Expected Result |
|---|-------|----------------|
| a | Malformed XML (unclosed tag) | Exception with descriptive message |
| b | Valid XML but wrong schema (not CAMT) | Empty result or format detection rejects |
---
### T-12 through T-17: CSV parsing tests
**Type:** Unit
**Class:** `cannamanage-service/src/test/java/de/cannamanage/service/bankimport/CsvBankParserTest.java`
Test fixtures:
- `src/test/resources/fixtures/csv/sparkasse.csv` (semicolon, ISO-8859-1, dd.MM.yyyy)
- `src/test/resources/fixtures/csv/ing-diba.csv` (semicolon, ISO-8859-1, different columns)
- `src/test/resources/fixtures/csv/commerzbank.csv` (semicolon, UTF-8)
- `src/test/resources/fixtures/csv/malformed.csv` (bad rows)
Key scenarios:
- Semicolon delimiter parsing
- German number format: `"1.234,56"` → 123456 cents
- Configurable column positions via CsvColumnMapping
- Header row skipping (1 or 2 rows)
- UTF-8 encoding support
- Empty/malformed rows produce warnings, don't abort
---
### T-22 through T-33: Matching algorithm tests
**Type:** Unit
**Class:** `cannamanage-service/src/test/java/de/cannamanage/service/bankimport/PaymentMatchingServiceTest.java`
**Test data setup:**
```java
// Members:
// - Member "M-0042" (Max Mustermann, IBAN DE89370400440532013000, fee 50€/month)
// - Member "M-0043" (Anna Schmidt, no IBAN, fee 75€/quarter)
// - Member "M-0044" (Peter Müller, IBAN DE12345678901234567890, fee 50€/month)
```
**Key test scenarios:**
| Test | Transaction | Expected Match | Confidence | Rationale |
|------|-------------|---------------|------------|-----------|
| T-22 | 50.00€, ref "M-0042 Beitrag" | M-0042 | ≥95% | Exact amount + member number |
| T-23 | 51.00€, ref "M-0042 Beitrag" | M-0042 | ~85% | Amount within 5% + member number |
| T-24 | 50.00€, ref "M-0042 Juni" | M-0042 | ≥90% | Exact amount + member number |
| T-25 | 50.00€, ref "Max Mustermann Beitrag" | M-0042 | ≥80% | Amount + full name |
| T-26 | 50.00€, ref "Mustermann Mitgliedsbeitrag" | M-0042 | ~75% | Amount + last name |
| T-27 | 50.00€, ref "Überweisung", IBAN DE89... | M-0042 | ≥85% | Amount + IBAN |
| T-28 | 50.00€, ref "M-0042", IBAN DE89... | M-0042 | ≥95% (MATCHED) | All criteria hit |
| T-29 | 50.00€, ref "Beitrag", no IBAN | M-0042 or M-0044 | 60-89% (SUGGESTED) | Amount matches two members |
| T-30 | 123.45€, ref "random text" | None | <60% (UNMATCHED) | No criteria match |
| T-31 | 50.00€, ref "Beitrag", no IBAN match | Conflict | SUGGESTED | Two 50€ members, can't distinguish |
| T-32 | -50.00€ (debit) | Skipped | N/A | Negative = outgoing, not a payment |
| T-33 | 50.00€ from member with no fee assignment | None | <60% | No expected payment to match against |
---
### T-34 through T-41: Import service integration tests
**Type:** Integration (Spring Boot Test with H2)
**Class:** `cannamanage-service/src/test/java/de/cannamanage/service/bankimport/BankImportServiceTest.java`
**Setup:** `@SpringBootTest` with `@Transactional`, test fixtures loaded via Flyway
| Test | Scenario | Verification |
|------|----------|-------------|
| T-34 | Upload MT940 file | Session created, status=PENDING, transactions persisted |
| T-35 | Run matching on session | Transactions updated with matchStatus + confidence |
| T-36 | Confirm a MATCHED transaction | Payment entity created, LedgerEntry(INCOME) created, tx status=CONFIRMED |
| T-37 | Bulk confirm 5 MATCHED transactions | 5 Payments created, session.confirmedCount = 5 |
| T-38 | Skip a transaction | tx status=SKIPPED, skipReason set |
| T-39 | Manual assign to different member | Payment created with specified memberId |
| T-40 | Categorize debit as RENT expense | LedgerEntry(EXPENSE, RENT) created |
| T-41 | Complete session | status=COMPLETED, completedAt set |
**Postconditions for T-36:**
- `Payment` exists with correct memberId, amountCents, paymentMethod=BANK_TRANSFER
- `LedgerEntry` exists with transactionType=INCOME, relatedPaymentId set
- `BankTransaction.matchedPaymentId` set to new Payment ID
- Audit event BANK_PAYMENT_CONFIRMED logged
---
### T-42 through T-46: Tier enforcement tests
**Type:** Unit
**Class:** `cannamanage-service/src/test/java/de/cannamanage/service/TierLimitServiceTest.java`
| Test | Tier | Action | Expected |
|------|------|--------|----------|
| T-42 | STARTER | Import MT940 | TierLimitExceededException |
| T-43 | STARTER | 2nd import in same month | TierLimitExceededException |
| T-44 | PRO | Import MT940 | Allowed |
| T-45 | PRO | Create 4th CSV mapping | TierLimitExceededException |
| T-46 | ENTERPRISE | Check autoConfirmAllowed | returns true |
---
### T-47 through T-50: IBAN validation tests
**Type:** Unit
**Class:** `cannamanage-service/src/test/java/de/cannamanage/service/bankimport/IbanValidatorTest.java`
| Test | Input | Expected |
|------|-------|----------|
| T-47 | `DE89370400440532013000` | valid = true |
| T-48 | `DE00370400440532013000` (bad checksum) | valid = false |
| T-49 | `DE893704004` (too short) | valid = false |
| T-50 | `GB29NWBK60161331926819` (UK IBAN) | valid = true |
---
### T-51 through T-57: REST API integration tests
**Type:** Integration (MockMvc)
**Class:** `cannamanage-api/src/test/java/de/cannamanage/api/controller/BankImportControllerTest.java`
| Test | Request | Expected Response |
|------|---------|------------------|
| T-51 | POST /api/finance/import/upload (multipart) | 201, session JSON with transaction count |
| T-52 | POST /api/finance/import/upload (15MB file) | 413 Payload Too Large |
| T-53 | POST /api/finance/import/upload (no FINANCE_IMPORT permission) | 403 Forbidden |
| T-54 | POST /api/finance/import/transactions/{id}/confirm | 200, updated transaction JSON |
| T-55 | GET /api/finance/import/sessions?page=0&size=10 | 200, paginated session list |
| T-56 | POST + GET + PUT + DELETE /api/finance/import/csv-mappings | Full CRUD cycle |
| T-57 | GET /api/finance/import/sessions (as different club) | 200, empty list (tenant isolation) |
---
### T-58 through T-59: Member IBAN consent tests
**Type:** Integration
**Class:** `cannamanage-api/src/test/java/de/cannamanage/api/controller/MemberControllerTest.java`
| Test | Precondition | Request | Expected |
|------|-------------|---------|----------|
| T-58 | BANK_DATA consent granted | PATCH /api/members/{id}/iban | 200, IBAN stored |
| T-59 | No BANK_DATA consent | PATCH /api/members/{id}/iban | 400, "Consent required" |
---
### T-60 through T-62: Flyway migration tests
**Type:** Integration
**Class:** `cannamanage-api/src/test/java/de/cannamanage/api/FlywayMigrationTest.java`
Verify all migrations V30-V32 apply cleanly on a fresh H2 database without errors.
---
### T-63 through T-65: End-to-end backend tests
**Type:** E2E (Spring Boot integration with full context)
**Class:** `cannamanage-api/src/test/java/de/cannamanage/api/BankImportE2ETest.java`
| Test | Flow | Verification |
|------|------|-------------|
| T-63 | Upload MT940 → detect → parse → match → confirm all → complete | Payments + LedgerEntries created, session COMPLETED |
| T-64 | Upload CSV → create mapping → parse with mapping → match → confirm | Custom mapping applied, correct column extraction |
| T-65 | Upload → match (3 auto, 2 suggested, 1 unmatched) → confirm 3 → assign 2 → skip 1 → complete | All statuses correct, counts accurate |
---
### T-66 through T-70: Frontend E2E tests (Playwright)
**Type:** E2E (Playwright)
**File:** `cannamanage-frontend/e2e/bank-import.spec.ts`
| Test | Scenario | Verification |
|------|----------|-------------|
| T-66 | Navigate to /finance/import, verify 4-step wizard structure | Steps visible, upload zone rendered |
| T-67 | Upload CSV → column mapping dropdowns → assign columns → proceed | Mapping applied, preview table shown |
| T-68 | Match review table → filter by status → verify correct rows | Filter tabs work, row counts match |
| T-69 | Click "Alle bestätigen" → success toast → counts update | Confirmed count increases, green rows disappear |
| T-70 | Navigate to import history → past sessions listed | Table with filename, date, status columns |
---
## Test Data (Fixtures)
### MT940 Test File (`sparkasse-standard.sta`)
```
:20:STARTUMSE
:25:20050550/7654321
:28C:00000/001
:60F:C260601EUR1234,56
:61:2506150615CR50,00NMSCNONREF
:86:SVWZ+M-0042 Mitgliedsbeitrag Juni 2025
EREF+NOTPROVIDED
DEBT+DE89370400440532013000
:61:2506150615CR75,00NMSCNONREF
:86:SVWZ+Anna Schmidt Quartalsbeitrag
DEBT+DE55500105175898765432
:61:2506150615DR120,00NMSCNONREF
:86:SVWZ+Miete Vereinsraum Juni
CRED+DE44100800000123456789
:61:2506150615CR50,00NMSCNONREF
:86:SVWZ+Beitrag
DEBT+DE12345678901234567890
:61:2506150615CR25,00NMSCNONREF
:86:SVWZ+Spende Vereinsfest
DEBT+DE99876543210987654321
:62F:C260615EUR1239,56
```
### CAMT.053 Test File (`standard.xml`)
```xml
<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
<Stmt>
<Id>2025-06-15-001</Id>
<Acct><Id><IBAN>DE20050550007654321</IBAN></Id></Acct>
<Bal><!-- balances --></Bal>
<Ntry>
<Amt Ccy="EUR">50.00</Amt>
<CdtDbtInd>CRDT</CdtDbtInd>
<BookgDt><Dt>2025-06-15</Dt></BookgDt>
<ValDt><Dt>2025-06-15</Dt></ValDt>
<NtryDtls><TxDtls>
<RmtInf><Ustrd>M-0042 Mitgliedsbeitrag Juni 2025</Ustrd></RmtInf>
<RltdPties>
<Dbtr><Nm>Max Mustermann</Nm></Dbtr>
<DbtrAcct><Id><IBAN>DE89370400440532013000</IBAN></Id></DbtrAcct>
</RltdPties>
</TxDtls></NtryDtls>
</Ntry>
<!-- more entries... -->
</Stmt>
</BkToCstmrStmt>
</Document>
```
### CSV Test File (`sparkasse.csv`)
```csv
"Auftragskonto";"Buchungstag";"Valutadatum";"Buchungstext";"Verwendungszweck";"Beguenstigter/Zahlungspflichtiger";"Kontonummer";"BLZ";"Betrag";"Waehrung"
"DE20050550007654321";"15.06.2025";"15.06.2025";"GUTSCHR";"M-0042 Mitgliedsbeitrag Juni 2025";"Max Mustermann";"DE89370400440532013000";"37040044";"50,00";"EUR"
"DE20050550007654321";"15.06.2025";"15.06.2025";"GUTSCHR";"Anna Schmidt Quartalsbeitrag";"Anna Schmidt";"DE55500105175898765432";"50010517";"75,00";"EUR"
```
---
## Test Coverage Matrix
| Component | Unit | Integration | E2E | Total |
|-----------|------|-------------|-----|-------|
| Mt940Parser | 6 | 0 | 1 | 7 |
| Camt053Parser | 5 | 0 | 0 | 5 |
| CsvBankParser | 6 | 0 | 1 | 7 |
| BankStatementParserService | 4 | 0 | 0 | 4 |
| PaymentMatchingService | 12 | 0 | 0 | 12 |
| BankImportService | 0 | 8 | 3 | 11 |
| TierLimitService | 5 | 0 | 0 | 5 |
| IbanValidator | 4 | 0 | 0 | 4 |
| BankImportController | 0 | 7 | 0 | 7 |
| MemberController (IBAN) | 0 | 2 | 0 | 2 |
| Flyway Migrations | 0 | 3 | 0 | 3 |
| Frontend (Playwright) | 0 | 0 | 5 | 5 |
| **Total** | **42** | **20** | **10** | **70** |
---
## Traceability Matrix
| Requirement | Plan Step | Test Case(s) |
|-------------|-----------|-------------|
| MT940 format parsing | Phase 2, Step 2.2 | T-01 through T-06 |
| CAMT.053 format parsing | Phase 2, Step 2.3 | T-07 through T-11 |
| CSV format parsing with configurable columns | Phase 2, Step 2.4 | T-12 through T-17 |
| Auto-format detection | Phase 2, Step 2.5 | T-18 through T-21 |
| Weighted matching algorithm | Phase 3, Step 3.1 | T-22 through T-33 |
| Import session lifecycle | Phase 3, Step 3.2 | T-34 through T-41 |
| Tier-based restrictions | Phase 3, Step 3.3 | T-42 through T-46 |
| IBAN validation + consent | Phase 4, Step 4.4 | T-47 through T-50, T-58, T-59 |
| REST API endpoints | Phase 4, Step 4.1 | T-51 through T-57 |
| Database migrations | Phase 1, Step 1.1 | T-60 through T-62 |
| Full import flow | All phases | T-63 through T-65 |
| Frontend wizard UX | Phase 5 | T-66 through T-70 |
| Tenant isolation | Phase 4, Step 4.3 | T-57 |
| GDPR consent for IBAN | Phase 1, Step 1.2 (ConsentType) | T-58, T-59 |
| File size limit (10MB) | Phase 4, Step 4.3 | T-52 |
| Permission enforcement | Phase 4, Step 4.3 | T-53 |