diff --git a/07-CodingStandards.md b/07-CodingStandards.md new file mode 100644 index 0000000..892f9a4 --- /dev/null +++ b/07-CodingStandards.md @@ -0,0 +1,825 @@ +# 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