From bf2448cd5580f9c0f4469a1bdb81eb7ece05d641 Mon Sep 17 00:00:00 2001 From: pplate Date: Thu, 11 Jun 2026 11:41:47 +0000 Subject: [PATCH] wiki: add 07 CodingStandards --- 07-CodingStandards.md | 825 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 825 insertions(+) create mode 100644 07-CodingStandards.md 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 list(...) { ... } + + @PostMapping + @PreAuthorize("hasRole('ADMIN')") + public DistributionDto create(...) { ... } +} + +// Member portal endpoints restricted to role + own data +@GetMapping("/api/v1/member/quota") +@PreAuthorize("hasRole('MEMBER') and #memberId == authentication.principal.memberId") +public QuotaDto getQuota(@RequestParam UUID memberId) { ... } +``` + +### CORS Configuration + +```java +@Bean +CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + // No wildcard — club subdomain only + config.setAllowedOriginPatterns(List.of("https://*.cannamanage.de")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("Authorization", "Content-Type")); + config.setAllowCredentials(true); + // ... +} +``` + +### Input Validation + +All DTOs must be annotated with Bean Validation constraints. The controller calls `@Valid` on request bodies. + +```java +public record CreateDistributionRequest( + + @NotNull(message = "Member ID is required") + UUID memberId, + + @NotNull(message = "Batch ID is required") + UUID batchId, + + @NotNull(message = "Weight is required") + @DecimalMin(value = "0.1", message = "Weight must be at least 0.1g") + @DecimalMax(value = "25.0", message = "Weight cannot exceed daily limit") + BigDecimal weightGrams +) {} +``` + +### SQL Injection Prevention + +- **JPA named queries only** — no string concatenation in JPQL +- Spring Data JPA repository methods generate parameterized queries automatically +- Native SQL queries use `@Query` with named parameters (`:param` syntax), never `+` + +```java +// ✅ Safe — parameterized +@Query("SELECT SUM(d.weightGrams) FROM Distribution d WHERE d.memberId = :memberId AND d.tenantId = :tenantId AND MONTH(d.distributedAt) = :month") +BigDecimal sumWeightByMemberAndMonth(@Param("memberId") UUID memberId, + @Param("tenantId") UUID tenantId, + @Param("month") int month); + +// ❌ Prohibited — SQL injection risk +String jpql = "SELECT ... WHERE name = '" + memberName + "'"; +``` + +### Password Hashing + +```java +@Bean +PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(12); // strength 12 — ~250ms per hash on modern hardware +} +``` + +### Sensitive Data Logging + +```java +// ❌ Never log PII +log.info("Processing distribution for member: {}", member.getEmail()); +log.info("Member {} requested quota", member.getFullName()); + +// ✅ Log with opaque identifiers only +log.info("Processing distribution for memberId={} tenantId={}", member.getId(), tenantId); +log.info("Quota check passed for memberId={}", memberId); +``` + +--- + +## 8. Environment Configuration + +### Environment Variables Reference + +All secrets and environment-specific configuration are provided via environment variables. Never commit secrets to version control. + +| Variable | Required | Default | Description | +|---|---|---|---| +| `DB_URL` | ✅ | — | JDBC URL, e.g. `jdbc:postgresql://localhost:5432/cannamanage` | +| `DB_USERNAME` | ✅ | — | PostgreSQL username | +| `DB_PASSWORD` | ✅ | — | PostgreSQL password | +| `JWT_SECRET` | ✅ | — | 256-bit (32-byte) random secret for JWT signing; generate with `openssl rand -base64 32` | +| `JWT_ACCESS_TTL_HOURS` | ❌ | `8` | Access token TTL in hours | +| `JWT_REFRESH_TTL_DAYS` | ❌ | `30` | Refresh token TTL in days | +| `STRIPE_SECRET_KEY` | ✅ (billing) | — | Stripe secret key (starts with `sk_live_` in production) | +| `STRIPE_WEBHOOK_SECRET` | ✅ (billing) | — | Stripe webhook signing secret for subscription events | +| `MAIL_HOST` | ✅ | — | SMTP host for transactional emails | +| `MAIL_USERNAME` | ✅ | — | SMTP username | +| `MAIL_PASSWORD` | ✅ | — | SMTP password | +| `MAIL_FROM` | ❌ | `noreply@cannamanage.de` | From address for system emails | +| `SENTRY_DSN` | ❌ | — | Sentry DSN for error tracking; omit to disable | +| `APP_BASE_URL` | ✅ | — | Application base URL, e.g. `https://meinclub.cannamanage.de` | +| `ADMIN_INITIAL_EMAIL` | ❌ | — | Seed admin email on first startup (Flyway data migration) | +| `ADMIN_INITIAL_PASSWORD` | ❌ | — | Seed admin password — change immediately after first login | + +### `application.properties` Pattern + +```properties +# application.properties — references env vars only; no values hardcoded + +spring.datasource.url=${DB_URL} +spring.datasource.username=${DB_USERNAME} +spring.datasource.password=${DB_PASSWORD} + +spring.jpa.hibernate.ddl-auto=validate +spring.flyway.enabled=true +spring.flyway.locations=classpath:db/migration + +jwt.secret=${JWT_SECRET} +jwt.access.ttl-hours=${JWT_ACCESS_TTL_HOURS:8} +jwt.refresh.ttl-days=${JWT_REFRESH_TTL_DAYS:30} + +stripe.secret-key=${STRIPE_SECRET_KEY} +stripe.webhook-secret=${STRIPE_WEBHOOK_SECRET} + +spring.mail.host=${MAIL_HOST} +spring.mail.username=${MAIL_USERNAME} +spring.mail.password=${MAIL_PASSWORD} + +sentry.dsn=${SENTRY_DSN:} +``` + +### Profile Strategy + +> **`spring.profiles.active=prod` is NOT a security mechanism.** Never use profile-based condition checks to gate security-relevant behavior (e.g., `@ConditionalOnProperty(name="spring.profiles.active", havingValue="prod")`). + +Profiles are used **only** for infrastructure wiring (in-memory H2 vs. real PostgreSQL for tests, Testcontainers vs. external DB). + +| Profile | Usage | +|---|---| +| `(none)` | Production — all config from environment variables | +| `test` | JUnit integration tests — Testcontainers PostgreSQL | +| `dev` | Local development — Docker Compose PostgreSQL, verbose SQL logging | + +### Local Development Setup + +```bash +# Start local PostgreSQL via Docker Compose +docker compose up -d postgres + +# Run with dev profile (verbose SQL, local DB) +./mvnw spring-boot:run -Dspring-boot.run.profiles=dev \ + -Dspring-boot.run.arguments="--DB_URL=jdbc:postgresql://localhost:5432/cannamanage_dev \ + --DB_USERNAME=cannamanage --DB_PASSWORD=dev_password \ + --JWT_SECRET=$(openssl rand -base64 32)" +``` + +--- + +*End of CannaManage coding standards. See also [03-ARCHITECTURE.md](03-ARCHITECTURE.md) for data model and [05-API-SPEC.md](05-API-SPEC.md) for REST contract.*