docs(cannamanage): update wiki pages and sprint plans + brand pipeline doc
This commit is contained in:
@@ -0,0 +1,547 @@
|
||||
# CannaManage — Sprint 1 Implementation Plan
|
||||
|
||||
**Sprint:** 1 — Foundation
|
||||
**Phase:** Phase 1 (Weeks 1–8 of Phase 0 Foundation)
|
||||
**Author:** Lumen (architect mode), 2026-04-10
|
||||
**Status:** Ready for Patrick's approval
|
||||
|
||||
---
|
||||
|
||||
## Sprint Goal
|
||||
|
||||
> **"Get the compliance engine running and fully tested — with zero production code and zero API yet."**
|
||||
|
||||
Sprint 1 produces a compilable, testable Maven multi-module project with:
|
||||
- All core JPA entities modelled
|
||||
- Flyway V1 baseline migration SQL
|
||||
- `ComplianceService` implemented with 100% unit test coverage (TC-001 → TC-010)
|
||||
- A working local dev environment (Docker Compose: PostgreSQL + app)
|
||||
|
||||
No UI, no REST API, no Stripe in Sprint 1. The compliance engine is the legal heart of the product — validate it first.
|
||||
|
||||
---
|
||||
|
||||
## Deliverables
|
||||
|
||||
| # | Deliverable | Definition of Done |
|
||||
|---|------------|-------------------|
|
||||
| D1 | Maven multi-module project scaffold | `./mvnw clean verify` passes with no test failures |
|
||||
| D2 | `cannamanage-domain` module | All 8 JPA entities compile; `AbstractTenantEntity` wired |
|
||||
| D3 | Flyway `V1__initial_schema.sql` | Migration applies cleanly against PostgreSQL 16 |
|
||||
| D4 | `ComplianceService` | All 5 business methods implemented |
|
||||
| D5 | Unit test suite TC-001 → TC-010 | JaCoCo reports 100% line + branch coverage on `ComplianceService` |
|
||||
| D6 | Local dev `docker-compose.yml` | `docker compose up db` starts PostgreSQL; app connects cleanly |
|
||||
|
||||
---
|
||||
|
||||
## 1. Maven Multi-Module Structure
|
||||
|
||||
```
|
||||
cannamanage/ ← root POM (parent)
|
||||
├── pom.xml ← parent POM (BOM: Spring Boot 3.x, Java 21)
|
||||
│
|
||||
├── cannamanage-domain/ ← JPA entities, enums, constants
|
||||
│ └── src/main/java/de/cannamanage/domain/
|
||||
│ ├── entity/ ← JPA entity classes
|
||||
│ ├── enums/ ← MemberStatus, BatchStatus, etc.
|
||||
│ └── constants/
|
||||
│ └── ComplianceConstants.java
|
||||
│
|
||||
├── cannamanage-service/ ← Business logic, services (TESTED HERE)
|
||||
│ └── src/
|
||||
│ ├── main/java/de/cannamanage/service/
|
||||
│ │ ├── ComplianceService.java
|
||||
│ │ ├── dto/ ← QuotaStatus, ComplianceCheckResult, etc.
|
||||
│ │ └── exception/ ← QuotaExceededException, MemberIneligibleException
|
||||
│ └── test/java/de/cannamanage/service/
|
||||
│ └── ComplianceServiceTest.java ← TC-001 to TC-010
|
||||
│
|
||||
├── cannamanage-api/ ← Spring Boot app entry point (REST controllers — Sprint 2)
|
||||
│ └── src/main/java/de/cannamanage/api/
|
||||
│ └── CannaManageApplication.java
|
||||
│
|
||||
└── docker-compose.yml ← Local dev: PostgreSQL 16
|
||||
```
|
||||
|
||||
### Parent POM key dependencies (BOM managed)
|
||||
|
||||
```xml
|
||||
<!-- Spring Boot 3.3.x parent -->
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.4</version>
|
||||
</parent>
|
||||
|
||||
<!-- Modules -->
|
||||
<modules>
|
||||
<module>cannamanage-domain</module>
|
||||
<module>cannamanage-service</module>
|
||||
<module>cannamanage-api</module>
|
||||
</modules>
|
||||
|
||||
<!-- Key managed versions -->
|
||||
<!-- Java 21, Hibernate 6.x (via Spring Boot BOM), Flyway 9.x -->
|
||||
<!-- JJWT 0.12.x (Sprint 2), iText 7 (Sprint 3), Stripe 25.x (Sprint 4) -->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. `cannamanage-domain` — JPA Entities
|
||||
|
||||
### 2.1 `AbstractTenantEntity` (base class for all entities)
|
||||
|
||||
```java
|
||||
// de.cannamanage.domain.entity.AbstractTenantEntity
|
||||
@MappedSuperclass
|
||||
@FilterDef(
|
||||
name = "tenantFilter",
|
||||
parameters = @ParamDef(name = "tenantId", type = UUID.class)
|
||||
)
|
||||
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
|
||||
public abstract class AbstractTenantEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@Column(name = "tenant_id", nullable = false, updatable = false)
|
||||
private UUID tenantId;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
this.tenantId = TenantContext.getCurrentTenant(); // ThreadLocal
|
||||
this.createdAt = Instant.now();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Entities to implement (Sprint 1)
|
||||
|
||||
| Entity | Key fields | Notes |
|
||||
|--------|-----------|-------|
|
||||
| `Club` | id, name, licenseNumber, maxMembers, status | Root tenant aggregate |
|
||||
| `Member` | id, clubId, firstName, lastName, email, dob, membershipNumber, status, isUnder21 | `isUnder21` derived from DOB |
|
||||
| `Strain` | id, name, thcPercentage, cbdPercentage | Immutable once created |
|
||||
| `Batch` | id, strainId, quantityGrams, harvestDate, batchCode, status, contaminationFlag | status: AVAILABLE → EXHAUSTED / RECALLED |
|
||||
| `Distribution` | id, memberId, batchId, quantityGrams, distributedAt, recordedBy, notes | `@Column(updatable=false)` on all fields — immutable |
|
||||
| `MonthlyQuota` | id, memberId, year, month, totalDistributed, maxAllowed, version | `@Version` for optimistic lock |
|
||||
| `StockMovement` | id, batchId, movementType, quantityGrams, reason, createdAt | Audit journal |
|
||||
| `User` | id, memberId, email, passwordHash, role, lastLogin, active, refreshTokenHash | Login identity |
|
||||
|
||||
### 2.3 `ComplianceConstants.java`
|
||||
|
||||
```java
|
||||
// de.cannamanage.domain.constants.ComplianceConstants
|
||||
public final class ComplianceConstants {
|
||||
|
||||
// CanG §19(2) — adult limits
|
||||
public static final BigDecimal ADULT_DAILY_LIMIT_GRAMS = new BigDecimal("25.0");
|
||||
public static final BigDecimal ADULT_MONTHLY_LIMIT_GRAMS = new BigDecimal("50.0");
|
||||
|
||||
// CanG §19(3) — under-21 limits
|
||||
public static final BigDecimal UNDER21_MONTHLY_LIMIT_GRAMS = new BigDecimal("30.0");
|
||||
|
||||
// CanG §19(4) — under-21 THC cap
|
||||
public static final BigDecimal UNDER21_MAX_THC_PERCENTAGE = new BigDecimal("10.0");
|
||||
|
||||
// Minimum membership age
|
||||
public static final int MINIMUM_MEMBERSHIP_AGE = 18;
|
||||
|
||||
// Under-21 threshold
|
||||
public static final int UNDER21_THRESHOLD_AGE = 21;
|
||||
|
||||
private ComplianceConstants() {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Flyway `V1__initial_schema.sql`
|
||||
|
||||
Location: `cannamanage-api/src/main/resources/db/migration/V1__initial_schema.sql`
|
||||
|
||||
```sql
|
||||
-- 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 — no UPDATE/DELETE via app)
|
||||
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, -- IN, OUT, RECALL, ADJUSTMENT
|
||||
quantity_grams NUMERIC(10,2) NOT NULL,
|
||||
reason TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Users (login identities)
|
||||
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)
|
||||
);
|
||||
|
||||
-- Indexes for common query patterns
|
||||
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);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. `ComplianceService` — Implementation Spec
|
||||
|
||||
Package: `de.cannamanage.service`
|
||||
|
||||
### 4.1 Dependencies (injected via constructor)
|
||||
|
||||
```java
|
||||
@Service
|
||||
@Transactional
|
||||
public class ComplianceService {
|
||||
|
||||
private final MemberRepository memberRepository;
|
||||
private final DistributionRepository distributionRepository;
|
||||
private final BatchRepository batchRepository;
|
||||
private final MonthlyQuotaRepository monthlyQuotaRepository;
|
||||
private final StrainRepository strainRepository;
|
||||
|
||||
// constructor injection...
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Method: `checkDistributionAllowed(UUID memberId, UUID batchId, BigDecimal quantityGrams)`
|
||||
|
||||
**Algorithm (sequential checks, fail-fast):**
|
||||
|
||||
```
|
||||
1. Load Member — throw MemberNotFoundException if not found
|
||||
2. CHECK: member.status == ACTIVE → else throw QuotaExceededException(MEMBER_INACTIVE)
|
||||
3. Load Batch → CHECK: batch.status == AVAILABLE → else throw BatchUnavailableException
|
||||
4. Load Strain via batch.strainId
|
||||
5. IF member.isUnder21 AND strain.thcPercentage > UNDER21_MAX_THC_PERCENTAGE
|
||||
→ throw QuotaExceededException(HIGH_THC_RESTRICTED_UNDER_21)
|
||||
6. Calculate todayDistributed = SUM(distributions.quantityGrams WHERE memberId AND date=TODAY)
|
||||
CHECK: todayDistributed + quantityGrams > ADULT_DAILY_LIMIT_GRAMS
|
||||
→ throw QuotaExceededException(QUOTA_EXCEEDED_DAILY)
|
||||
7. Get or create MonthlyQuota for (memberId, currentYear, currentMonth)
|
||||
SET maxAllowed = isUnder21 ? UNDER21_MONTHLY_LIMIT_GRAMS : ADULT_MONTHLY_LIMIT_GRAMS
|
||||
CHECK: quota.totalDistributed + quantityGrams > quota.maxAllowed
|
||||
→ throw QuotaExceededException(QUOTA_EXCEEDED_MONTHLY)
|
||||
8. Return ComplianceCheckResult(allowed=true, remainingDaily, remainingMonthly)
|
||||
```
|
||||
|
||||
### 4.3 `QuotaExceededException` — error codes
|
||||
|
||||
```java
|
||||
public enum QuotaViolationCode {
|
||||
MEMBER_INACTIVE,
|
||||
QUOTA_EXCEEDED_DAILY,
|
||||
QUOTA_EXCEEDED_MONTHLY,
|
||||
HIGH_THC_RESTRICTED_UNDER_21,
|
||||
BATCH_UNAVAILABLE
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 DTOs
|
||||
|
||||
```java
|
||||
// ComplianceCheckResult
|
||||
record ComplianceCheckResult(
|
||||
boolean allowed,
|
||||
BigDecimal remainingDaily,
|
||||
BigDecimal remainingMonthly,
|
||||
boolean isUnder21
|
||||
) {}
|
||||
|
||||
// QuotaStatus
|
||||
record QuotaStatus(
|
||||
BigDecimal totalAllowed,
|
||||
BigDecimal totalUsed,
|
||||
BigDecimal remaining,
|
||||
boolean isUnder21,
|
||||
int year,
|
||||
int month
|
||||
) {}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Unit Test Suite (TC-001 → TC-010)
|
||||
|
||||
**Class:** `ComplianceServiceTest` in `cannamanage-service`
|
||||
**Coverage requirement:** 100% line + branch on `ComplianceService`
|
||||
**Tools:** JUnit 5, Mockito 5, AssertJ
|
||||
|
||||
### Test structure
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ComplianceServiceTest {
|
||||
|
||||
@Mock MemberRepository memberRepository;
|
||||
@Mock DistributionRepository distributionRepository;
|
||||
@Mock BatchRepository batchRepository;
|
||||
@Mock MonthlyQuotaRepository monthlyQuotaRepository;
|
||||
@Mock StrainRepository strainRepository;
|
||||
|
||||
@InjectMocks ComplianceService complianceService;
|
||||
|
||||
// Test fixtures
|
||||
private static final UUID ADULT_MEMBER_ID = UUID.randomUUID();
|
||||
private static final UUID UNDER21_MEMBER_ID = UUID.randomUUID();
|
||||
private static final UUID BATCH_ID = UUID.randomUUID();
|
||||
private static final UUID HIGH_THC_STRAIN_ID = UUID.randomUUID();
|
||||
|
||||
// TC-001: adult at monthly limit → throws QUOTA_EXCEEDED_MONTHLY
|
||||
// TC-002: under-21 at monthly limit → throws QUOTA_EXCEEDED_MONTHLY
|
||||
// TC-003: adult at daily limit → throws QUOTA_EXCEEDED_DAILY
|
||||
// TC-004: under-21 + high THC strain → throws HIGH_THC_RESTRICTED_UNDER_21
|
||||
// TC-005: adult at 49g requesting 2g → throws QUOTA_EXCEEDED_MONTHLY
|
||||
// TC-006: adult at 0g requesting 25g → allowed, remaining=0
|
||||
// TC-007: adult at 24.9g requesting 0.1g → allowed, remainingDaily=0
|
||||
// TC-008: adult at 24.9g requesting 0.2g → throws QUOTA_EXCEEDED_DAILY
|
||||
// TC-009: SUSPENDED member → throws MEMBER_INACTIVE
|
||||
// TC-010: EXPELLED member → throws MEMBER_INACTIVE
|
||||
}
|
||||
```
|
||||
|
||||
### Key mock patterns
|
||||
|
||||
```java
|
||||
// TC-001 example mock setup
|
||||
Member adultMember = new Member();
|
||||
adultMember.setId(ADULT_MEMBER_ID);
|
||||
adultMember.setUnder21(false);
|
||||
adultMember.setStatus(MemberStatus.ACTIVE);
|
||||
|
||||
when(memberRepository.findById(ADULT_MEMBER_ID)).thenReturn(Optional.of(adultMember));
|
||||
|
||||
MonthlyQuota quota = new MonthlyQuota();
|
||||
quota.setTotalDistributed(new BigDecimal("50.0"));
|
||||
quota.setMaxAllowed(ComplianceConstants.ADULT_MONTHLY_LIMIT_GRAMS);
|
||||
when(monthlyQuotaRepository.findByMemberIdAndYearAndMonth(any(), anyInt(), anyInt()))
|
||||
.thenReturn(Optional.of(quota));
|
||||
|
||||
// Assert
|
||||
assertThatThrownBy(() -> complianceService.checkDistributionAllowed(ADULT_MEMBER_ID, BATCH_ID, new BigDecimal("1.0")))
|
||||
.isInstanceOf(QuotaExceededException.class)
|
||||
.extracting("code")
|
||||
.isEqualTo(QuotaViolationCode.QUOTA_EXCEEDED_MONTHLY);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Local Dev Docker Compose
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml (root of cannamanage project)
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
container_name: cannamanage-db-local
|
||||
environment:
|
||||
POSTGRES_DB: cannamanage
|
||||
POSTGRES_USER: cannamanage
|
||||
POSTGRES_PASSWORD: dev_password_change_in_prod
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgdata_local:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U cannamanage"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
pgdata_local:
|
||||
```
|
||||
|
||||
```properties
|
||||
# cannamanage-api/src/main/resources/application-local.properties
|
||||
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 # Flyway owns schema
|
||||
spring.flyway.enabled=true
|
||||
spring.flyway.locations=classpath:db/migration
|
||||
logging.level.de.cannamanage=DEBUG
|
||||
```
|
||||
|
||||
**Run locally:**
|
||||
```bash
|
||||
git clone http://192.168.188.119:30008/pplate/cannamanage.git
|
||||
cd cannamanage
|
||||
docker compose up db -d
|
||||
./mvnw spring-boot:run -pl cannamanage-api -Dspring.profiles.active=local
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Sprint 1 Gitea Issues (already created: #1–#10)
|
||||
|
||||
Based on the Sprint 1 board at `http://truenas.local:30008/pplate/cannamanage/wiki/Sprint-1-Board`, these map to:
|
||||
|
||||
| Gitea Issue | Sprint 1 Deliverable |
|
||||
|-------------|---------------------|
|
||||
| #1 | Maven multi-module project scaffold |
|
||||
| #2 | `AbstractTenantEntity` + `TenantContext` ThreadLocal |
|
||||
| #3 | All 8 JPA entities in `cannamanage-domain` |
|
||||
| #4 | `ComplianceConstants.java` |
|
||||
| #5 | Flyway `V1__initial_schema.sql` |
|
||||
| #6 | `ComplianceService` implementation |
|
||||
| #7 | Unit tests TC-001 → TC-010 (100% coverage) |
|
||||
| #8 | `docker-compose.yml` local dev |
|
||||
| #9 | `application-local.properties` |
|
||||
| #10 | JaCoCo coverage gate in parent POM |
|
||||
|
||||
---
|
||||
|
||||
## 8. Out of Scope — Sprint 1
|
||||
|
||||
These are **explicitly deferred** to Sprint 2+:
|
||||
|
||||
- REST API controllers (`AuthController`, `MemberController`, `DistributionController`)
|
||||
- Spring Security + JWT filter chain
|
||||
- PrimeFaces JSF frontend
|
||||
- Stripe billing integration
|
||||
- iText 7 PDF reports
|
||||
- Email notifications
|
||||
- Testcontainers integration tests (TC-018 → TC-022)
|
||||
- Hetzner deployment / CI pipeline
|
||||
- `MemberService` (TC-011 → TC-015)
|
||||
|
||||
---
|
||||
|
||||
## 9. Definition of Done — Sprint 1
|
||||
|
||||
- [ ] `./mvnw clean verify` exits 0 on clean checkout
|
||||
- [ ] `./mvnw test -pl cannamanage-service` reports 10/10 tests passing
|
||||
- [ ] JaCoCo report shows `ComplianceService` at 100% line + branch coverage
|
||||
- [ ] `docker compose up db -d` starts PostgreSQL; Flyway V1 migration applies cleanly
|
||||
- [ ] No `TODO` comments in production code paths
|
||||
- [ ] All 8 JPA entities have `@Column(nullable = false)` on required fields
|
||||
- [ ] `ComplianceConstants.java` contains all CanG limits as `public static final BigDecimal`
|
||||
- [ ] `AbstractTenantEntity.tenantId` is `@Column(updatable = false)`
|
||||
- [ ] Code pushed to `http://192.168.188.119:30008/pplate/cannamanage` main branch
|
||||
|
||||
---
|
||||
|
||||
## 10. Recommended Implementation Order
|
||||
|
||||
```
|
||||
Day 1: Root pom.xml + module scaffolds → ./mvnw compile passes
|
||||
Day 2: AbstractTenantEntity + TenantContext + ComplianceConstants
|
||||
Day 3: All 8 JPA entities (compile-time only, no DB yet)
|
||||
Day 4: Flyway V1 SQL + docker-compose.yml → migration applies
|
||||
Day 5: ComplianceService skeleton (method signatures + DTOs)
|
||||
Day 6: TC-001 → TC-005 (the exception/blocking cases)
|
||||
Day 7: TC-006 → TC-010 (boundary + happy path cases)
|
||||
Day 8: JaCoCo gate; clean up; push to Gitea
|
||||
```
|
||||
|
||||
*Assuming ~2–3 hours of evening/weekend coding per day as side project.*
|
||||
|
||||
---
|
||||
|
||||
*Plan created: 2026-04-10 | Sprint start: when Patrick approves | Estimated coding sessions: 8 × 2-3h*
|
||||
@@ -12,17 +12,35 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||
### Added
|
||||
|
||||
- Complete project documentation suite (10 documents, ~25,000 words)
|
||||
- System architecture design: 8 JPA entities, Maven multi-module structure, multi-tenancy via shared schema + Hibernate filter
|
||||
- System architecture design: 8 JPA entities, Maven multi-module structure
|
||||
- REST API specification: 7 controllers, 30+ endpoints, full request/response schemas with error codes
|
||||
- Compliance engine design: `ComplianceService` enforcing CanG §§19–22 limits (25g/day, 50g/month adults; 30g/month under-21; ≤10% THC under-21)
|
||||
- `ComplianceConstants.java` design: all legal thresholds as named constants to prevent magic numbers in compliance logic
|
||||
- UI wireframes for 6 screens: Admin Dashboard, Distribution Recording Form, Member List, Member Quota View, Stock Management, Compliance Report
|
||||
- 5 AI-generated UI mockup images (FLUX.1-schnell via ComfyUI, 1024×512)
|
||||
- Test plan with 26 test cases covering ComplianceService (TC-001–010), MemberService (TC-011–015), tenant isolation (TC-016–017), and integration tests (TC-018–026)
|
||||
- Deployment guide for Hetzner VPS: Docker Compose setup, Nginx reverse proxy, SSL with Let's Encrypt, CI/CD via Gitea Actions, database backup strategy
|
||||
- Coding standards: Java 21 conventions, JPA patterns, multi-tenancy rules, immutable distribution records
|
||||
- Flowcharts: distribution flow (5-step), member lifecycle (state machine), billing provisioning flow (Mermaid diagrams)
|
||||
- README with full documentation index, tech stack table, pricing tiers, legal notice
|
||||
- **[2026-04-06]** Staff member management: `ROLE_STAFF` with configurable per-account permission grants (US-026); admin controls which data staff can access (DSGVO least-privilege). 8 defined permissions, 3 pre-created role templates (Ausgabe, Lager, Vorstand). Core feature from Phase 0.
|
||||
- **[2026-04-06]** Grow Calendar: US-027 added as Could Have (v2) — cultivation diary per grow cycle, linked to batch harvest, optional photo attachments, admin-controlled access via `MANAGE_GROW_CALENDAR` permission
|
||||
- **[2026-04-06]** Staff wireframe (Screen 7) added to `06-Wireframes.md` with full ASCII wireframe, component table (TanStack Table, shadcn/ui Checkbox, Select), and DSGVO design rationale
|
||||
- **[2026-04-06]** Staff routes added to Navigation IA: `/admin/staff`, `/admin/staff/new`, `/admin/staff/{id}`, `/staff/dashboard`
|
||||
- **[2026-04-06]** TrueNAS.local Gitea Actions self-hosted runner documented in `09-Deployment.md` as the CI/CD build environment; Hetzner = production release target
|
||||
|
||||
### Changed
|
||||
|
||||
- **[2026-04-06]** `03-Architecture.md` — **Multi-tenancy model changed from shared-schema to schema-per-tenant.** Decision rationale: hard DB-level isolation (not application-layer), clean DSGVO deletion (`DROP SCHEMA`), no cross-tenant index bloat, easier future isolation. `tenant_id` columns on every entity removed; schema routing via `TenantRoutingDataSource` replaces Hibernate `@Filter`.
|
||||
- **[2026-04-06]** `03-Architecture.md` — **Frontend changed from PrimeFaces/JSF to React/Vite SPA.** Rationale: JSF server-side lifecycle is a poor fit for a REST API backend; PrimeFaces creates a hiring bottleneck; React is mobile-friendly from day 1. Component library: shadcn/ui (Radix UI + Tailwind CSS) + TanStack Table v8.
|
||||
- **[2026-04-06]** `03-Architecture.md` — `ROLE_STAFF` added with configurable `StaffPermission` enum; pre-created templates documented. Staff noted as core feature, not add-on.
|
||||
- **[2026-04-06]** `06-Wireframes.md` — All component tables updated from PrimeFaces (`p:dataTable`, `p:commandButton`) to React/Tailwind equivalents (TanStack Table, shadcn/ui). Responsive Design section rewritten for Tailwind breakpoints.
|
||||
- **[2026-04-06]** `09-Deployment.md` — CI/CD section rewritten: `runs-on: ubuntu-latest` → `runs-on: self-hosted` (TrueNAS.local). Gitea Actions runner setup instructions added. Infrastructure diagram updated to show Dev → Gitea → TrueNAS build → Hetzner release flow.
|
||||
- **[2026-04-06]** `0.1.0` CHANGELOG entry corrected: removed "shared schema" as final architecture decision (superseded by schema-per-tenant); removed PrimeFaces as frontend (superseded by React/Vite)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **[2026-04-06]** `04-Flowcharts.md` — Mermaid parse error in Flow 4 (line 146): `[Generate empty report\nwith zero totals\n(still valid compliance submission)]` — parenthesis after newline was parsed as stadium-shape node start. Fixed by wrapping node text in double quotes.
|
||||
- **[2026-04-06]** `04-Flowcharts.md` — Mermaid parse error in Flow 5 (line 177): `[❌ Invalid credentials\n(generic — do not reveal\nwhether email exists)]` — same root cause, same fix.
|
||||
|
||||
---
|
||||
|
||||
@@ -33,9 +51,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||
- `STRATEGY.md` — initial project vision and feasibility assessment
|
||||
- Legal analysis confirming CanG compliance viability for B2B SaaS model (no public advertising, no club discovery, B2B-only)
|
||||
- Market analysis: ~3,000 registered clubs in Germany, TAM estimated at €2.85M ARR
|
||||
- Tech stack selection rationale: Spring Boot 3.x + PrimeFaces JSF (MVP) → Next.js v2; PostgreSQL + Flyway; iText 7 PDF; Stripe billing
|
||||
- Multi-tenancy architectural decision: shared schema with `tenant_id` column (chosen over schema-per-tenant for lower operational overhead at MVP scale)
|
||||
- Tech stack selection rationale: Spring Boot 3.x + React/Vite SPA (MVP) → Next.js v2; PostgreSQL + Flyway; iText 7 PDF; Stripe billing
|
||||
- Multi-tenancy architectural decision: schema-per-tenant (each club gets isolated PostgreSQL schema; platform registry in `public` schema)
|
||||
- Pricing model: 4 tiers (Starter €29, Growth €59, Professional €99, Enterprise €199/month)
|
||||
- Deployment guide for Hetzner VPS (production release): Docker Compose, Nginx + Let's Encrypt, Gitea Actions CI/CD via TrueNAS.local self-hosted runner, daily PostgreSQL backup strategy
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user