feat(sprint-1): CannaManage foundation — compliance engine, JPA entities, tests TC-001→TC-025

- Maven multi-module project (parent + domain + service + api)
- AbstractTenantEntity with Hibernate @Filter for multi-tenancy (explicit getters/setters, Java 25 compatible)
- TenantContext ThreadLocal for request-scoped tenant isolation
- 8 JPA entities: Club, Member, Strain, Batch, Distribution, MonthlyQuota, StockMovement, User
- ComplianceConstants with CanG §19 limits (25g/day adult, 50g/month adult, 30g/month under-21, 10% THC cap)
- ComplianceService: checkDistributionAllowed() with fail-fast sequential CanG checks
- Unit tests TC-001→TC-025: 25/25 passing, 100% line+branch coverage on ComplianceService (JaCoCo 0.8.13)
- Flyway V1__initial_schema.sql: all 8 tables + indexes
- docker-compose.yml: PostgreSQL 16 local dev
- application-local.properties: local profile configuration

Closes #1 #2 #3 #4 #5 #6 #7 #8 #9 #10
This commit is contained in:
Patrick Plate
2026-04-12 20:30:12 +02:00
commit fa1eaf64e0
42 changed files with 2344 additions and 0 deletions
@@ -0,0 +1,21 @@
package de.cannamanage.api;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
/**
* CannaManage Spring Boot application entry point.
* REST controllers are deferred to Sprint 2.
* Sprint 1 focus: compliance engine validation only.
*/
@SpringBootApplication(scanBasePackages = "de.cannamanage")
@EntityScan(basePackages = "de.cannamanage.domain.entity")
@EnableJpaRepositories(basePackages = "de.cannamanage.service.repository")
public class CannaManageApplication {
public static void main(String[] args) {
SpringApplication.run(CannaManageApplication.class, args);
}
}
@@ -0,0 +1,8 @@
spring.datasource.url=jdbc:postgresql://localhost:5432/cannamanage
spring.datasource.username=cannamanage
spring.datasource.password=dev_password_change_in_prod
spring.jpa.hibernate.ddl-auto=validate
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
logging.level.de.cannamanage=DEBUG
logging.level.org.flywaydb=INFO
@@ -0,0 +1,4 @@
spring.application.name=cannamanage
# Default profile — override with -Dspring.profiles.active=local
spring.jpa.hibernate.ddl-auto=validate
spring.flyway.enabled=false
@@ -0,0 +1,120 @@
-- CannaManage V1 Initial Schema
-- Implements all tables required for CanG §19 compliance tracking
-- Clubs (root of tenant hierarchy)
CREATE TABLE clubs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
name VARCHAR(255) NOT NULL,
address TEXT,
license_number VARCHAR(100) NOT NULL UNIQUE,
max_members INT NOT NULL DEFAULT 500,
status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Members
CREATE TABLE members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
club_id UUID NOT NULL REFERENCES clubs(id),
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL,
date_of_birth DATE NOT NULL,
membership_date DATE NOT NULL DEFAULT CURRENT_DATE,
membership_number VARCHAR(50) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE',
is_under_21 BOOLEAN NOT NULL DEFAULT FALSE,
prevention_officer BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(email, tenant_id),
UNIQUE(membership_number, tenant_id)
);
-- Strains
CREATE TABLE strains (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
name VARCHAR(255) NOT NULL,
thc_percentage NUMERIC(5,2) NOT NULL,
cbd_percentage NUMERIC(5,2) NOT NULL DEFAULT 0.00,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Batches
CREATE TABLE batches (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
strain_id UUID NOT NULL REFERENCES strains(id),
quantity_grams NUMERIC(10,2) NOT NULL,
harvest_date DATE,
batch_code VARCHAR(100) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'AVAILABLE',
contamination_flag BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(batch_code, tenant_id)
);
-- Distributions (immutable — append-only for CanG §26 compliance)
CREATE TABLE distributions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
member_id UUID NOT NULL REFERENCES members(id),
batch_id UUID NOT NULL REFERENCES batches(id),
quantity_grams NUMERIC(10,2) NOT NULL,
distributed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
recorded_by UUID NOT NULL REFERENCES members(id),
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Monthly quotas (one row per member per calendar month)
CREATE TABLE monthly_quotas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
member_id UUID NOT NULL REFERENCES members(id),
year INT NOT NULL,
month INT NOT NULL CHECK (month >= 1 AND month <= 12),
total_distributed NUMERIC(10,2) NOT NULL DEFAULT 0.00,
max_allowed NUMERIC(10,2) NOT NULL,
version BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(member_id, year, month)
);
-- Stock movements (audit journal)
CREATE TABLE stock_movements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
batch_id UUID NOT NULL REFERENCES batches(id),
movement_type VARCHAR(50) NOT NULL,
quantity_grams NUMERIC(10,2) NOT NULL,
reason TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Users (login identities — Sprint 2 auth)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
member_id UUID REFERENCES members(id),
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'ROLE_MEMBER',
last_login TIMESTAMPTZ,
active BOOLEAN NOT NULL DEFAULT TRUE,
refresh_token_hash VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(email, tenant_id)
);
-- Performance indexes
CREATE INDEX idx_members_club_id ON members(club_id);
CREATE INDEX idx_members_tenant_id ON members(tenant_id);
CREATE INDEX idx_distributions_member_id ON distributions(member_id);
CREATE INDEX idx_distributions_tenant_id ON distributions(tenant_id);
CREATE INDEX idx_distributions_distributed_at ON distributions(distributed_at);
CREATE INDEX idx_monthly_quotas_member_month ON monthly_quotas(member_id, year, month);
CREATE INDEX idx_batches_tenant_status ON batches(tenant_id, status);