PHASE 2: Payment & Billing
Trigger: "Execute Phase 2 as per masterplan."
Goal: Membership can generate billing transactions, submit them to Cash360 via the My Factura Public API, and receive payment status feedback. Bank account management, SEPA mandates, and invoice cancellation (Storno) are operational.
Prerequisite: Phase 1 is completed. Cash360 API is accessible.
Reference:
membership/planning/My Factura Public API.docx
Step 2.0 — Technical Conception
Create doc/developer/conception/phase-2-conception.md.
(reference: Chapter 12 — Integration and Payment, My Factura API Review)
Content: - Scope: membership-payment module, Cash360 API client, billing engine, webhooks - Component Design: Cash360Client -> BillingService -> TransactionService -> Cash360 API - Data Flow: Contract due -> BillingScheduler -> Transaction -> Cash360 -> Webhook callback - Error Handling: Circuit breaker, retry, dead-letter queue, polling fallback - Edge Cases: Double billing prevention, partial batch failure, webhook replay - Financial Separation: Fremdbetraege (member fees) vs. Eigenrechnungen (SaaS fees) - Storno Flow: Full/partial cancellation -> credit note -> automatic reversal posting - Design Chapter References: Ch12, My Factura API Review
Result: Technical conception for Phase 2.
Step 2.1 — Cash360 API Client
(reference: Chapter 12, My Factura API Review, membership/planning/My Factura Public API.docx)
Approach:
-
Create
Cash360Client(SpringRestClientwith Resilience4j): - Configuration:Cash360PropertieswithbaseUrl,apiKey,timeout,retryAttempts- Authentication:API_KEYheader per request (entity-specific key from Organization.settings JSONB) - Circuit Breaker: Resilience4j: failure rate 50%, wait 30s, half-open 5 calls - Retry: 3 attempts, exponential backoff (1s, 2s, 4s), retry on 5xx and timeout - Logging: Request/response logging at DEBUG level (mask sensitive fields: IBAN, API key) -
My Factura API endpoints to implement:
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/public/p2/v1/consumer |
Create consumer (member sync) |
| PUT | /api/public/p2/v1/consumer/{id} |
Update consumer |
| GET | /api/public/p2/v1/consumer/external/{externalId} |
Lookup by external ID |
| GET | /api/public/p2/v1/consumer?email={email} |
Search by email |
| POST | /api/public/p2/v1/consumer/{id}/bank-account |
Add bank account + SEPA mandate |
| PUT | /api/public/p2/v1/consumer/{id}/bank-account/{bankId} |
Update bank account |
| POST | /api/public/p2/v1/transaction |
Create transaction(s) — bulk array |
| GET | /api/public/p2/v1/transaction/{id} |
Get transaction status (polling fallback) |
| POST | /api/public/p2/v1/payment/pay |
Record manual payment |
| PUT | /api/public/p2/v1/payment/storno |
Cancel/storno transaction |
| GET | /api/public/p2/v2/billing-statement-reporting |
Financial reporting |
- Create matching DTOs for all request/response objects:
-
Cash360ConsumerRequest,Cash360ConsumerResponse-Cash360BankAccountRequest-Cash360TransactionRequest,Cash360TransactionResponse-Cash360PaymentRequest,Cash360StornoRequest-Cash360WebhookPayload(inbound) -Cash360ReportResponsewith nestedPaymentByPeriod - Tests with WireMock: all endpoints, error responses (4xx, 5xx), circuit breaker tripping, retry behavior
Result: Cash360 API client with circuit breaker, retry, and full endpoint coverage.
Step 2.2 — Bank Account Management
(reference: Chapter 11 — Data Model, Chapter 12 — SEPA Mandate Management)
Approach:
- Create JPA entity:
BankAccount- BankAccount fields:
| Field | Type | Constraints | Description |
|---|---|---|---|
| id | Long | PK | |
| entityId | Long | FK NOT NULL | Organization |
| memberId | Long | FK NOT NULL | Owner member |
| accountHolder | String(255) | NOT NULL | Account holder name |
| iban | String(34) | NOT NULL | IBAN (validated) |
| bic | String(11) | nullable | BIC/SWIFT (auto-resolved from IBAN) |
| bankName | String(255) | nullable | Bank name (auto-resolved) |
| mandateReference | String(35) | NOT NULL | SEPA mandate ref: MBR-{entityId}-{memberId}-{seq} |
| mandateSignDate | LocalDate | NOT NULL | Mandate signature date |
| mandateTypeCd | String(20) | default 'CORE' | CORE / B2B |
| isDefault | boolean | default false | Default payment account for member |
| statusCd | String(50) | default 'ACTIVE' | ACTIVE / REVOKED / EXPIRED |
| externalBankAccountId | Long | nullable | Cash360 bank account ID (after sync) |
| createdAt | Instant | auto | |
| updatedAt | Instant | auto | |
| version | Long | @Version |
- Create
BankAccountService: - IBAN validation (checksum + country code + length per ISO 13616) - BIC auto-lookup from IBAN (via embedded IBAN4j library or lookup table) - Mandate reference generation:MBR-{entityId}-{memberId}-{sequence}- Set-default: ensure only one default per member (unset others first) - Cash360 sync: on create/update, push to Cash360 viaCash360Client- Revoke: set status REVOKED, notify Cash360 - Create controller:
-
POST /api/bank-accounts— Add bank account (validates IBAN, creates SEPA mandate) -GET /api/bank-accounts/member/{memberId}— List member's bank accounts -PUT /api/bank-accounts/{id}— Update bank account -PUT /api/bank-accounts/{id}/set-default— Set as default payment account -DELETE /api/bank-accounts/{id}— Revoke bank account - Flyway:
V200__create_bank_account.sql- Indexes: (memberId), (entityId, statusCd), UNIQUE (iban, memberId) - Tests:
BankAccountServiceTest- Test cases: IBAN validation (valid DE/AT/CH, invalid checksum, wrong length), BIC auto-fill, mandate generation, set-default logic, Cash360 sync
Step 2.3 — Transaction and Billing Engine
(reference: Chapter 12 — Integration and Payment, My Factura API Review)
Approach:
- Create JPA entity:
Transaction- Transaction fields:
| Field | Type | Constraints | Description |
|---|---|---|---|
| id | Long | PK | |
| entityId | Long | FK NOT NULL | Organization |
| memberId | Long | FK NOT NULL | Member |
| contractId | Long | FK nullable | Related contract |
| productId | Long | FK nullable | Related product (shop orders) |
| externalTransactionId | Long | nullable | Cash360 transaction ID (after submission) |
| typeCd | String(50) | NOT NULL | MEMBERSHIP_FEE, SETUP_FEE, PRODUCT_PURCHASE, ADJUSTMENT, REFUND |
| statusCd | String(50) | default 'PENDING' | See status mapping below |
| amount | BigDecimal(19,4) | NOT NULL | Positive = charge, negative = refund |
| currency | String(3) | default 'EUR' | |
| amountNet | BigDecimal(19,4) | NOT NULL | Net amount |
| vatRate | BigDecimal(5,2) | NOT NULL | VAT percentage |
| vatAmount | BigDecimal(19,4) | NOT NULL | VAT portion |
| description | String(500) | NOT NULL | e.g., "Monthly membership - Premium (Mar 2026)" |
| billingPeriodStart | LocalDate | nullable | Billing period start |
| billingPeriodEnd | LocalDate | nullable | Billing period end |
| dueDate | LocalDate | NOT NULL | Payment due date |
| paidDate | LocalDate | nullable | When payment was received |
| paymentMethod | String(50) | NOT NULL | SEPA_DIRECT_DEBIT, BANK_TRANSFER, CASH, CARD |
| failureReason | String(500) | nullable | Reason for failure (from Cash360 webhook) |
| stornoOfId | Long | FK nullable | If this is a credit note: references original transaction |
| createdAt | Instant | auto | |
| updatedAt | Instant | auto | |
| version | Long | @Version |
- Transaction status mapping (Cash360 -> Membership):
| Cash360 Status | Membership Status | User Label |
|---|---|---|
| — (local only) | PENDING | Not yet submitted |
| NEW, ACCEPTED | SUBMITTED | Sent to processor |
| EXPORTED | SUBMITTED | Sent to bank |
| PAID | PAID | Paid |
| SETTLED | SETTLED | Settled |
| RETURNED | FAILED | Returned by bank |
| REJECTED | FAILED | Rejected |
| CANCELLED | CANCELLED | Cancelled |
| FOR_DUNNING | OVERDUE | Overdue |
| SENT_TO_INKASSO | DEBT_COLLECTION | In debt collection |
-
Create
BillingService: - Nightly billing cycle (scheduled via@Scheduled, Redis distributed lock): ```- SELECT contracts WHERE nextBillingDate <= TODAY (FOR UPDATE SKIP LOCKED — prevents double-billing in multi-instance)
- For each contract: a. Calculate amount from contract.monthlyPrice + VAT b. Create Transaction (status: PENDING) c. Update contract.nextBillingDate += billingIntervalMonths
- Batch transactions by entity (one Cash360 API call per entity)
- POST /api/public/p2/v1/transaction with bulk array
- Store returned Cash360 transaction IDs as externalTransactionId
- Update status: PENDING -> SUBMITTED ``` - Idempotency: Check if transaction already exists for contract + billing period before creating - Error handling: If Cash360 API fails, keep PENDING status, retry next cycle - Circuit breaker: If Cash360 is down (circuit open), skip submission, queue for next cycle
-
Create
TransactionService: - Manual payment recording:recordPayment(transactionId, amount, method)— partial payments supported - Balance calculation:getMemberBalance(memberId)— sum of open transactions - Storno (cancellation):stornoFull(transactionId, reason)— creates credit note (negative amount), references original via stornoOfIdstornoPartial(transactionId, amount, reason)— creates partial credit note- Both automatically push cancellation to Cash360:
PUT /api/public/p2/v1/payment/storno - Member transaction history with pagination, filtering by status/type/date range
-
Create
WebhookController: -POST /api/webhooks/cash360/transaction— Receives Cash360 status updates - HMAC validation: Verify webhook signature using shared secret (reject if invalid) - Processing logic:- Lookup local transaction by
externalTransactionId - Validate status transition (prevent replay / out-of-order)
- Update local transaction status per mapping table
- If RETURNED/REJECTED: schedule dunning notification via RabbitMQ
- If PAID/SETTLED: update member's outstanding balance
- Respond with HTTP 200 (acknowledge) - Retry awareness: Cash360 retries up to 10x with exponential backoff (~26h total)
- Lookup local transaction by
-
Create
BillingPollingService: - Fallback: every 15 minutes, query Cash360 for transactions with status != SETTLED - Catches missed webhooks (network issues, 500 errors) -GET /api/public/p2/v1/transaction/{externalTransactionId}per pending transaction -
Create
BillingReportService: - Fetch:GET /api/public/p2/v2/billing-statement-reporting- Expose financial KPIs: totalPayoutAmount, fees, directDebitReturnAmount, reminderCasesAmount, pendingAmount, MRR, churn rate -
Create controller: -
GET /api/transactions— Search transactions (paginated, admin) -GET /api/transactions/member/{memberId}— Member's transactions -GET /api/transactions/{id}— Transaction detail -POST /api/transactions/{id}/storno— Full storno with credit note -POST /api/transactions/{id}/storno-partial— Partial storno -POST /api/transactions/{id}/payment— Record manual payment -GET /api/billing/balance/{memberId}— Member balance -POST /api/billing/trigger— Manual billing trigger (admin, for testing) -GET /api/billing/report— Financial report from Cash360 -
Flyway:
V201__create_transaction.sql- Indexes: (entityId, statusCd), (memberId), (contractId), (externalTransactionId) UNIQUE, (dueDate), (billingPeriodStart, billingPeriodEnd) -
Tests: -
BillingServiceTest: nightly cycle, skip locked, idempotency, circuit breaker fallback -TransactionServiceTest: manual payment, balance, full storno, partial storno -WebhookControllerTest: HMAC validation, status transitions, replay protection -BillingPollingServiceTest: fallback polling catches missed webhooks
Result: Automated billing cycle with Cash360 integration, webhook processing, Storno with credit notes, and debt tracking.
Step 2.4 — SEPA Export Integration
(reference: Chapter 12 — Integration and Payment)
Approach:
- Cash360 handles SEPA file generation (PAIN.008) and EBICS submission — Membership provides admin visibility
- Create controller:
-
GET /api/billing/sepa-exports— List SEPA export batches from Cash360 (date, amount, status, count) -GET /api/billing/sepa-exports/{id}— Export detail with transaction list -POST /api/billing/sepa-exports/trigger— Request immediate SEPA batch (admin, for testing) - Admin UI: SEPA export history table, batch detail view, export-to-CSV for reconciliation
- Financial separation awareness: - Member fees (Fremdbetraege) — pass-through, settlement statement (Abrechnungsbericht) - SaaS fees (Eigenrechnungen) — own invoices with VAT, separate accounting stream
Step 2.5 — Phase 2 Documentation
Update doc/developer/api-reference.md (My Factura integration), doc/business/marketing-guide.md.
Step 2.6 — Update Intranet
Run python doc/intranet/build.py.
Phase 2 — Quality Gate
| # | Check | Target |
|---|---|---|
| 1 | Conception document | exists |
| 2 | Compilation + tests | 0 errors, 0 failures |
| 3 | Coverage | >= 60% |
| 4 | Full billing flow | member -> bank account -> purchase -> billing -> Cash360 submission |
| 5 | Webhook | updates transaction status |
| 6 | Storno | full + partial creates credit note |
| 7 | Balance | reflects correctly |
| 8 | No float/double | BigDecimal only |
| 9 | Documentation + intranet | updated |
| 10 | CLAUDE.md | updated |
Report: "Phase 2 completed."