Phase 0–1 Deliverable · Discovery & Architecture

Ma3ak Fe Tareeqak & Ma3ak Delivery

Community mobility & delivery platform for Zahraa El Asmaa / Zahraa Capital — Badr City, built to expand city-by-city without a rewrite.

PHP 8.x · MySQL 8 · Shared hosting Mobile-first · RTL Arabic No code yet — awaiting sign-off

01 Executive Summary

Zahraa Capital's Zone 3 has a real, narrow transportation problem: people already travel in and out of the area by car, but there is no structured way for someone with a spare seat to be found by someone who needs one. Ma3ak Fe Tareeqak solves that first, narrow problem — a community ride board, not a dispatched taxi service — and Ma3ak Delivery reuses the same trust, verification, and matching machinery for small local deliveries.

The two services share one account model, one verification pipeline, one admin panel, and one database. A resident registers once; becoming a driver or delivery provider is an additional, admin-verified capability on the same account, not a different product. This is the single most important structural decision in the whole system and it shapes almost every table and screen described below.

Given the deployment target is ordinary shared PHP/MySQL hosting (no Docker, no queue workers, no guaranteed shell access to install system packages), the architecture is deliberately a clean, dependency-light custom PHP MVC rather than a full framework — see §4 for the reasoning. Everything is designed so a future native mobile app, GPS/maps, online payments, and multi-city expansion can be added additively, without re-architecting the core.

⚠ Read before anything else

This document is the output of Phase 0 (Discovery) and Phase 1 (Architecture). Per your own instructions, no implementation code follows this document. Section 18 lists the decisions and access I need from you before Phase 2 (Foundation) can start — most importantly, working SSH credentials for zahara.m3lsh.com (port 22 timed out with no prior config, so I could not inspect the real PHP version, extensions, or Composer availability before writing this — a few choices below are marked confirm on host for that reason).

02 Assumptions

Stated explicitly so they can be corrected rather than silently baked in.

  • Legal form: this is a community-organized service, not a licensed taxi/courier dispatch company. No claim of regulatory compliance with Egyptian transportation law is made anywhere in the product (see §7 and the Legal Pages module) until you've had professional legal review.
  • Single currency: EGP only for the MVP; no multi-currency support is built now (the currency column exists purely so it isn't a breaking schema change later).
  • Single timezone: Africa/Cairo, stored in UTC in the database, rendered in local time.
  • Cash-only MVP: per your spec, no payment gateway integration now — payment_methods/transactions exist as a real but currently CASH-only abstraction.
  • No live GPS tracking in the MVP — locations are City → Area → Zone → landmark text, exactly as specified. latitude/longitude columns exist and are nullable everywhere a location is recorded.
  • Single admin organization initially (one system admin, a handful of branch admins), not a multi-tenant SaaS product — branches model service lines (ride vs. delivery vs. future lines) inside one operator, not independent customers.
  • Traffic volume is modest (a single residential community, growing to a handful of neighborhoods) — this justifies avoiding queues, caches, and horizontal scaling infrastructure now (see §4).
  • Shared hosting has: PHP 8.1+, MySQL/MariaDB, .htaccess support (Apache), cron access, and SSH for deployment. unconfirmed — see §18.

03 Risks & Concerns

⚠ Legal / regulatory exposure

Ride-matching for a fee sits close to informal transportation regulation in Egypt. The spec already asks for a "legal compliance placeholder," which is correct — I'm flagging it here as a genuine risk, not just a checkbox: do not launch publicly, and do not process real money at scale, before a lawyer has reviewed the Terms of Service, driver agreement, and liability model. The architecture keeps this swappable (see §7, §65) but cannot make the business legally safe by itself.

⚠ Sensitive document storage on shared hosting

Shared hosting environments vary a lot in how strictly they isolate one account's files from another and in whether .htaccess directives are honored consistently. The document security model in §13 assumes we can (a) place private files outside the public web root, or (b) deny direct access inside it via .htaccess and serve everything through a PHP gate. Both are standard, but must be verified on the actual host before the first ID document is ever uploaded.

⚠ Seat/slot race conditions

Explicitly called out in your spec, and correctly so — this is the single most common correctness bug in ride-sharing-style systems. Addressed structurally in §9 and §10 via atomic conditional UPDATE statements, never "read-then-write" from PHP. This is non-negotiable and will be unit- and load-tested (§14).

⚠ Trust & safety with no live tracking

Without GPS, "safety" for the MVP reduces to: verified identity, a visible rating history, and a working complaint/admin-intervention path. That is real but limited — it should be communicated honestly to users, not oversold as a "safety platform," until Phase 2+ safety features (§69) exist.

⚠ Cold-start liquidity

Not a technical risk, but an architectural one: a ride/delivery board with too few active drivers looks broken. The "I'm going there" proactive-publish flow (§6, §10) exists specifically to solve this — it lets a handful of regular commuters seed supply even before formal ride requests exist.

04 Design Decisions Made (and why)

Your spec explicitly asked me to push back where something would be weak, unsafe, or overengineered rather than follow it blindly. Here is every place I made a judgment call.

No framework — a small custom MVC, not Laravel

Laravel is an excellent framework, but on unconfirmed shared hosting it adds real deployment risk: it needs Composer to be runnable on the host (or a build-and-upload step), a writable storage/ tree with specific permissions, a scheduler that depends on cron being configurable, and generally assumes more control over the PHP environment than shared hosting reliably grants. None of Laravel's flagship value (Eloquent, Artisan, queues, Blade) is load-bearing for a system this size — a request/response cycle, PDO with prepared statements, a router, and a handful of service classes cover 100% of the requirements above. Decision: a small, explicit PHP 8.x MVC (see §8), Composer used only for autoloading and 2–3 tiny, framework-agnostic libraries if the host supports it, with a documented zero-Composer fallback if it doesn't.

Tailwind CSS + Alpine.js, not Bootstrap, not a JS framework

Bootstrap's RTL build is turnkey but visually generic, and the brand asks for a distinctive, restrained pastel identity — fighting Bootstrap's own opinionated components to get that look creates more work, not less. A full JS framework (React/Vue) would need a build pipeline and turns every page into an API-consuming SPA, which is unnecessary for a server-rendered, mobile-first app and complicates the "PHP renders HTML" mental model this system otherwise has throughout. Decision: Tailwind CSS (compiled once at build time into a static CSS file — no Node.js needed on the production host) for layout/visual system, with Alpine.js (≈15 KB, no build step) for the sprinkles of interactivity server-rendered pages need — bottom sheets, tabs, toasts, confirm dialogs. This is a well-worn, low-risk pairing for exactly this kind of project.

One polymorphic documents table, not three

Your spec's suggested schema lists vehicle_documents and user_documents separately. They have identical lifecycle, state machine, storage, and review UI — the only difference is what they're attached to. Duplicating the table means duplicating the review queue, the expiration job, and the access-control code three times, which is exactly the "repeated logic" your spec asks me to avoid. Decision: one documents table with owner_type (USER / VEHICLE) + owner_id, and a document_types lookup table so admins can add new required document types later without a schema change.

Ride "offers" and "requests" collapse into three tables, not five

Read literally, the spec's ride section implies separate ride_requests, ride_offers, and rides tables, with an ambiguous relationship between a driver's "offer" (in the pricing sense) and a driver's "offer" (in the published-trip sense, §10). I've resolved this into: ride_offers = a driver's published trip ("I'm going there"), ride_requests = a passenger's search intent (optional — a passenger can also just browse and book an offer directly), and ride_bookings = the actual stateful link between one passenger and one ride offer, carrying the full state machine from §13. This is fewer tables, no loss of the required states, and one obvious place to look for "what's the status of this ride."

Roles ARE capabilities — implemented as many-to-many, not a single role column

This is the load-bearing decision from your §4. user_roles is a join table, not a single enum column on users — a user can hold DRIVER and DELIVERY_PROVIDER simultaneously. Current availability (§9) is a completely separate concept from capability (§5) and is modeled in its own table, keyed by which capability is currently active.

Sessions for the web app, bearer tokens for the future API — not JWT, not OAuth2

The web/PWA app authenticates with ordinary secure PHP sessions (simplest, safest default for a server-rendered app). The REST API (needed today for the PWA's own AJAX calls, and reused unchanged by a future native app) authenticates with a long-lived, hashed personal_access_token per device — the same pattern Laravel Sanctum popularized, implemented directly, without pulling in a JWT library or standing up an OAuth2 authorization server neither of which this system's threat model or client mix actually needs yet.

No maps/GPS, no chat, no queue, no cache — deliberately deferred

Consistent with §78 of your spec ("do not overengineer"). Matching in the MVP is a deterministic SQL query (area/zone match, time window, seat count), not a geospatial one. Notifications are DB-backed and rendered in-app/PWA; there's no Redis, no message queue, no job runner — a lightweight cron script handles the two time-based jobs the system actually needs (document expiration sweep, stale-offer expiry sweep).

05 Roles, Capabilities & Permissions

Two layers that must not be confused: capabilities (what a user is allowed to be — passenger, driver, delivery provider, admin) and availability (what a verified provider is doing right now — offline / available / busy). Only the first layer maps to permissions.

Capability catalog

Capability keyGrantedRequires verification?
NORMAL_USERAutomatically, on registrationNo
DRIVERBy admin, after driver + vehicle document reviewYes — §6, §7
DELIVERY_PROVIDERBy admin, after identity (+ vehicle, if applicable) reviewYes
BRANCH_ADMINBy a system admin, scoped to one or more branchesNo (internal staff)
SYSTEM_ADMINBy another system admin (never self-service)No

Permission matrix

PermissionNormal userDriverDelivery providerBranch adminSystem admin
ride.request.create
ride.offer.publish✅ if verified
ride.booking.accept_rejectown offers only
delivery.request.create
delivery.accept_rejectown only, if verified
document.upload_own
document.reviewown branch✅ all
document.view_sensitiveown branch, logged✅, logged
user.manageown branch scope
user.suspendown branch scope
branch.manage
area.manage
pricing_policy.manage
complaint.manageown only (create/view)own onlyown onlyown branch
audit_log.viewown branch actions
admin.manage

Permissions are checked server-side, on every request, via a small policy class per resource (e.g. RideBookingPolicy::canAccept($user, $booking)) — never by hiding a button in the template. Ownership checks (§13 IDOR requirement) live in the same policy layer: a driver can only accept bookings on their own ride offers, a branch admin can only review documents belonging to users assigned to their branch.

06 Complete User Flows

6.1 Registration → becoming a verified driver

Register (phone + password) Verify phone (OTP or admin-assisted for MVP) ACTIVE, capability = NORMAL_USER Tap "أريد أكون سائق" Fill driver profile + upload license Add vehicle + upload vehicle docs status = PENDING Branch admin reviews queue APPROVED capability DRIVER granted Notification sent Driver can go AVAILABLE

6.2 Ride: from search to completion (passenger side)

Open app "أريد مشوارًا" Pick from / to / passengers / time See matching offers (§11) Compare driver, rating, price Request a seat → booking=REQUESTED Driver accepts → CONFIRMED Driver marks picked up IN_PROGRESS COMPLETED Rate driver

6.3 "I'm going there" (driver proactively publishes)

Driver: "متاح الآن" Set from/to/time/seats/price Publish → ride_offer ACTIVE Visible in passenger search + community feed Passengers request seats Driver accepts up to seat capacity seats_remaining = 0 → offer FULL

6.4 Delivery: creation to completion

"أريد دليفري" Pickup / destination / category / size / price status=CREATED → AVAILABLE Visible to verified providers in matching areas First provider to accept wins (atomic, §9) ACCEPTED PICKED_UP IN_TRANSIT DELIVERED Customer confirms → COMPLETED Rate provider

6.5 Document rejection loop

Admin rejects with reason status=REJECTED Notification + reason shown to user User re-uploads replacement status=PENDING Back into review queue

6.6 Document expiration (automatic, cron-driven)

Daily job scans documents.expires_at ≤30 days left → notify provider expires_at passed → status=EXPIRED Provider's verification invalidated Availability forced to OFFLINE Cannot go AVAILABLE until re-approved

07 Business Rules (centralized, server-enforced)

Every rule below is enforced in one place — a service class the API/controllers call — never duplicated between the web controller and a future mobile API controller.

Driver / provider eligibility (checked before every state-changing action, not just at go-available time)

  • Cannot publish a ride offer or go AVAILABLE unless driver_profiles.verification_status = APPROVED and every required document for that user+vehicle is APPROVED and not expired, and users.status = ACTIVE.
  • A document expiring mid-availability immediately forces AVAILABLE → OFFLINE (checked by the expiration sweep, and re-checked defensively on every accept/publish action, not trusted from a cached flag).
  • Cannot accept a booking/delivery beyond capacity — enforced by the atomic seat/slot update in §9, never by an application-level "check then write."
  • Cannot accept a delivery already accepted by someone else — same atomic-conditional-update pattern, first writer wins, everyone else gets a clean "تم قبول هذا الطلب من مزود آخر" instead of a race.

Passenger / customer rules

  • Cannot accept/request an EXPIRED or already-FULL offer (re-validated server-side at request time, not just filtered out of the list client-side).
  • Cannot rate a ride/delivery that isn't COMPLETED, and cannot rate the same completed service twice (UNIQUE(service_type, service_id, rater_id)).

Account status gates

  • SUSPENDED/BLOCKED users cannot create requests, publish offers, accept anything, or message support beyond viewing their own complaint. Existing in-flight rides/deliveries are not silently deleted — they either complete normally or an admin explicitly cancels/reassigns them (audit-logged).

08 System Architecture

A single PHP application, server-rendered, following a light MVC + service-layer split. No microservices, no separate API server — the REST API (§11) is served by the same app, same codebase, same database, just a different route prefix and auth guard.

Browser / PWA ──┐
                 │  HTTPS
Future Mobile App┘
        │
        ▼
 Apache/LiteSpeed (shared hosting)
        │  .htaccess routes everything to public/index.php
        ▼
 Front Controller (public/index.php)
        │
        ├─ Middleware pipeline: HTTPS-enforce → session/token auth → CSRF (web only)
        │                        → rate limit → route → permission policy
        ▼
 Router  ──▶  Controllers (thin: parse input, call service, render/respond)
        │
        ▼
 Services (business rules, state machines, transactions)  ◀── this is where §7 lives
        │
        ▼
 Repositories (PDO, prepared statements only)
        │
        ▼
 MySQL 8 / MariaDB (utf8mb4, InnoDB, FKs)

 Cross-cutting: Logger, Notification dispatcher (IN_APP now; PUSH/SMS/EMAIL/WHATSAPP
 as pluggable channel drivers later), Document storage adapter (local disk now;
 swappable for S3-compatible storage later), Audit log writer.

Proposed project structure

/app
  /Controllers        Web/{...}.php   Api/{...}.php
  /Services           RideService, DeliveryService, VerificationService,
                       AvailabilityService, NotificationService, ...
  /Repositories        one per aggregate (RideOfferRepository, ...)
  /Policies             ownership + permission checks per resource
  /Models               thin data objects, no active-record magic
  /Middleware
  /Validators
  /Support             Fsm.php (generic state-machine helper), Money.php, Slug.php
/config                 config.php reads getenv(), never hard-coded secrets
/database
  /migrations           plain numbered .sql files, applied by a small migrate.php runner
  /seeders              seed_dev.php (clearly marked demo data), seed_reference.php (cities/areas/zones, doc types, roles)
/public
  index.php             single front controller
  /assets               compiled Tailwind CSS, JS, icons, manifest.json, sw.js
/resources
  /views                PHP templates, partial-based, RTL by default
  /lang                 ar.php (primary), en.php (admin fallback where useful)
/routes
  web.php               session-authenticated, CSRF-protected
  api.php                token-authenticated, versioned under /api/v1
/storage
  /private/documents/{users,vehicles}/...   never web-served directly
  /logs
/tests
/docs

09 Database Architecture

39 tables, grouped by concern. utf8mb4_unicode_ci, InnoDB, explicit foreign keys, soft-delete (deleted_at) on user-facing entities that must survive for history/audit, hard timestamps everywhere. Full column-level DDL is written during Phase 2 from this design — reproduced here at entity/relationship level so it can be reviewed before a single CREATE TABLE is run.

9.1 Identity, roles & capabilities

TablePurposeKey columns
usersOne row per person, ever.id, full_name, phone (unique), email (nullable, unique), password_hash, profile_photo_path, status ENUM(PENDING,ACTIVE,SUSPENDED,BLOCKED,DELETED), created_at, updated_at, deleted_at
rolesCapability catalog (§5).id, key UNIQUE, name_ar, name_en, description
permissionsFine-grained permission catalog.id, key UNIQUE, name_ar, description
role_permissionsM:N role↔permission.role_id FK, permission_id FK, PK(role_id, permission_id)
user_rolesM:N user↔capability — the "one account, many capabilities" table.user_id FK, role_id FK, granted_at, granted_by FK→users, revoked_at NULL
personal_access_tokensBearer tokens for API/future mobile clients.id, user_id FK, token_hash UNIQUE, device_label, abilities JSON, last_used_at, expires_at, created_at
password_resetsOne-time reset tokens.id, user_id FK, token_hash, expires_at, used_at
login_attemptsThrottling / brute-force detection.id, identifier (phone/email), ip_address, success BOOL, created_at, index on (identifier, created_at)

9.2 Availability

TablePurposeKey columns
user_availabilityCurrent, temporary status per capability. One row per (user, capability).user_id FK, capability ENUM(DRIVER,DELIVERY_PROVIDER), status ENUM(OFFLINE,AVAILABLE,BUSY), current_area_id FK, destination_area_id FK NULL, note, updated_at, UNIQUE(user_id, capability)

9.3 Geography / service areas

TablePurposeKey columns
citiesTop-level, admin-managed.id, name_ar, name_en, is_active
arease.g. Zahraa Capital, within a city.id, city_id FK, name_ar, name_en, is_active
zonese.g. Zone 3, within an area.id, area_id FK, name_ar, name_en, latitude NULL, longitude NULL, is_active

A "location" on a ride/delivery row is not a foreign key to one universal locations table — it's an inline snapshot (*_city_id, *_area_id, *_zone_id, *_landmark, *_latitude, *_longitude) on the ride/delivery row itself, because a request's pickup point is a fact about that request at that moment, not a reusable master-data record. Only the City/Area/Zone taxonomy itself is master data.

9.4 Branches

TablePurposeKey columns
branchesService lines under one operator.id, name_ar, name_en, service_type ENUM(RIDE,DELIVERY,GENERAL), is_active, settings JSON
branch_adminsWhich admin manages which branch.branch_id FK, user_id FK, PK(branch_id, user_id)

9.5 Verification: driver, vehicle, delivery provider, documents

TablePurposeKey columns
driver_profiles1:1 extension of users for driver-specific data.user_id PK/FK, license_number, license_expiry, verification_status ENUM(NOT_SUBMITTED,PENDING,UNDER_REVIEW,APPROVED,REJECTED,EXPIRED), rejection_reason, verified_by FK, verified_at
delivery_provider_profiles1:1 extension for delivery-provider-specific data.user_id PK/FK, verification_status (same enum), rejection_reason, verified_by, verified_at
vehicle_typesAdmin-managed (CAR, MOTORCYCLE, BICYCLE, OTHER, extensible).id, key, name_ar, icon, is_active
vehiclesA user may own several.id, user_id FK, vehicle_type_id FK, brand, model, year, color, plate_number UNIQUE, seats, verification_status, is_active
document_typesAdmin-managed catalog of what can be uploaded.id, key, name_ar, applies_to ENUM(USER,VEHICLE), is_required, max_size_mb, allowed_mime_types JSON, validity_period_days NULL
documentsPolymorphic — every uploaded file, one lifecycle (§decisions).id, owner_type ENUM(USER,VEHICLE), owner_id, document_type_id FK, storage_path (private, randomized), original_filename (metadata only, never used as path), mime_type, size_bytes, status ENUM(NOT_SUBMITTED,PENDING,UNDER_REVIEW,APPROVED,REJECTED,EXPIRED), rejection_reason, expires_at, reviewed_by FK, reviewed_at, uploaded_at
document_access_logsEvery view/download of a sensitive document.id, document_id FK, accessed_by FK, action ENUM(VIEW,DOWNLOAD), ip_address, created_at

9.6 Rides

TablePurposeKey columns
ride_offersA driver's published trip ("I'm going there").id, driver_id FK, vehicle_id FK, from_city_id/from_area_id/from_zone_id/from_landmark, to_* (same shape), departure_time, available_seats, seats_remaining, price, currency, note, status ENUM(ACTIVE,FULL,EXPIRED,CANCELLED,COMPLETED), created_at
ride_requestsA passenger's search intent (optional — browsing offers directly skips this).id, passenger_id FK, from_*/to_* (same shape), passengers_count, preferred_time, note, status ENUM(SEARCHING,MATCHED,EXPIRED,CANCELLED), created_at
ride_bookingsThe stateful link: one passenger's seat(s) on one offer. This is the ride state machine (§10).id, ride_offer_id FK, passenger_id FK, ride_request_id FK NULL, seats_booked, price, status ENUM(REQUESTED,ACCEPTED,CONFIRMED,DRIVER_ON_WAY,PASSENGER_PICKED_UP,IN_PROGRESS,COMPLETED,CANCELLED_BY_PASSENGER,CANCELLED_BY_DRIVER,REJECTED,EXPIRED,NO_SHOW,DISPUTED), cancelled_by, cancelled_at, cancellation_reason, completed_at, created_at

9.7 Deliveries

TablePurposeKey columns
delivery_item_categoriesAdmin-managed (DOCUMENTS, FOOD, GROCERIES, PACKAGE, PERSONAL_ITEM, OTHER, extensible).id, key, name_ar, icon, is_active
delivery_requestsThe delivery job + its state machine.id, customer_id FK, from_*/to_* (same location shape as rides), item_category_id FK, item_description, item_size ENUM(SMALL,MEDIUM,LARGE), preferred_time, suggested_price, notes, status ENUM(CREATED,AVAILABLE,OFFERED,ACCEPTED,PICKUP_PENDING,PICKED_UP,IN_TRANSIT,DELIVERED,COMPLETED,CANCELLED,EXPIRED,DISPUTED,FAILED), accepted_provider_id FK NULL, accepted_at, cancelled_by, cancelled_at, cancellation_reason, completed_at, created_at
delivery_offersOptional price counter-offers before acceptance.id, delivery_request_id FK, provider_id FK, offered_price, note, status ENUM(PENDING,ACCEPTED,REJECTED,WITHDRAWN), created_at

9.8 Ratings, notifications, complaints, payments, audit

TablePurposeKey columns
ratingsPost-completion feedback, either direction.id, service_type ENUM(RIDE,DELIVERY), service_id, rater_id FK, ratee_id FK, stars TINYINT(1-5), review NULL, created_at, UNIQUE(service_type, service_id, rater_id)
notificationsCentral notification store, channel-agnostic.id, user_id FK, type, title, body, data JSON, channel ENUM(IN_APP,PUSH,SMS,EMAIL,WHATSAPP), is_read, read_at, created_at
complaintsUser-reported problems.id, complainant_id FK, category ENUM(DRIVER,PASSENGER,DELIVERY_PROVIDER,PRICE,PAYMENT,SERVICE,SAFETY,OTHER), related_service_type NULL, related_service_id NULL, subject, description, status ENUM(OPEN,UNDER_REVIEW,WAITING_FOR_USER,RESOLVED,CLOSED,REJECTED), assigned_admin_id FK NULL, created_at, updated_at
complaint_messagesThread on a complaint, admin notes flaggable as internal.id, complaint_id FK, sender_type ENUM(USER,ADMIN), sender_id FK, message, is_internal_note BOOL, created_at
payment_methodsAdmin-managed (CASH active now; CARD/WALLET/ONLINE inactive placeholders).id, key, name_ar, is_active
transactionsMoney-movement ledger, currently all CASH/self-reported.id, service_type ENUM(RIDE,DELIVERY), service_id, payer_id FK, payee_id FK, amount, currency, payment_method_id FK, platform_fee, status ENUM(PENDING,COMPLETED,FAILED,REFUNDED), created_at
audit_logsEvery sensitive admin action.id, admin_id FK, action, target_type, target_id, ip_address, user_agent, metadata JSON, created_at
system_settingsKey/value config editable from the admin panel.id, key UNIQUE, value, value_type, description, updated_by FK, updated_at
legal_pagesToS, privacy policy, etc. — versioned, admin-editable.id, slug UNIQUE, title_ar, content_ar, version, published_at, updated_by FK

10 State Machines

10.1 Ride booking (per passenger, per offer)

REQUESTEDACCEPTEDCONFIRMED DRIVER_ON_WAYPASSENGER_PICKED_UPIN_PROGRESSCOMPLETED
REJECTEDCANCELLED_BY_PASSENGERCANCELLED_BY_DRIVEREXPIREDNO_SHOWDISPUTED

REQUESTED → ACCEPTED is the concurrency-critical transition: accepting attempts an atomic UPDATE ride_offers SET seats_remaining = seats_remaining - :n WHERE id = :offer_id AND seats_remaining >= :n inside a transaction. Zero affected rows ⇒ the accept fails cleanly with "لم تعد هناك مقاعد متاحة" and the booking is auto-set to a system-cancelled outcome instead of silently double-booking. ACCEPTED → CONFIRMED only fires once that update actually succeeds — they are kept as two distinct states specifically so "the driver said yes" and "the seat is actually locked" are never conflated in the data.

Terminal states are truly terminal — no code path transitions out of COMPLETED, CANCELLED_*, REJECTED, or EXPIRED. The generic Fsm helper (§8) is given an explicit adjacency map per entity and refuses any transition not in it, so this is enforced once, in code, not by convention.

10.2 Ride offer (the trip itself, independent of any one passenger)

ACTIVEFULL(if reopened by a cancellation)ACTIVE
EXPIREDCANCELLEDCOMPLETED

10.3 Delivery request

CREATEDAVAILABLEOFFEREDACCEPTED PICKUP_PENDINGPICKED_UPIN_TRANSITDELIVEREDCOMPLETED
CANCELLEDEXPIREDDISPUTEDFAILED

AVAILABLE → ACCEPTED uses the same atomic-conditional pattern: UPDATE delivery_requests SET status='ACCEPTED', accepted_provider_id=:pid WHERE id=:id AND status IN ('AVAILABLE','OFFERED'). Zero affected rows ⇒ "تم قبول هذا الطلب من مزود آخر."

10.4 Document verification

NOT_SUBMITTEDPENDINGUNDER_REVIEWAPPROVED
REJECTED → (re-upload) → PENDINGAPPROVED → (time) → EXPIRED → (re-upload) → PENDING

11 API Architecture

Versioned under /api/v1. Web pages authenticate with the session cookie + CSRF token; the same endpoints accept an Authorization: Bearer <token> header for the PWA's own fetch calls today and any future native app tomorrow — one implementation, two auth guards.

Response envelope (uniform, exactly as you specified)

// success
{
  "success": true,
  "message": "تم تنفيذ العملية بنجاح",
  "data": { },
  "errors": []
}

// validation / business error
{
  "success": false,
  "message": "حدث خطأ",
  "data": null,
  "errors": [ { "field": "phone", "message": "رقم الهاتف غير صحيح" } ]
}

Auth & profile

MethodEndpointAuthNotes
POST/api/v1/auth/registerRate-limited by IP + phone; creates user with capability NORMAL_USER, status PENDING until phone verified.
POST/api/v1/auth/verify-phoneOTP confirm → status ACTIVE.
POST/api/v1/auth/loginThrottled (login_attempts); issues session or token.
POST/api/v1/auth/logouttoken/sessionRevokes current token / destroys session.
POST/api/v1/auth/password/forgot
POST/api/v1/auth/password/reset
GET/api/v1/merequiredProfile + capabilities + current availability.
PUT/api/v1/merequiredName, photo — never phone/status via this endpoint.

Verification (driver / vehicle / delivery provider)

MethodEndpointAuthNotes
POST/api/v1/driver-profilerequiredCreates/updates driver_profiles → PENDING.
POST/api/v1/vehiclesrequiredOwner = current user only.
POST/api/v1/documentsrequiredMultipart upload; server validates real MIME/size/extension, stores under randomized name (§13).
GET/api/v1/documents/{id}owner or reviewerStreams the file only after an ownership/permission check + logs to document_access_logs.
POST/api/v1/delivery-profilerequired

Availability

MethodEndpointPermissionNotes
PUT/api/v1/availability/driverDRIVER + verifiedSets OFFLINE/AVAILABLE/BUSY, area, destination.
PUT/api/v1/availability/deliveryDELIVERY_PROVIDER + verified

Rides

MethodEndpointPermissionNotes
POST/api/v1/ride-offersDRIVER, verified"I'm going there" publish.
GET/api/v1/ride-offersany authenticatedFiltered by from/to/time — the deterministic matcher (§6 rules).
POST/api/v1/ride-offers/{id}/cancelowner driverCascades to CONFIRMED bookings → CANCELLED_BY_DRIVER + notification.
POST/api/v1/ride-requestsany authenticatedOptional search-intent record.
POST/api/v1/ride-offers/{id}/bookany authenticatedCreates ride_bookings row, status REQUESTED.
POST/api/v1/ride-bookings/{id}/acceptowner driver of the offerAtomic seat decrement (§10.1).
POST/api/v1/ride-bookings/{id}/rejectowner driver
POST/api/v1/ride-bookings/{id}/cancelpassenger or driver, own bookingcancelled_by / reason recorded.
POST/api/v1/ride-bookings/{id}/transitionowner driverDRIVER_ON_WAY → PASSENGER_PICKED_UP → IN_PROGRESS → COMPLETED, one legal step at a time.

Deliveries

MethodEndpointPermissionNotes
POST/api/v1/deliveriesany authenticated
GET/api/v1/deliveriesany authenticatedProvider view = matching AVAILABLE requests in their area; customer view = own requests.
POST/api/v1/deliveries/{id}/acceptDELIVERY_PROVIDER, verifiedAtomic first-writer-wins (§10.3).
POST/api/v1/deliveries/{id}/rejectinvited provider
POST/api/v1/deliveries/{id}/transitionaccepted providerPICKUP_PENDING → … → DELIVERED.
POST/api/v1/deliveries/{id}/completecustomerCustomer confirms receipt → COMPLETED.
POST/api/v1/deliveries/{id}/cancelcustomer or provider, own

Ratings, notifications, complaints

MethodEndpointPermissionNotes
POST/api/v1/ratingsparticipant of a COMPLETED serviceUnique-rating guard.
GET/api/v1/notificationsrequiredPaginated, own only.
POST/api/v1/notifications/{id}/readowner
POST/api/v1/complaintsrequired
GET/api/v1/complaints/{id}complainant or admin

Admin (web-first, mirrored to API as needed)

MethodEndpointPermissionNotes
GET/admin/verification-queuedocument.reviewBranch-scoped unless system admin.
POST/admin/documents/{id}/approvedocument.reviewAudit-logged.
POST/admin/documents/{id}/rejectdocument.reviewRequires reason; audit-logged.
GET/admin/usersuser.manageSearch/filter §54.
POST/admin/users/{id}/suspenduser.suspendReason required; audit-logged; forces availability OFFLINE.
GET/admin/reports/*branch-scoped read§55 report set.
GET/admin/audit-logsaudit_log.view

12 UI/UX Architecture

Visual system

Neutral warm-white surfaces, near-black ink text, soft borders — the four pastel brand tones (#FBF0B2 #FFC7EA #D8B4F8 #CAEDFF) appear only as small accents: status badges, capability chips, empty-state illustration backgrounds, and one accent color per major section of the app (rides lean blue, deliveries lean yellow) so a user develops a quiet visual sense of "which part of the app am I in" without a loud color scheme anywhere. Type: Cairo throughout, RTL by default (<html dir="rtl" lang="ar">), with numerals kept as Arabic-Indic or Western digits per a single configurable setting (Western digits recommended for price/phone clarity).

Screen inventory

ZoneScreens
PublicLanding, Login, Register, Phone verify, Forgot/reset password, Legal pages (ToS/Privacy/Guidelines)
UserHome dashboard (§21), Profile, Ride search, Ride offer detail, My bookings, Delivery request form, My deliveries, Notifications, Ratings given/received, Complaint form & history
DriverBecome-a-driver onboarding (profile+vehicle+documents), Driver dashboard (§22), Publish a trip, My published trips, Incoming booking requests, Trip-in-progress screen (status transitions)
Delivery providerBecome-a-provider onboarding, Provider dashboard (§23), Nearby requests feed, Delivery-in-progress screen
Branch adminVerification queue (scoped), Users (scoped), Rides/Deliveries (scoped), Complaints (scoped), Reports (scoped)
System adminFull dashboard (§24), Branch management, Service-area management (cities/areas/zones), Document-type & category management, Pricing policy settings, Admin management, Audit log, System settings

Reusable component set

Button, Input, Select, Textarea, Modal, Bottom Sheet, Card, Badge, Alert, Toast, Tabs, Bottom Navigation (user-facing), Top Navigation + Sidebar (admin), Status Indicator (dot + Arabic label, never color alone), Document Card, Vehicle Card, Ride Card, Delivery Card, User Card, Rating stars, Empty State, Loading skeleton, Error state, Confirmation Dialog — each a single PHP view-partial + Tailwind classes, never copy-pasted markup.

Example — status, never as a bare number

🟢 متاح الآن  ·  🟡 قيد المراجعة  ·  🔴 غير متاح  ·  ⛔ تم الرفض

13 Security Architecture

Authentication

  • password_hash() with PASSWORD_BCRYPT (cost 12), never a custom hash.
  • PHP sessions: httponly, secure, samesite=Lax cookies; session ID regenerated on login and on privilege change (capability grant).
  • Login throttling via login_attempts: exponential backoff per (identifier, IP) pair; generic "بيانات الدخول غير صحيحة" for both wrong-password and unknown-account, to resist enumeration.
  • Bearer tokens hashed at rest (never store the raw token), revocable per-device, expirable.

Authorization & IDOR

  • Every resource-fetching controller action resolves the resource, then calls its Policy with (current_user, resource) before touching the response — never trusts that a valid-looking ID in the URL implies access. This is a hard rule, not a guideline: repositories return the row, the policy decides if the caller may see it, the controller never skips the policy call.
  • Branch-scoping is enforced at the query layer (branch admins' list queries are pre-filtered by their assigned branches; there is no admin screen that shows unscoped data by omission).

Document security (§8 of your spec, non-negotiable)

  • Stored under /storage/private/documents/{users|vehicles}/{randomized-uuid}.{ext}, outside the web root if the host allows a directory above public_html; if not, inside it but behind a hard deny from all .htaccess, with a PHP streaming endpoint as the only access path.
  • Server-side validation on every upload: real MIME sniffed via finfo (never the client's Content-Type), extension cross-checked against an allow-list from document_types.allowed_mime_types, size checked against max_size_mb, and the file is never trusted by its original name — a fresh UUID filename is generated server-side.
  • Every view/download is authorization-checked and written to document_access_logs — "who looked at whose ID card, when" must always be answerable.

Standard web hardening

  • SQL injection: PDO prepared statements exclusively — no string-concatenated queries anywhere, enforced by code review discipline plus a lint check in CI.
  • XSS: all output through a single e() escaping helper in views; no raw echo $_....
  • CSRF: per-session token, required on every state-changing web form; the API's bearer-token requests are exempt (no ambient credential to forge) but still origin-checked.
  • Security headers: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Content-Security-Policy (script-src self + Google Fonts only), Strict-Transport-Security once HTTPS is confirmed on the host.
  • Rate limiting on auth + write endpoints, keyed by IP and by user, stored in a small counter table (no Redis needed at this scale).

Concurrency-as-a-security-property

Double-booking a seat or double-accepting a delivery isn't just a bug — it's a fairness/trust failure for a community platform. Both are closed exclusively via atomic, single-statement conditional UPDATE ... WHERE patterns inside a transaction, never a PHP-level "read the count, then decide" (§10, §61).

14 Testing Strategy

LayerCoverageTooling
UnitFSM transition legality, pricing/seat math, validators, policy logic in isolationPHPUnit
IntegrationFull service+repository+DB round-trips for each workflow (register→verify→available→ride→complete→rate)PHPUnit against a disposable test schema
APIAuth, permission denial paths, validation error shape, response envelope conformancePHPUnit HTTP tests
SecuritySQLi injection attempts, XSS payloads round-tripped, CSRF-missing rejected, IDOR cross-user access attempts, upload of a renamed .php file as ".jpg", path traversal filenamesManual + scripted checklist per release
ConcurrencyN simultaneous accept requests against 1 remaining seat/slot; exactly one must winA small PHP CLI script firing parallel requests via curl_multi
UI320/375/390/414px + common desktop widths; RTL layout, Arabic number/date/currency formattingManual pass + browser devtools device emulation
Workflow (E2E)The full scenario in §70 of your spec, run start to finishManual scripted QA pass each release, until volume justifies automation

Required edge cases (from your §71 — all must have an explicit test)

  • Two users request the last seat simultaneously → exactly one CONFIRMED, one clean rejection.
  • Driver cancels after acceptance → cascades to affected bookings + notifications, never a silent orphan.
  • Document expires while provider is AVAILABLE → forced OFFLINE, verified by the expiration sweep test.
  • Suspended user with an active ride → ride still resolves; user cannot start new ones.
  • Delivery provider double-accept race → same atomic guarantee as rides.
  • User requests another user's document by ID → 403, logged as a suspicious access attempt.
  • Ride/delivery ID substitution (IDOR) across every mutating endpoint.
  • Negative/zero/absurd price or passenger count → rejected server-side even if the client-side form was bypassed.
  • Malicious upload: PHP payload renamed .jpg, oversized file, disallowed MIME → all rejected before ever touching disk with the wrong name.
  • Direct role/capability tampering via a crafted request → rejected, since capabilities are only ever written by the verification service, never accepted as client input.
  • Session expiry mid-multi-step form (e.g., mid document upload) → clear re-login prompt, no partial/corrupt state persisted.

15 MVP Scope

P0 — Launch blocking

Auth & profiles · Driver + vehicle verification · Document security & review queue · Availability (driver + delivery) · Ride offers, requests, bookings, full state machine · Delivery requests, accept/reject, full state machine · Ratings · In-app notifications · Admin verification workflow · Core web security (§13)

P1 — Fast follow

Complaints module · Branches · Service-area admin UI · Reports/dashboard charts · Audit log viewer · PWA installability

P2 — Explicitly deferred, architecture keeps the door open

Live GPS/maps · Chat · Wallet/online payment · Coupons/referrals · Driver incentives · Scheduled/recurring rides · Corporate accounts · SOS/safety features · AI-based matching · Demand forecasting

16 Phase-by-Phase Implementation Plan

PhaseDeliversExit check before next phase
0 — DiscoveryThis document.Your sign-off + answers in §18.
1 — ArchitectureFinalized ERD/migrations plan, confirmed host constraints.Host access verified; PHP/MySQL versions confirmed.
2 — FoundationProject skeleton, DB connection/config, migrations runner, auth, roles/permissions, logging, error handling, security middleware.Register→login→logout works end-to-end; CSRF/session tests pass.
3 — VerificationDriver/delivery profiles, vehicles, documents, private storage, admin review queue, expiration sweep, audit logs.Full upload→review→approve/reject→expire cycle tested; IDOR tests on documents pass.
4 — Ride serviceAvailability, publish trip, search/match, booking + full state machine, cancellation, completion, rating.Concurrency test (last-seat race) passes; full ride E2E scenario passes.
5 — Delivery serviceDelivery requests, provider matching, accept/reject, state machine, completion, rating.Concurrency test (double-accept race) passes; full delivery E2E passes.
6 — AdminDashboard, users, branches, areas, complaints, reports, settings.Branch-scoping verified (a branch admin cannot see another branch's data).
7 — PWAManifest, service worker (safe caching only), install prompts, responsive pass.Installable on Android Chrome + iOS Safari "Add to Home Screen."
8 — QA hardeningFull security/edge-case/UI pass from §14.All P0/P1 items in §85 (your Final Acceptance Criteria) checked off.

Per your §87, each phase ends with: tests run, DB integrity checked, security re-checked, mobile UI + RTL checked, authorization re-checked, docs updated — then the next phase starts. No phase is skipped or parallelized into "build everything, test at the end."

17 Deployment Architecture (shared hosting)

No Docker, no container registry, no orchestrator — none of that is available on typical shared hosting, and none of it is needed at this scale. The realistic deployment shape is:

Local/dev machine
   │  git push
   ▼
GitHub repository (private)
   │  either:
   │   (a) SSH + git pull on the host, if the shared plan exposes SSH + git, or
   │   (b) SFTP/rsync of changed files, if it doesn't
   ▼
zahara.m3lsh.com  (public_html/ = /public, everything else one level up if the
                    plan allows a directory outside the web root; otherwise
                    /storage/private/ locked down via .htaccess as the fallback)
   │
   ├─ Apache + PHP-FPM (or mod_php) — confirm version on host
   ├─ MySQL/MariaDB database (separate credentials, never in Git)
   ├─ Cron (host-provided, cPanel-style) running:
   │     - php artisan-equivalent scripts/expire_documents.php   (daily)
   │     - php scripts/expire_stale_offers.php                    (hourly)
   └─ .env (uploaded once, out of Git, permissions 600)

Migrations are plain numbered .sql files applied by a tiny migrate.php CLI runner (tracks applied migrations in a migrations table) — no framework migration tool required. This keeps "how do I stand up a fresh copy of this database" a one-command answer on a host where Composer/Artisan may not be available at all.

18 Questions & Decisions Requiring Your Confirmation

① Host access

SSH to zahara.m3lsh.com timed out on the default port 22 with no prior credentials on file. Please share: SSH host/port, username, and either a password or a public key to install — or confirm SSH isn't offered on this plan and cPanel File Manager/FTP is the intended deployment path instead. This single answer changes several items in §17 and confirms whether Composer is usable at all.

② PHP/MySQL versions & extensions on the host

Needed to confirm: PHP ≥ 8.1, pdo_mysql, fileinfo (for real MIME sniffing), gd or imagick (image re-validation), and whether the plan allows a directory outside public_html for private document storage.

③ Phone verification method

Real SMS OTP requires a paid SMS gateway account (not free). For MVP, is admin-assisted manual verification acceptable (admin marks a phone verified after a call/WhatsApp check), or should I budget for integrating a specific SMS provider now?

④ Legal entity & ToS wording

Confirming per §3/§65: this launches as a community service with a clear "not a licensed transportation company" disclaimer, and the real Terms of Service / driver agreement wording will come from your own legal review before public launch, not from me. I'll ship the legal-pages module and reasonable placeholder copy, not final legal text.

⑤ Initial seed data

Please confirm the exact list of Zahraa Capital zones/landmarks you want seeded on day one (Zone 1–4 assumed from your spec — any more granular pickup points/landmarks worth pre-loading?), and who the first system admin account should belong to (name + phone).

⑥ Domain & branding assets

Is the app served at zahara.m3lsh.com itself, or a subdomain (e.g. app.zahara.m3lsh.com or a separate domain)? Any existing logo/icon set for the PWA manifest, or should I produce a simple wordmark from the brand palette for now?

Once you confirm §18, or tell me to proceed with the stated assumptions where you have no preference, Phase 2 (Foundation) begins — one module at a time, tested and reviewed before the next, exactly as your process requires.