Vendor Onboarding & Subscriptions (SRS-VOS)
Purpose
Owns the path from "business wants to sell on Yam3at" to "verified, subscribed, revenue-generating vendor": business registration, commercial-license KYC, the admin approval state machine, subscription plans and their enforcement, plan payments via the PSP (KNET-first), renewals, proration, and subscription invoices. It does not own the vendor's public storefront content (that is Vendor Listings) or customer-side payments (Payments module, Part 2).
Vendor subscriptions are the platform's primary revenue stream.
Actors
| Actor |
Description |
| Vendor applicant |
Authenticated user filling the business application. |
| Vendor owner |
Applicant after approval; manages subscription and billing. |
| Admin (Operations) |
Reviews applications, requests changes, approves/rejects. |
| Admin (Finance) |
Manages plan catalog and pricing, views subscription revenue, issues billing adjustments. |
| PSP |
MyFatoorah or Tap Payments, behind the payment-provider abstraction. |
| System |
Renewal jobs, grace-period expiry, limit counters. |
User stories
- As a business owner, I want to register my company with its license and category, so that I can start receiving event leads.
- As a vendor applicant, I want to save my application as a draft and see exactly what is missing, so that I can complete it over several sittings.
- As an operations admin, I want a review queue with the license document inline, so that I can approve or send back applications quickly with clear reasons.
- As a vendor owner, I want to compare plans and pay with KNET, so that I can unlock more quotations and a featured listing.
- As a vendor owner, I want upgrades to apply instantly with fair proration, so that I never pay twice for the same period.
- As a vendor owner, I want a grace period if my renewal payment fails, so that one card issue doesn't take my business offline.
- As a finance admin, I want every subscription charge to produce a numbered invoice, so that accounting is clean.
Functional requirements
Business registration
| ID |
Requirement |
Phase |
| SRS-VOS-001 |
An authenticated, phone-verified user MUST be able to start a vendor application. The application is a multi-step wizard with autosaved Draft state: (1) business identity, (2) license & documents, (3) categories & service area, (4) contact & branches, (5) review & submit. |
MVP |
| SRS-VOS-002 |
Business identity fields: business_name_ar (required), business_name_en (required), legal_name (as on license), business_type (establishment/company/freelancer-license), description_ar/description_en (optional at this stage), logo (optional). |
MVP |
| SRS-VOS-003 |
License step: commercial_license_number (Kuwait MOCI license), license_expiry_date, and an upload of the license document (PDF/JPEG/PNG, ≤ 10 MB). Additional optional documents: signature authorization (اعتماد التوقيع), Civil ID of the authorized signatory. Documents are stored privately with signed-URL access for admins only. |
MVP |
| SRS-VOS-004 |
Category selection: at least 1 and at most 3 categories from the marketplace taxonomy (SRS-MKT-001). Primary category is explicit. |
MVP |
| SRS-VOS-005 |
Contact & location: business phone (+965), business email, optional website/Instagram handle, headquarters address (governorate, area, block/street free text, Google Maps pin). At MVP exactly one branch (the HQ) is captured; the schema supports N branches. |
MVP |
| SRS-VOS-006 |
Submission is blocked until all mandatory fields and the license document are present; the review screen lists every gap with a deep link to fix it. |
MVP |
| SRS-VOS-007 |
License expiry monitoring: the system MUST warn the vendor 30 days before license_expiry_date and flag the vendor to admins at expiry; an expired license suspends the ability to receive new RFQs until a renewed license is uploaded and re-verified. |
MVP |
| SRS-VOS-008 |
Multi-branch management (add/edit branches, per-branch service area and contact) |
Scale |
| SRS-VOS-009 |
Team members: invite staff by email/phone with roles vendor_manager (all except billing) and vendor_staff (RFQs + messages only); seat count limited by plan. |
Scale |
KYC / verification & approval workflow
| ID |
Requirement |
Phase |
| SRS-VOS-020 |
On submission the application enters Submitted and appears in the admin review queue (FIFO, filterable by category/age). Target first-response SLA: 2 business days, tracked and reported. |
MVP |
| SRS-VOS-021 |
Admin review checklist (recorded per application): license number matches document, license is valid (not expired), business activity on license plausibly matches selected categories, business name matches, contact reachable. Each item is checked off individually. |
MVP |
| SRS-VOS-022 |
Admin decisions: Approve, Reject (terminal, reason mandatory, re-application allowed after 30 days), or Needs Changes (returns the application to the vendor with a mandatory bilingual-capable comment; vendor edits and resubmits, unlimited cycles). |
MVP |
| SRS-VOS-023 |
On approval the system creates the vendor aggregate, assigns the applicant the vendor_owner role scoped to it, starts the Free plan automatically, and notifies the vendor (email + push). The vendor's public profile remains unpublished until listing content passes moderation (SRS-LST-060). |
MVP |
| SRS-VOS-024 |
All state transitions and decisions are audit-logged with admin identity and reason; the vendor-facing history shows dates and comments but not admin names. |
MVP |
| SRS-VOS-025 |
A verified badge ("موثّق / Verified") is displayed on approved vendors whose license is currently valid. |
MVP |
Subscription plans
| ID |
Requirement |
Phase |
| SRS-VOS-030 |
Four plans: Free, Basic, Professional, Enterprise, each with monthly and annual billing cycles (annual = 11 × monthly price, i.e. 1 month free — see Confirmed Decisions). Plan catalog (names AR/EN, prices, limits, feature flags) is data-driven and editable by Finance admins without deployment. |
MVP |
| SRS-VOS-031 |
Plan limits are enforced server-side in real time (see launch matrix below). Counters reset at the start of each billing period (vendor's anchor date), not the calendar month. |
MVP |
| SRS-VOS-032 |
When a limit is reached, the vendor action is blocked with an explanatory message and a one-tap upgrade CTA; incoming customer demand is never silently dropped — RFQs beyond the quota remain visible but locked ("upgrade to respond"). |
MVP |
| SRS-VOS-033 |
Grandfathering: price/limit changes to a plan apply to new subscriptions and to renewals occurring ≥ 30 days after the change announcement; active periods are never modified mid-cycle. |
MVP |
| SRS-VOS-034 |
Promo codes for subscription discounts (percent or fixed, first-cycle or recurring N cycles). |
Scale |
Launch plan matrix (initial values — Finance-editable data, not hardcoded):
| Limit / feature |
Free |
Basic |
Professional |
Enterprise |
| Price (KWD/month) |
0.000 |
15.000 |
39.000 |
99.000 |
| Price (KWD/year, 1 month free) |
— |
165.000 |
429.000 |
1089.000 |
| Quotation responses / month |
5 |
30 |
100 |
Unlimited |
| Gallery images |
5 |
15 |
40 |
100 |
| Videos on listing |
0 |
1 |
5 |
15 |
| Packages / services |
3 |
10 |
30 |
Unlimited |
| Featured-listing eligibility |
— |
— |
✔ |
✔ (priority) |
| Analytics depth |
Views only |
Basic funnel |
Full funnel + trends |
Full + export |
| Team members (Scale) |
1 |
2 |
5 |
15 |
| Ad credits (Scale, KWD/month) |
— |
— |
5.000 |
20.000 |
| Support |
Standard |
Standard |
Priority |
Priority + account manager |
Payment, renewal & billing
| ID |
Requirement |
Phase |
| SRS-VOS-040 |
Plan checkout MUST support KNET, Visa/Mastercard, Apple Pay, and Google Pay via the PSP abstraction (MyFatoorah or Tap). Yam3at never handles raw card data (hosted/tokenized flows). Amounts are KWD with 3 decimals. |
MVP |
| SRS-VOS-041 |
Auto-renewal: card-tokenized subscriptions renew automatically. KNET does not support recurring charges, so KNET-paid subscriptions send renewal reminders at T-7, T-3, and T-0 days with a pay-now link; the vendor MAY store a card as backup for auto-renew. |
MVP |
| SRS-VOS-042 |
Failed/missed renewal starts a 7-day grace period: full plan features continue, banner + daily reminder shown. Retry schedule for tokenized cards: T+0, T+2, T+5. |
MVP |
| SRS-VOS-043 |
If grace expires unpaid, the subscription becomes Expired and the vendor is downgraded to Free limits immediately (content over Free limits is hidden, not deleted — restored on re-subscription). |
MVP |
| SRS-VOS-044 |
Upgrades apply immediately with proration: unused value of the current cycle (daily basis) is credited against the new plan's charge; the billing anchor date resets to the upgrade date. |
MVP |
| SRS-VOS-045 |
Downgrades take effect at the end of the current period (no mid-cycle refunds). If current usage/content exceeds the target plan's limits, the vendor is warned at scheduling time and over-limit content is hidden at switchover. A scheduled downgrade can be cancelled any time before it executes. |
MVP |
| SRS-VOS-046 |
Cancellation = scheduled downgrade to Free at period end. Subscription fees are non-refundable except where required by law or via Finance-admin goodwill refund (audit-logged). |
MVP |
| SRS-VOS-047 |
Every successful charge generates a sequential, immutable invoice (INV-YYYY-NNNNNN): vendor legal name, license number, plan, period, amount in KWD (3 decimals), payment method, PSP reference. Invoices are listed in the vendor dashboard and downloadable as PDF (AR/EN per locale). |
MVP |
| SRS-VOS-048 |
Webhook handling from the PSP MUST be idempotent (event id dedupe), signature-verified, and reconciled daily against PSP settlement reports; mismatches raise a Finance alert. |
MVP |
Business rules
- One commercial license number = at most one active vendor account. Duplicate license submissions are auto-flagged to admins.
- A vendor may operate in at most 3 categories; changing categories after approval re-triggers a lightweight admin review of the affected categories only.
- The Free plan is permanent (no trial expiry) — it is the funnel, not a trial.
- Quotation quota consumption: a quota unit is consumed when the vendor sends a quote, not when an RFQ is received. Withdrawn quotes do not refund quota.
- Subscription state never affects existing confirmed bookings or open conversations — only new demand capture (quota-gated actions, search boost, featured placement).
- All plan prices are VAT-ready: an invoice tax line exists with rate 0 % for Kuwait at launch (Kuwait currently has no VAT), so KSA/UAE expansion (15 %/5 %) is a configuration change.
- Approval decisions, plan changes, refunds, and manual limit overrides are audit-logged with actor and reason.
Validation rules
| Field |
Rule |
business_name_ar |
3–100 chars, Arabic letters/digits/spaces/common punctuation. |
business_name_en |
3–100 chars, Latin letters/digits/spaces/common punctuation. |
commercial_license_number |
Numeric, 4–12 digits (MOCI license format tolerance); uniqueness enforced against active vendors. |
license_expiry_date |
Must be a future date at submission. |
| License document |
PDF/JPEG/PNG, ≤ 10 MB, min 600 px shorter side for images; virus-scanned before admin exposure. |
business_phone |
E.164; Kuwait ^\+965[124569]\d{7}$ (landlines allowed for businesses). |
business_email |
RFC 5322, ≤ 254 chars. |
| Categories |
1–3 selections; primary must be among them. |
| Maps pin |
Valid lat/lng within Kuwait bounding box at launch (28.5–30.1 N, 46.5–48.5 E). |
| Plan checkout amount |
Exactly equals catalog price ± proration credit; server-computed, never client-supplied. |
Permissions matrix
| Action |
Vendor applicant |
Vendor owner |
Vendor manager (Scale) |
Admin Ops |
Admin Finance |
Admin Super |
| Create/edit draft application |
✔ |
— |
— |
— |
— |
— |
| Submit / resubmit application |
✔ |
— |
— |
— |
— |
— |
| Review, approve, reject, request changes |
— |
— |
— |
✔ |
— |
✔ |
| View license documents |
own |
own |
— |
✔ |
✔ |
✔ |
| Choose/upgrade/downgrade/cancel plan |
— |
✔ |
— |
— |
— |
✔ (support override) |
| Pay subscription |
— |
✔ |
— |
— |
— |
— |
| View invoices |
— |
✔ |
— |
— |
✔ |
✔ |
| Edit plan catalog & pricing |
— |
— |
— |
— |
✔ |
✔ |
| Issue goodwill refund / manual override |
— |
— |
— |
— |
✔ |
✔ |
Status / state machines
Vendor application (approval workflow):
stateDiagram-v2
[*] --> Draft: applicant starts wizard
Draft --> Submitted: submit (all mandatory data present)
Submitted --> UnderReview: admin picks from queue
UnderReview --> Approved: checklist passes
UnderReview --> NeedsChanges: admin returns with comments
UnderReview --> Rejected: reason recorded (terminal)
NeedsChanges --> Submitted: vendor resubmits
Approved --> [*]: vendor created, Free plan started
Rejected --> [*]: re-application allowed after 30 days
Subscription lifecycle:
stateDiagram-v2
[*] --> FreeActive: vendor approved
FreeActive --> PaidActive: checkout success
PaidActive --> PaidActive: renewal success / upgrade (proration)
PaidActive --> Grace: renewal fails or not paid at T0
Grace --> PaidActive: payment succeeds within 7 days
Grace --> ExpiredToFree: grace ends unpaid
PaidActive --> DowngradeScheduled: vendor downgrades or cancels
DowngradeScheduled --> PaidActive: vendor cancels the schedule
DowngradeScheduled --> FreeActive: period ends (to Free)
ExpiredToFree --> PaidActive: vendor re-subscribes
Edge cases
- License expires mid-subscription: subscription keeps billing; only new-RFQ intake is suspended (SRS-VOS-007). The vendor sees both banners independently.
- Upgrade during grace period: allowed — payment of the new plan clears the grace state; proration base is zero (the unpaid period has no value to credit).
- KNET payment succeeds but the redirect back drops (user closes browser): webhook is the source of truth; the subscription activates on webhook receipt and the vendor is notified. UI shows "payment processing" for pending references up to 15 min.
- Double-click / duplicate checkout: idempotency key per checkout session; a second successful charge for the same period is auto-flagged for refund.
- Downgrade scheduled, then limit-exceeding content added: warning recalculated and re-shown at execution; content hidden by lowest-quality-score-first order (SRS-LST-055).
- Vendor rejected but already paid (paid before approval — not possible in the normal flow since plans start post-approval; if it occurs via support flow, auto-refund full amount).
- Application submitted, then applicant deletes identity account: application auto-cancelled; documents purged with the identity per SRS-AUTH-061.
- Two admins open the same application: optimistic locking — first decision wins, second admin gets a "state changed" refresh, never a silent overwrite.
Empty / loading / error states
| Screen |
Empty |
Loading |
Error |
| Application wizard |
Fresh form with progress 0/5; autosave indicator |
Step-level skeletons; upload progress bars with cancel |
Field-level errors AR/EN; upload failures retryable without losing other fields |
| Admin review queue |
"No applications waiting 🎉" with SLA stats |
Skeleton table |
Retry banner; decision submit failures preserve entered comments |
| Plans page |
n/a (catalog always present) |
Price skeletons |
If catalog fetch fails, show cached last-known prices flagged "may be outdated", block checkout |
| Checkout |
n/a |
PSP redirect interstitial "Do not close this window" |
Failure screen with PSP reason mapped to friendly bilingual text + retry / change-method |
| Invoices list |
"No invoices yet — you're on the Free plan" |
Skeleton rows |
Retry; PDF generation failure falls back to HTML view |
Acceptance criteria
Data entities touched
vendor_applications, vendor_application_documents, vendor_application_reviews (checklist + decisions), vendors, vendor_branches, vendor_category (pivot), subscription_plans, plan_prices, subscriptions, subscription_usage_counters, subscription_schedules (downgrades/cancellations), invoices, invoice_lines, payment_transactions (shared with Payments), psp_webhook_events, audit_logs.
POST /api/v1/vendor-applications
GET /api/v1/vendor-applications/current
PATCH /api/v1/vendor-applications/{id}
POST /api/v1/vendor-applications/{id}/documents
POST /api/v1/vendor-applications/{id}/submit
GET /api/v1/admin/vendor-applications
GET /api/v1/admin/vendor-applications/{id}
POST /api/v1/admin/vendor-applications/{id}/decision (approve|reject|needs_changes)
GET /api/v1/subscription-plans
GET /api/v1/vendors/{vendorId}/subscription
POST /api/v1/vendors/{vendorId}/subscription/checkout
POST /api/v1/vendors/{vendorId}/subscription/change (upgrade|downgrade|cancel)
DELETE /api/v1/vendors/{vendorId}/subscription/schedule
GET /api/v1/vendors/{vendorId}/invoices
GET /api/v1/vendors/{vendorId}/invoices/{invoiceId}.pdf
POST /api/v1/webhooks/psp (signature-verified)
GET /api/v1/admin/subscriptions
PATCH /api/v1/admin/subscription-plans/{planId}