Skip to content

Broker submits a lead

This is the reference flow for onboarding. If you understand this one end-to-end, you understand how every write-flow in the platform is shaped: React SPA → Laravel API → CRM middleware → Microsoft Dynamics 365. Everything here is verified against the current code (backend develop @ 8434e47, middleware develop @ 10a41eb, frontend develop, 2026-07-05).

The broker logs into the portal (www.broker.orascom.robustastudio.com, email + password), opens My Deals, clicks New Deal, fills the form, and hits Create Deal.

Broker portal — My Deals (leads list): Active/Past tabs, phone search, status filters, and the New Deal button.

The New Deal form is the entire client-side contract — every field here maps to one field in the API request:

Broker portal — the New Deal form: First/Last name, Email, Phone (up to 3), Destination, Unit type/name, Client’s budget, Preferred way/time of contact, Additional information.

sequenceDiagram
actor B as Broker
participant SPA as Broker SPA (React)
participant API as Backend (Laravel)
participant MW as CRM middleware (Laravel)
participant LA as Azure Logic App
participant D365 as Dynamics 365 (per country)
B->>SPA: Fill "New Deal" form, Create Deal
SPA->>API: POST /api/broker/leads (Bearer, JSON)
Note over API: auth:sanctum + ability:access-broker-api<br/>SubmitLeadRequest validation
API->>MW: GET /leads/count (leads-limit gate)
MW-->>API: count
Note over API: if over limit → 422, stop (never reaches CRM)
API->>MW: POST /api/portals/brokers/{id}/leads<br/>header country: egypt (OAuth client_credentials)
Note over MW: country header → id (eg=1/om=2/mne=3)<br/>map unified → Dynamics 'lead' fields
MW->>LA: POST {LEAD_CRM_SUBMIT_NEW_LEAD_URL} { "Lead": {...} }
LA->>D365: create lead in the country's org
D365-->>LA: leadid, customerid
LA-->>MW: 200 { Lead: { leadid, customerid } }
MW-->>API: 200 { data: { lead_source_id, customer_source_id } }
Note over API: on exact 200 → write a UserRequest audit row
API-->>SPA: 200 { data: {...}, message: "Success!" }
SPA->>B: toast "Deal Created Successfully" → back to My Deals
Broker lead submit — write path (verified)
  1. The form builds a snake_case payload. new-deal.tsxsubmitHandler assembles a NewDealFormInterface and calls DealUtils.createLead()Network.fetch to POST {NX__BROKER_API_ENDPOINT}{NX__BROKER_ENDPOINT_PREFIX}/leads (i.e. …/api/broker/leads). The broker’s bearer token (localStorage['broker_access_token']) is attached automatically by Network.getHeaders. Two fields are hardcoded by the form: customer_inquiry = '2' (Unit Purchasing) and portal_page = '1' (Unit Page).

  2. The backend authenticates and validates. Route routes/api/broker.php:150 runs user-provider:brokersauth:sanctumability:access-broker-api, then SubmitLeadRequest validates 15 fields before the controller body runs. The UserProvider middleware is what makes Auth::user() return a Broker.

  3. A leads-limit gate runs first. LeadController@submitLead calls GetLeadsCount — if the broker’s brokerage has a per-destination monthly limit and it’s reached, the request returns HTTP 422 and never touches the CRM. (This gate was added May 2026, commit 79e22614 / ORASCOM-3526 — a stack from before then won’t have it.)

  4. The backend forwards to the CRM middleware. SubmitNewLead resolves the broker’s CRM broker_source_id for that country, swaps destination_slug → destination_source_id, strips spaces from the phone, and POSTs to {CRMS_MIDDLEWARE_API_URL}/brokers/{brokerSourceId}/leads with header country: egypt, authenticated as an OAuth2 client_credentials service client.

  5. The middleware maps to Dynamics and calls a Logic App. It converts the country header to a numeric id (egypt=1, oman=2, montenegro=3), maps the unified fields to Dynamics lead field names via SubmitNewLeadRequestMapper (e.g. first_name → firstname, phone → fullmobilenumber, broker_source_id → salesagentid), wraps it as { "Lead": {…} }, and POSTs to a single Azure Logic App (LEAD_CRM_SUBMIT_NEW_LEAD_URL). The Logic App fans the lead out to the correct Dynamics org based on that country field.

  6. Dynamics creates the lead; the ids flow back. The Logic App returns { Lead: { leadid, customerid } } → the middleware returns { data: { lead_source_id, customer_source_id } } → the backend, on an exact HTTP 200, writes one user_requests audit row (see below) and returns { data: {…}, message: "Success!" } to the SPA, which toasts success and returns to My Deals.

The one thing that surprises everyone: writes and reads take different paths

Section titled “The one thing that surprises everyone: writes and reads take different paths”

Almost nothing. The authoritative lead lives in Dynamics. On a successful submit the backend writes exactly one user_requests audit row (user_id = broker, user_type = Broker, request_type = lead, a date key) plus one destination_user_request pivot row. The lead’s PII — name, phone, email, budget — is NOT stored locally (those keys aren’t $fillable, so UserRequest::create() silently drops them). This matters for support: to see a lead’s details you query the CRM (via a read endpoint), not the portal database.

The backend double-wraps: FormatApiResponse adds an outer { data, message }, and the response mapper adds an inner envelope. A successful submit looks like:

{
"data": { "message": "...", "data": { "lead_source_id": "..." }, "errors": null },
"message": "Success!"
}

message is "Success!" when the HTTP status starts with 2, else "Failure!". The full field list, every status code, and the Dynamics field map are in the Reference page.