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:

  1. Create Cash360Client (Spring RestClient with Resilience4j): - Configuration: Cash360Properties with baseUrl, apiKey, timeout, retryAttempts - Authentication: API_KEY header 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)

  2. 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
  1. Create matching DTOs for all request/response objects: - Cash360ConsumerRequest, Cash360ConsumerResponse - Cash360BankAccountRequest - Cash360TransactionRequest, Cash360TransactionResponse - Cash360PaymentRequest, Cash360StornoRequest - Cash360WebhookPayload (inbound) - Cash360ReportResponse with nested PaymentByPeriod
  2. 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:

  1. 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
  1. 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 via Cash360Client - Revoke: set status REVOKED, notify Cash360
  2. 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
  3. Flyway: V200__create_bank_account.sql - Indexes: (memberId), (entityId, statusCd), UNIQUE (iban, memberId)
  4. 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:

  1. 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
  1. Create BillingService: - Nightly billing cycle (scheduled via @Scheduled, Redis distributed lock): ```

    1. SELECT contracts WHERE nextBillingDate <= TODAY (FOR UPDATE SKIP LOCKED — prevents double-billing in multi-instance)
    2. For each contract: a. Calculate amount from contract.monthlyPrice + VAT b. Create Transaction (status: PENDING) c. Update contract.nextBillingDate += billingIntervalMonths
    3. Batch transactions by entity (one Cash360 API call per entity)
    4. POST /api/public/p2/v1/transaction with bulk array
    5. Store returned Cash360 transaction IDs as externalTransactionId
    6. 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
  2. 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 stornoOfId
    • stornoPartial(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
  3. Create WebhookController: - POST /api/webhooks/cash360/transaction — Receives Cash360 status updates - HMAC validation: Verify webhook signature using shared secret (reject if invalid) - Processing logic:

    1. Lookup local transaction by externalTransactionId
    2. Validate status transition (prevent replay / out-of-order)
    3. Update local transaction status per mapping table
    4. If RETURNED/REJECTED: schedule dunning notification via RabbitMQ
    5. If PAID/SETTLED: update member's outstanding balance
    6. Respond with HTTP 200 (acknowledge) - Retry awareness: Cash360 retries up to 10x with exponential backoff (~26h total)
  4. 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

  5. Create BillingReportService: - Fetch: GET /api/public/p2/v2/billing-statement-reporting - Expose financial KPIs: totalPayoutAmount, fees, directDebitReturnAmount, reminderCasesAmount, pendingAmount, MRR, churn rate

  6. 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

  7. Flyway: V201__create_transaction.sql - Indexes: (entityId, statusCd), (memberId), (contractId), (externalTransactionId) UNIQUE, (dueDate), (billingPeriodStart, billingPeriodEnd)

  8. 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:

  1. Cash360 handles SEPA file generation (PAIN.008) and EBICS submission — Membership provides admin visibility
  2. 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)
  3. Admin UI: SEPA export history table, batch detail view, export-to-CSV for reconciliation
  4. 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."

---