arc42 §6c — Feature Data Flows: Commerce & Administration
Status: Draft v0.1 · Date: 2026-07-20
Covers: SRC-BRD §7.1 — Payment · Marketplace · Administration · Reports · Notifications
Companion: 06b-feature-flows-core.md · 06-runtime-view.md
⚠️ Every flow in this document is provisional on
R-01(SAMA licensing). If Qwizin may not hold and disburse funds, the payment topology changes — not the features, but who moves the money. All flows below are drawn for Model B (a licensed PSP moves funds; Qwizin transmits instructions), which is the lower-risk posture regardless of the legal answer.
Flow Index#
| BRD §7.1 Feature | Flow |
|---|---|
| Consultation payment | C-01 |
| Customized request payment | F-08 in 06b |
| Provider subscription payment | C-02 |
| Featured listing payment | C-03 |
| Featured listings (ranking effect) | C-04 |
| Payout to consultants & providers | C-05 |
| Multi-role administration · user approval | C-06 |
| Content management · consultant & provider management | C-07 |
| Payment monitoring · reports · dashboard | C-08 |
| Notifications (BRD §13 deps 8/12/13) | C-09 |
C-00 — Money Flow Topology (Model B)#
SRC-BRD §9.4 describes revenue flowing Customer → Qwizin Platform → Provider/Consultant. That is Model A, and it is the shape that triggers licensing.
flowchart TB
subgraph MA["❌ Model A — Qwizin holds funds"]
direction LR
CA["👤 Customer"] -->|pays| QA["Qwizin<br/>merchant account"]
QA -->|"holds balance"| QB[("Qwizin<br/>ledger")]
QB -->|disburses| PA["🎓 Consultant"]
end
subgraph MB["✅ Model B — Qwizin never touches funds"]
direction LR
CB["👤 Customer"] -->|pays| PSP["💳 SAMA-licensed PSP"]
PSP -->|"split settlement"| PB["🎓 Consultant<br/><i>sub-merchant under<br/>PSP's licence</i>"]
PSP -->|"commission"| QC["Qwizin<br/><i>separate merchant txn</i>"]
QD["Qwizin API"] -.->|"transmits split<br/>instructions only"| PSP
end
style MA fill:#fde8e8,stroke:#c94a4a
style MB fill:#e8f5e9,stroke:#2d8659
| Model A | Model B ✅ | |
|---|---|---|
| Funds rest in a Qwizin-controlled account | Yes | No |
| Qwizin runs a balance ledger | Yes | No |
| Likely regulatory status | Payment institution | Technology provider |
| Consultant onboarding | Qwizin's problem | PSP's KYC/AML process |
Decision: design for Model B. If counsel later confirms Model A is permissible, moving toward it is straightforward. The reverse — discovering mid-build that a licence is required — is the expensive direction. Do not build a ledger with user-owned balances until
R-01is answered.
Gateway selection criteria generated by these flows#
Feeding OQ-01. A gateway failing any of these is disqualified:
| Requirement | Source flow |
|---|---|
| mada support | KSA market reality |
| Authorize / capture / void separation | C-01 — resolves CONF-02 |
| Refunds, full and partial | C-01, F-08 |
| Marketplace split payments / sub-merchant onboarding | C-05 — Model B |
| Recurring billing with retry logic | C-02 |
| Signed, replayable webhooks | all |
| Hosted fields or redirect (card data never touches Qwizin) | PCI scope minimisation |
C-01 — Consultation Payment#
Detailed sequence in Flow 3. Summarised here as the state machine, because the states are what the rest of the system reacts to.
stateDiagram-v2
[*] --> pending_payment: Booking created,<br/>slot locked
pending_payment --> awaiting_consultant: 💳 AUTHORIZE (hold)<br/><i>funds NOT taken</i>
pending_payment --> abandoned: Payment window expires<br/>→ release slot
awaiting_consultant --> confirmed: Consultant accepts<br/>→ 💳 CAPTURE
awaiting_consultant --> declined: Consultant rejects<br/>→ 💳 VOID
awaiting_consultant --> expired: SLA timer elapses<br/>→ 💳 VOID + release slot
confirmed --> completed: Session held<br/>→ eligible for payout
confirmed --> no_show_customer: Customer absent
confirmed --> no_show_consultant: Consultant absent<br/>→ 💳 REFUND
confirmed --> cancelled_by_customer: → refund per policy ⚠
confirmed --> failed_technical: Session never established<br/>→ 💳 REFUND ⚠
completed --> [*]
declined --> [*]
expired --> [*]
note right of awaiting_consultant
AUTHORIZE not CAPTURE.
This is the fix for CONF-02 —
money is never taken for a
service not yet agreed.
end note
note right of cancelled_by_customer
⚠ Refund policy UNDEFINED
in all source documents.
Free cancellation window?
Partial refund tiers?
→ OQ-05
end note
Idempotency — non-negotiable#
Every payment-mutating call carries a client-supplied idempotency key, and every gateway webhook is signature-verified and processed idempotently keyed on the gateway's event ID. Gateways retry. Duplicate processing of a capture is a double charge, which is both a customer-trust event and a chargeback.
C-02 — Service Provider Subscription#
SRC-BRD §7.1, §9.3.3 · SRC-MOM BR-3 · CON-07 (plans configurable via Admin Panel)
Subscription state is what gates directory visibility — this is the platform's recurring revenue backbone.
sequenceDiagram
autonumber
participant SP as 🚚 Service Provider
participant API as Qwizin API
participant PAY as 💳 PSP
participant SCH as Scheduler
participant IDX as Search Index
participant N as Notifications
rect rgb(240,240,255)
Note over SP,IDX: Initial subscription
SP->>API: GET /v1/subscription-plans
Note over API: Plans are ADMIN-CONFIGURABLE data,<br/>not code (CON-07).<br/>Monthly + annual tiers.
SP->>API: POST /v1/subscriptions {plan_id}
API->>API: ⚠ Precondition: account.state == active<br/>(documents approved — BR-RL-1)
API->>PAY: Create subscription / initial charge
PAY-->>API: Confirmed
API->>DB: subscription(state: active, period_end)
API->>IDX: 🔍 Index listing as VISIBLE
N->>SP: 🔔 Subscription active
end
rect rgb(255,245,235)
Note over SCH,IDX: Renewal cycle
SCH->>API: T-7d: upcoming renewal
N->>SP: 🔔 Renewal reminder
SCH->>PAY: Charge on period_end
alt Payment succeeds
PAY-->>API: Confirmed
API->>DB: period_end += interval
else Payment fails
API->>DB: subscription(state: past_due)
N->>SP: 🔔 Payment failed
loop Dunning: retry schedule
SCH->>PAY: Retry
end
alt Still failing after grace period
API->>DB: subscription(state: lapsed)
API->>IDX: 🔍 Remove listing from index
N->>SP: 🔔 Listing no longer visible
Note over API,IDX: ⚠ Grace period length UNDEFINED<br/>→ OQ-20
end
end
end
rect rgb(255,240,240)
Note over SP,IDX: Cancellation
SP->>API: DELETE /v1/subscriptions/{id}
API->>DB: cancel_at_period_end = true
Note over API: Access retained until period_end —<br/>they paid for it. Do NOT revoke immediately.
SCH->>API: At period_end
API->>IDX: 🔍 Remove from index
end
Subscription state gates directory visibility#
stateDiagram-v2
[*] --> none: Provider approved,<br/>no subscription
none --> active: Payment succeeds
active --> past_due: Renewal fails
past_due --> active: Retry succeeds
past_due --> lapsed: Grace period exhausted
lapsed --> active: Provider resubscribes
active --> cancelling: Provider cancels
cancelling --> lapsed: period_end reached
note right of none
Visible in directory: ❌
end note
note right of active
Visible in directory: ✅
end note
note right of past_due
Visible: ✅ (grace)
— do not punish a
transient card failure
end note
note right of lapsed
Visible: ❌
end note
⚠️ The index must never be authoritative here. A stale search index showing a lapsed provider as active is simultaneously a commercial problem (unpaid visibility) and a trust problem (customers contacting inactive vendors). Search results are post-filtered against the database before returning (Flow 5).
C-03 — Featured Listing Purchase#
SRC-BRD §7.1, §9.3.4 · SRC-MOM REQ-06
flowchart LR
SP["🚚 Provider"] --> CHK{"Precondition:<br/>active subscription?"}
CHK -->|No| DENY["❌ Featured requires<br/>an active base subscription"]
CHK -->|Yes| PLANS["Featured packages<br/><i>admin-configurable</i><br/>duration · category · placement"]
PLANS --> PAY2["💳 Payment"]
PAY2 --> GRANT["featured_placement<br/>(category, starts_at, ends_at)"]
GRANT --> IDX2["🔍 Reindex with<br/>featured_rank + expiry"]
IDX2 --> RANK["→ C-04 ranking effect"]
SCH2["⏰ Scheduler"] -->|"at ends_at"| EXPIRE["Expire placement"]
EXPIRE --> IDX3["🔍 Reindex without boost"]
style DENY fill:#fde8e8,stroke:#c94a4a
style GRANT fill:#e8f5e9,stroke:#2d8659
Inventory question raised by this flow: is featured placement unlimited (anyone who pays gets boosted) or scarce (N slots per category)?
| Model | Consequence |
|---|---|
| Unlimited | Simple to build. But if every provider in a category is featured, the boost is worthless and the product degrades into a fee for nothing. |
| Scarce (N per category) | Preserves value and supports premium pricing. Requires inventory management, a waitlist, and conflict handling at purchase time. |
Not addressed in any source document → OQ-21. Recommendation: scarce, because an unlimited boost is self-cancelling — but this is a business-model decision.
C-04 — Featured Ranking Effect#
SRC-MOM REQ-06: "providers subscribed to the Featured package appear at the top"
This is where commercial interest and search quality collide, and getting it wrong destroys both.
flowchart TB
Q["🔍 Query: 'مورد أغذية' + filters"] --> NORM["Arabic normalisation<br/><i>alef · teh marbuta · yeh ·<br/>diacritics · tatweel</i>"]
NORM --> REL["Compute relevance score<br/>per candidate"]
REL --> BOOST["Apply featured multiplier"]
BOOST --> CHECK{"Boost model?"}
CHECK -->|"❌ Sort by featured DESC,<br/>then relevance"| BAD["Irrelevant featured providers<br/>appear above relevant ones.<br/><b>Users stop trusting search.</b><br/>Featured product loses value."]
CHECK -->|"✅ Relevance × bounded multiplier"| GOOD["Featured providers outrank<br/>comparable non-featured.<br/>An irrelevant result<br/><b>cannot</b> be promoted."]
GOOD --> DECAY["Apply recency decay<br/><i>stale featured listing must not<br/>outrank a perfect match forever</i>"]
DECAY --> HYD2["Hydrate from DB"]
HYD2 --> POST["⚠ Post-filter:<br/>approved AND subscription current"]
POST --> OUT["Ranked results<br/>+ featured badge"]
style BAD fill:#fde8e8,stroke:#c94a4a
style GOOD fill:#e8f5e9,stroke:#2d8659
style POST fill:#fff4e5,stroke:#d98c1f
The design rule#
Featured boost is a bounded multiplier on relevance, never an override of it. A featured supplier of packaging must not appear above a relevant food supplier when the user searched for food. The moment users learn that top results are bought rather than relevant, they stop reading them — and the featured product they were bought with becomes worthless.
⚠️ This constrains the search engine choice. Research found that one leading candidate (Meilisearch) has no query-time boosting at all — boosting is an index-level ranking rule that, due to bucket sort, only breaks ties among documents already equal on every preceding rule. Promoting it in the rule order degrades text relevance globally, for every query, since it is a setting rather than a parameter. Engines with expressive query-time scoring handle this requirement natively. See
R-11.
C-05 — Payouts to Consultants & Providers ⚠#
The flow most affected by R-01.
sequenceDiagram
autonumber
participant SCH as ⏰ Scheduler
participant API as Qwizin API
participant PSP as 💳 SAMA-licensed PSP
participant CON as 🎓 Consultant
participant ADM as 🛡️ Admin
rect rgb(240,240,255)
Note over CON,PSP: Onboarding — Model B
CON->>API: Provide payout details
API->>PSP: Initiate sub-merchant onboarding
PSP->>CON: KYC / AML directly with PSP
Note over PSP,CON: ⚠ KYC is the PSP's obligation<br/>under ITS licence — not Qwizin's.<br/>This is the point of Model B.
PSP-->>API: {sub_merchant_ref, status}
API->>DB: Store reference ONLY — never bank details
end
rect rgb(240,255,240)
Note over SCH,PSP: Earnings & settlement
SCH->>API: Compute settlement period
API->>DB: Aggregate completed sessions + delivered requests
API->>API: Apply commission
API->>DB: statement(gross, commission, net) — immutable
API->>PSP: Instruct split settlement
PSP->>CON: 💰 Funds — direct, never via Qwizin
API->>DB: 📝 Hash-chained financial audit entry
end
rect rgb(255,240,240)
Note over CON,ADM: ⚠ Highest-value fraud target
CON->>API: Change payout bank details
API->>API: 🔐 Step-up MFA required
API->>API: ⏳ Delay window before effective
API->>CON: 🔔 Out-of-band notification to<br/>PREVIOUS contact channel
API->>DB: 📝 Audit with actor + IP
Note over API,CON: Account takeover → redirect payouts<br/>is the classic marketplace fraud.<br/>MFA + delay + out-of-band alert<br/>are all required, not optional.
end
Controls#
| Control | Rationale |
|---|---|
| Never store bank details — only the PSP's sub-merchant reference | Removes an entire class of breach and compliance burden |
| Step-up MFA on payout-detail change | The single highest-value fraud target in the system |
| Delay window + out-of-band notification to the previous channel | Gives the legitimate owner a chance to intervene after a takeover |
| Hash-chained audit for all financial events | Repudiation risk — an ordinary mutable log is insufficient when anyone with database write access can rewrite history |
| Immutable statements | Corrections are new adjusting entries, never edits |
C-06 — Administration & Approval Workflows#
SRC-BRD §7.1 ("Multi-role administration") · SRC-UFC (Admin Portal)
flowchart TB
subgraph ROLES["⚠ Multi-role administration — CONF-03 unresolved"]
R1["Approval Reviewer<br/>documents, accounts"]
R2["Content Manager<br/>templates, learning, assessments"]
R3["Marketplace Manager<br/>consultants, providers, featured"]
R4["Finance<br/>payments, payouts, refunds"]
R5["Support<br/>read-mostly, impersonation"]
R6["Super Admin<br/>role assignment"]
end
subgraph GUARD["Admin access controls"]
G1["🔐 MFA mandatory"]
G2["Separate origin from<br/>user-facing app"]
G3["📝 EVERY action audited"]
G4["Impersonation explicitly<br/>logged + time-boxed"]
end
subgraph QUEUES["Work queues"]
Q1["📥 Document review<br/><i>oldest-first, SLA-tracked</i>"]
Q2["📥 Custom template requests"]
Q3["📥 Disputes / refunds"]
Q4["📥 Reported content"]
end
ROLES --> GUARD --> QUEUES
style R6 fill:#fde8e8,stroke:#c94a4a
style G1 fill:#fff4e5,stroke:#d98c1f
⚠️ The admin role split above is a proposal, not a specification.
SRC-BRD§7.1 says "multi-role administration" without enumerating the roles, and §11's matrix is internally inconsistent (CONF-03). Authorization cannot be built until a corrected matrix is issued (OQ-06).
Why admin is the highest-value target#
A platform admin can approve accounts, view every tenant's legal documents, and touch payments. Compromise of one admin account is equivalent to compromise of the platform. Hence: mandatory MFA, a separate origin, complete audit coverage, and explicitly logged, time-boxed impersonation — support staff will need to see what a user sees, and that capability must be observable rather than invisible.
C-07 — Content & Marketplace Management#
flowchart LR
subgraph CM["Content management"]
T["Templates<br/>create · version · publish · retire"]
L["Learning modules<br/>author · publish"]
A["Assessments<br/>question banks · passing score ⚠"]
CAT["Categories & taxonomy<br/><i>bilingual</i>"]
end
subgraph MM["Marketplace management"]
CN["Consultants<br/>approve · suspend · verify specialties"]
PR["Providers<br/>approve · suspend · categories"]
FT["Featured inventory<br/><i>if scarce — OQ-21</i>"]
PL["Subscription plans<br/><i>CON-07: configurable</i>"]
end
CM --> PUB["Publish → versioned,<br/>previous version retained"]
MM --> IDX4["→ Reindex affected listings"]
A -.->|"⚠ CON-08"| SCORE["Passing score is<br/>ADMIN-CONFIGURABLE<br/>per assessment"]
style SCORE fill:#fff4e5,stroke:#d98c1f
Two constraints from SRC-BRD §12 that make these data, not code:
CON-07— subscription packages configurable through the Admin PanelCON-08— assessment passing score configurable by the Administrator
Both are trivially satisfied if designed as configuration from the start, and require a code change plus deployment for every adjustment if hardcoded. Changing a passing score must never require a release.
⚠️ Retroactivity question: if an admin lowers a passing score, do previously-failed attempts retroactively pass? Almost certainly not — but the system must decide explicitly rather than accidentally. → OQ-22
C-08 — Reporting & Dashboard#
SRC-BRD §7.1 · SRC-MOM §13 (the four KPIs)
flowchart TB
subgraph SOURCES["Event sources — Pipeline 1"]
E1["BookingConfirmed<br/>ConsultationCompleted"]
E2["TemplateDownloaded<br/>CustomRequestDelivered"]
E3["AssessmentPassed<br/>CertificateIssued"]
E4["SubscriptionActivated<br/>FeaturedPurchased<br/>PaymentCaptured"]
end
subgraph PROJ["Projection — Pipeline 4"]
OUT2["Outbox"] --> P["Projectors"]
P --> RM[("Reporting read model<br/><i>pre-aggregated</i>")]
end
subgraph KPI["SRC-MOM §13 KPIs"]
K1["📊 Consultations booked<br/>& completed / month"]
K2["📊 Templates downloaded<br/>& customised"]
K3["📊 % employees passing<br/>& certified"]
K4["📊 MRR — subscriptions<br/>+ featured packages"]
end
subgraph VIEWS["Dashboards"]
V1["🛡️ Platform admin<br/><i>all tenants</i>"]
V2["🏢 Company admin<br/><i>own employees only —<br/>RLS enforced</i>"]
V3["🎓 Consultant<br/><i>own bookings + earnings</i>"]
V4["🚚 Provider<br/><i>own listing performance</i>"]
end
E1 & E2 & E3 & E4 --> OUT2
RM --> K1 & K2 & K3 & K4
K1 & K2 & K3 & K4 --> V1
RM --> V2 & V3 & V4
style V2 fill:#fff4e5,stroke:#d98c1f
Two design points#
The four KPIs are the minimum viable analytics surface, and they must be designed in from the start. Each requires event capture in a specific module. Reconstructing "templates downloaded" from access logs a year later is far more expensive — and less accurate — than emitting an event at download time.
⚠️ Reporting is a tenant-isolation risk surface. Aggregate queries are exactly where a developer is tempted to bypass tenant scoping "because it's just a count". A company dashboard leaking another company's employee statistics is the same breach as leaking their records (
QAS-SEC-01). The reporting read model is tenant-scoped and RLS-protected identically to transactional tables — no exceptions for aggregates.
C-09 — Notification Fan-Out#
SRC-BRD §13 deps 8/12/13 · SRC-MOM "Could Have" (appointment reminders)
flowchart TB
subgraph TRIG["Triggers"]
T1["Account state changed"]
T2["Booking confirmed / declined /<br/>expired / reminder"]
T3["Document approved / rejected"]
T4["Certificate issued"]
T5["Training assigned / overdue"]
T6["Payment succeeded / failed"]
T7["Custom request delivered"]
end
TRIG --> EV["Domain event → Outbox"]
EV --> DISP["Notification dispatcher"]
DISP --> PREF{"User channel<br/>preferences<br/>+ locale"}
PREF --> EMAIL["✉️ Email"]
PREF --> SMS["📱 SMS ⚠"]
PREF --> PUSH["🔔 Push (APNs/FCM)"]
PREF --> INAPP["📥 In-app inbox"]
EMAIL & SMS & PUSH --> RETRY{"Delivered?"}
RETRY -->|No| BACKOFF["Exponential backoff<br/>retry queue"]
BACKOFF --> RETRY
RETRY -->|"Exhausted"| DLQ["Dead letter<br/>+ alert"]
RETRY -->|Yes| LOG2["📝 Delivery record"]
style SMS fill:#fff4e5,stroke:#d98c1f
style DLQ fill:#fde8e8,stroke:#c94a4a
Design rules#
| Rule | Rationale |
|---|---|
| Never in the synchronous path | A notification provider outage must not fail a booking (QAS-AVL-04) |
| Templates are bilingual, resolved from the recipient's locale | Not the sender's, and not the app's current locale |
| In-app inbox is the durable channel | Email and SMS are best-effort; the in-app record is the one that must survive |
| Deduplicate | Reminder jobs retry; a customer receiving five copies of one reminder is a trust event |
| Quiet hours for non-urgent notifications | KSA business-hours awareness |
⚠️ SMS is flagged. SRC-BRD §13 dep 13 hedges with "if implemented", but OTP is a hard requirement if phone registration ships (F-01). Two additional constraints surfaced in research: KSA sender-ID registration requirements with the telecom regulator (unverified — confirm before selecting a provider), and SMS pumping fraud, which is a direct financial loss vector requiring per-phone, per-IP, and per-country quotas.
Consolidated New Open Questions#
| ID | Question | Flow | Blocking |
|---|---|---|---|
OQ-20 |
Subscription grace period length before a listing is delisted? | C-02 | Medium |
OQ-21 |
Is featured placement unlimited or scarce (N per category)? | C-03 | Yes — business model |
OQ-22 |
Does lowering a passing score retroactively pass earlier attempts? | C-07 | Medium |
OQ-23 |
Commission rate model — flat %, tiered, or per-category? | C-05 | Yes — blocks settlement design |
OQ-24 |
Settlement frequency — weekly, monthly, on-demand? | C-05 | Yes |
OQ-25 |
Who absorbs payment-gateway fees — platform, consultant, or customer? | C-01, C-05 | Yes |
Traceability — Full BRD §7.1 Coverage#
Every in-scope MVP feature now has a documented flow.
| BRD §7.1 group | Features | Covered by |
|---|---|---|
| User Management | 9 features | F-01, F-02, F-03, Flow 1 |
| Account Verification | 5 features | Flow 2, F-04 |
| Consultancy Module | 7 features | F-05, F-06, Flow 3 |
| Templates | 4 features | F-07, F-08 |
| Training | 4 features | F-09 |
| Assessments | 4 features | Flow 4 |
| Certification | 3 features | Flow 4 |
| Marketplace | 4 features | Flow 5, C-03, C-04 |
| Payment | 4 features | C-01, C-02, C-03, F-08 |
| Administration | 8 features | C-06, C-07, C-08 |
| (implied) | Notifications | C-09 |
| (implied) | Payouts | C-05 |
The last two rows are worth noting: payouts and notifications appear only as dependencies in
SRC-BRD§13, never as scoped features — yet payouts carry the project's largest regulatory risk (R-01) and notifications are load-bearing for nearly every flow. Both need explicit scoping.