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_statusis set byListLeadsMapper::map(app/ResponseMapper/Lead/ListLeadsMapper.php:38-47) andLeadDetailsMapper::map(app/ResponseMapper/Lead/LeadDetailsMapper.php:26-35). It can only ever belead_submitted,lead_verified, orpast_deals. - Sale-level
deal_statusis set per-sale byUnitSaleMapper::map(app/ResponseMapper/Unit/UnitSaleMapper.php:30-56). This is the only placelead_reserved,deal_closed,submitted_invoice, andinvoice_approvedare 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.
The whole trip, in one diagram
Section titled “The whole trip, in one diagram”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_approvedWhat the broker sees
Section titled “What the broker sees”The broker app lists deals split into active and past buckets, each carrying its computed status:

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

Walking each transition
Section titled “Walking each transition”-
Lead created →
lead_submitted. The agent submits the lead form.POST /leads(routes/api/broker.php:150) hitsLeadController@submitLead(app/Http/Controllers/Api/Broker/LeadController.php:29-76), which first enforces the per-destination cap (GetLeadsCount,:46-50), thenSubmitNewLeadPOSTs to the CRM atbrokers/{brokerSourceId}/leads(app/Actions/Lead/SubmitNewLead.php:12-31). On a200, an auditUserRequestrow is written (:62-67) — that is the only local write; there is no deal record. A newly created lead falls through every country map’sdefaultbranch tolead_submitted. -
Lead verified →
lead_verified. A CRM sales admin qualifies the lead. On the next fetch,getLeadDetails(:116-152) /getLeadsre-derive the status through the country map — e.g.EgyptCRMLeadStatusEnums::getStatusmaps status codes1, 2, 3, 5tolead_verified(app/Enums/EgyptCRMLeadStatusEnums.php). Nothing in the portal drives this; it is a pure read of CRM state. -
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) →SubmitNewLeadMeetingPOSTsbrokers/{sid}/leads/{leadSourceId}/meetings(app/Actions/Lead/SubmitNewLeadMeeting.php:12-27). The request needsmeeting_date_timestamp,meeting_location, and acountry_id(app/Http/Requests/Broker/SubmitLeadMeetingRequest.php:16-19). The meeting is never persisted in the portal — see the meetings section below. -
Reservation →
lead_reserved. Once a sale is created in the CRM in a reserved-ish contracting state,UnitSaleMappermaps itscontracting_statusthrough the sale enum. For Egypt,EgyptCRMSaleStatusEnums::getStatusmaps the whole pending/reserved family (127990001, 100000008, 100000000, 127990000, 100000001, 100000009, 100000002, 100000005, 100000003, 100000007) tolead_reserved(app/Enums/EgyptCRMSaleStatusEnums.php:26-34). A finer sub-state (reserved_sale_status) is attached viagetReservedStatus(:36-47). -
Sale closed →
deal_closed. When the sale reachesCONTRACTEDor any handover/delivered status (100000004, 753210000, 753210001, 753210002, 753210003), the same map returnsdeal_closed(app/Enums/EgyptCRMSaleStatusEnums.php:31). -
Invoice submitted →
submitted_invoice. A broker withcan_submit_invoicesubmits 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 localAnalyticsBrokerSaletable (:76-82), fetches the commission from the CRM (GetCommissionForSale→commissions/{saleSourceId},:87-92), and writes a localSalesInvoiceat statusPENDING(app/Actions/SalesInvoice/CreateLeadSalesInvoice.php:28-42). From then on,UnitSaleMapperoverrides that sale’sdeal_statustosubmitted_invoicebecause an invoice row now exists (app/ResponseMapper/Unit/UnitSaleMapper.php:48-56). -
Invoice approved →
invoice_approved. When the invoice’s local status becomesapproved(App\Enums\SalesInvoiceStatus, admin/CRM-driven), the same override promotes the sale’sdeal_statustoinvoice_approved(app/ResponseMapper/Unit/UnitSaleMapper.php:50-55).
The deal status table
Section titled “The deal status table”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). |
How meetings are scheduled
Section titled “How meetings are scheduled”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, callsGetMeetingsonce per country (app/Actions/Meeting/GetMeetings.php:11-51), and merges the results, tagging each with itscountry_id/country_slug.GET /leads/{leadSourceId}/meetings(:155) →getLeadMeetings(:55-87) →GetLeadMeetingsproxiesbrokers/{sid}/leads/{leadSourceId}/meetingsfor one lead (app/Actions/Meeting/GetLeadMeetings.php:13-32).POST /leads/{leadSourceId}/meetings(:156) →submitLeadMeeting→SubmitNewLeadMeetingforwards 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.
