548 lines
19 KiB
Markdown
548 lines
19 KiB
Markdown
# 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*
|