arc42 §6b — Feature Data Flows: Core Platform
Status: Draft v0.1 · Date: 2026-07-20
Covers: SRC-BRD §7.1 — User Management · Account Verification · Consultancy · Templates · Training · Assessments · Certification
Companion: 06c-feature-flows-commerce-admin.md · 06-runtime-view.md
Feature → Flow Index#
Every MVP feature in SRC-BRD §7.1, mapped to the flow that describes it.
| BRD §7.1 Feature | Module | Flow | Document |
|---|---|---|---|
| Individual / Company / Consultant / Provider registration | MOD-IAM |
F-01 | this |
| Google Sign-In · Email · Phone registration | MOD-IAM |
Flow 1 | 06 |
| Profile management | MOD-IAM |
F-02 | this |
| Employee accounts | MOD-IAM |
F-03 | this |
| Legal document upload · Admin review · Approve/Reject/Resubmit | MOD-ONB |
Flow 2 | 06 |
| Account activation state machine | MOD-ONB |
F-04 | this |
| Consultant directory · profiles | MOD-CONS |
F-05 | this |
| Consultation booking · appointment approval · status | MOD-CONS |
Flow 3 | 06 |
| Video consultation | MOD-VID |
Flow 3 | 06 |
| On-site consultation | MOD-CONS |
F-06 | this |
| Free operational templates · downloads · category browsing | MOD-TPL |
F-07 | this |
| Customized template requests | MOD-TPL |
F-08 | this |
| Educational content · employee learning · company assignment · progress | MOD-LRN |
F-09 | this |
| MCQ examinations · auto-grading · passing score · history | MOD-ASMT |
Flow 4 | 06 |
| Certificate generation · download · history | MOD-CERT |
Flow 4 | 06 |
| Supplier / provider directory · search · filtering | MOD-MKT |
Flow 5 | 06 |
| Featured listings | MOD-MKT |
C-04 | 06c |
| All payment flows | MOD-PAY |
C-01…C-05 | 06c |
| Administration · approvals · reports · dashboard | MOD-ADM |
C-06…C-08 | 06c |
| Notifications | MOD-NOTIF |
C-09 | 06c |
Level 0 — System-Wide Data Flow Map#
How data moves through the platform's pipelines. Four distinct pipelines with different latency, durability, and security characteristics.
flowchart TB
subgraph SRC["Data Sources"]
U["👤 Users<br/>5 actor types"]
ADM["🛡️ Admins"]
EXT["🔌 External<br/>payment · video · SMS"]
end
subgraph P1["Pipeline 1 — Transactional (synchronous)"]
direction LR
API1["API"] --> VAL["Validate<br/>+ AuthZ<br/>+ Tenant scope"] --> TX[("PostgreSQL<br/>ACID")]
end
subgraph P2["Pipeline 2 — Document (async, untrusted)"]
direction LR
UP["Upload"] --> QUAR["Quarantine<br/>bucket"] --> SCAN["Malware<br/>scan"] --> PROM["Promote"] --> STORE[("Object Storage<br/>encrypted")]
end
subgraph P3["Pipeline 3 — Generation (async, burst)"]
direction LR
TRIG["Trigger"] --> JQ["Job queue"] --> REND["Render<br/>Arabic-capable"] --> ART[("Artifacts<br/>certificates · docs")]
end
subgraph P4["Pipeline 4 — Projection (async, eventually consistent)"]
direction LR
EVT["Domain<br/>events"] --> OUT["Outbox"] --> PROJ["Projectors"]
PROJ --> IDX[("Search index")]
PROJ --> RPT[("Reporting<br/>read model")]
PROJ --> NOT["Notifications"]
end
U --> API1
ADM --> API1
EXT -->|"webhooks<br/>signature-verified<br/>idempotent"| API1
U --> UP
TX -.->|"emits"| EVT
TX -.->|"triggers"| TRIG
STORE -.-> REND
ART --> STORE
style P1 fill:#e8f0fe,stroke:#1168bd
style P2 fill:#fde8e8,stroke:#c94a4a
style P3 fill:#e8f5e9,stroke:#2d8659
style P4 fill:#fff4e5,stroke:#d98c1f
Pipeline characteristics#
| Pipeline | Latency | Consistency | Failure behaviour | Security posture |
|---|---|---|---|---|
| 1 — Transactional | p95 < 300 ms | Strong (ACID) | Fail fast, surface to user | Tenant scope + RLS enforced here |
| 2 — Document | Accept < 1 s, scan p95 < 60 s | Eventually visible | Fail closed — never promote unscanned | Highest — untrusted input, isolated bucket |
| 3 — Generation | p95 < 2 min | Eventually available | Retry; source record already durable | Renders tenant data — scope must be re-established in job |
| 4 — Projection | Seconds | Eventually consistent | Retry; never authoritative | Index is for ranking only; DB is truth |
The governing rule: pipelines 2–4 may fail without losing user work, because pipeline 1 has already durably committed the record that matters. An assessment result survives a PDF failure; a booking survives a notification failure; a listing survives an indexing failure.
F-01 — Registration by Account Type#
SRC-BRD §7.1, §10 · SRC-MOM REQ-01 · SRC-UFC
The single most branched flow in the system. Four self-registering types with different required data and different activation paths.
flowchart TB
START["User opens app"] --> CHOOSE{"Select<br/>account type"}
CHOOSE -->|Individual| I1["Collect: name, email/phone<br/>Optional: profession"]
CHOOSE -->|Company| C1["Collect: company name, CR number,<br/>contact person, city, business type"]
CHOOSE -->|Consultant| K1["Collect: name, specialties,<br/>credentials, bio"]
CHOOSE -->|Service Provider| S1["Collect: company name, CR,<br/>service categories, coverage area"]
I1 --> VERIFY["Verify identity channel<br/>OTP or Google ID token"]
C1 --> VERIFY
K1 --> VERIFY
S1 --> VERIFY
VERIFY --> CREATE["Create user + type-specific profile<br/><i>one transaction</i>"]
CREATE --> GATE{"Approval<br/>required?<br/><i>CON-04</i>"}
GATE -->|"Individual — No"| ACTIVE["✅ state: active<br/>Full access to<br/>individual capabilities"]
GATE -->|"Company / Consultant /<br/>Provider — Yes"| PENDING["⏳ state: pending_documents"]
PENDING --> DOCS["Prompt: upload legal documents<br/><i>→ Flow 2 in 06</i>"]
DOCS --> REVIEW["state: under_review"]
REVIEW --> DECIDE{"Admin<br/>decision"}
DECIDE -->|Approved| ACTIVE2["✅ state: active"]
DECIDE -->|Rejected| REJ["❌ state: rejected + reason"]
REJ --> RESUB["Resubmission allowed<br/><i>new document version</i>"]
RESUB --> REVIEW
ACTIVE --> CAP["Capabilities resolved from<br/>actor type + account state"]
ACTIVE2 --> CAP
style PENDING fill:#fff4e5,stroke:#d98c1f
style REJ fill:#fde8e8,stroke:#c94a4a
style ACTIVE fill:#e8f5e9,stroke:#2d8659
style ACTIVE2 fill:#e8f5e9,stroke:#2d8659
Data written#
| Store | Data |
|---|---|
users |
identity, credentials, actor_type discriminator, locale |
{type}_profiles |
type-specific attributes — owned by the relevant module, not bolted onto users |
account_state_transitions |
immutable audit — actor, from, to, reason, timestamp |
Design note. Six actor types are not six roles on one table. One
usersrow carries identity and credentials; each actor type gets a profile entity owned by its module (Consultancy\ConsultantProfile,Directory\ProviderProfile). This keepsMOD-IAMfrom becoming the dumping ground every module reaches into — the classic Laravel failure whereUseraccumulates relations from everywhere and becomes unextractable (R-08).
Open questions#
OQ-02— exact required document set per type. Currently unknown whether Company needs CR only, or also municipal licence and food-safety certificate. Blocks theMOD-ONBdata model.OQ-12— is registration data captured bilingually?
F-02 — Profile Management#
Deceptively simple; the authorization matters.
flowchart LR
subgraph EDIT["Edit request"]
REQ["PATCH /v1/profile"] --> AUTHZ{"Who is<br/>editing?"}
end
AUTHZ -->|"Self — Individual/Consultant/Provider"| FULL["Full profile fields<br/>editable"]
AUTHZ -->|"Self — Employee"| LIMITED["⚠ LIMITED fields only<br/><i>BRD §11: 'Limited'</i><br/>name, phone, password, locale"]
AUTHZ -->|"Company admin editing employee"| CO["Employment fields only<br/>NOT credentials"]
AUTHZ -->|"Platform admin"| ADMIN["All fields<br/>📝 audit-logged"]
FULL --> SENSITIVE{"Sensitive<br/>field?"}
LIMITED --> SENSITIVE
CO --> APPLY
ADMIN --> APPLY
SENSITIVE -->|"email / phone"| REVERIFY["Re-verify channel<br/>before applying"]
SENSITIVE -->|"password"| REVOKE["Apply + revoke<br/>ALL token families"]
SENSITIVE -->|"No"| APPLY["Apply change"]
REVERIFY --> APPLY
REVOKE --> APPLY
APPLY --> REINDEX["Emit ProfileUpdated<br/>→ reindex if listing-visible"]
style LIMITED fill:#fff4e5,stroke:#d98c1f
style REVOKE fill:#fde8e8,stroke:#c94a4a
Controls:
- Changing a verified contact channel must re-verify before it becomes the recovery channel — otherwise profile edit becomes an account-takeover path.
- Password change revokes all sessions (
QAS-SEC-04). - Employee profile scope is deliberately narrow per
SRC-BRD§11 — an employee must not be able to alter employment-relevant attributes their employer relies on. - ⚠️ Consultant/provider profile edits that affect directory visibility or pricing should re-enter review if they materially change what was approved. Not specified in sources → raise with sponsor.
F-03 — Employee Account Provisioning#
SRC-BRD §10.2, §10.3 · CON-09
Employees cannot self-register — they exist only under an approved company.
sequenceDiagram
autonumber
participant CO as 🏢 Company Admin
participant API as Qwizin API
participant DB as PostgreSQL
participant N as Notifications
participant EMP as 👷 Employee
CO->>API: POST /v1/company/employees {name, email/phone, job title}
API->>API: AuthZ: is company_admin AND company.state == active
Note over API: ⚠ CON-09: employee cannot exist<br/>under a non-approved company
API->>DB: BEGIN TX
API->>DB: Create user(actor_type: employee, state: invited)
API->>DB: Link employment(company_id, employee_id, job_title)
API->>DB: 📝 Audit: admin X created employee Y
API->>DB: COMMIT
API->>N: Dispatch invitation
N->>EMP: 🔔 Invitation + one-time activation link
EMP->>API: Activate {token, set password}
API->>API: Validate token (single-use, expiring)
API->>DB: user.state = active
API-->>EMP: {access_token, refresh_token}
rect rgb(255,240,240)
Note over CO,EMP: ⚠ Cascade — OQ-07 UNRESOLVED
CO->>API: Deactivate employee
API->>DB: employment.state = terminated
API->>DB: Revoke all token families
Note over DB: Open: what happens to<br/>in-flight assessments?<br/>Already-issued certificates?<br/>→ OQ-07 needs a sponsor ruling
end
The cascade problem#
Company suspension or rejection must propagate to every employee. OQ-07 is unresolved, but the mechanism is settled: tenant context resolution is one place, so the propagation point is single rather than scattered across modules.
Three sub-questions needing a ruling:
| Question | Options |
|---|---|
| In-flight assessment attempts | Void / freeze / allow completion |
| Already-issued certificates | Remain valid / marked suspended / revoked |
| Employee personal access | Lose all access / retain personal certificates only |
The certificate verification endpoint returns current status computed live (
QAS-INT-03), so it must have an answer. This is a business-policy question with a directly visible technical consequence.
F-04 — Account Activation State Machine#
CON-04 · SRC-UFC · QAS-CMP-02
stateDiagram-v2
[*] --> pending_documents: Registration complete<br/>(approval-gated types)
[*] --> active: Registration complete<br/>(Individual)
pending_documents --> under_review: All required documents<br/>uploaded + scanned clean
under_review --> active: ✅ Admin approves ALL<br/>required documents
under_review --> rejected: ❌ Admin rejects<br/>(reason required)
rejected --> pending_documents: User resubmits<br/>(new document version)
active --> suspended: Admin action /<br/>subscription lapse
suspended --> active: Reinstated
active --> closed: User or admin closes
suspended --> closed: Escalation
closed --> [*]
note right of under_review
Activation requires ALL required
documents approved — not the first.
Partial approval keeps the account
in under_review.
end note
note right of rejected
Resubmission creates document
version n+1. NEVER overwrites n —
the evidence behind the original
decision must survive (QAS-CMP-02).
end note
Authorization implication#
"Account is approved and active" is a precondition on the whole session, not a permission. Modelling it as a permission means every new endpoint is unprotected by default — exactly backwards for an approval-gated platform.
Implement as an explicit allowlist: everything is denied while pending, except a short reviewable list (view own profile, upload documents, resubmit, browse public marketplace, log out). A capability added next year is denied by default until someone deliberately allows it.
⚠️ Documented framework footgun. The intuitive implementation — a global
beforehook returningfalsefor unapproved accounts — breaks every policy in the application, because a non-null return short-circuits all authorization. It also disables the admin's ability to review pending accounts. The hook must returnnullon the happy path, and account state belongs in a dedicated capability-resolution layer rather than in the global hook. Reserve the global hook for super-admin elevation only.
F-05 — Consultant Directory & Profile#
flowchart TB
subgraph IDX["Indexing — async, Pipeline 4"]
CE["Consultant approved /<br/>profile updated /<br/>availability changed"] --> DEB["Debounce"]
DEB --> IX[("Search index<br/>name_ar · name_en · specialties ·<br/>city · price_band · rating · languages")]
end
subgraph BROWSE["Browse — Pipeline 1"]
Q["GET /v1/consultants<br/>?specialty=&city=&type=&q="] --> NORM["Normalise Arabic query"]
NORM --> SR["Query index"]
SR --> HYD["Hydrate from DB by ID"]
HYD --> FILT["⚠ Post-filter:<br/>state == active<br/>AND approved"]
FILT --> RES["Ranked results"]
end
subgraph PROFILE["Consultant detail"]
RES --> DET["GET /v1/consultants/{id}"]
DET --> PUB["Public: bio, specialties,<br/>credentials, pricing,<br/>consultation types offered"]
DET --> AVAIL["Availability calendar<br/><i>⚠ OQ-03</i>"]
end
style FILT fill:#fde8e8,stroke:#c94a4a
style AVAIL fill:#fff4e5,stroke:#d98c1f
OQ-03 — availability model is unresolved. Three candidate designs with materially different complexity:
| Model | Consultant effort | System complexity |
|---|---|---|
| Recurring weekly availability + exceptions | Set once | Moderate — slot generation, timezone handling |
| Explicit slot publishing | Ongoing | Low |
| Request-then-negotiate (no published calendar) | Reactive | Lowest, but worst UX and conflicts with the booking-expiry design in Flow 3 |
Recommendation: recurring availability with exceptions. It matches how professionals actually work, and the booking flow's slot-locking already assumes discrete slots exist.
F-06 — On-Site Consultation Request#
SRC-BRD §7.1 · SRC-MOM FR-2
Materially different from a video consultation and not interchangeable — it involves travel, a physical address, and no platform-observable completion signal.
sequenceDiagram
autonumber
participant C as 👤 Customer
participant API as Qwizin API
participant CON as 🎓 Consultant
participant PAY as 💳 Payment
participant N as Notifications
C->>API: POST /v1/bookings {type: on_site, consultant, preferred dates[], site address, scope notes}
API->>API: Validate: consultant offers on_site<br/>+ covers this city
Note over API: ⚠ Coverage area is a<br/>consultant profile attribute —<br/>on-site is geographically bounded
API->>DB: booking(type: on_site, state: pending_quote)
rect rgb(255,245,235)
Note over CON,PAY: Quotation — on-site pricing is not a fixed rate
API->>CON: 🔔 On-site request + scope
CON->>API: Provide quote {fee, travel cost, duration, date}
API->>C: 🔔 Quote received
alt Customer accepts
C->>API: Accept quote
API->>PAY: AUTHORIZE hold
API->>DB: booking(state: confirmed)
else Customer declines / no response
API->>DB: booking(state: expired)
end
end
rect rgb(240,255,240)
Note over C,N: Visit & completion — NO automatic signal
N->>C: 🔔 Reminder T-24h
N->>CON: 🔔 Reminder T-24h
Note over C,CON: Visit happens off-platform
CON->>API: Mark visit completed + upload report
C->>API: Confirm completion
API->>PAY: CAPTURE
Note over API,PAY: ⚠ Capture requires confirmation.<br/>Video sessions have provider webhooks;<br/>on-site has NOTHING — a human must<br/>assert it happened.
end
rect rgb(255,240,240)
alt Dispute — customer says visit did not occur
API->>API: Flag for admin resolution
Note over API: ⚠ Policy undefined in all sources<br/>→ OQ-05
end
end
Why this flow needs separate design#
| Aspect | Video consultation | On-site consultation |
|---|---|---|
| Pricing | Fixed consultant rate | Quoted per request — travel, scope, duration |
| Geography | Irrelevant | Coverage area constrains matching |
| Completion signal | Provider webhook (SessionEnded) |
None — human attestation only |
| Capture trigger | Session completed | Both parties confirm |
| Dispute evidence | Session duration, quality events | Consultant's uploaded report only |
Architecturally significant: on-site consultations have no machine-observable truth. Every state transition after confirmation depends on human assertion, so the audit trail and dispute path carry the entire integrity burden.
SRC-BRDtreats both consultation types as one feature; they are not.
F-07 — Template Catalogue & Free Download#
SRC-BRD §7.1 · SRC-MOM REQ-04 · BR-RL-2 (templates are fully free to all account types)
flowchart TB
subgraph PUB["Publishing — admin, Pipeline 1+2"]
A["🛡️ Admin uploads template"] --> META["Capture structured metadata:<br/>title_ar/en · category · subcategory ·<br/>description · file format · version"]
META --> SCAN2["Malware scan"]
SCAN2 --> STORE[("Object Storage<br/>templates/")]
META --> DBT[("templates table")]
DBT --> IXT["→ Search index"]
end
subgraph BR["Browse — Pipeline 1"]
U["Any authenticated user<br/><i>all types — BR-RL-2</i>"] --> CAT["GET /v1/templates?category="]
CAT --> LIST["Category tree + listings"]
LIST --> DET2["GET /v1/templates/{id}"]
end
subgraph DL["Download"]
DET2 --> DLREQ["POST /v1/templates/{id}/download"]
DLREQ --> AUTHZ2["AuthZ: authenticated<br/><i>no payment gate</i>"]
AUTHZ2 --> LOG["📊 Record download event<br/><i>KPI: templates downloaded</i>"]
LOG --> SIGN["Issue signed URL, short TTL"]
SIGN --> FETCH["Client fetches DIRECTLY<br/>from object storage"]
end
style FETCH fill:#e8f5e9,stroke:#2d8659
Two design points that matter more than they appear:
- The file never passes through the API tier. Bulk transfer through PHP-FPM workers would consume a worker for the full duration of each download — the fastest way to exhaust the request tier. Signed URL, direct fetch, CDN in front.
- Download events are a first-class KPI (
SRC-MOM§13: "number of free templates downloaded and customised"). Capturing them at download time is trivial; reconstructing them later from access logs is not.
SEAM-03 — structured template modelling#
The six specimen files in the source set (SRC-CNT) include three audit workbooks that are row-per-control checklists with compliance-status and responsible-person columns. That structure is a strong signal that "downloadable file" is a transitional form.
Therefore: model templates as structured entities with typed sections and items from day one, even though the MVP only serves a file. The MVP cost is a richer schema behind an unchanged API; the retrofit cost is remodelling the module and migrating every existing record (QAS-MOD-04).
erDiagram
TEMPLATE ||--o{ TEMPLATE_SECTION : contains
TEMPLATE_SECTION ||--o{ TEMPLATE_ITEM : contains
TEMPLATE ||--o{ TEMPLATE_FILE : "has renditions"
TEMPLATE {
id id
string title_ar
string title_en
string category
int version
enum kind "document | checklist | form"
}
TEMPLATE_SECTION {
id id
string title_ar
string title_en
int order
}
TEMPLATE_ITEM {
id id
text prompt_ar
text prompt_en
enum response_type "yes_no | na | text | numeric"
bool required
}
TEMPLATE_FILE {
id id
string format "xlsx | pdf | docx"
string storage_ref
}
Phase 2 adds a
template_submissionstable and the audit product exists. Nothing about the MVP changes.
F-08 — Customized Template Request 💰#
SRC-BRD §7.1, §9.3.2 · SRC-MOM FR-3 / REQ-04 · CON-11 (minimal UI steps — bounce-rate concern)
A revenue-generating flow with a human fulfilment step.
sequenceDiagram
autonumber
participant U as 👤 Customer
participant API as Qwizin API
participant PAY as 💳 Payment
participant ADM as 🛡️ Admin
participant SPC as ✍️ Specialist
participant OBJ as Object Storage
participant N as Notifications
rect rgb(240,240,255)
Note over U,API: Intake — CON-11: MINIMAL steps
U->>API: POST /v1/template-requests<br/>{base_template_id?, business_type, business_size,<br/>requirements_text, attachments?}
Note over U,API: ⚠ Deliberately short form.<br/>Every extra field costs conversion<br/>on a paid action.
API->>DB: request(state: submitted)
API->>API: Determine price<br/><i>fixed / tiered / quoted</i>
end
rect rgb(255,245,235)
Note over U,PAY: Payment
alt Fixed or tiered price
API-->>U: {price}
U->>API: Pay {idempotency_key}
API->>PAY: AUTHORIZE + CAPTURE
Note over API,PAY: Capture immediately — unlike bookings,<br/>there is no counterparty acceptance step
API->>DB: request(state: paid)
else Quote required
API->>ADM: 🔔 Quote needed
ADM->>API: Provide quote
API->>U: 🔔 Quote
U->>API: Accept + pay
API->>DB: request(state: paid)
end
end
rect rgb(240,255,240)
Note over ADM,OBJ: Fulfilment — HUMAN work, off-platform
API->>ADM: 🔔 New paid request in queue
ADM->>API: Assign to specialist
API->>DB: request(state: in_progress, assignee, due_date)
N->>U: 🔔 In progress, expected by {date}
SPC->>API: Upload deliverable
API->>API: Malware scan (Pipeline 2)
API->>OBJ: Store → deliverables/
API->>DB: request(state: delivered)
N->>U: 🔔 Ready for download
U->>API: Download via signed URL
API->>DB: 📊 Record delivery acceptance
end
rect rgb(255,240,240)
alt Revision requested
U->>API: Request revision {notes}
API->>DB: request(state: revision_requested)
Note over API: ⚠ How many revisions are included?<br/>Undefined in all sources → OQ-14
end
end
Operational reality this flow exposes#
This is not an automated feature. It is a service-delivery workflow with a human in the middle, and it has properties the source documents do not address:
| Gap | Why it matters | New OQ |
|---|---|---|
| Turnaround SLA | Customer has paid and is waiting. No stated delivery commitment. | OQ-15 |
| Revision policy | Unlimited revisions on a fixed fee is an unbounded liability | OQ-14 |
| Fulfilment capacity | Demand is unbounded; specialist capacity is not. Needs a queue-depth signal and possibly intake throttling. | OQ-16 |
| Refund on non-delivery | Money captured up front with no automated delivery guarantee | OQ-05 |
Architectural consequence: because fulfilment is manual, the system's job is queue management, SLA tracking, and evidence retention — not document generation. Designing it as a generation feature would be a category error.
F-09 — Training Assignment & Learning Progress#
SRC-BRD §7.1, §10.2 · SRC-TRN · ⚠️ subject to CONF-01
⚠️ Scope conflict — read before implementing.
SRC-BRD§7.2 andSRC-MOM§14 exclude video e-learning, learning paths, and learning analytics.SRC-TRNspecifies videos as a learning method, practical assessments, branch comparison, and overdue-course tracking. The flow below implements the BRD's narrower scope. IfSRC-TRNis in scope, this flow changes materially — video storage, transcoding, CDN, a human-graded assessment path, and a company→branch hierarchy all become required.
flowchart TB
subgraph AUTHOR["Content authoring — Admin"]
A1["🛡️ Admin creates learning content"] --> A2["Structured: module → lessons<br/>title_ar/en · body_ar/en · order ·<br/>estimated_duration · category"]
A2 --> A3["Link assessment<br/><i>MOD-ASMT</i>"]
A3 --> A4["Publish → available to assign"]
end
subgraph ASSIGN["Assignment — Company Admin"]
B1["🏢 Company admin selects content"] --> B2["Select employees<br/><i>individual / all / by job title</i>"]
B2 --> B3["Set due date (optional)"]
B3 --> B4["Create assignment records<br/><i>one per employee</i>"]
B4 --> B5["🔔 Notify employees"]
end
subgraph LEARN["Learning — Employee"]
C1["👷 Employee opens assigned module"] --> C2["Read lesson content"]
C2 --> C3["Mark lesson complete"]
C3 --> C4{"All lessons<br/>complete?"}
C4 -->|No| C2
C4 -->|Yes| C5["Unlock assessment"]
C5 --> C6["→ Flow 4: MCQ assessment"]
end
subgraph TRACK["Progress — Company Admin"]
D1["Progress events"] --> D2[("Progress read model<br/>Pipeline 4")]
D2 --> D3["Company dashboard:<br/>assigned · started · completed ·<br/>passed · certificate issued"]
end
A4 --> B1
B5 --> C1
C3 -.->|"emits LessonCompleted"| D1
C6 -.->|"emits AssessmentPassed"| D1
style C6 fill:#e8f0fe,stroke:#1168bd
Data model#
erDiagram
LEARNING_MODULE ||--o{ LESSON : contains
LEARNING_MODULE ||--o| ASSESSMENT : "gated by"
LEARNING_MODULE ||--o{ ASSIGNMENT : "assigned via"
ASSIGNMENT }o--|| EMPLOYEE : "to"
ASSIGNMENT ||--o{ LESSON_PROGRESS : tracks
ASSIGNMENT {
id id
id company_id "tenant scope"
id employee_id
id module_id
date due_date
enum state "assigned|in_progress|completed|overdue"
timestamp assigned_at
}
LESSON_PROGRESS {
id assignment_id
id lesson_id
timestamp completed_at
}
Tenant scoping note: ASSIGNMENT carries company_id and is subject to RLS. Learning content is platform-global and not tenant-scoped — a common modelling mistake would be scoping content to companies, which would prevent platform-authored material from being shared.
Progress tracking is a projection, not a query#
Company dashboards read from a projection built in Pipeline 4, not by aggregating transactional tables live. SRC-MOM KPI 3 ("% of employees passing assessments and receiving certificates") is a reporting concern, and computing it from assignments ⋈ attempts ⋈ certificates on every dashboard load will degrade as employee counts grow.
SEAM-04 and SEAM-06#
SEAM-04—MOD-LRNandMOD-ASMTare separate modules with a contract between them, because a full LMS is the loudest deferred capability and it grows fast if it grows at all.SEAM-06— assignment targets are modelled through a level of indirection (employee, or a group of employees) rather than hardcoding employee-only. Departments are deferred (SRC-BRD§10.2) and branches are implied bySRC-TRN; retrofitting hierarchy into a flat assignment model is expensive.
New Open Questions Raised by This Analysis#
| ID | Question | Flow | Blocking |
|---|---|---|---|
OQ-14 |
How many revisions are included in a customized template request? | F-08 | Yes — unbounded liability |
OQ-15 |
What is the delivery SLA for customized requests? | F-08 | Yes — customer has paid |
OQ-16 |
Is there a fulfilment capacity limit / intake throttle? | F-08 | Medium |
OQ-17 |
Do consultant profile edits affecting pricing or visibility re-enter review? | F-02 | Medium |
OQ-18 |
On-site consultations: is pricing quoted per request or a fixed consultant rate? | F-06 | Yes — blocks pricing model |
OQ-19 |
Who bears the cost when an on-site visit is disputed? | F-06 | Yes — no machine-observable truth |
Continue to: 06c-feature-flows-commerce-admin.md — payments, subscriptions, featured listings, admin portal, notifications, reporting.