Skip to content

Sales-Man API reference

The Sales-Man persona is the API behind the Orascom sales-agent SPA (https://salesman.orascom.robustastudio.com). It is the widest persona in the backend: 120 routes that let an agent work leads, build pipelines, qualify customers, close sales, submit EOIs for launches, run Taskeen (hand-over) lists, and read sales analytics. Almost everything here is a thin proxy over the CRM middleware — the controllers validate, attach the agent identity, forward to crms-middleware, then map the response back through the standard envelope.

Route file: backend/routes/api/sales-man.php. Controller namespace: App\Http\Controllers\Api\SalesMan (RouteServiceProvider.php:157-160).

All routes are registered under the api/sales-man prefix with the api middleware group (RouteServiceProvider.php:157-160).

Environment Base URL
Staging https://api.orascom.robustastudio.com/api/sales-man
Local http://localhost/api/sales-man

Example: GET https://api.orascom.robustastudio.com/api/sales-man/leads.

Every response is wrapped by FormatApiResponse into { "data": ..., "message": ... }, except the two routes marked withoutMiddleware([FormatApiResponse]) (the signed PDF download and the broken inline-PDF route). The throttle:api limiter allows 60 requests/min per token, raised to 180/min for any path containing /analytics (RouteServiceProvider.php:131-136).

Authentication — Microsoft SSO to Sanctum

Section titled “Authentication — Microsoft SSO to Sanctum”

Sales agents do not log in with a password. They authenticate through Microsoft Azure SSO (Laravel Socialite azure driver), which mints a Sanctum personal-access token carrying the access-sales-man-api ability.

The two SSO entry points live in routes/web.php (namespace App\Http\Controllers), not in the persona API file:

Method Path Controller Purpose
GET login/microsoft SalesMan\Auth\MicrosoftController@microsoftLogin Redirect to Microsoft (MicrosoftController.php:18)
GET login/microsoft/callback SalesMan\Auth\MicrosoftController@handleProviderCallback Handle callback, mint token, redirect to SPA (MicrosoftController.php:23)

On callback the controller reads the Azure profile (email, name, microsoft_id), upserts the agent via SaveUserInfo, then issues the token with the sales-man ability and redirects the browser to SALESMAN_FRONTEND_URL/?token=<token> (MicrosoftController.php:52-57). The SPA stores that token and sends it as Authorization: Bearer <token> on every subsequent API call.

Inside sales-man.php, the authenticated block stacks auth:sanctum + ability:access-sales-man-api (sales-man.php:20). The user-provider:sales_man middleware rebinds the web guard to the SalesMan model (table sales_team) for the whole file (sales-man.php:8).

Three auth tiers exist in this file:

  • Public (no token) — user-provider:sales_man only. The 5 sale-info lookup filters plus the signed PDF download.
  • Authenticated — adds auth:sanctum + ability:access-sales-man-api. Units, destinations, saved units, notifications, sales-offer PDFs.
  • Authenticated + country-scoped — additionally requires the country header (see below). Leads, pipelines, customers, sales, meetings, EOIs, Taskeen, CRM lookups, analytics.

ValidateSalesManCountry reads the country header and resolves a published Country by slug, then binds it into the container as a scoped Country instance that the country-scoped controllers type-hint (ValidateSalesManCountry.php:27-37):

$country = Country::published()
->where('slug', $request->header('country'))->first();
if (! $country) {
throw new FailedToGetCountry();
}
  • Header name: country.
  • Accepted values are published country slugs: egypt, oman, montenegro (CountryEnums).
  • Missing/unknown/unpublished value → FailedToGetCountry is thrown before the controller runs.

The header does more than gate access — it selects per-country validation rules and date handling. create_lead, create_pipeline, create_sale, disqualify_lead, submit_meeting, update_pipeline, mark_pipeline_as_lost, and create_unit_type each dispatch to an Egypt / Oman / Montenegro ValidatorInterface implementation via CountryRequestValidator::make(...). So the accepted request body for the same endpoint differs by the country header value.

Which groups need it:

Route group Needs country header
Auth (logout) No
Public sale-info filters + signed PDF No
Units, filters, saved units, sales-offer PDF No
Destinations No
User / notifications No
Leads Yes
Pipelines Yes
Customers (qualify) Yes
Sales Yes
Meetings Yes
EOI & launches Yes
Taskeen Yes
CRM lookups / conditional fields Yes
Analytics Yes

In the tables below, {sid} = the authenticated agent’s CRM source id, and {api} = CRMS_MIDDLEWARE_API_URL.

Base: user-provider:sales_man + auth:sanctum + ability:access-sales-man-api. No country header.

Method Path Purpose Auth / notes
GET logout Revoke the current access token Authed. Broken — namespace/method mismatch, see Gotchas

Base: user-provider:sales_man only. No token, no country header.

Method Path Purpose Auth / notes
GET sales-nationalities List sale nationalities Public · CRM GET {api}/nationalities
GET sales-countries List sale countries Public · CRM GET {api}/countries
GET sales-cities List sale cities Public · CRM GET {api}/cities
GET sales-occupations List sale occupations Public · CRM GET {api}/occupations
GET sales-connections List sale connections Public · CRM GET {api}/connections
GET units/{publishedUnitSalesMan}/sales-offer/{salesOfferPdfGeneration}/download Stream a generated sales-offer PDF Public but signed URL · raw response (no envelope)

Base: authed, no country header. {publishedUnitSalesMan} resolves to a sales-man-published Unit; {publishedAvailableUnitSalesMan} additionally requires the unit to be available (both 404 out-of-scope ids before the controller runs).

Method Path Purpose Auth / notes
GET filters Available unit filters Authed
GET units List/filter units Authed
GET units/export Export units to xlsx Authed
GET units/compare Compare units by ids Authed
GET units/{publishedAvailableUnitSalesMan} Show unit details Authed · binding (published + available)
GET units/{publishedUnitSalesMan}/payment-terms Fetch unit payment terms Authed · CRM GET {api}/units/{source_id}/payment-terms
POST units/{publishedUnitSalesMan}/{saleSourceId}/reservation-form Download reservation-form PDF Authed · CRM POST {api}/sales/{saleSourceId}/reservation-form
POST units/{publishedUnitSalesMan}/share-reservation-link Email a reservation link to a lead Authed · local (mail)
GET user/saved-units List saved units Authed
POST user/saved-units/{publishedAvailableUnitSalesMan} Save a unit Authed · salesman-resource-visibility:publishedAvailableUnitSalesMan
DELETE user/saved-units/{savedUnit} Remove a saved unit Authed · salesman-resource-visibility:savedUnit

Base: authed, no country header. Generation is async — POST .../generate dispatches GenerateSalesOfferPdfJob to the PDF_SERVICE_URL microservice; poll .../status, then download via the signed URL above.

Method Path Purpose Auth / notes
GET units/{publishedUnitSalesMan}/sales-offer Download the CRM sales-offer PDF Authed · CRM GET {api}/units/{source_id}/sales-offer
GET units/{publishedUnitSalesMan}/sales-offer/{paymentPlanId} (legacy inline PDF) Broken — target method missing, see Gotchas
POST units/{publishedUnitSalesMan}/sales-offer/{paymentPlanId}/generate Start async PDF generation Authed · throttle:sales-offer-generate (default 5/min)
GET units/{publishedUnitSalesMan}/sales-offer/{salesOfferPdfGeneration}/status Poll generation status + file URL Authed

Base: authed, no country header. {destinationSlug} resolves to a published Destination.

Method Path Purpose Auth / notes
GET destinations List assigned published destinations Authed
GET destinations/{destinationSlug} Show destination details Authed
GET destinations/{destinationSlug}/projects List allowed projects Authed
GET destinations/{destinationSlug}/projects/{project}/marketing-materials Download project marketing zip Authed · builds zip
GET destinations/{destinationSlug}/unit-types List destination unit types Authed
GET destinations/{destinationSlug}/locations List destination locations Authed
GET destinations/{destinationSlug}/testimonials List destination testimonials Authed
GET destinations/{destinationSlug}/facilities List destination facilities Authed
GET destinations/{destinationSlug}/faqs List destination FAQs Authed
GET destinations/{destinationSlug}/construction-updates List construction updates Authed
GET destinations/{destinationSlug}/educational-hubs List educational hubs Authed

Base: authed + country-scoped (country header required).

Method Path Purpose Auth / notes
GET leads List leads Country · CRM GET {api}/salesman/{sid}/leads
GET leads/search Search leads Country · CRM GET {api}/salesman/{sid}/leads/search
POST leads/check-lead-eligible Check reservation eligibility, return a reservation link Country · CRM POST {api}/salesman/{sid}/leads/check-lead-eligible
GET leads/{leadSourceId} Show lead details Country · CRM GET {api}/salesman/{sid}/leads/{leadSourceId}
POST leads Create a lead Country · CRM POST {api}/salesman/{sid}/leads
POST leads/{leadSourceId}/disqualify Disqualify a lead Country · CRM POST {api}/salesman/{sid}/leads/{leadSourceId}/disqualify
GET leads/{leadSourceId}/feedback List lead feedback Country · CRM GET {api}/salesman/{sid}/leads/{leadSourceId}/feedback
GET pipelines List pipelines Country · CRM GET {api}/salesman/{sid}/pipelines
POST pipelines Create a pipeline Country · CRM POST {api}/salesman/{sid}/pipelines
PATCH pipelines/{pipelineSourceId} Update a pipeline Country · CRM PATCH {api}/salesman/{sid}/pipelines/{pipelineSourceId}
PATCH pipelines/{pipelineSourceId}/lost Mark a pipeline as lost Country · CRM PATCH {api}/salesman/{sid}/pipelines/{pipelineSourceId}/lost
GET pipelines/{pipelineSourceId}/unit-types List pipeline unit types Country
POST pipelines/{pipelineSourceId}/unit-types Add a pipeline unit type Country
GET pipelines/{pipelineSourceId}/feedback List pipeline feedback Country
GET pipelines/{pipelineSourceId}/units List pipeline units Country

Base: authed + country-scoped. Qualifying a lead means creating the CRM customer record it graduates into.

Method Path Purpose Auth / notes
GET customers List customers Country · CRM GET {api}/salesman/{sid}/customers
GET customers/search Search customers Country · CRM GET {api}/salesman/{sid}/customers/search
GET customers/{customerSourceId} Show customer details Country · CRM GET {api}/salesman/{sid}/customers/{customerSourceId}
POST customers Create (qualify) a customer from a lead Country · CRM POST {api}/salesman/{sid}/customers
POST customers/{customerSourceId}/attach Attach a file to a customer Country · CRM POST {api}/salesman/{sid}/customers/{customerSourceId}/attach

Base: authed + country-scoped. {publishedUnitSalesMan} binding gates the unit.

Method Path Purpose Auth / notes
GET sales List the agent’s sales Country · CRM GET {api}/salesman/{sid}/sales
GET sales/search Search sales Country · CRM GET {api}/salesman/{sid}/sales/search
POST units/{publishedUnitSalesMan}/sales Create a sale Country · CRM POST {api}/salesman/{sid}/sales
POST units/{publishedUnitSalesMan}/sales/{saleSourceId}/attach Attach a file to a sale Country · CRM POST {api}/salesman/{sid}/sales/{saleSourceId}/attach
PATCH units/{publishedUnitSalesMan}/sales/{saleSourceId} Update a sale Country · CRM PATCH {api}/salesman/{sid}/sales/{saleSourceId}

Base: authed + country-scoped.

Method Path Purpose Auth / notes
GET meetings List the agent’s meetings Country · CRM GET {api}/salesman/{sid}/meetings
GET pipelines/{pipelineSourceId}/meetings List meetings for a pipeline Country
POST pipelines/{pipelineSourceId}/meetings Submit a meeting on a pipeline Country · per-country validation

Base: authed + country-scoped. These back the conditional dropdowns in the lead/pipeline/sale forms; each proxies the matching CRM list.

Method Path Purpose Auth / notes
GET ambassadors List ambassadors Country · CRM GET {api}/salesman/{sid}/ambassadors
GET events List events Country · CRM GET {api}/salesman/{sid}/events
GET offices List offices Country · CRM GET {api}/salesman/{sid}/offices
GET system-users List system users Country · CRM GET {api}/salesman/{sid}/system-users
GET exhibitions List exhibitions Country · CRM GET {api}/salesman/{sid}/exhibitions
GET crm-units List CRM units Country · CRM GET {api}/salesman/{sid}/units
GET brokers List brokers Country · CRM GET {api}/salesman/{sid}/brokers
GET sales-agents List sales agents Country · CRM GET {api}/salesman/{sid}/sales-agents
GET competitions List competitions Country · CRM GET {api}/salesman/{sid}/competitions
GET corporates List corporates Country · CRM GET {api}/salesman/{sid}/corporates
GET lead-sources List lead sources Country · CRM GET {api}/salesman/{sid}/lead-sources
GET lead-sources/{sourceId}/subsources List lead sub-sources Country
GET lead-countries List lead countries Country · CRM GET {api}/salesman/{sid}/lead-countries

Base: authed + country-scoped. {publishedLaunch} resolves to a published Launch; {launchValue} resolves a published launch by slug or id. Full-EOI submission (storeAll) is async — it stores uploads to disk and dispatches SaveEOIToCrmJob.

Method Path Purpose Auth / notes
GET launches List the agent’s launches Country · local
GET launches/{launchValue} Show launch details Country · local
GET launches/{launchValue}/facilities List launch facilities Country · local
GET launches/{launchValue}/faqs List launch FAQs Country · local
GET bank-accounts List bank accounts Country · local
GET eois List EOIs Country · CRM GET {api}/eois
GET eois/search Search EOIs Country · CRM GET {api}/eois/search
GET eois/export Export filtered EOIs Country · CRM GET {api}/eois
POST launches/{publishedLaunch}/eois Submit a full EOI (async) Country · dispatches SaveEOIToCrmJob
PATCH launches/{publishedLaunch}/eois/{eoiBySourceId} Update an EOI + notify Country · CRM PATCH {api}/launches/{source_id}/eois/{eoiSourceId}
PATCH launches/{publishedLaunch}/eois/{eoiSourceId}/require-action Set EOI require-action Country
POST launches/{publishedLaunch}/eois/{eoiSourceId}/attach Attach a file to an EOI Country
PATCH launches/{publishedLaunch}/eois/{eoiSourceId}/cancel Cancel an EOI Country
GET launches/{publishedLaunch}/eois/{eoiSourceId} Show EOI details Country
GET launches/{publishedLaunch}/eois/{eoiSourceId}/export Export a single EOI as PDF Country
GET launches/{publishedLaunch}/eois/{eoiSourceId}/attachments List EOI attachments Country
DELETE launches/{publishedLaunch}/eois/{eoiSourceId}/attachments/{attachmentSourceId} Delete an EOI attachment Country
GET launches/{publishedLaunch}/unit_types/{priceSourceId}/preferences Fetch launch unit-type preferences Country
GET launches/{publishedLaunch}/eois/{eoiBySourceId}/resend-payment-link Resend the EOI payment invitation Country · local (mail)
POST launches/{publishedLaunch}/eois/{eoiSourceId}/products Create an EOI product Country
PATCH launches/{publishedLaunch}/eois/{eoiSourceId}/products/{productSourceId} Update an EOI product Country
PATCH launches/{publishedLaunch}/eois/{eoiBySourceId}/products/{productSourceId}/delete Delete a product, sync payment Country
POST launches/{publishedLaunch}/eois/{eoiSourceId}/products/{productSourceId}/unit-types Create a product unit type Country
PATCH launches/{publishedLaunch}/eois/{eoiBySourceId}/products/{productId}/unit-types/{unitTypeId}/delete Delete a unit type, sync payment Country
POST launches/{publishedLaunch}/eois/{eoiSourceId}/products/{productSourceId}/unit-types/{unitTypeSourceId}/preferences Create a unit-type preference Country
DELETE launches/{publishedLaunch}/eois/{eoiSourceId}/products/{productSourceId}/unit-types/{unitTypeSourceId}/preferences/{prefId} Delete a preference Country
POST launches/{publishedLaunch}/eois/{eoiBySourceId}/products/full-product Create a full product tree Country · composite
POST launches/{publishedLaunch}/eois/{eoiBySourceId}/products/{productSourceId}/unit-types/store-bulk Bulk-create product unit types Country · composite

Base: authed + country-scoped. Taskeen = the unit hand-over list.

Method Path Purpose Auth / notes
GET taskeen List taskeen records Country · CRM GET {api}/taskeen
GET taskeen/export Export taskeen to xlsx/csv Country · CRM GET {api}/taskeen

Base: authed + country-scoped. All are local (query the mirrored DB, not the CRM). Paths under /analytics get the raised 180/min rate limit.

Method Path Purpose Auth / notes
GET analytics/filters/destinations Destinations filter Country · local
GET analytics/filters/unit-types Unit-types filter Country · local
GET analytics/filters/sales-team Sales-team filter Country · local
GET analytics/sales/overview-summary Sales overview summary Country · local
GET analytics/sales/deals-vs-closed-deals-per-month Deals vs closed deals per month Country · local
GET analytics/sales/sale-sources-breakdown Sales by sale-source Country · local
GET analytics/sales/pipeline-types-breakdown Sales by pipeline-type Country · local
GET analytics/sales/unit-categories-breakdown Sales by unit-category Country · local
GET analytics/sales/sum-sales-revenues-daily-breakdown Daily sales-revenue breakdown Country · local
GET analytics/leads/categories-breakdown Leads by category Country · local
GET analytics/agents-ranking Sales-agent ranking Country · local
GET analytics/meetings/overview-summary Meetings overview summary Country · local
GET analytics/meetings/meetings-with-no-feedback Meetings lacking feedback Country · local
GET analytics/meetings/meetings-count-per-salesman Meetings count per salesman Country · local

Base: authed, no country header.

Method Path Purpose Auth / notes
GET user/notifications List the agent’s notifications Authed · local
POST user/notifications/read Mark notifications read Authed · local

Request / response detail for key endpoints

Section titled “Request / response detail for key endpoints”

The five write endpoints below drive the core sales workflow. All require the country header; the request body for create lead, create pipeline, and create sale is the union of common rules plus a per-country rule set selected by CountryRequestValidator (the Egypt variant is shown).

LeadController@store (LeadController.php:60), validated by SubmitLeadRequest (SubmitLeadRequest.php:19).

Common body (SubmitLeadRequest.php:26):

Field Rule
first_name required, string
last_name required, string
phone required, string
phone_2phone_4 nullable, string
email nullable, string
destination_slug required, must exist in destinations
preferred_communication_method nullable, CommunicationMethod enum
preferred_communication_time nullable, CommunicationTime enum
customer_inquiry nullable, CustomInquiryEnums
interested_in_unit_id nullable, must exist in units
budget nullable, numeric, min 0
portal_comments nullable, string

Egypt-specific additions (Country/Egypt/CreateLeadRequest.php:13): channel_id (required, ChannelEnums), feedback_id (EgyptLeadFeedbackEnums), lead_quality_code (LeadQualityEnums), plus nullable source ids ambassador_source_id, broker_source_id, salesagent_source_id, office_source_id, corporate_source_id, event_source_id, exhibition_source_id, and referral fields.

Behavior (LeadController.php:77-105): forwards to POST {api}/salesman/{sid}/leads. On CRM 200 it also records a local UserRequest and returns the mapped CRM body. On non-200 it extracts the CRM message; if it contains "duplicate" it fires the DuplicateLead notification to the agent and returns 400 with that message.

POST pipelines — create a (lead-discovery) pipeline

Section titled “POST pipelines — create a (lead-discovery) pipeline”

PipelinesController@store (PipelinesController.php:56), validated by CreatePipelineRequest (CreatePipelineRequest.php:15) which delegates to the per-country validator.

Egypt body (Country/Egypt/CreatePipelineRequest.php:16, extends PipelineFeedbackRequest):

Field Rule
lead_source_id nullable, required without customer_source_id, string
customer_source_id nullable, required without lead_source_id, string
pipeline_type required, LeadPipelineTypeEnums (1 = single unit, 2 = multiple units)
destination_slug required, must exist in destinations
plus feedback fields inherited from PipelineFeedbackRequest

A pipeline must be anchored to either a lead or a customer (mutually-required pair). Forwards to POST {api}/salesman/{sid}/pipelines; returns the mapped body on 200, else 400 with the CRM message (PipelinesController.php:83-90).

POST customers — qualify (create) a customer

Section titled “POST customers — qualify (create) a customer”

CustomersController@store (CustomersController.php:78), validated by CreateCustomerRequest (CreateCustomerRequest.php:11).

Field Rule
lead_source_id required, string
country_id required, string
address required, string
marital_status required, int
nationality_id, origin_nationality_id nullable, string
city_id / city_name, district_id / district_name, other_district_name nullable
occupation_code nullable, int
occupation_name, other_occupation_name nullable, string
national_id_number, passport_number nullable, string
expiry_date, issue_date, date_of_birth nullable, int (timestamps)
gender nullable, GenderEnums
email, issued_from, arabic_issue_from, arabic_address nullable, string

Forwards to POST {api}/salesman/{sid}/customers; returns the mapped body on 200, else 400 with the CRM message (CustomersController.php:104-112).

POST units/{publishedUnitSalesMan}/sales — create a sale

Section titled “POST units/{publishedUnitSalesMan}/sales — create a sale”

SalesController@store (SalesController.php:56), validated by CreateSaleRequest (Sale/CreateSaleRequest.php:10) delegating to the per-country validator.

Egypt body (Country/Egypt/CreateSaleRequest.php:12):

Field Rule
customer_source_id required, string
pipeline_source_id required, string
paymentplan_source_id required, string
reservation_amount required, numeric
reservation_payment_method required, PaymentMethods enum
selling_price required, numeric
currency nullable, CurrencyEnums
currency_rate nullable, decimal (0–3 places)

Forwards to POST {api}/salesman/{sid}/sales. A sale ties together an existing customer, pipeline, and payment plan.

POST launches/{publishedLaunch}/eois — submit a full EOI (async)

Section titled “POST launches/{publishedLaunch}/eois — submit a full EOI (async)”

EOIController@storeAll (EOIController.php:382), validated by SubmitFullEOIRequest (SubmitFullEOIRequest.php:18).

Key fields:

Field Rule
name, phone, email required, string
phone_2, phone_3, comment nullable
paid_amount, currency_id required, int
payment_status required, EOIPaymentStatusEnums
payment_method required, EOIPaymentMethodEnums
attachment_1 required file (1–5120 KB)
attachment_2, cheque_attachment nullable file
customer_identification_number_type nullable, EOICustomerIdentificationNumberTypeEnums
instapay_attachment[] nullable array of { attachment (file), referenceid (string) }
products[] present array; each item quantity (int) + unittypes[]
products.*.unittypes.*.unit_type_id required, must exist in unit_types
products.*.unittypes.*.project_id required, must exist in projects
products.*.unittypes.*.preferences.*.preference_source_id required, string
products.*.unittypes.*.preferences.*.rank required, integer

Behavior (EOIController.php:385-431): sets source_of_sale = DIRECT, persists every uploaded file to the eoi-documents disk, mints a unique EOI-<uuid> id, and dispatches SaveEOIToCrmJob. It returns respondSuccess() immediately — the CRM write happens on the queue, so a 200 here means “accepted”, not “created in CRM”.

POST leads/check-lead-eligible — eligibility gate + reservation link

Section titled “POST leads/check-lead-eligible — eligibility gate + reservation link”

LeadController@checkLeadEligible (LeadController.php:211), validated by CheckLeadEligibleRequest. Forwards to POST {api}/salesman/{sid}/leads/check-lead-eligible. If the CRM returns is_eligible: true, the controller generates a reservation link via GenerateUnitReservationLinkAction::handleForSalesMan(...) and returns { "reservation_link": "..." }. Otherwise it returns 400 with “This lead is not eligible for a new reservation because an active reservation already exists.” (LeadController.php:226-236).

The country header is silent-but-fatal. Any of the ~85 country-scoped routes (sales-man.php:88-269) returns a FailedToGetCountry error when the country header is missing, empty, or set to a slug that is not a published country (ValidateSalesManCountry.php:27-37). There is no default fallback. The accepted values are egypt, oman, and montenegro. Beyond gating access, the header also selects the country-specific validation rules for create/update lead, pipeline, sale, meeting, and disqualify — so a request body that validates for egypt may be rejected for oman/montenegro.