Skip to content

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.

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
Unit-service request — owner picks a CR_* code, CRM records, portal mirrors as unit_service

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).

  1. Route. POST units/{publishedUnitShopper}/customer-requestsCustomerRequestController@submitUnitService (routes/api/shopper.php:136-139). This route sits inside the auth:sanctum + ability:ACCESS_SHOPPER_API group (routes/api/shopper.php:93-96), so the owner must be logged in — there is no token-optional path here (contrast the referral POST leads endpoint, which is outside the authed group).

  2. Validation. SubmitUnitServiceRequest (app/Http/Requests/CustomerRequest/SubmitUnitServiceRequest.php:14-27) requires customer_source_id and service_slug. The service_slug is constrained to Rule::in(UnitServiceRequestCodeEnum::names()) (:22) — the names of the enum, e.g. CR_SITEVISIT. Every other field is nullable: preferred_time_of_contact (a ContactTime), preferred_way_of_contact (a CommunicationMethod), request_details, change_contact_details, sign_contract_place, sign_contract_requested_option (a ContractRequestedOption), and sign_contract_visit_date_and_time. prepareForValidation() uppercases the incoming slug first — strtoupper($this->service_slug) (:29-34) — so the SPA can send CR_SiteVisit or cr_sitevisit and it still matches the uppercase enum name CR_SITEVISIT.

  3. 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 is 200 (:48) calls SaveUserRequest with request_type = UserRequestsEnums::UNIT_SERVICE->value (:54). A non-200 returns the mapped error and writes nothing locally (:61).

  4. CRM submit (Action). SubmitUnitService::handle (app/Actions/CustomerRequests/SubmitUnitService.php:18-33) resolves the country header 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) sets unit_source_id = $unit->source_id, translates the slug to the CRM code with service_code = UnitServiceRequestCodeEnum::array()[$data['service_slug']] (:38), drops the raw service_slug key, then POSTs to {crms-middleware}/customer-requests via CrmMiddlewareClient::postWithAuthentication (:24-32).

  5. 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’s destination_slug for the pivot. SaveUserRequest::handle (app/Actions/UserRequest/SaveUserRequest.php:12-21) stamps created_at_dm, creates the UserRequest, and syncs the destinations pivot.

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