Architecture العربية

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 users row carries identity and credentials; each actor type gets a profile entity owned by its module (Consultancy\ConsultantProfile, Directory\ProviderProfile). This keeps MOD-IAM from becoming the dumping ground every module reaches into — the classic Laravel failure where User accumulates 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 the MOD-ONB data 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 before hook returning false for 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 return null on 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=&amp;city=&amp;type=&amp;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-BRD treats 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:

  1. 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.
  2. 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_submissions table 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 and SRC-MOM §14 exclude video e-learning, learning paths, and learning analytics. SRC-TRN specifies videos as a learning method, practical assessments, branch comparison, and overdue-course tracking. The flow below implements the BRD's narrower scope. If SRC-TRN is 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-04MOD-LRN and MOD-ASMT are 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 by SRC-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.

Tech Lead · Amir Haroun Draft v0.1 · research & design only