Unit-service requests
Once a shopper owns a unit they enter the Pre-Delivery owner area — the same post-purchase suite
that lets them refer a buyer and request resale assistance. This page
covers the third outbound thing they can do there: raise a unit-service request against a unit
they own — for example ask for a site visit, flag a complaint, request an account statement, or start
a handover. Every one of these is the unit_service variant of App\Models\UserRequest
(app/Models/UserRequest.php:11), tagged with UserRequestsEnums::UNIT_SERVICE (value unit_service,
app/Enums/UserRequestsEnums.php:11). The authoritative record lives in the CRM; the portal keeps
only a thin user_requests mirror.
The whole trip, in one diagram
Section titled “The whole trip, in one diagram”sequenceDiagram actor OW as Owner (Shopper) participant SPA as Pre-Delivery SPA participant API as Backend (Laravel) participant MW as crms-middleware participant CRM as CRM participant DB as Portal DB OW->>SPA: Pick a service code (CR_SiteVisit, CR_Complaint, one of 14) and fill details SPA->>API: POST units customer-requests (authed) with service_slug Note over API: validate service_slug is a UnitServiceRequestCodeEnum name API->>API: map the slug name to the CRM service_code value API->>MW: POST /customer-requests with country header from the unit MW->>CRM: Create customer request keyed on unit_source_id CRM-->>API: 200 API->>DB: SaveUserRequest writes UserRequest request_type = unit_service Note over DB: service_type stores the uppercased slug name not the CRM value API-->>SPA: 200 mapped response
The shape matches the sibling flows: submit to the CRM first, then — only on an HTTP 200 — write
the local UserRequest mirror. If the CRM call is non-200, no local row is written and the request
never appears in the portal’s owner list or analytics (CustomerRequestController.php:48).
The flow, step by step
Section titled “The flow, step by step”-
Route.
POST units/{publishedUnitShopper}/customer-requests→CustomerRequestController@submitUnitService(routes/api/shopper.php:136-139). This route sits inside theauth:sanctum+ability:ACCESS_SHOPPER_APIgroup (routes/api/shopper.php:93-96), so the owner must be logged in — there is no token-optional path here (contrast the referralPOST leadsendpoint, which is outside the authed group). -
Validation.
SubmitUnitServiceRequest(app/Http/Requests/CustomerRequest/SubmitUnitServiceRequest.php:14-27) requirescustomer_source_idandservice_slug. Theservice_slugis constrained toRule::in(UnitServiceRequestCodeEnum::names())(:22) — the names of the enum, e.g.CR_SITEVISIT. Every other field isnullable:preferred_time_of_contact(aContactTime),preferred_way_of_contact(aCommunicationMethod),request_details,change_contact_details,sign_contract_place,sign_contract_requested_option(aContractRequestedOption), andsign_contract_visit_date_and_time.prepareForValidation()uppercases the incoming slug first —strtoupper($this->service_slug)(:29-34) — so the SPA can sendCR_SiteVisitorcr_sitevisitand it still matches the uppercase enum nameCR_SITEVISIT. -
Controller.
submitUnitService(app/Http/Controllers/Api/Shopper/CustomerRequestController.php:32-62) reads$shopper = Auth::user(), calls the CRM action, maps the response, and only if the mapped status is200(:48) callsSaveUserRequestwithrequest_type = UserRequestsEnums::UNIT_SERVICE->value(:54). A non-200 returns the mapped error and writes nothing locally (:61). -
CRM submit (Action).
SubmitUnitService::handle(app/Actions/CustomerRequests/SubmitUnitService.php:18-33) resolves thecountryheader from the unit itself —$unit->project->destination->country->slug(:20) — so unit-service is multi-country, unlike the Egypt-pinned EOI flow.prepareRequestData(:35-42) setsunit_source_id = $unit->source_id, translates the slug to the CRM code withservice_code = UnitServiceRequestCodeEnum::array()[$data['service_slug']](:38), drops the rawservice_slugkey, then POSTs to{crms-middleware}/customer-requestsviaCrmMiddlewareClient::postWithAuthentication(:24-32). -
Local mirror (on 200 only).
formatUserRequestData(CustomerRequestController.php:97-107) builds the row:user_type = Shopper::class,user_id = $shopper->id,request_type = unit_service,service_type = $requestData['service_slug'](the uppercased name),unit_type_id = $unit->unit_type_id, and the unit’sdestination_slugfor the pivot.SaveUserRequest::handle(app/Actions/UserRequest/SaveUserRequest.php:12-21) stampscreated_at_dm, creates theUserRequest, and syncs thedestinationspivot.
The 14 service codes
Section titled “The 14 service codes”UnitServiceRequestCodeEnum (string-backed, app/Enums/UnitServiceRequestCodeEnum.php:7-24) defines
exactly 14 codes. The enum name (uppercase) is what the SPA sends as service_slug and what
the local mirror stores in service_type; the value is the mixed-case service_code sent to the
CRM. The meanings below are read from the case names.
CRM service_code (value) |
Enum case (service_slug / service_type) |
What the owner is asking for |
|---|---|---|
CR_BouncedPayment |
CR_BOUNCEDPAYMENT |
A bounced / failed installment payment |
CR_Cancellation |
CR_CANCELLATION |
Cancel a contract or unit |
CR_ChangeContactDetails |
CR_CHANGECONTACTDETAILS |
Update the owner’s contact details |
CR_ChangeOfOwnership |
CR_CHANGEOFOWNERSHIP |
Transfer ownership of the unit |
CR_HandoverReq |
CR_HANDOVERREQ |
Start / request the unit handover |
CR_Complaint |
CR_COMPLAINT |
Raise a complaint |
CR_FinancialClearance |
CR_FINANCIALCLEARANCE |
A financial-clearance letter |
CR_FinancialReq |
CR_FINANCIALREQ |
A general financial request |
CR_SiteVisit |
CR_SITEVISIT |
Book a site visit |
CR_ModificationReq |
CR_MODIFICATIONREQ |
A unit-modification request |
CR_General |
CR_GENERAL |
A general / uncategorized request |
CR_AddonInterest |
CR_ADDONINTEREST |
Interest in a unit add-on |
CR_SignContract |
CR_SIGNCONTRACT |
Arrange to sign a contract |
CR_AccountStatement |
CR_ACCOUNTSTATEMENT |
An account statement |
