feat(sprint10): Phase 3 — BankImportService + REST API

Implements the orchestrator and REST endpoints for the bank statement
import wizard (Sprint 10 Phase 3).

Service layer (cannamanage-service):
- BankImportService: upload → SHA-256 dedup → parse → match → persist
  in two transactional steps (file I/O outside @Transactional, persist
  in @Transactional helper). Methods: uploadAndParse, confirmMatch,
  confirmAllMatched (≥90% confidence), manualAssign, skipTransaction,
  completeSession, query helpers.
- GoBD §147 AO immutability guard: assertSessionMutable() rejects any
  mutation on COMPLETED/FAILED sessions with German error messages.
- Hard 5MB upload cap enforced before parsing.
- Audit events: BANK_IMPORT_STARTED / BANK_PAYMENT_CONFIRMED /
  BANK_IMPORT_COMPLETED. Uploader notified via NotificationService.

REST layer (cannamanage-api):
- BankImportController under /api/v1/finance/import/*:
  POST sessions (multipart), GET sessions/single/transactions(?status=),
  POST {id}/transactions/{txnId}/confirm|assign|skip,
  POST {id}/confirm-all, POST {id}/complete,
  GET/POST/DELETE csv-mappings.
- Permission: FINANCE_IMPORT with MANAGE_FINANCES fallback.
- Defence-in-depth tenant check on every path-parameter ID.

DTOs (cannamanage-api/dto/bankimport):
- ImportSessionResponse, TransactionResponse, ConfirmRequest,
  AssignRequest, SkipRequest, BulkConfirmResponse, CreateMappingRequest.

Persistence:
- V33__bank_import_file_hash.sql: adds file_hash VARCHAR(64) + unique
  partial index (club_id, file_hash) for duplicate-upload detection.
- BankImportSession.fileHash field, repository.existsByClubIdAndFileHash.

Configuration:
- application.properties: multipart enabled, max-file-size=5MB,
  max-request-size=6MB.

Build: mvn package -DskipTests  (cannamanage-api fat JAR 92MB).
This commit is contained in:
Patrick Plate
2026-06-15 17:47:27 +02:00
parent 527e9b1219
commit 5defe42d67
13 changed files with 1061 additions and 0 deletions
@@ -54,6 +54,14 @@ public class BankImportSession extends AbstractTenantEntity {
@Column(name = "completed_at")
private Instant completedAt;
/**
* SHA-256 hex digest of the uploaded file content (Sprint 10 Phase 3).
* Used together with {@code clubId} to reject byte-identical re-uploads (HTTP 409),
* even when the file has been renamed. Nullable for legacy rows created before V33.
*/
@Column(name = "file_hash", length = 64)
private String fileHash;
// Getters and setters
public UUID getClubId() { return clubId; }
@@ -88,4 +96,7 @@ public class BankImportSession extends AbstractTenantEntity {
public Instant getCompletedAt() { return completedAt; }
public void setCompletedAt(Instant completedAt) { this.completedAt = completedAt; }
public String getFileHash() { return fileHash; }
public void setFileHash(String fileHash) { this.fileHash = fileHash; }
}