# Engagements & Bookings — the Property Booking product line

**Portal:** Manage · **Namespace:** `Src\Engagement` ·
**Route groups:** `manage.sales-projects.*` · `manage.engagements.*` · `manage.bookings.*` ·
`manage.closing-modes.*` · `manage.pipeline-roles.*` · `manage.leads.referrals.*` ·
**Surfaces:** `/manage/sales-projects` (the hub, five `?view=` panels + a project Show page),
the Lead Show **"Pipeline"** tab, and **Setting → Closing Modes**

**Nav:** Sales → **Property Booking** — the third of the Sales hub's main tabs
([`SalesTabs.vue`](/resources/js/Components/SalesTabs.vue)), whose three stages render as a
`StageTabs` chevron rail on `/manage/sales-projects`. No sidebar entry of its own
(GUIDELINES §15).

> **This file is the map.** It carries only what a reader needs before choosing a chapter: what an
> engagement is, the shape of the hub, and where each subject is written down. Everything else —
> the statuses, the money, the schema, the surfaces — lives in the chapters below, so no single
> page has to be re-read to change one thing.

## What it does

Models the real sales lifecycle of a **property agent team**: a lead is worked **per project**,
not once overall. An **engagement** is one `(lead, project)` sales cycle, worked by a team whose
roles are **defined by admins at runtime**, and it runs a single ordered status ladder from *New*
to either *Converted* or *Lost*. A lead interested in two projects has **two engagements**, each
with its own status and its own team.

This is the **reform of the old `Lead::status`**: the lifecycle state that used to be one flat
enum on the lead now lives on the engagement, one row per project. `leads.status` survives only as
a **synced roll-up** so existing list / export / inbox readers keep working — it is no longer
edited directly.

When a deal closes, the closer records a **Booking** (the unit + its prices + the SPA paperwork),
and the deal's **commission** and its **split between teammates** are derived from that booking,
the project's rate, and the team — none of it stored as an amount.

> **Phase 1 scope.** This ships the per-project pipeline + assignment + Booking/SPA + the
> commission configuration. The wider "Lead Pool Engine" from `docs/lead_temp/LEAD_LIFECYCLE_SPEC`
> — fresh/recycle/cancelled/converted categories, A/B/C/D buckets, `distribution_count`,
> caller/closer skill tiers, pull-based *Request Leads*, weekly promotion, AI-bot auto-routing —
> is **deferred to Phase 2**, and will attach to the engagement (per-project), not to the lead.

## The chapters

| File | Read it when you need |
|---|---|
| [lifecycle.md](/docs/modules_handbook/manage/engagement/lifecycle.md) | the **Engagement** itself — its three status maps, the stage derivation, assignment + the standing team, `EngagementRepository`, `ChangeEngagementStatus`, the `leads.status` roll-up, and how holding a role grants lead visibility |
| [booking.md](/docs/modules_handbook/manage/engagement/booking.md) | the **Booking** — the unit, its prices, the *derived* SPA state, `BookingRepository`, and the status ⇄ booking invariant (including the two states that violate it today) |
| [commission.md](/docs/modules_handbook/manage/engagement/commission.md) | the **money** — how one commission figure is produced, and how it divides between the people on the deal |
| [closing-modes.md](/docs/modules_handbook/manage/engagement/closing-modes.md) | **Setting → Closing Modes** — the admin-editable role + closing-mode vocabulary that the team UI and the whole split are built on, and the mode ⟺ project-fee-receipt rule |
| [sales-projects.md](/docs/modules_handbook/manage/engagement/sales-projects.md) | the **surfaces** — what a sales project is as data, and every panel of `SalesProjectsController`, its tables, sorts and export |
| [referrals.md](/docs/modules_handbook/manage/engagement/referrals.md) | **stage 3** — who counts as a customer, the ask-for-a-referral worklist, and the attribution columns on `leads` |
| [imports.md](/docs/modules_handbook/manage/engagement/imports.md) | the **two CSV importers** — project info on the index, legacy bookings on a project's Show page |
| [perimeter.md](/docs/modules_handbook/manage/engagement/perimeter.md) | the **boundary** — every route + permission, both scoping rules and the surfaces that skip them, the full column tables, and who reads/writes this data from outside the module |
| [retired.md](/docs/modules_handbook/manage/engagement/retired.md) | the **dead list** — columns, flags, tables and components that still exist but are no longer load-bearing, and whether each is safe to drop |

## The Property Booking hub — three stages, one page

`/manage/sales-projects` is not a list with tabs; it is a **funnel**, and the tab strip says so.
[`SalesTabs.vue`](/resources/js/Components/SalesTabs.vue) feeds [`HubTabs`](/resources/js/Components/HubTabs.vue)
a `stageTabs` config, which renders [`StageTabs.vue`](/resources/js/Components/StageTabs.vue) — a
chevron rail whose arrow shape carries the ORDER, so no label ever has to spell it out (the
strip's predecessor read "Pipeline: Property Match", which is the smell it replaces).

**Each stage owns every `?view=` of its own pivots**, mapped back through `BOOKING_VIEW_STAGES`:

| # | Stage | Label | `?view=` | What it is |
|---|-------|-------|----------|------------|
| 1 | **Pipeline** | Property Match | `match` | Buyer-quiz submissions, addable straight into a project's pipeline |
| 2 | **Sales** | Bookings | *(none — the default)* + `projects` | Booked / Converted / Lost deals. Two **pivots** of one stage: *By Booking List* and *By Project* (the catalogue) |
| 3 | **Referral & Repeat** | Customers | `referrals` + `referral-chain` | The loop: people who already bought → who to ask for an introduction, and who could buy again |

⚠️ **There is a fifth `?view=` value and it is not a panel.** `?view=pipeline` **302-redirects** to
`manage.sales.pipeline` (the cross-project kanban board, now a Dashboard-hub page), forwarding
every other query parameter — an old bookmark keeps working. Do not add a `pipeline` panel back to
this page.

Stage colours are a value ramp inside ONE family (`navy-800` → `brand-700` → `brand-500`), never
separate hues, and every fill is solid: `clip-path` leaves no border, so a pale tint would lose the
chevron silhouette. Counts come from `hubStageCounts()` and are sent on **every** view, because the
rail is always on screen.

**A pivot is not a stage.** A control that re-pivots only ONE stage — stage 2's *By Booking List* /
*By Project*, stage 3's *Customers* / *Referral Chain* — stays an in-page segmented control styled
quieter than the rail.

### Property Booking is one of the Sales hub's product lines

Every line in the Sales hub carries the same three stages, because that is the shape of the
business: strangers become customers, and customers bring more strangers. **Property Booking is the
only line with all three stages built**; the rest show `{ soon: true }` chevrons so each line
displays where it is going rather than a shorter funnel.

⚠️ **That product-line table is not this module's to own.** It is declared in `PRODUCT_STAGES` in
[`SalesTabs.vue`](/resources/js/Components/SalesTabs.vue) and spans lines belonging to several other
modules (Memberships, MLTA, Rental Estimates …). Read it there — a copy here goes stale the moment a
line is added, which is exactly how this handbook came to list six lines when the code declares
eight main tabs.

## Perimeter at a glance

Six route-name groups, each gated differently. **The full table — every route, its permission, and
which scope the controller re-checks — is in [perimeter.md](/docs/modules_handbook/manage/engagement/perimeter.md);**
re-derive the list with `php artisan route:list --name=sales-projects` (and the five other prefixes)
rather than trusting a number written here.

| Group | Gate to view | Gate to write | Owns |
|---|---|---|---|
| `manage.sales-projects.*` | `view-projects` | `manage-projects` (project CRUD) / `manage-leads` (leads + booking import) | the hub's five panels, the project Show page, project CRUD, both entry points for adding a lead, the export |
| `manage.engagements.*` | any `view-leads-*` | `manage-leads` | open / status / assign / reopen / remove, the booking create, the payment-candidate picker |
| `manage.bookings.*` | any `view-leads-*` | `manage-leads` | booking update + cancel |
| `manage.closing-modes.*` | `view-closing-modes` | `manage-closing-modes` | the modes and their role pools |
| `manage.pipeline-roles.*` | `manage-closing-modes` | `manage-closing-modes` | the role vocabulary (writes only — the read is the closing-modes page) |
| `manage.leads.referrals.*` | any `view-leads-*` | `manage-leads` | stage 3's writes. They live in the **leads** group because they are lead writes ⚠️ and `{id}` means a **different lead** in each of the three |

Two scoping rules apply on top of the permissions and are **not** interchangeable: `GroupScope`
partitions **projects** by agency, `LeadVisibility` partitions **leads** by who may see the person.
Both are re-checked inside the controllers, per record — see
[perimeter.md](/docs/modules_handbook/manage/engagement/perimeter.md), which also names the surfaces
that apply neither.

## Everything here is derived, and that is the design

Four things a reader will look for a table of, and not find one, because each is computed from
facts the deal already carries:

| What | Derived from | Written up in |
|---|---|---|
| The coarse **stage** | the status, via `Engagement::stageForStatus()` | [lifecycle.md](/docs/modules_handbook/manage/engagement/lifecycle.md) |
| `leads.status` | the lead's engagements, via `SyncLeadStatusFromEngagements` | [lifecycle.md](/docs/modules_handbook/manage/engagement/lifecycle.md) |
| The **SPA state** | the deal's own facts, via `Booking::spaState()` | [booking.md](/docs/modules_handbook/manage/engagement/booking.md) |
| The commission **amount** and its **split** | the booking's price × the project's rate, then the closing mode's role pools × each holder's share | [commission.md](/docs/modules_handbook/manage/engagement/commission.md) |

A reader who learns this once stops hunting for the missing tables — and stops adding a stored
column that can contradict the facts it restates. `bookings.spa_status` is what that mistake looked
like the first time; see [retired.md](/docs/modules_handbook/manage/engagement/retired.md).

## Related modules

- [Leads (Manage)](/docs/modules_handbook/manage/leads/readMe.md) — the lead this pipeline hangs off; its Show page hosts the **Pipeline** tab, and `leads.status` is a roll-up of engagements.
- [Property Match](/docs/modules_handbook/main/property-match/readMe.md) — stage 1. The public buyer quiz, its decision engine and the standalone person workbench belong to that module; this hub only adds one surface over the same rows.
- [Users · Leads · Admins · Login](/docs/modules_handbook/shared/user-lead-admin-login-register-merge/readMe.md) — the identity foundation. A lead *is* a user, and every assignee id is a `users.id`.
- [Lead Linking](/docs/modules_handbook/shared/lead-linking/readMe.md) — the `LeadLinker` both importers, the Add-Lead modal and the referral modal all go through.
- [Payments ledger](/docs/modules_handbook/manage/payments/purchase-histories/readMe.md) — `engagements.purchase_history_id`, the project-fee receipt, and the admin lane that links one by hand.
- [Appointments / Calendar](/docs/modules_handbook/manage/calendar/readMe.md) — appointments carry a nullable `engagement_id` so a scheduling event groups under its deal.
- [Meta Pixel & Conversions API](/docs/modules_handbook/manage/meta-ads/pixel/readMe.md) — ⚠️ when a booking becomes a confirmed, paid sale is the moment a Meta `Purchase` event should fire, with the amount, so ads optimise for people who *spend* rather than people who merely register. Not built; the pixel handbook holds the payload shape and its prerequisites.
