# 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.
     *
     * <p>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<DistributionDto> 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.*
