# 04 — Business Logic Flow Charts **Project:** CannaManage — B2B SaaS for German Cannabis Social Clubs (Anbauvereinigungen) **Phase:** 2 of 5 — Architecture & Data Model **Last updated:** 2026-04-06 All flows are implemented in the Spring Boot service layer. Mermaid `flowchart TD` syntax. --- ## Flow 1: Distribution Recording Records a cannabis distribution to a member. This is the most compliance-critical path in the system. Every step that can fail returns a user-facing error with actionable detail (remaining quota, batch status, etc.). ```mermaid flowchart TD START([🟢 Admin clicks\n'Record Distribution']) --> SEL_MEMBER[Select member from list] SEL_MEMBER --> LOAD_MEMBER[Load member profile\nfrom MemberRepository] LOAD_MEMBER --> CHECK_ACTIVE{Member status\n= ACTIVE?} CHECK_ACTIVE -->|No — SUSPENDED\nor EXPELLED| ERR_MEMBER[āŒ Error: Member not eligible\nShow status reason] CHECK_ACTIVE -->|Yes| CHECK_AGE{is_under_21\n= true?} CHECK_AGE -->|Under 21| MAX_MONTHLY_30[Monthly limit = 30g] CHECK_AGE -->|Adult ≄ 21| MAX_MONTHLY_50[Monthly limit = 50g] MAX_MONTHLY_30 --> ENTER_QTY[Admin enters quantity\nin grams] MAX_MONTHLY_50 --> ENTER_QTY ENTER_QTY --> VALIDATE_QTY{quantity > 0\nand ≤ 25g?} VALIDATE_QTY -->|No| ERR_QTY[āŒ Error: Invalid quantity\nDaily max is 25g per visit] VALIDATE_QTY -->|Yes| CHECK_DAILY[ComplianceService:\nSum distributions today\nfor this member] CHECK_DAILY --> DAILY_OK{today_total +\nquantity ≤ 25g?} DAILY_OK -->|No| ERR_DAILY[āŒ Error: Daily limit exceeded\nShow remaining today] DAILY_OK -->|Yes| CHECK_MONTHLY[ComplianceService:\nLoad MonthlyQuota\ncurrent month] CHECK_MONTHLY --> MONTHLY_OK{monthly_total +\nquantity ≤ max_allowed?} MONTHLY_OK -->|No| ERR_MONTHLY[āŒ Error: Monthly quota exceeded\nShow remaining this month\nand reset date] MONTHLY_OK -->|Yes| SEL_BATCH[Admin selects batch] SEL_BATCH --> LOAD_BATCH[Load batch from\nBatchRepository] LOAD_BATCH --> CHECK_BATCH{Batch status\n= AVAILABLE?} CHECK_BATCH -->|RECALLED| ERR_RECALLED[āŒ Error: Batch recalled\nSelect a different batch] CHECK_BATCH -->|EXHAUSTED| ERR_EXHAUSTED[āŒ Error: Batch exhausted\nNo stock remaining] CHECK_BATCH -->|AVAILABLE| CHECK_STOCK{batch.quantity_grams\n≄ requested quantity?} CHECK_STOCK -->|No| ERR_STOCK[āŒ Error: Insufficient stock\nShow available quantity] CHECK_STOCK -->|Yes| CONFIRM[Admin reviews and confirms\ndistribution details] CONFIRM --> SAVE_DIST["šŸ’¾ Save Distribution record\n(immutable = true,\nrecorded_by = currentUser)"] SAVE_DIST --> UPD_QUOTA["šŸ’¾ UPDATE MonthlyQuota\ntotal_distributed += quantity\n(@Version optimistic lock)"] UPD_QUOTA --> UPD_STOCK["šŸ’¾ INSERT StockMovement\n(type = OUT, batch_id, qty)"] UPD_STOCK --> UPD_BATCH["šŸ’¾ UPDATE Batch\nquantity_grams -= quantity\n(if = 0 → status = EXHAUSTED)"] UPD_BATCH --> SUCCESS([āœ… Success\nShow confirmation\nwith updated quota display]) ``` --- ## Flow 2: Member Registration Registers a new member in the club. Includes DSGVO consent, age validation, under-21 flag assignment, and automatic portal account creation. ```mermaid flowchart TD START([🟢 Admin opens\n'Add Member' form]) --> ENTER_DATA[Admin enters member data:\nfirst/last name, email,\ndate of birth, address] ENTER_DATA --> VALIDATE_EMAIL{Email unique\nin this club?} VALIDATE_EMAIL -->|Already exists| ERR_EMAIL[āŒ Error: Email already\nregistered in this club] VALIDATE_EMAIL -->|Unique| VALIDATE_AGE{Age ≄ 18?} VALIDATE_AGE -->|Under 18| ERR_AGE[āŒ Error: Member must be\nat least 18 years old\n§ 10 KCanG] VALIDATE_AGE -->|18 or older| CHECK_UNDER21{18 ≤ age < 21?} CHECK_UNDER21 -->|Yes| SET_FLAG_TRUE["Set is_under_21 = true\nMonthly limit will be 30g"] CHECK_UNDER21 -->|No, ≄ 21| SET_FLAG_FALSE["Set is_under_21 = false\nMonthly limit will be 50g"] SET_FLAG_TRUE --> CHECK_CAPACITY[Check Club.max_members\nvs current member count] SET_FLAG_FALSE --> CHECK_CAPACITY CHECK_CAPACITY --> CAPACITY_OK{Club has\nfree capacity?} CAPACITY_OK -->|No| ERR_CAPACITY[āŒ Error: Club at max capacity\nCannot register more members] CAPACITY_OK -->|Yes| GEN_NUMBER["Generate membership_number\n(club prefix + sequential ID)"] GEN_NUMBER --> DSGVO[Show DSGVO consent dialog:\n• Data usage explanation\n• Right to erasure\n• Admin must confirm consent obtained] DSGVO --> DSGVO_OK{Admin confirms\nconsent obtained?} DSGVO_OK -->|No| ABORT([šŸ”“ Abort — member\ncannot be registered\nwithout DSGVO consent]) DSGVO_OK -->|Yes| SAVE_MEMBER["šŸ’¾ Save Member\n(status = ACTIVE,\nmembership_date = today)"] SAVE_MEMBER --> CREATE_USER["šŸ’¾ Create User account\n(role = ROLE_MEMBER,\ngenerate temp password)"] CREATE_USER --> SEND_EMAIL["šŸ“§ Send welcome email:\n• Membership number\n• Temp login credentials\n• Portal URL\n• DSGVO information sheet PDF"] SEND_EMAIL --> SUCCESS([āœ… Member registered\nShow member profile\nwith membership number]) ``` --- ## Flow 3: Contamination Batch Recall Handles the recall of a contaminated batch. This flow is time-critical — speed of notification is essential for member safety. All affected distributions are identified and the prevention officer is notified. ```mermaid flowchart TD START([🟢 Admin selects batch\nand clicks 'Flag Recall']) --> CONFIRM_RECALL{Confirm recall\nof batch?\nThis cannot be undone.} CONFIRM_RECALL -->|Cancel| CANCEL([šŸ”“ Cancelled — batch\nstatus unchanged]) CONFIRM_RECALL -->|Confirm| QUERY_DIST["šŸ” Query all Distributions\nWHERE batch_id = :batchId\n(across all members)"] QUERY_DIST --> HAS_DIST{Any distributions\nfound?} HAS_DIST -->|No distributions| NO_DIST["āš ļø Batch was never distributed\n(still flag as RECALLED\nfor inventory integrity)"] HAS_DIST -->|Yes| BUILD_LIST["Build affected member list:\n• member name\n• distribution date\n• quantity received\n• contact email"] NO_DIST --> FLAG_BATCH BUILD_LIST --> SHOW_LIST[Show affected member list\nto admin for review] SHOW_LIST --> ADMIN_REVIEW{Admin reviews\nand confirms recall?} ADMIN_REVIEW -->|Cancel| CANCEL ADMIN_REVIEW -->|Proceed| FLAG_BATCH["šŸ’¾ UPDATE Batch\nstatus = RECALLED\ncontamination_flag = true"] FLAG_BATCH --> LOG_MOVEMENT["šŸ’¾ INSERT StockMovement\n(type = RECALL,\nbatch_id, reason)"] LOG_MOVEMENT --> EXPORT_LIST["šŸ“„ Generate export:\n• CSV: affected_members_recall_{batchCode}.csv\n• PDF: recall_report_{batchCode}.pdf\n(via iText 7)"] EXPORT_LIST --> NOTIFY_OFFICER["šŸ“§ Email Prevention Officer:\n• Batch code and details\n• Affected member count\n• Attached CSV/PDF"] NOTIFY_OFFICER --> AUDIT_LOG["šŸ’¾ INSERT AuditLog\n(action = BATCH_RECALL,\nperformedBy, timestamp)"] AUDIT_LOG --> SUCCESS([āœ… Recall complete\nOffer download of\nexport files]) ``` --- ## Flow 4: Compliance Report Generation Generates the monthly compliance report required by § 22 KCanG. Covers all distributions within a calendar month, with per-member quota analysis and club metadata for regulatory submission. ```mermaid flowchart TD START([🟢 Admin opens\nReports section]) --> SELECT_PERIOD[Admin selects\nmonth and year] SELECT_PERIOD --> VALIDATE_PERIOD{Period in the\npast or current\nmonth?} VALIDATE_PERIOD -->|Future month| ERR_FUTURE[āŒ Error: Cannot generate\nreport for future periods] VALIDATE_PERIOD -->|Valid| LOAD_CLUB[Load Club metadata:\nlicense number,\nprevention officer name] LOAD_CLUB --> QUERY_DIST["šŸ” ReportService:\nSELECT * FROM distributions\nWHERE month = :month\nAND year = :year\nAND tenant_id = :tenantId"] QUERY_DIST --> HAS_DATA{Any distributions\nin this period?} HAS_DATA -->|No data| EMPTY_REPORT[Generate empty report\nwith zero totals\n(still valid compliance submission)] HAS_DATA -->|Yes| AGG_MEMBER["Aggregate by member:\n• total_distributed_grams\n• number_of_visits\n• quota_usage_percent\n• is_under_21 flag"] EMPTY_REPORT --> AGG_STRAIN AGG_MEMBER --> AGG_STRAIN["Aggregate by strain/batch:\n• strain name, THC%, CBD%\n• quantity distributed\n• batch codes used"] AGG_STRAIN --> ADD_METADATA["Add club metadata:\n• Club name + license number\n• Prevention officer name\n• Report generation timestamp\n• Total members active in period"] ADD_METADATA --> RENDER_PDF["šŸ“„ iText 7:\nRender PDF report\n• Cover page with club details\n• Summary table\n• Per-member breakdown\n• Strain/batch appendix"] RENDER_PDF --> RENDER_CSV["šŸ“Š Generate CSV:\n• One row per distribution\n• member_id, name, date,\n quantity, strain, batch_code"] RENDER_CSV --> STORE_FILES["šŸ’¾ Store generated files\ntemporarily in server /tmp\n(TTL: 1 hour)"] STORE_FILES --> SUCCESS([āœ… Report ready\nOffer download:\nšŸ“„ PDF šŸ“Š CSV]) ``` --- ## Flow 5: Member Login & Quota Display The member portal entry flow. Members log in to view their current monthly quota, remaining allowance, and recent distribution history. This is a read-only portal — members cannot modify any data. ```mermaid flowchart TD START([🟢 Member navigates\nto member portal URL]) --> SHOW_LOGIN[Show login form:\nemail + password] SHOW_LOGIN --> SUBMIT[Member submits credentials] SUBMIT --> FIND_USER["šŸ” Spring Security:\nSELECT FROM users\nWHERE email = ?\nAND active = true"] FIND_USER --> USER_FOUND{User found?} USER_FOUND -->|No| ERR_NOTFOUND[āŒ Invalid credentials\n(generic — do not reveal\nwhether email exists)] USER_FOUND -->|Yes| VERIFY_PW{BCrypt.verify\n(password, hash)\nmatches?} VERIFY_PW -->|No| ERR_PW[āŒ Invalid credentials] VERIFY_PW -->|Yes| CHECK_MEMBER{User has\nmember_id set?} CHECK_MEMBER -->|No — admin account| ERR_NOTMEMBER[āŒ Error: Use admin portal\nfor admin accounts] CHECK_MEMBER -->|Yes| ISSUE_JWT["šŸ”‘ Issue JWT:\n• role = ROLE_MEMBER\n• tenantId = user.tenantId\n• memberId = user.memberId\n• expiry = 8h"] ISSUE_JWT --> UPDATE_LOGIN["šŸ’¾ UPDATE users\nlast_login = NOW()"] UPDATE_LOGIN --> LOAD_PORTAL["Load member portal\n(JSF view or SPA)"] LOAD_PORTAL --> CALL_QUOTA["šŸ“” GET /api/v1/members/me/quota\n(JWT in Authorization header)"] CALL_QUOTA --> FETCH_QUOTA["šŸ” QuotaController:\nLoad MonthlyQuota\nfor current month\n(create if not exists)"] FETCH_QUOTA --> CALC_REMAINING{Quota record\nexists?} CALC_REMAINING -->|No — new month| CREATE_QUOTA["Create MonthlyQuota row:\ntotal_distributed = 0\nmax_allowed = 30g or 50g"] CALC_REMAINING -->|Yes| RETURN_QUOTA["Return QuotaStatus:\n• totalAllowed\n• totalUsed\n• remaining\n• percentUsed"] CREATE_QUOTA --> RETURN_QUOTA RETURN_QUOTA --> DISPLAY_PROGRESS["Display quota progress bar:\n🟩🟩🟩⬜⬜ e.g. 15g of 50g used\nColor: green < 60% / yellow < 85% / red ≄ 85%"] DISPLAY_PROGRESS --> CALL_HISTORY["šŸ“” GET /api/v1/distributions\n?memberId=me&limit=10\n&sort=distributed_at,desc"] CALL_HISTORY --> DISPLAY_HISTORY["Display last 10 distributions:\n• Date, quantity, strain name\n• Batch code\n• Recorded by (staff name)"] DISPLAY_HISTORY --> SUCCESS([āœ… Member portal loaded\nQuota + history visible]) ``` --- ## Flow Summary | Flow | Trigger | Key Service | Critical Constraint | |---|---|---|---| | Distribution Recording | Admin records handout | `ComplianceService` | Daily 25g + monthly 30g/50g limits | | Member Registration | Admin adds new member | `MemberService` | Age ≄ 18, DSGVO consent mandatory | | Batch Recall | Admin flags contamination | `ComplianceService.recallBatch()` | Immediate prevention officer notification | | Report Generation | Admin requests monthly report | `ReportService` | iText 7 PDF + CSV for regulatory filing | | Member Login | Member accesses portal | `AuthService` + `QuotaController` | JWT stateless, read-only member view | ### Error Handling Conventions All flows follow these conventions for user-facing error messages: - **Compliance errors** (`422 Unprocessable Entity`): Always include remaining quota/allowance so the admin knows what quantity would be valid - **Validation errors** (`400 Bad Request`): Include the specific `field` and a human-readable `message` in German (UI locale) - **Permission errors** (`403 Forbidden`): Generic message — do not reveal tenant or role details - **System errors** (`500 Internal Server Error`): Log full stack trace; show generic user message; alert via email to club admin ### Transaction Boundaries The Distribution Recording flow (Flow 1) executes steps `SAVE_DIST → UPD_QUOTA → UPD_STOCK → UPD_BATCH` in a **single `@Transactional` block**. If any step fails (e.g., optimistic lock collision on `MonthlyQuota`), the entire transaction rolls back and no partial state is persisted.