# Onboarding / Auth — Business Rules

Numbered, implementable rules the demo encodes, with our status.

## Onboarding gate chain
1. A shop with `verificationStatus` `requested` or `rejected` is **inactive** — owner
   cannot proceed past an inactive notice. (`lib/onboarding.ts:19-20`)
   - Ours: `verification_status=PENDING(0)` + `STATUS_NEW` blocks dashboard access;
     `actionActivationStatus` shows the notice. **Partial** — no explicit "rejected"
     status; rejection is not modelled as a distinct shop state.
2. Phone must be verified before set-password/login are reachable.
   (`lib/onboarding.ts:21`) — Ours: `user.phone_verified == PHONE_VERIFIED(1)` enforced at
   login (`SignInController.php:183-186`). **Done.**
3. The owner must have a password set before login is reachable.
   (`lib/onboarding.ts:22`) — Ours: password is always set at signup, so the gate is
   implicit / never fails. **Partial** (the admin-created-owner case is unmodelled).
4. After login, the contract/terms must be accepted before reaching the portal — a **hard
   gate** on first login. (`lib/onboarding.ts:24`, `store.ts:1242-1245`) — Ours: policy
   consent in KeyStorage + `AWAITING_CONTRACT -> ACTIVE` flip
   (`SiteController.php:899-919`). **Done.**

## Sign-up validation
5. Required to submit: commercial/marketing name, store type, **≥1 service category**,
   owner full name, mobile, email, password, password-confirm-match, **accept terms AND
   accept privacy**. (`SignUp.tsx:127-137`)
   - Ours (`SignupForm::rules`): requires email, password, password_confirm,
     terms_conditions, category_ids, fullname, owner_mobile, commercial_name, gender.
     **Privacy is NOT required server-side** (only the terms checkbox). **Partial.**
6. Mobile is normalized to `+966` + the entered digits; input strips non-digits.
   (`SignUp.tsx:154,256`) — Ours: validated against Saudi pattern
   `/^(05|5)[013456789][0-9]{7}$/`, length 9–10 (`SignupForm.php:66-67`). **Done / stronger.**
7. Store type is one of `men | women | all`. (`SignUp.tsx:198-200`) — Ours: `gender in
   {GENDER_MALE, GENDER_FEMALE, GENDER_ALL}` (`SignupForm.php:71-75`). **Done.**
8. Password confirm must equal password. (`SignUp.tsx:126`) — Ours: `compare`
   (`SignupForm.php:78`). **Done.**
9. Email must be unique. (implicit in demo store) — Ours: `unique` against User
   (`SignupForm.php:55-58`). **Done / stronger.**
10. Password length: demo has **no explicit min** (only strength meter is advisory). Ours:
    **min 6, max 12** (`SignupForm.php:77`). **Divergent** — note the 12-char *maximum* is
    unusually restrictive and not in the demo.
11. New signup creates shop in non-active state pending admin review; owner is **not**
    auto-logged-in and sees a "submitted / pending review" panel. (`SignUp.tsx:163-165`,
    `store.ts:1152-1186`) — Ours: shop `STATUS_NEW`/`PENDING` but the controller
    **auto-logs in and auto-accepts policies** (`SignInController.php:143-148`).
    **Divergent / missing pending-review confirmation.**
12. Password-strength scoring: length×4 capped 50 + 12.5 per class (lower/upper/digit/
    symbol), thresholds 30/60/80 = weak/fair/good/strong. (`SignUp.tsx:18-43`,
    `SetPassword.tsx:14-37`) — Ours: kartik widget meter with different scoring.
    **Partial.**

## OTP / activation
13. Activation OTP is generated only when the shop is in the "activated, pending auth"
    state. (`ActivateEmail.tsx:17`, `store.ts:1211`) — Ours: OTP generated on visiting a
    valid, unexpired activation **token** link (`SignInController.php:211-293`).
    **Done (different mechanism).**
14. OTP is **4 digits**, numeric only. (`OtpEntry.tsx:65-74`, `store.ts:1212`) — Ours:
    **6 digits** via `SmsLog` (web flow); the mobile API uses 4-digit `mt_rand(1000,9999)`
    (`UserController.php:547`). **Divergent (digit count) but functionally done.**
15. Wrong OTP -> rejected, no state change. (`store.ts:1225`) — Ours: invalid OTP error,
    plus **expiry check** and **resend cooldown/rate-limit** the demo lacks
    (`SignInController.php:404-432`, `SmsLog::checkOtpResendLimit`). **Done / stronger.**
16. Successful OTP sets `phoneVerified:true` and clears the pending code.
    (`store.ts:1226`) — Ours: sets `status=ACTIVE, phone_verified=PHONE_VERIFIED`, deletes
    OTP + token (`SignInController.php:431-441`). **Done.**
17. After OTP, route by the gate chain: to set-password if password not set, else login.
    (`OtpEntry.tsx:35-40`) — Ours: always logs the user out and routes to **login**
    (`SignInController.php:447-464`) since password is already set. **Partial** (no
    set-password branch).

## Login
18. Login looks up the user by email; **rejected if password not set**. (`store.ts:1234-
    1235`) — Ours: by username OR email; real password hash check + activation +
    RBAC `shopOwner` (`LoginForm.php:66-94,120-123`). **Done / stronger.**
19. Demo has no rate limiting. Ours adds **IP brute-force lockout**: 5 failed attempts ->
    escalating block 5/15/60 min; counter resets after 30 min idle
    (`LoginForm.php:101-173`). **Extra (ours only).**
20. Only `user_type == SHOP_OWNER (2)` may log into the shop portal
    (`LoginForm.php:89-90`). Demo: `role === 'owner'`. **Done.**

## Set-password (admin-created owner)
21. Owner sets + confirms a password (strength-metered), then is routed to login.
    (`SetPassword.tsx:79-83`) — Ours: **no such screen/flow.** **Missing.**

## Terms acceptance side-effects
22. Accepting terms sets `contractAccepted:true`, `verificationStatus:'active'`,
    `status:'active'` atomically. (`store.ts:1244`) — Ours: KeyStorage record + shop flips
    to `STATUS_ACTIVE` when `AWAITING_CONTRACT`/`NEW` (`SiteController.php:912-919`).
    **Done.**

## Shop-scoping / permissions
23. The owner is scoped to exactly one shop (`user.shopId`). (`store.ts:1137-1140`) —
    Ours: `User.shop_id` / `user->shop` one-to-one. **Done.**
