2
CannaManage 05 API
Patrick Plate edited this page 2026-06-12 11:50:55 +02:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

CannaManage REST API Specification v1.0

Base URL: https://{club-domain}/api/v1
Content-Type: application/json
Authentication: Bearer JWT token via Authorization: Bearer <token> header
Versioning: URL-based — current version /api/v1/, next version /api/v2/


Table of Contents

  1. Authentication & Conventions
  2. Error Format
  3. Pagination Envelope
  4. Custom Error Codes
  5. Auth Controller
  6. Club Controller
  7. Member Controller
  8. Distribution Controller
  9. Stock Controller
  10. Report Controller
  11. Compliance Controller
  12. Staff Controller
  13. Portal Controller
  14. Prevention Controller

1. Authentication & Conventions

JWT Claims

The access token payload contains:

{
  "sub": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "tenant_id": "8e1a2b3c-4d5e-6f70-8091-a2b3c4d5e6f7",
  "role": "ADMIN",
  "email": "admin@gruener-daumen-ev.de",
  "iat": 1712345678,
  "exp": 1712349278
}

tenant_id is ALWAYS resolved from the JWT. It is never accepted as a request parameter or path variable. Any attempt to access data belonging to a different tenant returns 403 TENANT_VIOLATION.

Roles

Role Description Auth Mechanism
ADMIN Club administrator — full access to club data, staff management JWT (stateless)
STAFF Club staff member — configurable permission subset from 8 granular permissions JWT (stateless)
MEMBER Club member — self-service portal: quota, history, dashboard Session (HttpSession)
PREVENTION_OFFICER Designated prevention officer — under-21 member reports, prevention data JWT (stateless)

Token Lifetimes

Token Lifetime
Access token 1 hour
Refresh token 30 days

2. Error Format

All error responses use application/problem+json format:

{
  "status": 400,
  "error": "BAD_REQUEST",
  "code": "QUOTA_EXCEEDED_MONTHLY",
  "message": "Monthly distribution quota of 50g exceeded for member.",
  "timestamp": "2026-04-06T10:15:30.000Z",
  "path": "/api/v1/distributions"
}
Field Type Description
status integer HTTP status code
error string HTTP status reason phrase
code string Machine-readable application error code (see §4)
message string Human-readable description
timestamp string ISO 8601 UTC timestamp
path string Request path that caused the error

3. Pagination Envelope

All list endpoints returning paginated results use this envelope:

{
  "content": [ "...array of items..." ],
  "page": 0,
  "size": 20,
  "totalElements": 150,
  "totalPages": 8
}

Standard query parameters for paginated endpoints:

Parameter Type Default Description
page integer 0 Zero-based page index
size integer 20 Page size (max: 100)
sort string varies Field name + direction, e.g. createdAt,desc

4. Custom Error Codes

Code HTTP Status Description
QUOTA_EXCEEDED_DAILY 422 Member has reached the 25 g/day distribution limit (KCanG §19 Abs. 5)
QUOTA_EXCEEDED_MONTHLY 422 Member has reached the monthly limit: 50 g (≥21 yrs) or 30 g (<21 yrs)
BATCH_RECALLED 422 The requested batch has an active contamination/recall flag
MEMBER_INACTIVE 422 Member status is SUSPENDED or EXPELLED — distributions blocked
MEMBER_UNDERAGE 422 Member date of birth indicates they are under 18 years old
DISTRIBUTION_IMMUTABLE 422 Attempt to modify or delete an existing distribution record (audit trail protected)
TENANT_VIOLATION 403 Requested resource belongs to a different tenant than the JWT claims
DSGVO_CONSENT_MISSING 422 Member has no DSGVO (GDPR) data processing consent on record
BATCH_INSUFFICIENT_STOCK 422 Batch does not have sufficient remaining quantity for the requested distribution
INVALID_CREDENTIALS 401 Email/password combination is incorrect
TOKEN_EXPIRED 401 Access or refresh token has expired
TOKEN_INVALID 401 Token signature is invalid or malformed
TOKEN_REVOKED 401 Token has been explicitly revoked (logout or password change)
MEMBER_NOT_FOUND 404 No member with the given UUID exists in this tenant
BATCH_NOT_FOUND 404 No batch with the given UUID exists in this tenant
DISTRIBUTION_NOT_FOUND 404 No distribution with the given UUID exists in this tenant
STAFF_NOT_FOUND 404 No staff account with the given UUID exists in this tenant
INVITE_EXPIRED 422 The staff invite token has expired (72-hour TTL) or has already been used
PREVENTION_LIMIT_EXCEEDED 422 Club has reached the maximum number of designated prevention officers (default: 2)
PERMISSION_DENIED 403 Staff member does not have the required permission for this operation

5. Auth Controller (/auth)

5.1 POST /auth/login

Authenticate with email and password. Returns a short-lived access token and a long-lived refresh token.

Authentication: None required

Request Body:

{
  "email": "admin@gruener-daumen-ev.de",
  "password": "supersecret123"
}
Field Type Required Description
email string Club administrator email address
password string Plaintext password (transmitted over TLS)

Success Response — 200 OK:

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
  "tokenType": "Bearer",
  "expiresIn": 3600,
  "member": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "email": "admin@gruener-daumen-ev.de",
    "role": "ADMIN",
    "clubId": "8e1a2b3c-4d5e-6f70-8091-a2b3c4d5e6f7",
    "clubName": "Grüner Daumen e.V."
  }
}

Error Responses:

HTTP Status Error Code Condition
401 INVALID_CREDENTIALS Email not found or password does not match
400 BAD_REQUEST Missing required fields
429 TOO_MANY_REQUESTS Rate limit exceeded (5 failed attempts per 15 minutes)

5.2 POST /auth/refresh

Exchange a valid refresh token for a new access token.

Authentication: Refresh token in request body (not a Bearer token)

Request Body:

{
  "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..."
}

Success Response — 200 OK:

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "tokenType": "Bearer",
  "expiresIn": 3600
}

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_EXPIRED Refresh token has passed its 30-day lifetime
401 TOKEN_INVALID Refresh token signature is invalid or has been revoked
400 BAD_REQUEST refreshToken field is missing

5.3 POST /auth/logout

Invalidate the current refresh token. Access tokens remain valid until natural expiry (TTL-based, no server-side revocation).

Authentication: Bearer access token required

Request Body: None

Success Response — 204 No Content

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token is invalid or expired

6. Club Controller (/clubs)

6.1 GET /clubs/me

Retrieve the authenticated admin's club details. Tenant is resolved from JWT.

Authentication: Bearer token — role ADMIN

Success Response — 200 OK:

{
  "id": "8e1a2b3c-4d5e-6f70-8091-a2b3c4d5e6f7",
  "name": "Grüner Daumen e.V.",
  "registrationNumber": "VR 12345",
  "address": {
    "street": "Hanfstraße 42",
    "city": "Berlin",
    "postalCode": "10115",
    "state": "Berlin"
  },
  "contactEmail": "info@gruener-daumen-ev.de",
  "contactPhone": "+49 30 12345678",
  "foundedDate": "2024-04-01",
  "maxMembers": 500,
  "currentMemberCount": 87,
  "status": "ACTIVE",
  "createdAt": "2024-04-01T09:00:00.000Z",
  "updatedAt": "2026-03-15T14:22:00.000Z"
}

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

6.2 PUT /clubs/me

Update the authenticated admin's club details.

Authentication: Bearer token — role ADMIN

Request Body:

{
  "name": "Grüner Daumen e.V.",
  "registrationNumber": "VR 12345",
  "address": {
    "street": "Hanfstraße 42",
    "city": "Berlin",
    "postalCode": "10115",
    "state": "Berlin"
  },
  "contactEmail": "info@gruener-daumen-ev.de",
  "contactPhone": "+49 30 12345678",
  "maxMembers": 500
}
Field Type Required Description
name string Official club name as registered
registrationNumber string Vereinsregister number
address object Club registered address
contactEmail string Public contact email
contactPhone string Public contact phone
maxMembers integer Maximum membership capacity (default: 500, KCanG limit)

Success Response — 200 OK: Full club object (same as GET /clubs/me)

Error Responses:

HTTP Status Error Code Condition
400 BAD_REQUEST Validation failure (invalid email format, missing required fields)
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

6.3 GET /clubs/me/stats

Retrieve dashboard statistics for the authenticated club.

Authentication: Bearer token — role ADMIN

Success Response — 200 OK:

{
  "memberCount": {
    "total": 87,
    "active": 82,
    "pending": 3,
    "suspended": 1,
    "expelled": 1
  },
  "distributionsThisMonth": {
    "count": 214,
    "totalGrams": 3280.5,
    "uniqueMembers": 74
  },
  "stock": {
    "totalGrams": 12500.0,
    "activeBatches": 4,
    "strainCount": 6
  },
  "complianceAlerts": {
    "membersAtDailyLimit": 2,
    "membersAtMonthlyLimit": 5,
    "recalledBatchesActive": 0
  },
  "generatedAt": "2026-04-06T10:00:00.000Z"
}

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

7. Member Controller (/members)

7.1 GET /members

List all members of the authenticated club, paginated and optionally filtered by status.

Authentication: Bearer token — role ADMIN

Query Parameters:

Parameter Type Default Description
page integer 0 Page index (zero-based)
size integer 20 Page size
sort string lastName,asc Sort field and direction
status string (all) Filter by status: ACTIVE, PENDING, SUSPENDED, EXPELLED
search string (none) Full-text search against first name, last name, or member number

Success Response — 200 OK:

{
  "content": [
    {
      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "memberNumber": "GD-2024-001",
      "firstName": "Max",
      "lastName": "Mustermann",
      "email": "max@example.de",
      "status": "ACTIVE",
      "dateOfBirth": "1990-05-15",
      "joinDate": "2024-05-01",
      "monthlyQuotaGrams": 50,
      "createdAt": "2024-05-01T10:00:00.000Z"
    }
  ],
  "page": 0,
  "size": 20,
  "totalElements": 87,
  "totalPages": 5
}

Note: dateOfBirth is only included for ADMIN role. Member role responses omit it.

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

7.2 POST /members

Create a new club member. Generates a unique member number automatically.

Authentication: Bearer token — role ADMIN

Request Body:

{
  "firstName": "Max",
  "lastName": "Mustermann",
  "email": "max@example.de",
  "dateOfBirth": "1990-05-15",
  "address": {
    "street": "Musterstraße 1",
    "city": "Berlin",
    "postalCode": "10115",
    "state": "Berlin"
  },
  "phone": "+49 176 12345678",
  "dsgvoConsentDate": "2026-04-06",
  "joinDate": "2026-04-06",
  "notes": "Referred by member GD-2024-015"
}
Field Type Required Description
firstName string Legal first name
lastName string Legal last name
email string Contact email (must be unique within tenant)
dateOfBirth string ISO 8601 date — used for quota calculation (≥21: 50 g/month, <21: 30 g/month)
address object Member's registered home address
phone string Contact phone number
dsgvoConsentDate string Date member signed DSGVO consent form
joinDate string Official membership start date
notes string Internal admin notes (not visible to member)

Success Response — 201 Created:

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "memberNumber": "GD-2026-088",
  "firstName": "Max",
  "lastName": "Mustermann",
  "email": "max@example.de",
  "status": "ACTIVE",
  "dateOfBirth": "1990-05-15",
  "monthlyQuotaGrams": 50,
  "joinDate": "2026-04-06",
  "dsgvoConsentDate": "2026-04-06",
  "createdAt": "2026-04-06T10:30:00.000Z"
}

monthlyQuotaGrams is computed server-side based on age at time of creation: 50 g for members ≥21 years old, 30 g for members aged 1820.

Error Responses:

HTTP Status Error Code Condition
400 BAD_REQUEST Missing required fields or validation failure
409 CONFLICT Email already registered for another member in this tenant
422 MEMBER_UNDERAGE Computed age from dateOfBirth is under 18
422 DSGVO_CONSENT_MISSING dsgvoConsentDate is null or missing
422 QUOTA_EXCEEDED_DAILY Club has reached maximum member capacity (500 members per KCanG §15)
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

7.3 GET /members/{id}

Retrieve full details for a single member.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
id UUID Member's unique identifier

Success Response — 200 OK:

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "memberNumber": "GD-2026-088",
  "firstName": "Max",
  "lastName": "Mustermann",
  "email": "max@example.de",
  "phone": "+49 176 12345678",
  "dateOfBirth": "1990-05-15",
  "address": {
    "street": "Musterstraße 1",
    "city": "Berlin",
    "postalCode": "10115",
    "state": "Berlin"
  },
  "status": "ACTIVE",
  "monthlyQuotaGrams": 50,
  "joinDate": "2026-04-06",
  "dsgvoConsentDate": "2026-04-06",
  "notes": "Referred by member GD-2024-015",
  "createdAt": "2026-04-06T10:30:00.000Z",
  "updatedAt": "2026-04-06T10:30:00.000Z"
}

Error Responses:

HTTP Status Error Code Condition
404 MEMBER_NOT_FOUND No member with given UUID exists in this tenant
403 TENANT_VIOLATION Member UUID exists but belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

7.4 PUT /members/{id}

Update an existing member's details.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
id UUID Member's unique identifier

Request Body: Same structure as POST /members (all fields optional except those required for compliance)

Success Response — 200 OK: Full updated member object

Error Responses:

HTTP Status Error Code Condition
400 BAD_REQUEST Validation failure
404 MEMBER_NOT_FOUND No member with given UUID exists in this tenant
403 TENANT_VIOLATION Member UUID belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

7.5 DELETE /members/{id}

Soft-delete / expel a member. Sets status to EXPELLED and records an expulsion timestamp. No data is physically deleted — all historical records (distributions, etc.) are retained for compliance auditing.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
id UUID Member's unique identifier

Request Body:

{
  "reason": "Voluntary membership resignation",
  "effectiveDate": "2026-04-06"
}
Field Type Required Description
reason string Reason for expulsion/resignation (stored in audit log)
effectiveDate string ISO 8601 date when expulsion takes effect

Success Response — 200 OK:

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "memberNumber": "GD-2026-088",
  "status": "EXPELLED",
  "expelledAt": "2026-04-06T11:00:00.000Z",
  "expulsionReason": "Voluntary membership resignation"
}

Error Responses:

HTTP Status Error Code Condition
400 BAD_REQUEST Missing reason or effectiveDate
404 MEMBER_NOT_FOUND No member with given UUID in this tenant
409 CONFLICT Member already has EXPELLED status
403 TENANT_VIOLATION Member UUID belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

7.6 GET /members/me

Retrieve the authenticated member's own profile.

Authentication: Bearer token — role MEMBER

Success Response — 200 OK:

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "memberNumber": "GD-2026-088",
  "firstName": "Max",
  "lastName": "Mustermann",
  "email": "max@example.de",
  "status": "ACTIVE",
  "monthlyQuotaGrams": 50,
  "joinDate": "2026-04-06"
}

dateOfBirth, address, notes, and internal fields are omitted from member self-view.

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid
422 MEMBER_INACTIVE Member account is suspended or expelled

7.7 GET /members/{id}/quota

Get the current month's quota usage for a member.

Authentication: Bearer token — role ADMIN, or MEMBER accessing their own ID

Path Parameters:

Parameter Type Description
id UUID Member's unique identifier

Query Parameters:

Parameter Type Default Description
month string current month ISO 8601 year-month, e.g. 2026-04

Success Response — 200 OK:

{
  "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "memberNumber": "GD-2026-088",
  "month": "2026-04",
  "monthlyLimitGrams": 50,
  "distributedThisMonthGrams": 22.5,
  "remainingMonthlyGrams": 27.5,
  "dailyLimitGrams": 25,
  "distributedTodayGrams": 5.0,
  "remainingTodayGrams": 20.0,
  "distributionCount": 3,
  "quotaExceeded": false,
  "nearLimit": false
}

nearLimit is true when remaining grams ≤ 10 g (monthly) or ≤ 5 g (daily).

Error Responses:

HTTP Status Error Code Condition
403 FORBIDDEN MEMBER role accessing another member's quota
404 MEMBER_NOT_FOUND No member with given UUID in this tenant
403 TENANT_VIOLATION Member UUID belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid

7.8 GET /members/{id}/distributions

Get distribution history for a member, paginated.

Authentication: Bearer token — role ADMIN, or MEMBER accessing their own ID

Path Parameters:

Parameter Type Description
id UUID Member's unique identifier

Query Parameters:

Parameter Type Default Description
page integer 0 Page index
size integer 20 Page size
sort string distributedAt,desc Sort field and direction
from string ISO 8601 date filter start, e.g. 2026-01-01
to string ISO 8601 date filter end

Success Response — 200 OK:

{
  "content": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "distributedAt": "2026-04-06T09:30:00.000Z",
      "strainName": "Blue Dream",
      "batchId": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
      "quantityGrams": 5.0,
      "notes": null
    }
  ],
  "page": 0,
  "size": 20,
  "totalElements": 42,
  "totalPages": 3
}

Error Responses:

HTTP Status Error Code Condition
403 FORBIDDEN MEMBER role accessing another member's distributions
404 MEMBER_NOT_FOUND No member with given UUID in this tenant
403 TENANT_VIOLATION Member UUID belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid

8. Distribution Controller (/distributions)

8.1 GET /distributions

List all distributions for the authenticated club, filterable by date range and member.

Authentication: Bearer token — role ADMIN

Query Parameters:

Parameter Type Default Description
page integer 0 Page index
size integer 20 Page size
sort string distributedAt,desc Sort field and direction
from string ISO 8601 date filter start
to string ISO 8601 date filter end
memberId UUID Filter by specific member
batchId UUID Filter by specific batch

Success Response — 200 OK:

{
  "content": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "distributedAt": "2026-04-06T09:30:00.000Z",
      "member": {
        "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "memberNumber": "GD-2026-088",
        "firstName": "Max",
        "lastName": "Mustermann"
      },
      "strain": {
        "id": "c4d5e6f7-a8b9-0123-cdef-456789abcdef",
        "name": "Blue Dream",
        "thcPercent": 18.5,
        "cbdPercent": 0.3
      },
      "batchId": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
      "batchCode": "BATCH-2026-003",
      "quantityGrams": 5.0,
      "handedOutBy": "admin@gruener-daumen-ev.de",
      "notes": null,
      "correctionNotes": []
    }
  ],
  "page": 0,
  "size": 20,
  "totalElements": 214,
  "totalPages": 11
}

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

8.2 POST /distributions

Record a new distribution. All compliance rules are checked before the record is created. This operation is atomic — either all checks pass and the record is written, or the entire request fails.

Authentication: Bearer token — role ADMIN

Request Body:

{
  "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "batchId": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
  "quantityGrams": 5.0,
  "notes": "Member requested half portion"
}
Field Type Required Description
memberId UUID Receiving member's UUID
batchId UUID Stock batch UUID to draw from
quantityGrams number Amount in grams (max 2 decimal places)
notes string Optional admin note for this distribution

Success Response — 201 Created:

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "distributedAt": "2026-04-06T09:30:00.000Z",
  "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "memberNumber": "GD-2026-088",
  "batchId": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
  "batchCode": "BATCH-2026-003",
  "strainName": "Blue Dream",
  "quantityGrams": 5.0,
  "remainingMonthlyQuotaGrams": 22.5,
  "remainingDailyQuotaGrams": 15.0,
  "handedOutBy": "admin@gruener-daumen-ev.de"
}

Compliance Checks (in order of precedence):

  1. Member exists and belongs to this tenant
  2. Member status is ACTIVE
  3. Member has DSGVO consent on record
  4. Batch exists, belongs to this tenant, and is not recalled
  5. Batch has sufficient remaining stock
  6. quantityGrams ≤ remaining daily quota (25 g/day - already distributed today)
  7. quantityGrams ≤ remaining monthly quota (50 g or 30 g - already distributed this month)

Error Responses:

HTTP Status Error Code Condition
422 MEMBER_INACTIVE Member status is SUSPENDED or EXPELLED
422 DSGVO_CONSENT_MISSING Member has no DSGVO consent date
422 BATCH_RECALLED Batch has an active contamination recall
422 BATCH_INSUFFICIENT_STOCK Batch has less remaining stock than quantityGrams
422 QUOTA_EXCEEDED_DAILY Distribution would exceed 25 g/day limit
422 QUOTA_EXCEEDED_MONTHLY Distribution would exceed monthly quota
404 MEMBER_NOT_FOUND Member UUID not found in this tenant
404 BATCH_NOT_FOUND Batch UUID not found in this tenant
403 TENANT_VIOLATION Member or batch belongs to different tenant
400 BAD_REQUEST quantityGrams ≤ 0 or > 25
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

KCanG Compliance Note: Single distributions of more than 25 g are rejected even if monthly quota remains. The legal daily limit is 25 g per person per calendar day (KCanG §19 Abs. 5).


8.3 GET /distributions/{id}

Retrieve a single distribution record by ID.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
id UUID Distribution's unique identifier

Success Response — 200 OK: Full distribution object (same structure as list items in §8.1)

Error Responses:

HTTP Status Error Code Condition
404 DISTRIBUTION_NOT_FOUND No distribution with given UUID in this tenant
403 TENANT_VIOLATION Distribution belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

8.4 POST /distributions/{id}/notes

Add a correction note to an existing distribution. Distribution records themselves are immutable — amount, member, batch, and timestamp cannot be changed. Correction notes provide an auditable annotation layer for errors or clarifications.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
id UUID Distribution's unique identifier

Request Body:

{
  "note": "Entry error — scale was miscalibrated. Actual weight was approximately 4.8g. Scale recalibrated and verified on 2026-04-06.",
  "correctedBy": "admin@gruener-daumen-ev.de"
}
Field Type Required Description
note string Correction note text (max 2000 characters)
correctedBy string Email of admin entering the correction

Success Response — 201 Created:

{
  "noteId": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
  "distributionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "note": "Entry error — scale was miscalibrated. Actual weight was approximately 4.8g.",
  "correctedBy": "admin@gruener-daumen-ev.de",
  "createdAt": "2026-04-06T11:15:00.000Z"
}

Error Responses:

HTTP Status Error Code Condition
404 DISTRIBUTION_NOT_FOUND No distribution with given UUID in this tenant
403 TENANT_VIOLATION Distribution belongs to a different tenant
400 BAD_REQUEST Missing note field or note exceeds 2000 chars
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

Audit Trail Note: Correction notes do NOT change the original distribution amount. For compliance reporting, the original amount is always used. Correction notes appear in distribution detail views and audit exports but do not affect quota calculations.


9. Stock Controller (/stock)

9.1 GET /stock/strains

List all cannabis strains registered in the club's catalogue.

Authentication: Bearer token — role ADMIN

Query Parameters:

Parameter Type Default Description
page integer 0 Page index
size integer 50 Page size
active boolean Filter by active strains only

Success Response — 200 OK:

{
  "content": [
    {
      "id": "c4d5e6f7-a8b9-0123-cdef-456789abcdef",
      "name": "Blue Dream",
      "variety": "HYBRID",
      "thcPercent": 18.5,
      "cbdPercent": 0.3,
      "description": "Classic hybrid, earthy and sweet aroma",
      "active": true,
      "createdAt": "2025-01-15T10:00:00.000Z"
    }
  ],
  "page": 0,
  "size": 50,
  "totalElements": 6,
  "totalPages": 1
}

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

9.2 POST /stock/strains

Register a new cannabis strain in the club's catalogue.

Authentication: Bearer token — role ADMIN

Request Body:

{
  "name": "OG Kush",
  "variety": "INDICA",
  "thcPercent": 22.0,
  "cbdPercent": 0.1,
  "description": "Classic indica, piney and citrus notes"
}
Field Type Required Description
name string Strain name
variety string SATIVA, INDICA, or HYBRID
thcPercent number THC content percentage (0100)
cbdPercent number CBD content percentage (0100)
description string Optional descriptive notes

Success Response — 201 Created: Full strain object

Error Responses:

HTTP Status Error Code Condition
409 CONFLICT Strain with same name already exists in this tenant
400 BAD_REQUEST Invalid variety value or missing required fields
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

9.3 GET /stock/batches

List all stock batches for the authenticated club.

Authentication: Bearer token — role ADMIN

Query Parameters:

Parameter Type Default Description
page integer 0 Page index
size integer 20 Page size
status string Filter: AVAILABLE, DEPLETED, RECALLED
strainId UUID Filter by strain

Success Response — 200 OK:

{
  "content": [
    {
      "id": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
      "batchCode": "BATCH-2026-003",
      "strain": {
        "id": "c4d5e6f7-a8b9-0123-cdef-456789abcdef",
        "name": "Blue Dream"
      },
      "initialQuantityGrams": 2000.0,
      "remainingQuantityGrams": 850.5,
      "status": "AVAILABLE",
      "harvestDate": "2026-02-15",
      "labTestDate": "2026-03-01",
      "labTestReference": "LAB-2026-1234",
      "thcPercent": 19.2,
      "addedAt": "2026-03-10T08:00:00.000Z"
    }
  ],
  "page": 0,
  "size": 20,
  "totalElements": 4,
  "totalPages": 1
}

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

9.4 POST /stock/batches

Add a new stock batch for a registered strain.

Authentication: Bearer token — role ADMIN

Request Body:

{
  "strainId": "c4d5e6f7-a8b9-0123-cdef-456789abcdef",
  "initialQuantityGrams": 2000.0,
  "harvestDate": "2026-02-15",
  "labTestDate": "2026-03-01",
  "labTestReference": "LAB-2026-1234",
  "thcPercent": 19.2,
  "cbdPercent": 0.4,
  "notes": "Batch from indoor cultivation cycle 3"
}
Field Type Required Description
strainId UUID Strain this batch belongs to
initialQuantityGrams number Starting weight in grams
harvestDate string ISO 8601 date of harvest
labTestDate string ISO 8601 date of laboratory analysis
labTestReference string Lab report reference number (audit requirement)
thcPercent number Actual THC % from lab test
cbdPercent number Actual CBD % from lab test
notes string Internal notes

Success Response — 201 Created: Full batch object (same as list item above)

Error Responses:

HTTP Status Error Code Condition
400 BAD_REQUEST Missing required fields or invalid quantities
404 strainId not found in this tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

9.5 GET /stock/batches/{id}

Retrieve full details for a specific batch including distribution history summary.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
id UUID Batch unique identifier

Success Response — 200 OK:

{
  "id": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
  "batchCode": "BATCH-2026-003",
  "strain": {
    "id": "c4d5e6f7-a8b9-0123-cdef-456789abcdef",
    "name": "Blue Dream",
    "variety": "HYBRID"
  },
  "initialQuantityGrams": 2000.0,
  "remainingQuantityGrams": 850.5,
  "distributedQuantityGrams": 1149.5,
  "distributionCount": 48,
  "status": "AVAILABLE",
  "harvestDate": "2026-02-15",
  "labTestDate": "2026-03-01",
  "labTestReference": "LAB-2026-1234",
  "thcPercent": 19.2,
  "cbdPercent": 0.4,
  "notes": "Batch from indoor cultivation cycle 3",
  "recallInfo": null,
  "addedAt": "2026-03-10T08:00:00.000Z",
  "updatedAt": "2026-04-06T09:30:00.000Z"
}

recallInfo is null for non-recalled batches. See §9.6 for recall structure.

Error Responses:

HTTP Status Error Code Condition
404 BATCH_NOT_FOUND No batch with given UUID in this tenant
403 TENANT_VIOLATION Batch belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

9.6 POST /stock/batches/{id}/recall

Flag a batch as recalled due to contamination or safety concerns. This immediately prevents any new distributions from this batch. Members who received product from this batch can be identified via GET /reports/recall/{batchId}.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
id UUID Batch unique identifier

Request Body:

{
  "reason": "Pesticide residue detected above legal threshold in repeat lab test",
  "detectedAt": "2026-04-06",
  "severity": "HIGH",
  "labReference": "LAB-2026-9876"
}
Field Type Required Description
reason string Description of the contamination/recall reason
detectedAt string ISO 8601 date contamination was detected
severity string LOW, MEDIUM, or HIGH
labReference string Lab report reference for contamination finding

Success Response — 200 OK:

{
  "id": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
  "batchCode": "BATCH-2026-003",
  "status": "RECALLED",
  "recallInfo": {
    "reason": "Pesticide residue detected above legal threshold",
    "detectedAt": "2026-04-06",
    "severity": "HIGH",
    "labReference": "LAB-2026-9876",
    "recalledAt": "2026-04-06T11:30:00.000Z",
    "recalledBy": "admin@gruener-daumen-ev.de",
    "affectedMemberCount": 23
  }
}

Error Responses:

HTTP Status Error Code Condition
404 BATCH_NOT_FOUND No batch with given UUID in this tenant
409 CONFLICT Batch is already in RECALLED status
403 TENANT_VIOLATION Batch belongs to a different tenant
400 BAD_REQUEST Missing required fields or invalid severity value
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

9.7 GET /stock/summary (ADMIN view)

Retrieve a complete stock summary showing totals by strain.

Authentication: Bearer token — role ADMIN

Success Response — 200 OK:

{
  "totalAvailableGrams": 12500.0,
  "activeBatches": 4,
  "strains": [
    {
      "strainId": "c4d5e6f7-a8b9-0123-cdef-456789abcdef",
      "strainName": "Blue Dream",
      "availableGrams": 850.5,
      "batchCount": 1
    },
    {
      "strainId": "d5e6f7a8-b9c0-1234-defa-5678901bcdef",
      "strainName": "OG Kush",
      "availableGrams": 11649.5,
      "batchCount": 3
    }
  ],
  "recalledBatches": 0,
  "generatedAt": "2026-04-06T10:00:00.000Z"
}

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

9.8 GET /stock/summary (MEMBER view)

Retrieve stock availability for members — shows strain availability status only. No quantities are exposed to members.

Authentication: Bearer token — role MEMBER

Success Response — 200 OK:

{
  "strains": [
    {
      "strainName": "Blue Dream",
      "variety": "HYBRID",
      "available": true
    },
    {
      "strainName": "OG Kush",
      "variety": "INDICA",
      "available": true
    }
  ],
  "generatedAt": "2026-04-06T10:00:00.000Z"
}

MEMBER view is served by the same endpoint path — the response schema differs based on the JWT role claim. No grams, no batch codes, no THC percentages beyond what's in the strain catalogue.

Error Responses:

HTTP Status Error Code Condition
401 TOKEN_INVALID Bearer token missing or invalid

10. Report Controller (/reports)

All report endpoints are ADMIN-only. Reports are tenant-scoped — all data is filtered to the requesting club's tenant_id.

10.1 GET /reports/monthly

Generate the monthly compliance report. Supports JSON (default), PDF, and CSV output.

Authentication: Bearer token — role ADMIN

Query Parameters:

Parameter Type Default Description
month string current month ISO 8601 year-month, e.g. 2026-03
format string json Output format: json, pdf, csv

Success Response — 200 OK (format=json):

{
  "reportId": "RPT-2026-04-GD",
  "clubName": "Grüner Daumen e.V.",
  "month": "2026-03",
  "generatedAt": "2026-04-06T10:00:00.000Z",
  "summary": {
    "totalDistributions": 214,
    "totalGramsDistributed": 3280.5,
    "uniqueMembers": 74,
    "activeMembers": 82,
    "newMembers": 3,
    "expelledMembers": 1
  },
  "complianceSummary": {
    "quotaViolationsDetected": 0,
    "recallsTriggered": 0,
    "dsgvoNonCompliantMembers": 0
  },
  "distributions": [
    {
      "date": "2026-03-01",
      "count": 9,
      "totalGrams": 132.5
    }
  ]
}

Success Response — format=pdf:

  • Content-Type: application/pdf
  • Content-Disposition: attachment; filename="cannamanage-report-2026-03.pdf"
  • Binary PDF body

Success Response — format=csv:

  • Content-Type: text/csv; charset=UTF-8
  • Content-Disposition: attachment; filename="cannamanage-report-2026-03.csv"
  • UTF-8 CSV with BOM for Excel compatibility

Error Responses:

HTTP Status Error Code Condition
400 BAD_REQUEST Invalid month format or format value
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

10.2 GET /reports/members

Export the full member list — intended for presentation to authorities during official inspections.

Authentication: Bearer token — role ADMIN

Query Parameters:

Parameter Type Default Description
format string json Output format: json, pdf, csv
status string ACTIVE Filter by status
asOf string today ISO 8601 date — membership as of this date

Success Response — 200 OK (format=json):

{
  "clubName": "Grüner Daumen e.V.",
  "asOf": "2026-04-06",
  "totalCount": 82,
  "members": [
    {
      "memberNumber": "GD-2024-001",
      "firstName": "Max",
      "lastName": "Mustermann",
      "dateOfBirth": "1990-05-15",
      "joinDate": "2024-05-01",
      "status": "ACTIVE",
      "dsgvoConsentDate": "2024-05-01"
    }
  ]
}

This report includes date of birth and DSGVO consent date as required by KCanG inspection protocols.

Error Responses:

HTTP Status Error Code Condition
400 BAD_REQUEST Invalid format or status value
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

10.3 GET /reports/recall/{batchId}

Generate a recall impact report for a specific batch — identifies all members who received product from the recalled batch.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
batchId UUID Recalled batch unique identifier

Query Parameters:

Parameter Type Default Description
format string json Output format: json, pdf, csv

Success Response — 200 OK (format=json):

{
  "batchCode": "BATCH-2026-003",
  "strainName": "Blue Dream",
  "recallReason": "Pesticide residue detected above legal threshold",
  "recalledAt": "2026-04-06T11:30:00.000Z",
  "severity": "HIGH",
  "affectedMembers": [
    {
      "memberNumber": "GD-2024-001",
      "firstName": "Max",
      "lastName": "Mustermann",
      "email": "max@example.de",
      "phone": "+49 176 12345678",
      "totalReceivedGrams": 15.0,
      "lastDistributionDate": "2026-04-05",
      "distributionCount": 3
    }
  ],
  "totalAffectedMembers": 23,
  "totalAffectedGrams": 345.5,
  "generatedAt": "2026-04-06T11:35:00.000Z"
}

Error Responses:

HTTP Status Error Code Condition
404 BATCH_NOT_FOUND No batch with given UUID in this tenant
400 BAD_REQUEST Invalid format value
403 TENANT_VIOLATION Batch belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

11. Compliance Controller (/compliance)

11.1 GET /compliance/quota/{memberId}

Check the current compliance status for a specific member's quota. Intended for real-time verification before handing out product at the counter.

Authentication: Bearer token — role ADMIN

Path Parameters:

Parameter Type Description
memberId UUID Member's unique identifier

Success Response — 200 OK:

{
  "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "memberNumber": "GD-2026-088",
  "memberStatus": "ACTIVE",
  "dsgvoConsentPresent": true,
  "quota": {
    "month": "2026-04",
    "monthlyLimitGrams": 50,
    "distributedThisMonthGrams": 22.5,
    "remainingMonthlyGrams": 27.5,
    "dailyLimitGrams": 25,
    "distributedTodayGrams": 0.0,
    "remainingTodayGrams": 25.0
  },
  "canReceive": true,
  "blockingReasons": []
}

Blocked member example:

{
  "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "memberNumber": "GD-2026-089",
  "memberStatus": "ACTIVE",
  "dsgvoConsentPresent": true,
  "quota": {
    "month": "2026-04",
    "monthlyLimitGrams": 30,
    "distributedThisMonthGrams": 30.0,
    "remainingMonthlyGrams": 0.0,
    "dailyLimitGrams": 25,
    "distributedTodayGrams": 0.0,
    "remainingTodayGrams": 25.0
  },
  "canReceive": false,
  "blockingReasons": ["QUOTA_EXCEEDED_MONTHLY"]
}

blockingReasons is an array — multiple blocks can apply simultaneously (e.g., both MEMBER_INACTIVE and DSGVO_CONSENT_MISSING).

Error Responses:

HTTP Status Error Code Condition
404 MEMBER_NOT_FOUND No member with given UUID in this tenant
403 TENANT_VIOLATION Member belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

11.2 GET /compliance/check

Dry-run compliance pre-check for a proposed distribution. Nothing is recorded — this is a read-only validation endpoint used to preview whether a specific distribution would pass all rules.

Authentication: Bearer token — role ADMIN

Query Parameters:

Parameter Type Required Description
memberId UUID Member who would receive
batchId UUID Batch to distribute from
quantityGrams number Proposed quantity in grams

Example request:

GET /api/v1/compliance/check?memberId=3fa85f64-...&batchId=f1e2d3c4-...&quantityGrams=10

Success Response — 200 OK (all checks pass):

{
  "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "batchId": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
  "quantityGrams": 10.0,
  "allowed": true,
  "checks": {
    "memberActive": true,
    "dsgvoConsentPresent": true,
    "batchAvailable": true,
    "batchNotRecalled": true,
    "batchSufficientStock": true,
    "dailyQuotaOk": true,
    "monthlyQuotaOk": true
  },
  "quotaAfter": {
    "remainingMonthlyGrams": 17.5,
    "remainingTodayGrams": 15.0
  }
}

Success Response — 200 OK (checks fail — still HTTP 200, not an error):

{
  "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "batchId": "f1e2d3c4-b5a6-9780-bcde-fa0987654321",
  "quantityGrams": 30.0,
  "allowed": false,
  "checks": {
    "memberActive": true,
    "dsgvoConsentPresent": true,
    "batchAvailable": true,
    "batchNotRecalled": true,
    "batchSufficientStock": true,
    "dailyQuotaOk": false,
    "monthlyQuotaOk": true
  },
  "violations": ["QUOTA_EXCEEDED_DAILY"],
  "quotaAfter": null
}

This endpoint always returns 200 OK. The allowed field indicates pass/fail. Use violations array to display which rules would be broken.

Error Responses (true errors only):

HTTP Status Error Code Condition
400 BAD_REQUEST Missing required query parameters or quantityGrams ≤ 0
404 MEMBER_NOT_FOUND Member UUID not found in this tenant
404 BATCH_NOT_FOUND Batch UUID not found in this tenant
403 TENANT_VIOLATION Member or batch belongs to a different tenant
401 TOKEN_INVALID Bearer token missing or invalid
403 FORBIDDEN Authenticated user does not have ADMIN role

12. Staff Controller (/staff)

12.1 POST /staff

Create a new staff account for this club.

Authentication: Bearer token — role ADMIN

Request Body:

{
  "email": "lisa@gruener-daumen-ev.de",
  "displayName": "Lisa Müller",
  "permissions": ["RECORD_DISTRIBUTION", "VIEW_MEMBER_LIST", "VIEW_MEMBER_QUOTA"],
  "roleTemplate": "Ausgabe"
}
Field Type Required Description
email string Staff member email (used for invite)
displayName string Display name for the staff member
permissions string[] Array of StaffPermission enum values
roleTemplate string Pre-configured template: Ausgabe, Lager, Vorstand

Available Permissions:

Permission Description
RECORD_DISTRIBUTION Can record distributions to members
VIEW_MEMBER_LIST Can view the member roster
VIEW_MEMBER_QUOTA Can view individual member quota status
ADD_MEMBER Can register new members
VIEW_STOCK Can view batch/strain inventory
RECORD_STOCK_IN Can add new batches to inventory
VIEW_COMPLIANCE_REPORT Can generate/download compliance reports
MANAGE_GROW_CALENDAR Can manage cultivation calendar entries

Success Response — 201 Created:

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "email": "lisa@gruener-daumen-ev.de",
  "displayName": "Lisa Müller",
  "permissions": ["RECORD_DISTRIBUTION", "VIEW_MEMBER_LIST", "VIEW_MEMBER_QUOTA"],
  "active": true,
  "createdAt": "2026-06-10T14:00:00.000Z"
}

12.2 GET /staff

List all staff accounts for this club.

Authentication: Bearer token — role ADMIN

Success Response — 200 OK: Array of staff account objects.


12.3 PUT /staff/{id}

Update staff account permissions or display name.

Authentication: Bearer token — role ADMIN

Request Body:

{
  "displayName": "Lisa Müller-Schmidt",
  "permissions": ["RECORD_DISTRIBUTION", "VIEW_MEMBER_LIST", "VIEW_MEMBER_QUOTA", "VIEW_STOCK"]
}

Success Response — 200 OK: Updated staff account object.


12.4 DELETE /staff/{id}

Deactivate a staff account. Revokes all active tokens for this staff member.

Authentication: Bearer token — role ADMIN

Success Response — 204 No Content

Error Responses:

HTTP Status Error Code Condition
404 STAFF_NOT_FOUND No staff account with this UUID in this tenant

12.5 POST /staff/invite

Send an email invite to a new staff member. Creates an InviteToken with 72-hour expiry.

Authentication: Bearer token — role ADMIN

Request Body:

{
  "staffId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Success Response — 200 OK:

{
  "message": "Invite sent",
  "expiresAt": "2026-06-13T14:00:00.000Z"
}

Error Responses:

HTTP Status Error Code Condition
404 STAFF_NOT_FOUND Staff account does not exist
422 INVITE_EXPIRED Staff already has an active, non-expired invite

12.6 POST /auth/set-password

Set password using an invite token. Completes staff onboarding.

Authentication: None required (token-based)

Request Body:

{
  "token": "abc123def456...",
  "password": "newSecurePassword123!"
}

Success Response — 200 OK:

{
  "message": "Password set successfully. You can now log in."
}

Error Responses:

HTTP Status Error Code Condition
422 INVITE_EXPIRED Token has expired or already been used
400 BAD_REQUEST Password does not meet minimum requirements

13. Portal Controller (/portal)

Authentication: Session-based (HttpSession). Members authenticate via form login at /portal/login. All portal endpoints require an active session with ROLE_MEMBER.

13.1 GET /portal/dashboard

Returns the member's personal dashboard summary.

Success Response — 200 OK:

{
  "memberName": "Max Mustermann",
  "memberNumber": "GD-2024-001",
  "clubName": "Grüner Daumen e.V.",
  "quotaStatus": {
    "monthlyLimitGrams": 50.0,
    "consumedThisMonthGrams": 32.5,
    "remainingGrams": 17.5,
    "dailyLimitGrams": 25.0,
    "consumedTodayGrams": 3.5,
    "percentUsed": 65
  },
  "lastDistribution": {
    "date": "2026-06-09T16:30:00.000Z",
    "strainName": "Northern Lights",
    "quantityGrams": 3.5
  },
  "memberSince": "2024-05-01"
}

13.2 GET /portal/quota

Returns detailed quota information for the current calendar month.

Success Response — 200 OK:

{
  "year": 2026,
  "month": 6,
  "monthlyLimitGrams": 50.0,
  "consumedGrams": 32.5,
  "remainingGrams": 17.5,
  "dailyLimitGrams": 25.0,
  "consumedTodayGrams": 3.5,
  "isUnder21": false,
  "daysRemainingInMonth": 19,
  "resetDate": "2026-07-01"
}

13.3 GET /portal/history

Returns the member's distribution history, paginated.

Query Parameters:

Parameter Type Default Description
page integer 0 Page index
size integer 20 Page size
month integer (current) Filter by month
year integer (current) Filter by year

Success Response — 200 OK:

{
  "content": [
    {
      "date": "2026-06-09T16:30:00.000Z",
      "strainName": "Northern Lights",
      "quantityGrams": 3.5,
      "batchCode": "NL-2026-003"
    }
  ],
  "page": 0,
  "size": 20,
  "totalElements": 8,
  "totalPages": 1,
  "monthlyTotal": 32.5
}

14. Prevention Controller (/prevention)

14.1 POST /prevention/officers

Designate a member as a prevention officer for this club.

Authentication: Bearer token — role ADMIN

Request Body:

{
  "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}

Success Response — 201 Created:

{
  "memberId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "memberName": "Dr. Sarah Weber",
  "designatedAt": "2026-06-10T09:00:00.000Z"
}

Error Responses:

HTTP Status Error Code Condition
422 PREVENTION_LIMIT_EXCEEDED Club already has maximum prevention officers (default: 2)
404 MEMBER_NOT_FOUND Member UUID not found in this tenant

14.2 DELETE /prevention/officers/{memberId}

Revoke prevention officer designation from a member.

Authentication: Bearer token — role ADMIN

Success Response — 204 No Content


14.3 GET /prevention/under21

List all under-21 members with their consumption data for prevention oversight.

Authentication: Bearer token — role ADMIN or PREVENTION_OFFICER

Success Response — 200 OK:

{
  "content": [
    {
      "memberId": "...",
      "memberName": "Jonas Klein",
      "age": 19,
      "dateOfBirth": "2007-03-15",
      "monthlyLimitGrams": 30.0,
      "consumedThisMonthGrams": 18.0,
      "lastDistributionDate": "2026-06-08T14:00:00.000Z",
      "memberSince": "2025-09-01"
    }
  ],
  "totalUnder21Members": 5
}

Appendix A: Member Status Lifecycle

PENDING → ACTIVE → SUSPENDED → ACTIVE   (reinstatement possible)
ACTIVE  → EXPELLED                       (permanent, no reinstatement via API)
PENDING → EXPELLED                       (rejected application)
Status Distributions allowed Login allowed
PENDING No No
ACTIVE Yes Yes
SUSPENDED No Yes (read-only)
EXPELLED No No

Appendix B: KCanG Compliance Reference

Key limits implemented in the distribution compliance engine (as of KCanG 2024):

Rule Limit Applied To
Daily distribution 25 g/day All adult members
Monthly distribution (≥21 years) 50 g/month Members aged 21+
Monthly distribution (1820 years) 30 g/month Members aged 1820
Minimum age 18 years All members
Maximum club members 500 Per Anbauvereinigung
Simultaneous memberships 1 club Per person (enforced externally)

Age is calculated at the time of each distribution request, not at membership creation time. A member who turns 21 during the month automatically becomes eligible for the 50 g limit on their birthday.


Appendix C: Tenant Isolation Guarantee

Every database query includes an implicit WHERE tenant_id = ? clause derived from the JWT. The following guarantees hold:

  1. A request authenticated with tenant A's JWT cannot read, modify, or delete data belonging to tenant B — even if a valid UUID belonging to tenant B is provided.
  2. Any attempt to access cross-tenant resources returns 403 TENANT_VIOLATION.
  3. Cross-tenant violations are logged server-side to the security audit log.
  4. Tenant ID is never accepted from the request (body, headers, or query parameters) — it is always derived from the validated JWT signature.