Skip to content

The broker deal lifecycle

A broker deal is the whole relationship between an agent and a prospective buyer: it starts as a lead, gets meetings booked against it, becomes a reservation, closes as a sale, and ends with a commission invoice. This page traces that lifecycle end-to-end and shows how meetings are scheduled. Everything here is verified against the current backend (develop @ 8434e475).

The one vocabulary, projected from three CRMs

Section titled “The one vocabulary, projected from three CRMs”

Every deal state the apps render is a case of App\Enums\DealStatusEnums (string-backed, app/Enums/DealStatusEnums.php:10-16). That single vocabulary is projected out of three country-specific CRM status sets — Egypt, Oman, and Montenegro each number their Dynamics statuses differently, and each has its own getStatus() that collapses those integers down to the shared DealStatusEnums strings.

The subtlety worth internalising: the lifecycle is computed across two different objects.

  • Lead-level lead_status is set by ListLeadsMapper::map (app/ResponseMapper/Lead/ListLeadsMapper.php:38-47) and LeadDetailsMapper::map (app/ResponseMapper/Lead/LeadDetailsMapper.php:26-35). It can only ever be lead_submitted, lead_verified, or past_deals.
  • Sale-level deal_status is set per-sale by UnitSaleMapper::map (app/ResponseMapper/Unit/UnitSaleMapper.php:30-56). This is the only place lead_reserved, deal_closed, submitted_invoice, and invoice_approved are produced.

So a deal doesn’t have one status — it has a lead status and a per-sale status, and the app stitches them into a single timeline.

sequenceDiagram
actor BR as Broker
participant SPA as Broker SPA
participant API as Backend (Laravel)
participant MW as crms-middleware
participant D365 as Dynamics 365 CRM
participant DB as Portal DB
BR->>SPA: Submit new deal (the lead form)
SPA->>API: POST /leads → submitLead
API->>MW: POST brokers/{sid}/leads
MW->>D365: create lead
D365-->>API: 200
Note over D365: status NEW → lead_submitted
API->>DB: SaveUserRequest (audit only, no deal row)
D365->>API: POST leads/{id}/notifications
Note over API: notify broker the lead landed
Note over D365: sales admin verifies → lead_verified
BR->>SPA: Schedule a meeting (date + location)
SPA->>API: POST /leads/{id}/meetings → submitLeadMeeting
API->>MW: POST brokers/{sid}/leads/{id}/meetings
Note over API: forward-only — nothing stored locally
D365->>API: POST meetings/{id}/notifications
Note over API: AcceptedMeeting or RejectedMeeting
Note over D365: sale reserved → per-sale deal_status lead_reserved
Note over D365: sale contracted or handover → deal_closed
BR->>SPA: Submit commission invoice
SPA->>API: POST /leads/{id}/sales_invoices
API->>MW: GET commissions/{saleSourceId}
API->>DB: SalesInvoice PENDING → submitted_invoice
Note over D365: admin approves invoice → invoice_approved
A broker deal — lead to meeting to reservation to sale to invoice (verified)

The broker app lists deals split into active and past buckets, each carrying its computed status:

Broker app — the leads/deals list, split into active and past deals with a per-deal status.

New deal opens the lead form the agent fills for the prospective buyer — the entry point of the whole lifecycle:

Broker app — the create-deal (lead) form: buyer details, destination, and interest.

  1. Lead created → lead_submitted. The agent submits the lead form. POST /leads (routes/api/broker.php:150) hits LeadController@submitLead (app/Http/Controllers/Api/Broker/LeadController.php:29-76), which first enforces the per-destination cap (GetLeadsCount, :46-50), then SubmitNewLead POSTs to the CRM at brokers/{brokerSourceId}/leads (app/Actions/Lead/SubmitNewLead.php:12-31). On a 200, an audit UserRequest row is written (:62-67) — that is the only local write; there is no deal record. A newly created lead falls through every country map’s default branch to lead_submitted.

  2. Lead verified → lead_verified. A CRM sales admin qualifies the lead. On the next fetch, getLeadDetails (:116-152) / getLeads re-derive the status through the country map — e.g. EgyptCRMLeadStatusEnums::getStatus maps status codes 1, 2, 3, 5 to lead_verified (app/Enums/EgyptCRMLeadStatusEnums.php). Nothing in the portal drives this; it is a pure read of CRM state.

  3. Meeting scheduled. The agent books a meeting against the lead — POST /leads/{leadSourceId}/meetings (routes/api/broker.php:156) → MeetingController@submitLeadMeeting (app/Http/Controllers/Api/Broker/MeetingController.php:89-121) → SubmitNewLeadMeeting POSTs brokers/{sid}/leads/{leadSourceId}/meetings (app/Actions/Lead/SubmitNewLeadMeeting.php:12-27). The request needs meeting_date_timestamp, meeting_location, and a country_id (app/Http/Requests/Broker/SubmitLeadMeetingRequest.php:16-19). The meeting is never persisted in the portal — see the meetings section below.

  4. Reservation → lead_reserved. Once a sale is created in the CRM in a reserved-ish contracting state, UnitSaleMapper maps its contracting_status through the sale enum. For Egypt, EgyptCRMSaleStatusEnums::getStatus maps the whole pending/reserved family (127990001, 100000008, 100000000, 127990000, 100000001, 100000009, 100000002, 100000005, 100000003, 100000007) to lead_reserved (app/Enums/EgyptCRMSaleStatusEnums.php:26-34). A finer sub-state (reserved_sale_status) is attached via getReservedStatus (:36-47).

  5. Sale closed → deal_closed. When the sale reaches CONTRACTED or any handover/delivered status (100000004, 753210000, 753210001, 753210002, 753210003), the same map returns deal_closed (app/Enums/EgyptCRMSaleStatusEnums.php:31).

  6. Invoice submitted → submitted_invoice. A broker with can_submit_invoice submits a commission invoice — POST /leads/{leadSourceId}/sales_invoices (routes/api/broker.php:159) → SalesInvoiceController@createLeadSalesInvoice (app/Http/Controllers/Api/Broker/SalesInvoiceController.php:54-118). It looks the sale up in the local AnalyticsBrokerSale table (:76-82), fetches the commission from the CRM (GetCommissionForSalecommissions/{saleSourceId}, :87-92), and writes a local SalesInvoice at status PENDING (app/Actions/SalesInvoice/CreateLeadSalesInvoice.php:28-42). From then on, UnitSaleMapper overrides that sale’s deal_status to submitted_invoice because an invoice row now exists (app/ResponseMapper/Unit/UnitSaleMapper.php:48-56).

  7. Invoice approved → invoice_approved. When the invoice’s local status becomes approved (App\Enums\SalesInvoiceStatus, admin/CRM-driven), the same override promotes the sale’s deal_status to invoice_approved (app/ResponseMapper/Unit/UnitSaleMapper.php:50-55).

App\Enums\DealStatusEnums (app/Enums/DealStatusEnums.php:10-16). “Computed by” is the mapper that can emit the value — note the lead axis and the sale axis are distinct:

Value Enum Computed by Meaning
lead_submitted LEAD_SUBMITTED lead mappers (default branch) Lead created, not yet verified by the CRM.
lead_verified LEAD_VERIFIED lead mappers via country getStatus() CRM sales admin qualified the lead.
lead_reserved LEAD_RESERVED UnitSaleMapper via sale enum A sale exists in a reserved / pending-reservation state.
deal_closed DEAL_CLOSED UnitSaleMapper via sale enum Sale contracted or in handover / delivered.
submitted_invoice INVOICE_SUBMITTED UnitSaleMapper override A local SalesInvoice exists and is still pending.
invoice_approved INVOICE_APPROVED UnitSaleMapper override The SalesInvoice status is approved.
past_deals PAST_DEALS lead enums, then re-labeled Lost / cancelled / blacklisted lead — rewritten before it reaches the app (see gotchas).

Meetings hang off a lead and are entirely CRM-owned. The portal is a thin proxy with three endpoints, none of which touch a local meetings table:

  • GET /meetings (routes/api/broker.php:154) → MeetingController@getMeetings (app/Http/Controllers/Api/Broker/MeetingController.php:20-53). Lists all of a broker’s meetings by fanning out across every country the broker operates in — it loops $broker->brokerCountrySourceIds, calls GetMeetings once per country (app/Actions/Meeting/GetMeetings.php:11-51), and merges the results, tagging each with its country_id / country_slug.
  • GET /leads/{leadSourceId}/meetings (:155) → getLeadMeetings (:55-87) → GetLeadMeetings proxies brokers/{sid}/leads/{leadSourceId}/meetings for one lead (app/Actions/Meeting/GetLeadMeetings.php:13-32).
  • POST /leads/{leadSourceId}/meetings (:156) → submitLeadMeetingSubmitNewLeadMeeting forwards straight to the CRM (app/Actions/Lead/SubmitNewLeadMeeting.php:12-27).

The approval decision happens inside Dynamics 365 (a sales manager approves or rejects). It comes back through the inbound webhook POST meetings/{meetingSourceId}/notifications (routes/api/crms-middleware.php:16, Passport client_credentials) → CrmsMiddleware\NotificationController@meetingNotification (app/Http/Controllers/Api/CrmsMiddleware/NotificationController.php:53-69) → MeetingSubmissionNotification (app/Actions/User/Broker/MeetingSubmissionNotification.php:12-27), which notifies the broker with AcceptedMeeting or RejectedMeeting.