إنتقل إلى المحتوى الرئيسي

Connected operations model

ShamBus surfaces one connected transportation system. Applications may present different tasks and permissions, but they must not invent separate meanings or disconnected copies of the same Company, Route, Trip, Bus, Driver, Booking, Traveler, Passenger, Financial Document, or Payment.

The canonical product language lives in the repository root CONTEXT.md.

Operational graph

Company
├─ Company Profile
├─ Company Members
├─ Buses ─ Bus Layout ─ Seat Types
├─ Drivers
└─ Routes ─ Stops
└─ Trips ─ Trip Media
├─ Bus
├─ Driver
├─ Seat Inventory
└─ Booking Segments
└─ Journey Booking ─ Booking Passengers ─ Tickets
├─ Payments ─ Refunds
├─ Financial Documents
├─ Notifications
└─ Support Cases

Traveler Account
├─ Authentication Identities
├─ Saved Passengers
├─ Bookings
├─ Notifications
├─ Preferences
└─ Saved Views / recent discovery state

Journey, Trip, and inventory boundary

A Journey is the traveler product. It contains one or more ordered Journey Legs; every leg is fulfilled by exactly one dated Trip between two Stop Calls. A Trip remains the shared operational truth for the driver, Company control center, Platform control, support, tracking, quality, and settlement.

Journey discovery never conflates a reusable physical Stop with a Trip-specific Stop Call. Each boarding/alighting projection exposes stop_id for place, mapping, and navigation plus stop_call_id for quote, hold, and operations. The legacy id field aliases stop_call_id only to keep already-released clients compatible; new code must use the explicit fields.

The customer-facing aggregate is journey_bookings. The compatibility bookings table now stores one Booking Segment per Journey Leg so existing payment, notification, QR, dashboard, and mobile consumers continue to operate during migration. Every segment carries a non-null journey_booking_id; a transfer is never represented as one synthetic Trip or a collection grouped only in application memory. ADR-007 records this deliberate compatibility boundary.

Seat Inventory and Resource Inventory are Trip-specific. Baggage, special baggage, bicycle spaces, accessibility resources, and adjacent empty seats cannot be represented as unchecked Booking metadata: each consumes an authoritative capacity row and follows the same hold, expiry, purchase, rebooking, release, and audit lifecycle as a seat.

Journey Booking transaction boundary

The quote command validates one to four chronologically ordered Journey Legs, same-city transfer safety, Trip sale state, seat labels, resource capacity, currency support, caller ownership, and server-side prices. It creates an opaque checkout session and an immutable price snapshot; a repeated idempotency key returns the same quote and rejects a different request.

The hold command reads only that checkout-session quote snapshot. It locks each Trip/resource pair in deterministic order, reserves exact seats through the compatibility seat-lock contract, reserves non-seat resources from the same inventory authority, and rolls back every leg when one resource conflicts. A PostgreSQL worker expires abandoned batches every minute.

Confirmation consumes the complete hold in one transaction and creates or updates:

  • the Journey Booking aggregate and guest/account ownership;
  • named Booking Passengers with category and accessibility requirements;
  • one compatibility Booking Segment for each Journey Leg;
  • per-Passenger Seat Assignments where exact seats were selected;
  • capacity-backed Extras;
  • one HMAC-signed Ticket for each Passenger and Journey Leg;
  • compatibility Trip seat counters and canonical resource inventory.

Failed validation leaves every hold active and unchanged; a successful idempotent replay returns the original segments and Tickets without duplicating records. Guest checkout may create or reuse a phone-scoped Traveler record, but it never creates an Authentication Identity or marks the phone verified; claiming the account still requires the OTP identity-linking contract.

Boarding credential boundary

The boarding QR is a strict, PII-free credential with one shape: type = shambus.journey-ticket, an integer version, a Ticket UUID, and a 64-character HMAC signature. It represents one Passenger on one Journey Leg—not the Booking party and not a mutable Booking reference. Customer web and mobile render the same credential; the Driver app parses the same schema online and from its encrypted offline manifest.

The database validates and records one atomic journey_ticket_checkin. It binds the caller to the assigned Driver and assignment, rejects wrong-Trip, forged, stale, cancelled, expired, reissued, or already-used credentials, bounds offline timestamps and coordinates, and makes replay idempotent for that Ticket only. The compatibility segment attendance becomes checked in only after every active Ticket on that segment has been recorded.

Public Route and Trip references use database sequences rather than UUID fragments. This keeps references stable and unique even for deterministic seed UUIDs.

Live operations command boundary

Driver telemetry, Stop Call events, check-in, announcements, and disruption reports are caller-bound database commands. Each command:

  • verifies the authenticated Driver or authorized Company/Platform actor;
  • locks the affected assignment and operational records;
  • rejects out-of-order or cross-Trip relationships;
  • accepts a stable idempotency key for offline replay where applicable;
  • updates the canonical Trip projection;
  • appends an immutable operational event;
  • creates dispatcher alerts only for actionable exceptions.

CONFIRMED → CHECKED_IN → COMPLETED is the canonical Booking attendance lifecycle. Boarding must never mark a Booking completed. A Trip cannot depart while a confirmed Passenger remains neither checked in nor explicitly marked no-show.

Operational Alerts are intentionally hidden from traveler and Driver RLS projections. Drivers receive task-specific messages and announcement controls; dispatchers and Platform control own the exception queue.

Live-location, Trip Event, Stop Call, Announcement, Disruption, and Alert policies do not join identity tables under the caller's table privileges. Narrow SECURITY DEFINER predicates return only an authorization decision for the current identity. Direct Driver writes to bus_locations are disabled; record_driver_telemetry is the sole authenticated command path. Guest tracking remains available only through the booking-reference projection and never through direct location-table access.

The Company control snapshot ranks active services, critical/open alerts, active disruptions, delay, and active-trip GPS health before routine scheduled work. Missing vehicle or Driver assignment becomes urgent only inside a two-hour departure horizon. The bounded response reports both the total Trips in the requested window and the number returned, so pagination limits cannot masquerade as operational totals.

Quality and partner settlement

Quality Snapshots freeze explainable, weighted evidence for one Company and period. The current baseline measures punctuality, completion reliability, check-in compliance, passenger rating, vehicle quality, and incident-free operation. SLA breaches create connected operational alerts and can carry a contractual financial effect.

Commercial Terms are versioned and period-effective. A Settlement freezes those terms, the Quality Snapshot, every scheduled Trip reconciliation line, bonuses, deductions, tax, and a source hash. Approval requires all Trip lines to be resolved plus an immutable statement and Trip breakdown document. Confirmed payments close the outstanding balance exactly; overpayment is rejected.

Monthly generation is durable and idempotent. PostgreSQL runs a daily scheduler at 00:15 UTC, enqueues the prior Damascus calendar month once per eligible Company/currency, and retries failures with bounded exponential backoff before dead-lettering. A recent-2FA Platform finance administrator can explicitly retry a failed run. The scheduler excludes suspended and demo Companies.

Internal QA world and role suite

The gated internal production seed provisions distinct Platform Admin, Platform Support, Company Owner, Company Admin, Dispatcher, Staff, Driver, and optional Traveler identities. Its Company world includes Routes, historical/current/future Trips, Bookings, Reviews, Support Tickets, default Seat Types, and one active validated layout per Bus.

INTERNAL_QA_SEED_DAYS and INTERNAL_QA_SEED_BOOKINGS_PER_TRIP bound fixture volume. Existing complete QA worlds are preserved on credential reseeding; the seed updates identities, verifies layouts, records a versioned marker in companies.settings.internal_qa_seed, and does not add a different sample of Bookings on every restart.

Legacy QA worlds created before that durable marker are classified once from the explicit internal-company signature. Only active Company members, active Drivers, and Travelers with a Booking on that internal tenant receive trusted app_metadata; a self-authored metadata value never promotes an account.

Foreign keys remain the source of relationship truth. Labels, route names, company names, and status text are presentation fields and never replace identifiers.

Entity reference contract

Every detail response may expose compact related references, but must not recursively embed the entire graph.

An entity reference contains:

  • a stable entity kind;
  • the canonical UUID or public slug where applicable;
  • the localized primary label;
  • optional secondary context such as booking code or plate number;
  • a lifecycle status when the caller is authorized to see it;
  • the canonical destination path for the current application;
  • a permission state when the record exists but is not actionable.

Related collections return counts and a small ordered preview. Full lists are loaded through their collection endpoint with the relationship encoded as a filter. This prevents N+1 detail queries and makes every “view all” destination shareable.

Contextual navigation

  • Collection state lives in the URL, not only component memory.
  • Detail links include a validated relative returnTo value when the source is a filtered collection.
  • Back actions prefer returnTo; invalid, absolute, or cross-origin values are rejected and fall back to the canonical collection route.
  • Breadcrumbs describe domain hierarchy, not browser history.
  • Related-record links are permission-aware. Hidden data is not inferred from disabled links.
  • Mobile detail flows use native back navigation while retaining the same serialized return state.

Collection query contract

Every large collection defines an allow-listed query schema. The common vocabulary is:

KeyMeaning
qBounded full-text search text.
Resource filter keysAllow-listed scalar, boolean, enum, relation, or range filters.
sortOne or more allow-listed field:asc or field:desc rules.
page / per_pageBounded offset pagination for ordinary administrative lists.
cursorStable cursor pagination where records change frequently or collections are large.
viewOptional saved-view identifier owned by the current principal.

The UI-facing collection state is typed as search, filters, sort rules, pagination, visible fields, and density. Resource definitions map that state to URL keys and server schemas. APIs never accept arbitrary database column names or raw filter expressions.

Query execution rules

  • Tenant/company scope is applied before user filters.
  • Search is normalized and length-limited.
  • Every filter and sort field has an explicit database expression and authorization rule.
  • Stable ordering always ends with a unique key such as id.
  • Counts and summaries use batched aggregates, not per-row queries.
  • Indexes are added for actual filter/sort combinations and verified with representative query plans.
  • Empty, invalid, or unauthorized filters return a typed response rather than silently widening scope.

Saved views

A Saved View belongs to one authenticated principal and one resource kind. It stores a versioned query document, visible fields, density, and an optional default flag. Server validation replays the document through the current resource definition so an old or manipulated view cannot bypass allow-lists or authorization.

Named views are private by default. Shareable URLs carry only public query state; sharing a private Saved View identifier does not grant access.

Company profile publication

Company settings and Company Profile are separate concepts:

  • settings contain private operational and account configuration;
  • the Company Profile contains moderated public information;
  • public profile media uses explicit purpose, ordering, alt text, crop/focal metadata, and publication state;
  • the Company can edit only its own draft profile;
  • Platform Administrators can review, publish, suspend, or annotate verification;
  • customer applications read only the published projection.
  • the published projection, company-review endpoint, marketing routes, and sitemap all exclude internal-QA and demo Companies at their service-role boundary; a known Company id or slug is not an access capability.

Operating cities and routes are relationships, not comma-separated text. Policies are versioned records so a Booking can retain the policy version accepted at purchase time.

Trip media

Trip Media is a first-class relation, not a screen-only URL column. Each record carries purpose, source, storage path, processing state, width/height, aspect ratio, focal point, alt text, sort order, and audit timestamps.

The original upload is private to processing. Published responsive variants are immutable and may be served to customer applications. A Trip with no published media uses the deterministic Route Illustration fallback; it never substitutes the Company logo as a trip image.

Seat-type provisioning

Standard, Comfort, Premium, and VIP are ordinary Company-owned Seat Type rows created by an idempotent provisioning function. The function runs for existing Companies through a backfill and for new Companies during provisioning. It never overwrites a Company-customized row.

The stable default codes are onboarding keys, not permanent display labels. Companies may rename, disable, reorder, recolor, add amenities, and configure pricing behavior while historical bookings retain their issued seat classification.

Traveler authentication identities

Supabase Auth owns Authentication Identities and sessions. ShamBus does not store provider passwords or a has_google boolean.

Google sign-in

  1. Web requests a short-lived server nonce, renders Google's official Identity Services button, and returns only the signed ID token plus a validated relative return path.
  2. iOS and Android use the native Google library and obtain an ID token for the same configured Web/server audience; they never embed the desktop sign-in page.
  3. ShamBus verifies the Google signature, issuer, audience, expiry, subject, verified email, and—on web—the nonce before issuing a Supabase session.
  4. A service-role-only resolver maps the verified identity to exactly one Traveler Account and rejects staff-account reuse. Web and mobile call the same canonical session/linking module.
  5. Verified email profile data may prefill missing Traveler fields; it never overwrites user-confirmed data silently.
  6. A missing phone does not block authentication. The booking flow requests it only before a phone-dependent operation.

Linking and duplicate prevention

  • A verified Passenger email is synchronized to the existing Supabase auth user before a Google session is issued; an existing password and phone identity are preserved.
  • Google subject and verified-email linking is serialized in PostgreSQL. Profile linking additionally proves the signed-in Traveler owns the target Passenger.
  • A provider/email conflict fails closed into account recovery; it never creates a second Traveler silently and never moves bookings based only on frontend claims.
  • Connected methods are read from the verified Traveler identity graph; legacy Supabase-provider identities remain disconnect-compatible during migration.
  • Disconnect is allowed only when another usable Sign-in Method remains.

Booking continuity

Authentication state contains a validated relative returnTo plus a short-lived server-side checkout-draft identifier. The draft references the Trip, seat-lock session, selected seats, entered Passenger data, pricing version, and search context. The OAuth URL never carries private Passenger details or prices as trusted values. After authentication, the server revalidates the seat lock and price before restoring the flow.

Current implementation evidence

  • Traveler web uses Google's official button, a flow-specific HttpOnly nonce-digest cookie, and a same-origin server exchange. No OAuth secret or trusted profile claim exists in browser code.
  • Native clients exchange only the provider ID token. The same API trust boundary verifies it with Google's supported verifier, preserves existing password/phone methods, and consumes a single-use internal Supabase magic-link token to issue the canonical session.
  • A phone-less Google traveler can complete an SMS or Telegram challenge through the shared E.164 phone and single-code controls. The transactional RPC returns the canonical Passenger ID after a safe merge; the mobile coordinator updates encrypted session data before navigation.
  • Relative return paths preserve the complete Journey query and reject external and authentication-loop destinations.
  • The google_sign_in adapter, iOS URL scheme, Android prod flavor, server exchange, account-race checks, release compilation, and Flutter/browser regression gates are implemented. Real Google client registrations, the release Android signing fingerprints, and physical-device sign-in evidence remain external deployment gates; production readiness must not be claimed until they pass.

Deployment order

  1. Add backward-compatible schema, indexes, RLS, provisioning functions, and read contracts.
  2. Backfill existing seat types, profile projections, verified identity email links, and media metadata idempotently.
  3. Deploy shared types, formatting, UI primitives, and server adapters behind disabled feature flags where configuration is required.
  4. Migrate application callers and remove obsolete primitives.
  5. Enable Google only after web JavaScript origins, native client registrations, bundle/package identifiers, Android signing fingerprints, iOS callback scheme, and account-linking audit checks pass.
  6. Run the role, RTL/LTR, device, performance, and production smoke matrix.
  7. Enforce source guards and remove compatibility paths.

No migration phase may make a private record public, widen tenant access, or discard an existing capability to simplify presentation.