# CannaManage — Coding Standards & Git Strategy **Phase 4a | Document 7 of 7** **Date:** 2026-04-06 **Stack:** Java 21 · Spring Boot 3.x · JPA/Hibernate · PrimeFaces JSF · PostgreSQL --- ## Table of Contents 1. [Project Structure](#1-project-structure) 2. [Java Coding Standards](#2-java-coding-standards) 3. [Compliance Code Rules](#3-compliance-code-rules) 4. [Git Strategy](#4-git-strategy) 5. [Testing Standards](#5-testing-standards) 6. [Code Review Checklist](#6-code-review-checklist) 7. [Security Standards](#7-security-standards) 8. [Environment Configuration](#8-environment-configuration) --- ## 1. Project Structure ### Maven Multi-Module Layout ``` cannamanage/ ├── pom.xml # Parent POM — dependency management, versions ├── cannamanage-domain/ # JPA entities, enums, exceptions, value objects │ └── src/main/java/de/cannamanage/domain/ │ ├── member/ # Member, MemberStatus, MembershipType │ ├── distribution/ # Distribution, DistributionRecord │ ├── stock/ # Strain, Batch, BatchStatus │ ├── compliance/ # ComplianceConstants, QuotaExceededException │ └── common/ # AbstractTenantEntity, TenantId │ ├── cannamanage-service/ # Business logic, compliance engine, repositories │ └── src/main/java/de/cannamanage/service/ │ ├── member/ # MemberService, MemberRepository │ ├── distribution/ # DistributionService, DistributionRepository │ ├── stock/ # StockService, BatchRepository │ ├── compliance/ # ComplianceService, QuotaCalculator │ └── report/ # ReportDataService │ ├── cannamanage-web/ # PrimeFaces JSF backing beans + XHTML views │ └── src/main/ │ ├── java/de/cannamanage/web/ │ │ ├── admin/ # AdminDashboardBean, DistributionFormBean │ │ ├── member/ # MemberDashboardBean │ │ └── common/ # AuthBean, NavigationBean │ └── webapp/ │ ├── admin/ # dashboard.xhtml, distribution-form.xhtml, stock.xhtml │ ├── member/ # dashboard.xhtml, stock.xhtml │ └── WEB-INF/ # faces-config.xml, web.xml │ ├── cannamanage-api/ # REST controllers (Spring Boot MVC) │ └── src/main/java/de/cannamanage/api/ │ ├── member/ # MemberController, MemberDto │ ├── distribution/ # DistributionController, DistributionDto │ ├── stock/ # StockController, BatchDto │ ├── auth/ # AuthController, JwtFilter │ └── report/ # ReportController │ └── cannamanage-report/ # iText 7 PDF generation └── src/main/java/de/cannamanage/report/ ├── monthly/ # MonthlyComplianceReport ├── recall/ # BatchRecallReport └── export/ # MemberCsvExporter ``` ### Module Dependencies ``` cannamanage-domain (no deps on other modules) ↑ cannamanage-service (depends on domain) ↑ cannamanage-api (depends on service, domain) cannamanage-web (depends on service, domain) cannamanage-report (depends on service, domain) ``` `cannamanage-api` and `cannamanage-web` are siblings — they do not depend on each other. The web module is the PrimeFaces JSF frontend (MVP); the API module provides the REST layer (future mobile / integration use). --- ## 2. Java Coding Standards ### Language Version Java 21. All modern language features are permitted and preferred: | Feature | Use Case | Example | |---|---|---| | Records | DTOs, value objects, query results | `record MemberSummary(UUID id, String name, BigDecimal quotaUsed)` | | Sealed classes | Result types, compliance outcomes | `sealed interface QuotaResult permits QuotaOk, QuotaWarning, QuotaExceeded` | | Text blocks | JPQL, SQL in tests, JSON fixtures | `String jpql = """ SELECT m FROM Member m WHERE... """` | | Pattern matching `instanceof` | Type checks in services | `if (result instanceof QuotaExceeded e) { ... }` | | Switch expressions | Status mapping, report routing | `yield` syntax preferred | ### Package Structure Pattern: `de.cannamanage.[module].[layer]` ``` de.cannamanage.domain.member # Member entity de.cannamanage.domain.compliance # ComplianceConstants, exceptions de.cannamanage.service.distribution # DistributionService de.cannamanage.api.stock # StockController, BatchDto de.cannamanage.web.admin # DistributionFormBean de.cannamanage.report.monthly # MonthlyComplianceReport ``` ### Class Naming Conventions | Type | Pattern | Example | |---|---|---| | JPA Entity | `{Domain}` | `Member`, `Distribution`, `Batch` | | Spring Service | `{Domain}Service` | `MemberService`, `ComplianceService` | | Repository | `{Domain}Repository` | `DistributionRepository` | | REST Controller | `{Domain}Controller` | `StockController` | | JSF Backing Bean | `{Screen}Bean` | `DistributionFormBean`, `AdminDashboardBean` | | DTO (request) | `{Domain}Request` | `CreateDistributionRequest` | | DTO (response) | `{Domain}Response` / `{Domain}Dto` | `MemberSummaryDto` | | Exception | `{Condition}Exception` | `QuotaExceededException`, `BatchRecalledException` | | Enum | `{Domain}Status` / `{Domain}Type` | `BatchStatus`, `MembershipType` | | Constants class | `{Domain}Constants` | `ComplianceConstants` | ### Dependency Injection **Constructor injection only.** Field injection (`@Autowired` on fields) is prohibited. ```java // ✅ Correct @Service @RequiredArgsConstructor public class DistributionService { private final DistributionRepository distributionRepository; private final ComplianceService complianceService; private final MemberRepository memberRepository; } // ❌ Prohibited @Service public class DistributionService { @Autowired private DistributionRepository distributionRepository; } ``` Lombok `@RequiredArgsConstructor` is the preferred way to generate the constructor. ### Entity Base Class All `@Entity` classes must extend `AbstractTenantEntity`. No raw entities without tenant isolation. ```java // de.cannamanage.domain.common.AbstractTenantEntity @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public abstract class AbstractTenantEntity { @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; @Column(name = "tenant_id", nullable = false, updatable = false) private UUID tenantId; @CreatedDate @Column(name = "created_at", nullable = false, updatable = false) private Instant createdAt; @LastModifiedDate @Column(name = "updated_at", nullable = false) private Instant updatedAt; } // ✅ All entities extend this @Entity @Table(name = "members") @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class Member extends AbstractTenantEntity { // domain fields only — no id/tenantId/audit fields here } ``` ### Transaction Boundaries - `@Transactional` belongs on **service layer** methods only - Controllers and repositories must not declare `@Transactional` - Use `@Transactional(readOnly = true)` for query-only methods — improves performance with Hibernate's read-only session optimization ```java // ✅ Service layer — correct @Service @RequiredArgsConstructor public class MemberService { @Transactional(readOnly = true) public MemberSummaryDto getById(UUID memberId, UUID tenantId) { ... } @Transactional public void updateMemberStatus(UUID memberId, MemberStatus status, UUID tenantId) { ... } } // ❌ Controller — prohibited @RestController public class MemberController { @Transactional // Never here @GetMapping("/members/{id}") public MemberSummaryDto getMember(@PathVariable UUID id) { ... } } ``` ### Lombok Usage | Annotation | Allowed | Notes | |---|---|---| | `@Getter` | ✅ | On entities and DTOs | | `@Setter` | ✅ | Use sparingly on entities; prefer builder pattern | | `@Builder` | ✅ | On entities and DTOs | | `@RequiredArgsConstructor` | ✅ | Services, beans (for DI) | | `@NoArgsConstructor` | ✅ | JPA requires no-arg constructor | | `@AllArgsConstructor` | ✅ | With `@Builder` | | `@ToString` | ✅ | Exclude sensitive fields: `@ToString.Exclude` on `passwordHash` etc. | | `@EqualsAndHashCode` | ✅ | Entities: only on `id` field | | `@Data` | ❌ | **Prohibited on entities** — generates mutable setters for all fields, breaks JPA proxy patterns | | `@SneakyThrows` | ❌ | Never hide checked exceptions | ### Code Style - **Checkstyle config:** Google Java Style Guide (`checkstyle-google.xml` in parent POM) - **Indentation:** 4 spaces (no tabs) - **Line length:** 120 characters max - **No magic numbers** — use named constants or enums: ```java // ❌ Magic number if (member.getAge() < 21) { limit = 30; } // ✅ Named constant if (member.getAge() < ComplianceConstants.AGE_LIMIT_UNDER21) { limit = ComplianceConstants.MONTHLY_LIMIT_UNDER21_GRAMS; } ``` --- ## 3. Compliance Code Rules These rules apply exclusively to code that enforces CanG (Cannabisgesetz) distribution limits. Violations here carry legal risk. ### Compliance Constants All legal limits live in a single, centrally tested constants class. **Never hardcode these values inline.** ```java // de.cannamanage.domain.compliance.ComplianceConstants public final class ComplianceConstants { private ComplianceConstants() {} // no instantiation /** Maximum grams per single distribution for any member. */ public static final BigDecimal DAILY_LIMIT_GRAMS = new BigDecimal("25.0"); /** Monthly gram limit for adult members (age ≥ 21). */ public static final BigDecimal MONTHLY_LIMIT_ADULT_GRAMS = new BigDecimal("50.0"); /** Monthly gram limit for members under 21 years of age (CanG §10 Abs.1). */ public static final BigDecimal MONTHLY_LIMIT_UNDER21_GRAMS = new BigDecimal("30.0"); /** Age threshold below which the reduced monthly limit applies. */ public static final int AGE_LIMIT_UNDER21 = 21; /** Minimum age for club membership (CanG §15 Abs.1). */ public static final int MINIMUM_MEMBER_AGE = 18; } ``` ### ComplianceService Rules 1. `ComplianceService` methods **must always execute within a `@Transactional` boundary** — either by being called from a service method already in a transaction, or by declaring `@Transactional` themselves. The compliance check and the distribution record creation must be atomic. 2. Every public method in `ComplianceService` must have a corresponding test in `ComplianceServiceTest` that exercises its boundary conditions. 3. `ComplianceService` is the **only** class permitted to read `ComplianceConstants` limits and make pass/fail decisions. No other class performs limit arithmetic. ```java @Service @RequiredArgsConstructor public class ComplianceService { private final DistributionRepository distributionRepository; /** * Validates whether a distribution of the given weight is permitted for the member. * *
Checks the daily single-distribution limit and the member's monthly quota.
* Must be called inside an existing @Transactional boundary — the calling
* DistributionService is responsible for the transaction.
*
* @param memberId the member receiving the distribution
* @param tenantId the club's tenant identifier
* @param weightGrams the proposed distribution weight in grams
* @return QuotaOk if permitted; QuotaWarning if >80% used; QuotaExceeded if over limit
* @throws IllegalArgumentException if weightGrams exceeds DAILY_LIMIT_GRAMS
*/
public QuotaResult checkDistributionAllowed(UUID memberId, UUID tenantId, BigDecimal weightGrams) {
if (weightGrams.compareTo(ComplianceConstants.DAILY_LIMIT_GRAMS) > 0) {
throw new IllegalArgumentException(
"Single distribution exceeds daily limit of " + ComplianceConstants.DAILY_LIMIT_GRAMS + "g");
}
// ... monthly quota logic using ComplianceConstants
}
}
```
### Distribution Record Immutability
Once written, a `Distribution` record may never be modified (legal audit trail requirement). Enforce this at the JPA level:
```java
@Entity
@Table(name = "distributions")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Distribution extends AbstractTenantEntity {
@Column(name = "member_id", nullable = false, updatable = false)
private UUID memberId;
@Column(name = "batch_id", nullable = false, updatable = false)
private UUID batchId;
@Column(name = "weight_grams", nullable = false, updatable = false,
precision = 8, scale = 2)
private BigDecimal weightGrams;
@Column(name = "distributed_at", nullable = false, updatable = false)
private Instant distributedAt;
@Column(name = "recorded_by_admin_id", nullable = false, updatable = false)
private UUID recordedByAdminId;
// No setters — @Getter only, no @Setter
// updatable = false on ALL columns — Hibernate will reject any UPDATE attempt
}
```
### Compliance Test Coverage Requirement
`ComplianceServiceTest` must include at minimum:
| Test Method | What It Covers |
|---|---|
| `checkDistributionAllowed_givenWeightAt25g_shouldReturnQuotaOk` | Exactly at daily limit |
| `checkDistributionAllowed_givenWeightOver25g_shouldThrowIllegalArgument` | Daily limit exceeded |
| `checkDistributionAllowed_givenMemberAtMonthlyLimit_shouldReturnQuotaExceeded` | Adult at 50g |
| `checkDistributionAllowed_givenUnder21MemberAt30g_shouldReturnQuotaExceeded` | Under-21 at 30g |
| `checkDistributionAllowed_givenUnder21MemberAtAdultLimit_shouldReturnQuotaExceeded` | Under-21 must not reach 50g |
| `checkDistributionAllowed_givenMemberAt80Percent_shouldReturnQuotaWarning` | Warning threshold |
| `checkDistributionAllowed_givenMemberAt40g_shouldReturnQuotaOk` | Normal adult, within limit |
---
## 4. Git Strategy
### Branching Model — GitHub Flow (Solo Dev)
```
main ──────────────────────────────────────────────────────► (production-ready)
│ │ │
└─► feature/US-042─┘ └─► fix/member-age-edge ─┘
```
| Branch | Purpose | Merge Via |
|---|---|---|
| `main` | Production-ready code only; protected | PR only |
| `develop` | Integration branch for in-progress work | Merge to main when stable |
| `feature/US-XXX-short-description` | New feature tied to a user story | PR → develop → main |
| `fix/short-description` | Bug fix | PR → main (or develop if risk is low) |
| `chore/short-description` | Dependency updates, config, CI | PR → main |
**Branch naming examples:**
- `feature/US-042-compliance-quota-check`
- `feature/US-015-member-registration-form`
- `fix/member-under21-age-boundary`
- `chore/update-spring-boot-3.3.1`
### Commit Message Format — Conventional Commits
```
type(scope): short description (imperative, ≤72 chars)
[optional body — explain WHY, not WHAT; reference CanG sections if relevant]
[optional footer]
BREAKING CHANGE: description if applicable
Closes #issue-number
```
#### Types
| Type | When to Use |
|---|---|
| `feat` | New feature or user-visible behavior |
| `fix` | Bug fix |
| `docs` | Documentation only |
| `style` | Formatting, whitespace — no logic change |
| `refactor` | Code restructuring — no behavior change |
| `test` | Adding or updating tests |
| `chore` | Build, deps, config, CI — no production code |
#### Scopes
| Scope | Module / Area |
|---|---|
| `member` | Member management |
| `distribution` | Distribution recording and history |
| `stock` | Strain and batch management |
| `compliance` | `ComplianceService`, `ComplianceConstants`, CanG limits |
| `auth` | JWT, Spring Security, login |
| `report` | PDF/CSV generation |
| `infra` | Docker, CI, Flyway migrations |
| `web` | PrimeFaces JSF views and backing beans |
| `api` | REST controllers and DTOs |
#### Commit Examples
```bash
feat(compliance): add daily 25g distribution limit check
Implements CanG §10 Abs.1 single-distribution cap. ComplianceService
now throws IllegalArgumentException before any quota calculation if
weightGrams > ComplianceConstants.DAILY_LIMIT_GRAMS.
fix(member): correct under-21 flag when age is exactly 21
Age comparison was using < instead of <=. Members who turn 21 on the
exact distribution date now correctly receive the adult (50g) limit.
Closes #17
test(distribution): add quota boundary tests for 30g under-21 limit
Adds 6 parameterized test cases covering 28g, 29g, 29.9g, 30g, 30.1g,
and 31g for under-21 members. All reference ComplianceConstants — no
hardcoded values in test assertions.
chore(deps): update Spring Boot to 3.3.1
CVE-2024-38821 fix included. No API changes required.
docs(compliance): document ComplianceConstants usage policy in README
```
### Tag Strategy
Semantic versioning: `v{MAJOR}.{MINOR}.{PATCH}`
```bash
git tag -a v1.0.0 -m "Initial release — core member + distribution management"
git tag -a v1.1.0 -m "Add member portal with quota view"
git tag -a v1.0.1 -m "Fix under-21 monthly limit boundary condition"
```
---
## 5. Testing Standards
### Framework Stack
| Layer | Framework | Annotation / Config |
|---|---|---|
| Unit tests | JUnit 5 + Mockito | `@ExtendWith(MockitoExtension.class)` |
| Integration tests | Spring Boot Test + Testcontainers | `@SpringBootTest`, `@Testcontainers` |
| Web layer tests | `MockMvc` | `@WebMvcTest(DistributionController.class)` |
| Repository tests | `DataJpaTest` + Testcontainers | Real PostgreSQL via Testcontainers |
| PDF generation tests | JUnit 5 + iText assertions | Verify PDF structure, not pixel comparison |
### Test Naming Convention
```
methodName_givenCondition_shouldExpectedBehavior
```
```java
// ✅ Correct
@Test
void checkDistributionAllowed_givenMemberAtMonthlyLimit_shouldThrowQuotaExceededException()
@Test
void createDistribution_givenValidRequest_shouldPersistAndReturnDto()
@Test
void getQuotaRemaining_givenUnder21Member_shouldCapAt30g()
@Test
void generateMonthlyReport_givenNoPriorDistributions_shouldReturnZeroTotals()
```
### Unit Test Structure
```java
@ExtendWith(MockitoExtension.class)
class ComplianceServiceTest {
@Mock
private DistributionRepository distributionRepository;
@InjectMocks
private ComplianceService complianceService;
@Test
void checkDistributionAllowed_givenMemberAtMonthlyLimit_shouldReturnQuotaExceeded() {
// GIVEN
UUID memberId = UUID.randomUUID();
UUID tenantId = UUID.randomUUID();
BigDecimal currentMonthTotal = ComplianceConstants.MONTHLY_LIMIT_ADULT_GRAMS;
when(distributionRepository.sumWeightByMemberAndMonth(eq(memberId), eq(tenantId), any()))
.thenReturn(currentMonthTotal);
// WHEN
QuotaResult result = complianceService.checkDistributionAllowed(
memberId, tenantId, new BigDecimal("1.0"));
// THEN
assertThat(result).isInstanceOf(QuotaExceeded.class);
}
}
```
### Integration Test Structure
```java
@SpringBootTest
@Testcontainers
@Transactional // rolls back after each test
class DistributionServiceIntegrationTest {
@Container
static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@DynamicPropertySource
static void configureDataSource(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
// Tests run against real PostgreSQL — Flyway migrations apply automatically
}
```
### Coverage Target
| Module | Line Coverage Target |
|---|---|
| `cannamanage-service` | **≥ 80%** (enforced by JaCoCo in CI) |
| `cannamanage-domain` | ≥ 70% (entities + value objects) |
| `cannamanage-api` | ≥ 70% (controllers via MockMvc) |
| `cannamanage-report` | ≥ 60% (PDF generation harder to test) |
| `cannamanage-web` | Best effort (JSF backing beans — limited testability) |
### Test Rules
1. **No test may hardcode a compliance limit value.** All assertions must reference `ComplianceConstants`:
```java
// ❌ Prohibited
assertThat(limit).isEqualTo(new BigDecimal("50.0"));
// ✅ Required
assertThat(limit).isEqualTo(ComplianceConstants.MONTHLY_LIMIT_ADULT_GRAMS);
```
2. Parameterized tests (`@ParameterizedTest`) are strongly preferred for boundary condition coverage.
3. Test data builders (or fixtures) must live in `src/test/java/.../fixtures/` — no anonymous object creation scattered across test methods.
---
## 6. Code Review Checklist
Since CannaManage is a solo development project, a self-review checklist replaces a peer review process. All items must be checked before merging any PR to `main`.
### Self-Review Checklist
```markdown
## Compliance & Legal
- [ ] All distribution limits reference `ComplianceConstants` — zero hardcoded values
- [ ] `Distribution` entity fields are annotated `@Column(updatable = false)` where required
- [ ] `ComplianceService` calls are only made inside `@Transactional` boundaries
- [ ] New compliance rules have corresponding unit tests in `ComplianceServiceTest`
## Data & Multi-Tenancy
- [ ] New entity extends `AbstractTenantEntity`
- [ ] `tenant_id` is never accepted from user input (HTTP body, query param, path variable)
- [ ] All repository queries filter by `tenantId` — no cross-tenant data leakage possible
## Security & DSGVO
- [ ] No PII in log statements (no email, full name, member number in log lines)
- [ ] No passwords, tokens, or secrets hardcoded anywhere
- [ ] New REST endpoints annotated with `@PreAuthorize`
- [ ] DTOs validated with Bean Validation annotations (`@NotNull`, `@Size`, etc.)
## Database
- [ ] Flyway migration file added for any schema change (`V{n}__description.sql`)
- [ ] Migration file is backward-compatible or includes rollback notes
- [ ] No `@Column(nullable = false)` added without corresponding DB migration
## Code Quality
- [ ] Constructor injection used — no `@Autowired` field injection
- [ ] No `@Data` on JPA entities
- [ ] No magic numbers — named constants or enums used
- [ ] Checkstyle passes locally (`./mvnw checkstyle:check`)
- [ ] Javadoc on all public service methods
## Testing
- [ ] Unit test added for new service method
- [ ] Integration test updated if schema or contract changed
- [ ] Test coverage does not decrease in `cannamanage-service`
- [ ] Test method names follow `method_givenCondition_shouldExpect` pattern
## General
- [ ] Commit message follows Conventional Commits format
- [ ] Branch name follows `feature/US-XXX-` or `fix/` convention
- [ ] No `TODO` comments left in production code (use GitHub Issues instead)
```
---
## 7. Security Standards
### Authentication & Authorization
```java
// JWT secret from environment only — never in application.properties
@Value("${JWT_SECRET}")
private String jwtSecret;
// All endpoints behind @PreAuthorize — no security by obscurity
@RestController
@RequestMapping("/api/v1/distributions")
public class DistributionController {
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public Page