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.

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:
The owner area, end to end
Section titled “The owner area, end to end”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-requestsEverything 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.
The key owner actions
Section titled “The key owner actions”-
List owned units —
GET user/units→UserController@getUnits(app/Http/Controllers/Api/Shopper/UserController.php:57, routeroutes/api/shopper.php:128). This is a local projection, not a CRM call: it plucksunit_idfromUnitSalerows whereuser_id = shopper,status = DONE, andcrm_statusis neithercancelednordraft, then loads thoseUnits withimages,unitType,project.destination.country, andprojectPhase. A unit only appears once its sale finished the buy state machine — a mid-flightDRAFT/PENDING_INFOsale is invisible in the owner area. -
Open one owned unit —
GET user/units/{publishedUnitShopper}→UserController@getUnitDetails(UserController.php:76, routeroutes/api/shopper.php:129). This route carries theownershipmiddleware (OwnershipForUser), which runsIsUnitOwner(app/Actions/Unit/IsUnitOwner.php) — the sameDONE+ not-canceled/draftUnitSaleexistence check — and404s if the logged-in shopper does not own that unit. The detail response eager-loadsconstructionUpdates.images,addons.images, andaccessCard.imagesso the unit page can surface progress, purchasable add-ons, and the owner card in one payload. -
Read the payment schedule —
GET sales/{saleSourceId}/installments→SalesController@getInstallments(app/Http/Controllers/Api/Shopper/SalesController.php:12, routeroutes/api/shopper.php:121). This is a live CRM read:GetSaleInstallments(app/Actions/Sale/GetSaleInstallments.php) callsGET {crms-middleware}/sales/{saleSourceId}/installmentswith acountryheader, then runs a per-country mapper —EgyptInstallmentMapper,OmanInstallmentMapper, orMontenegroInstallmentMapper. The request requires acountry_slugquery 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. -
Pay an installment — installment payment reuses the shared online-payment pipeline (the same Paymob / Thawani / WSPG gateway-by-country, the polymorphic
Transactionledger, 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-only —UnitSalefilteredcrm_status = DRAFTandstatus = DRAFT(app/Providers/RouteServiceProvider.php:76). That binding exists for the reservation charge, so aDONEsale cannot re-enter it — see Gotchas for what this means for owner-side installment collection. -
Sign the contract — the
/pre-delivery/sign-contract/:unitIdscreen does not re-run the reservation-form signing from the buy flow. It submits a unit-service request with service codeCR_SignContract(app/Enums/UnitServiceRequestCodeEnum.php:22) viaPOST units/{publishedUnitShopper}/customer-requests→CustomerRequestController@submitUnitService(CustomerRequestController.php:32, routeroutes/api/shopper.php:136-139), carrying the contract-specific fieldssign_contract_place,sign_contract_requested_option, andsign_contract_visit_date_and_time(app/Http/Requests/CustomerRequest/SubmitUnitServiceRequest.php:20-26).sign_contract_requested_optionis aContractRequestedOption—1 = SIGN_ONLINE,2 = SIGN_OFFLINE,3 = REQUEST_DELIVARY(the enum name is misspelled in source). The fullCR_*mechanics — the slug-to-service_codemapping and the 200-only local mirror — live on the unit-service requests page. -
Owner cards —
GET user/cards→UserController@getAccessCards(UserController.php:49, routeroutes/api/shopper.php:123) returns the shopper’saccessCards(Shopper::accessCardsHasMany,app/Models/Shopper.php:60) with images, shaped byAccessCardResourceintoid, coverimage,project_name, anddestination_name.AccessCard(app/Models/AccessCard.php) is a local, soft-deletable modelbelongsToaShopperand aUnit— an admin-managed membership/access card, not a CRM mirror.
The owner-area endpoints
Section titled “The owner-area endpoints”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 |
