Skip to content

The Pre-Delivery owner area

Once a shopper’s purchase reaches DONE, the app drops them out of the public browse-and-reserve journey and into the Pre-Delivery owner area — a separate owner dashboard for everything that happens between purchase and handover. It is a read-mostly dashboard over the shopper’s owned units: view the units, read the payment schedule and pay installments, sign the contract, hold owner (access) cards, and reach out for help. The CRM stays the source of truth for sale, schedule, and installments — the portal serves a thin local projection for the unit list and cards, and reads the schedule live from crms-middleware on demand.

Shopper Pre-Delivery owner portal — My Units / Payments / Services / Add-ons / Resell Assistance (greeting name blurred as PII, shown here as a fresh empty-state account).

The three outbound owner actions — buying add-ons, referring a buyer / requesting resale, and raising a service request — each have their own flow page and are not re-documented here:

sequenceDiagram
actor OW as Owner (Shopper)
participant SPA as Pre-Delivery SPA
participant API as Backend (Laravel)
participant MW as crms-middleware
participant GW as Gateway (Paymob/Thawani/WSPG)
Note over OW,MW: Sale reaches DONE — the owner enters the Pre-Delivery area
OW->>SPA: Open My Units
SPA->>API: GET user/units (authed)
API-->>SPA: owned units (local DONE projection)
OW->>SPA: Open a unit then its payment schedule
SPA->>API: GET sales/{saleSourceId}/installments
API->>MW: GET /sales/{id}/installments (country header)
MW-->>API: raw installment schedule
API-->>SPA: per-country mapped installments
OW->>SPA: Pay an installment
SPA->>GW: shared online-payment pipeline (see linked flow)
OW->>SPA: Sign the contract
SPA->>API: POST units/{unit}/customer-requests (CR_SignContract)
API->>MW: POST /customer-requests
Entering the Pre-Delivery area and the read-mostly owner actions

Everything in this diagram sits inside the auth:sanctum + ability:ACCESS_SHOPPER_API group in routes/api/shopper.php:93-96 — the owner is always a logged-in Shopper, so unlike the token-optional referral path there are no owner-less rows here.

  1. List owned unitsGET user/unitsUserController@getUnits (app/Http/Controllers/Api/Shopper/UserController.php:57, route routes/api/shopper.php:128). This is a local projection, not a CRM call: it plucks unit_id from UnitSale rows where user_id = shopper, status = DONE, and crm_status is neither canceled nor draft, then loads those Units with images, unitType, project.destination.country, and projectPhase. A unit only appears once its sale finished the buy state machine — a mid-flight DRAFT/PENDING_INFO sale is invisible in the owner area.

  2. Open one owned unitGET user/units/{publishedUnitShopper}UserController@getUnitDetails (UserController.php:76, route routes/api/shopper.php:129). This route carries the ownership middleware (OwnershipForUser), which runs IsUnitOwner (app/Actions/Unit/IsUnitOwner.php) — the same DONE + not-canceled/draft UnitSale existence check — and 404s if the logged-in shopper does not own that unit. The detail response eager-loads constructionUpdates.images, addons.images, and accessCard.images so the unit page can surface progress, purchasable add-ons, and the owner card in one payload.

  3. Read the payment scheduleGET sales/{saleSourceId}/installmentsSalesController@getInstallments (app/Http/Controllers/Api/Shopper/SalesController.php:12, route routes/api/shopper.php:121). This is a live CRM read: GetSaleInstallments (app/Actions/Sale/GetSaleInstallments.php) calls GET {crms-middleware}/sales/{saleSourceId}/installments with a country header, then runs a per-country mapperEgyptInstallmentMapper, OmanInstallmentMapper, or MontenegroInstallmentMapper. The request requires a country_slug query param that must exist as a published country (InstallmentRequest, app/Http/Requests/Unit/InstallmentRequest.php) — the slug both routes the call to the right CRM and selects the mapper. A slow schedule is a slow CRM, not a slow database.

  4. Pay an installment — installment payment reuses the shared online-payment pipeline (the same Paymob / Thawani / WSPG gateway-by-country, the polymorphic Transaction ledger, and the success callback documented on the online payment pipeline page) rather than a bespoke installment rail. Note the sharp edge: the only card-payment route on a sale, POST units/{publishedUnitShopper}/{salePayment}/payment (routes/api/shopper.php:107), binds {salePayment} DRAFT-onlyUnitSale filtered crm_status = DRAFT and status = DRAFT (app/Providers/RouteServiceProvider.php:76). That binding exists for the reservation charge, so a DONE sale cannot re-enter it — see Gotchas for what this means for owner-side installment collection.

  5. Sign the contract — the /pre-delivery/sign-contract/:unitId screen does not re-run the reservation-form signing from the buy flow. It submits a unit-service request with service code CR_SignContract (app/Enums/UnitServiceRequestCodeEnum.php:22) via POST units/{publishedUnitShopper}/customer-requestsCustomerRequestController@submitUnitService (CustomerRequestController.php:32, route routes/api/shopper.php:136-139), carrying the contract-specific fields sign_contract_place, sign_contract_requested_option, and sign_contract_visit_date_and_time (app/Http/Requests/CustomerRequest/SubmitUnitServiceRequest.php:20-26). sign_contract_requested_option is a ContractRequestedOption1 = SIGN_ONLINE, 2 = SIGN_OFFLINE, 3 = REQUEST_DELIVARY (the enum name is misspelled in source). The full CR_* mechanics — the slug-to-service_code mapping and the 200-only local mirror — live on the unit-service requests page.

  6. Owner cardsGET user/cardsUserController@getAccessCards (UserController.php:49, route routes/api/shopper.php:123) returns the shopper’s accessCards (Shopper::accessCards HasMany, app/Models/Shopper.php:60) with images, shaped by AccessCardResource into id, cover image, project_name, and destination_name. AccessCard (app/Models/AccessCard.php) is a local, soft-deletable model belongsTo a Shopper and a Unit — an admin-managed membership/access card, not a CRM mirror.

All of these sit inside the authed shopper group (routes/api/shopper.php:93-96); {p} = the pre-delivery/shopper API surface.

Endpoint Controller / Action Source of truth Notes
GET user/units UserController@getUnits local UnitSale projection owned units, DONE and not canceled/draft
GET user/units/{publishedUnitShopper} UserController@getUnitDetails local Unit (+ addons, accessCard) ownership middleware → IsUnitOwner, else 404
GET sales/{saleSourceId}/installments SalesController@getInstallments live CRM per-country mapper, country_slug required
GET user/cards UserController@getAccessCards local AccessCard admin-managed owner/access cards, not a CRM mirror
GET user/offers UserController@getOffers local (mock — TODO) returns paginated Offer rows, not owner-scoped yet
GET user/news NewsController@getUserNews local real filtered news via GetNewsWithFilters
GET user/referrals ReferralController@getUserReferrals local (mock — TODO) hardcoded referral list, never queries the CRM
GET user/customer-requests CustomerRequestController@index live CRM owner’s submitted service requests, read from the CRM
POST units/{publishedUnitShopper}/customer-requests CustomerRequestController@submitUnitService CRM + thin mirror service requests incl. CR_SignContract
POST units/{publishedUnitShopper}/resell-assistance CustomerRequestController@submitResellAssistance CRM + thin mirror resale request (see referrals flow)
GET issue-types IssueTypeController@index local help topics for the Get-Help screen