Skip to content

Qualifying a lead into a customer

Between “a promising lead” and “a closed sale” sits one quiet but load-bearing step: promoting a qualified lead into a customer record, then binding that customer to the sale. In the Sales-Man portal this is a chain of source ids — a qualified lead’s lead_source_id becomes a customer_source_id (a Dynamics contact), which the sale then requires as customer_source_id. Like every other sales-man write, the backend is a thin pass-through to crms-middleware, which writes to Microsoft Dynamics 365; the operating country rides on a country header on every call. Verified against current code.

The link that makes this a promotion, not a fresh entry

Section titled “The link that makes this a promotion, not a fresh entry”

The reason “create customer” is a promotion and not a new blank record is a single required field: CreateCustomerRequest requires lead_source_id (app/Http/Requests/SalesMan/Customer/CreateCustomerRequest.php:15). The customer is minted from the qualified lead, carrying its identity forward. The tell that a customer and its originating lead are the same Dynamics contact: reading a customer back (CustomersController@show) maps the CRM payload through the lead mapper — LeadDetailsMapper::map (app/Http/Controllers/Api/SalesMan/CustomersController.php:73).

That customer then unlocks the sale: CreateSaleRequest requires customer_source_id (alongside pipeline_source_id and paymentplan_source_id) — app/Http/Requests/SalesMan/Country/Egypt/CreateSaleRequest.php:15-24 (Oman and Montenegro require the same trio; Montenegro also demands two sign-off dates, Montenegro/CreateSaleRequest.php:15-26).

sequenceDiagram
actor SM as Sales-Man
participant SPA as Sales-Man SPA
participant API as Backend /api/sales-man
participant MW as crms-middleware
participant D365 as Dynamics 365
Note over SM,SPA: Lead is already QUALIFIED in the CRM (state set CRM-side, not by the portal)
SM->>SPA: Open qualified lead - Create customer
SPA->>API: POST /customers with lead_source_id (country header)
Note over API: salesman-country-validation gate<br/>resolve agent per-country CRM source id
API->>MW: POST /salesman/{sourceId}/customers
MW->>D365: create or link contact
D365-->>MW: customer_source_id
MW-->>API: 200
API-->>SPA: customer_source_id
SM->>SPA: Create-sale wizard - pick unit + payment plan
SPA->>API: POST /units/{unit}/sales with customer_source_id + pipeline_source_id + paymentplan_source_id
Note over API: enrich body with unit_source_id from the bound Unit
API->>MW: POST /salesman/{sourceId}/sales
MW->>D365: create sale - new_contracting (EG/OM) or ldv_deal (MNE)
D365-->>MW: sale_source_id
MW-->>API: 200
API-->>SPA: sale_source_id
Qualified lead → customer → sale — the source-id chain (verified)

The sequence, with the code behind each hop

Section titled “The sequence, with the code behind each hop”

Every route below lives inside the salesman-country-validation group (routes/api/sales-man.php:88), so all of them are gated by both the Sanctum SSO token and a valid country header. The agent’s per-country CRM identity is resolved on every write by SalesManUtil::getSourceIdByCountryIdAndUserId (app/Actions/SalesMan/SalesManUtil.php:24-36), which throws if the agent has no source id in that country.

  1. Start from a qualified lead. Qualification is CRM-driven (see the note above). The agent picks a lead already at QUALIFIED and reads it via GET /api/sales-man/leads/{leadSourceId} (sales-man.php:92LeadController@show).

  2. ★ Promote the lead to a customerPOST /api/sales-man/customers (sales-man.php:109) → CustomersController@store (CustomersController.php:78-111) → CreateCustomer@handle (app/Actions/SalesMan/Customer/CreateCustomer.php:13-29). The request requires lead_source_id (CreateCustomerRequest.php:15). The action resolves the agent’s $salesManSourceId, then posts to crms-middleware at /salesman/{sourceId}/customers with the country header. On a CRM 200 the mapped body (carrying the new customer_source_id) is returned; otherwise the controller unwraps the CRM message and returns respondBadRequestWithMsg (CustomersController.php:103-110).

  3. (Optional) attach an ID document to the customerPOST /api/sales-man/customers/{customerSourceId}/attach (sales-man.php:110) → CustomersController@attachAttachCrm@handle (app/Actions/SalesMan/Customer/AttachCrm.php), a multipart upload to /salesman/{sourceId}/customers/{customerSourceId}/attach.

  4. Have an opportunity to sell into — the sale needs a pipeline_source_id, created earlier via POST /api/sales-man/pipelines (sales-man.php:127PipelinesController@store) with a unit-type attached at POST /api/sales-man/pipelines/{pipelineSourceId}/unit-types (sales-man.php:135).

  5. ★ Create the sale, bound to that customerPOST /api/sales-man/units/{publishedUnitSalesMan}/sales (sales-man.php:102) → SalesController@store (SalesController.php:56-95) → CreateSale@handle (app/Actions/SalesMan/Sale/CreateSale.php:14-36). The request requires customer_source_id, pipeline_source_id, paymentplan_source_id, reservation_amount, reservation_payment_method and selling_price (Egypt/CreateSaleRequest.php:15-24). CreateSale::prepareRequestData enriches the body with unit_source_id from the route-bound Unit (CreateSale.php:38-43), then posts to /salesman/{sourceId}/sales. It returns the sale_source_id.

  6. Finalize the sale — attach the signed reservation form (POST .../sales/{saleSourceId}/attach) and PATCH .../sales/{saleSourceId}. Those two “close” calls are documented on Sales-Man closes a sale.

The portal-side sale state: UnitSaleStatus

Section titled “The portal-side sale state: UnitSaleStatus”

The local UnitSale model (app/Models/UnitSale.php:12, table unit_sale) is where the portal mirrors a sale. It carries the full source-id chain built above — sale_source_id, customer_source_id, lead_source_id, paymentplan_source_id — plus status, which casts to the string-backed enum UnitSaleStatus (UnitSale.php:18-45, app/Enums/UnitSaleStatus.php:7-16). This is the portal’s own sale-lifecycle state machine:

Value Enum Meaning
draft DRAFT Sale record started but not yet submitted with buyer info.
pending_info PENDING_INFO Awaiting the customer/buyer information needed to proceed.
download_form DOWNLOAD_FORM Info complete — the reservation form is ready to download and sign.
pending_finish PENDING_FINISH Signed form uploaded — awaiting the final finalize step.
done DONE Sale finalized on the portal side.
canceled CANCELED Sale cancelled.
  1. “Where’s the qualify endpoint?” — there isn’t one. Qualification is set in Dynamics; the portal only reads QUALIFIED and lets the agent create the customer. The only lead-state write the portal owns is disqualify (sales-man.php:94). If someone reports “I can’t qualify the lead in the portal”, that is by design — the CRM owns that transition.

  2. country header first, always. Every route here is inside the salesman-country-validation group (sales-man.php:88). A missing/unknown country header fails at the country gate (app/Http/Middleware/ValidateSalesManCountry.phpFailedToGetCountry) before the controller runs. This is the #1 sales-man support cause across all these flows.

  3. Agent has no CRM source id in that country. SalesManUtil::getSourceIdByCountryIdAndUserId throws when the sales_man_country_source_id row is missing (SalesManUtil.php:24-36) — so the agent can browse but every customer/sale write in that country fails. Fix: seed the per-country source id.

  4. lead_source_id is required to create a customer. Omitting it is a validation failure (CreateCustomerRequest.php:15) — you cannot mint a bare customer with no originating lead through this endpoint. The customer inherits the lead’s Dynamics contact identity.

  5. The sale needs three source ids, not one. customer_source_id and pipeline_source_id and paymentplan_source_id are all required (Egypt/CreateSaleRequest.php:15-24). A common failure is having the customer but no pipeline/opportunity yet — create the pipeline (sales-man.php:127) and attach its unit-type first.

  6. A 200 is not proof it reached Dynamics. Because both writes are pass-through, a clean backend 200 only means the middleware accepted the call. If the customer or sale is missing in Dynamics later, suspect the middleware → Logic App → Dynamics hops and check the middleware logs plus the Azure Logic App run history — the same triage as Sales-Man closes a sale.