55110c95af961919504588d5d8e4d6e5c9c297de
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>
CannaManage
Multi-tenant cannabis club management platform for German Anbauvereinigungen (cultivation associations) under CanG §19.
Overview
CannaManage handles member management, distribution tracking, and legal compliance for cannabis cultivation clubs in Germany. It enforces the strict quotas mandated by the Cannabis Act (CanG) — including monthly limits (50g adult / 30g under-21), daily limits (25g), and THC restrictions for minors.
Tech Stack
| Component | Technology |
|---|---|
| Runtime | Java 21 (Temurin) |
| Framework | Spring Boot 4.0.6 |
| Security | Spring Security 7.0 + JWT (JJWT 0.12.6) |
| ORM | Hibernate 7 / JPA |
| Database | PostgreSQL (prod), H2 (test) |
| Migrations | Flyway 10 |
| API Docs | SpringDoc OpenAPI 2.8.6 |
| Build | Maven (multi-module) |
| Container | Docker Compose (Postgres + app) |
Project Structure
cannamanage/
├── cannamanage-domain/ # JPA entities, enums, TenantContext
├── cannamanage-service/ # Business logic, repositories, ComplianceService
├── cannamanage-api/ # Spring Boot app, controllers, security, DTOs
├── docs/
│ └── sprint-2/ # Sprint planning docs
└── docker-compose.yml # Local dev environment
Modules
cannamanage-domain
JPA entities with multi-tenant isolation via @Filter("tenantFilter"):
Member— club members with age trackingDistribution— cannabis distribution recordsMonthlyQuota— per-member monthly usage trackingBatch/Strain/StockMovement— inventory managementClub— association registrationUser— authentication accounts
cannamanage-service
ComplianceService— CanG §19 quota enforcement (25 unit tests)- Repositories for all entities
cannamanage-api
- Auth — JWT login + refresh token rotation (SHA-256 hashed)
- Members — CRUD for association members
- Distributions — compliance-gated distribution recording
- Stock — batch and inventory management
- Compliance — quota status API
- Multi-tenant isolation via
TenantFilterAspect(Hibernate @Filter activation)
API Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/auth/login |
Public | Login with email + password |
| POST | /api/v1/auth/refresh |
Public | Refresh token rotation |
| GET | /api/v1/compliance/quota/{memberId} |
ADMIN, MEMBER | Monthly quota status |
| GET/POST/PUT | /api/v1/members/** |
ADMIN, MEMBER | Member CRUD |
| POST | /api/v1/distributions/** |
ADMIN, MEMBER | Record distributions |
| GET/POST | /api/v1/stock/** |
ADMIN | Stock management |
Swagger UI: http://localhost:8080/swagger-ui.html
Running Locally
# Start PostgreSQL
docker compose up -d
# Run the app
JAVA_HOME=/path/to/jdk-21 ./mvnw spring-boot:run -pl cannamanage-api
# Run all tests (H2 in-memory)
JAVA_HOME=/path/to/jdk-21 ./mvnw clean verify
Testing
- 37 tests total — all green
- 25 unit tests (
ComplianceServiceTest) — quota enforcement logic - 7 integration tests (
AuthControllerIntegrationTest) — full HTTP auth flow - 5 integration tests (
ComplianceControllerIntegrationTest) — quota API with JWT
Integration tests use @SpringBootTest(webEnvironment = RANDOM_PORT) with H2 and Spring's RestClient.
Security Model
- Stateless JWT — no session, no UserDetailsService
- Roles: ADMIN (full access), MEMBER (self-service), STAFF (Sprint 3)
- Multi-tenancy: Hibernate
@Filteractivated per-request via AOP aspect - Refresh tokens: SHA-256 hashed (Spring Security 7 enforces BCrypt 72-byte limit)
- Token rotation on refresh — old tokens invalidated
Sprint History
| Sprint | Focus | Status |
|---|---|---|
| 1 | Domain entities, ComplianceService, 25 tests | ✅ Done |
| 2 | REST API, Spring Security, JWT, OpenAPI, integration tests | ✅ Done |
| 3 | Member portal, STAFF role, real-time notifications | 📋 Planned |
License
Private — Patrick Plate
Description
Languages
Java
61%
TypeScript
38.1%
JavaScript
0.4%
CSS
0.3%
Shell
0.2%