# Decision review and the Sales booking bridge

## What it does

Step 7 records an advisor's decision checklist and connects a purchase journey to the existing Sales booking. An actual booking may be recorded while the readiness checklist is incomplete, including a blocked dimension. The staff member must record the exception; no readiness field is promoted to make the booking look complete.

The journey shows the live Sales transaction status separately: Active, Cancelled, Completed or Released, plus the derived SPA status and recorded signing dates. Booking cancellations, commission splits, banker rows, closing mode and Sales lifecycle remain owned by the existing Sales workflow.

## How it works

### Decision checklist

- `DecisionReviewRepository::request()` runs through the customer-first idempotent command transaction. It reloads current source access, financial dependencies and the displayed dependency hash before writing.
- Only a scoped journey manager with the advisor-assessment permission can request review. A supported consultation is required. A blocked decision gate, blocked understanding or contact restriction cannot be waived by an exception.
- Checklist references capture written terms and the instruction to proceed, and the staff member identifies the decision-makers who were present. Presence covers all confirmed decider/funder parties, not just any selected name. These ticks do not create or overwrite readiness evidence.
- A reference is either an accessible current source explicitly matched to this purchase, or a staff-attested external message/document/signature reference with a note and occurrence time. Source references preserve their source identity/revision. Every level of a nested reference chain is revalidated; a wrapper cannot restore a revoked, edited or inaccessible original.
- Incomplete readiness or manual items require a reason and expiry within 14 days. The exception covers exactly the current readiness gaps and dependency hash. Material changes, new blocks, restricted backing sources or expiration reopen the review. The review exception never waives a blocked gate.
- Screening age is evaluated as a requested decision review before the write, avoiding a temporary ready state for expired financing assessments.

### Real booking

`BookingBridgeRepository::record()` accepts a staff-selected existing Sales engagement. The server checks the exact customer and the current target's project identity. If this purchase has no Sales link, the command creates an explicit, audited link to that chosen engagement. It never chooses a customer or engagement by name/project similarity. If no matching Sales engagement exists, the UI links to the customer's existing Sales pipeline.

There are two paths:

1. **New booking:** requires unit, net price, booking fee, date, private receipt, explicit instruction and transaction partner name. It calls the existing `BookingRepository::create()` in the same transaction. Assignments, closing mode and commission are left as the Sales engagement already defines them. A live same-unit booking on that engagement must be selected instead of recreated.
2. **Existing booking:** links the exact selected canonical booking after checking customer, engagement and project. It does not mutate its status, historical price, fees, dates, commission or team. A canonical booking already linked to another purchase cannot be linked again.

A booking outside current decision review or with outstanding checklist/financial items requires an explicit exception reason. Existing incomplete historical paperwork is recorded as another gap. The command still records the real booking. An accepted retry returns the original result; any failure rolls back the instruction, Sales link, booking and journey event together.

The decision and booking forms share a dependency hash. A stale financial/target state is rejected before any booking write. If the actual booking price differs from the current quoted target, the existing payment schedule stops proving cash sufficiency, and Loan/Fit require reassessment against revised terms. The immutable old quote remains history. Reservations remain protected; a receipt alone never identifies which funding pool paid the fee.

### Receipt privacy and deletion

- `BookingReceiptRequest` accepts PDF/JPEG/PNG up to 10 MiB. Storage must be private; public disks are refused.
- `BookingReceiptRepository::upload()` stores the file outside database locks, then rechecks the live customer, opportunity and actor permissions before attaching its Media row to that opportunity. Failed attachment compensates by deleting the newly stored object.
- `resolve()` requires current journey access plus the existing Sales write capability and an exact opportunity-owned Media UUID in the receipt collection. The UI receives an authenticated receipt route, not a public storage URL.
- Lead purge resolves opportunity-owned receipt media before deleting roots. Under the customer lock, `JourneyIdentity::purgeLeads()` also records durable shared Media cleanup intents for any receipt attached after that preflight. The normal Media cleanup worker removes remaining objects and their indexes. Customer identity merge preserves the opportunity, so its receipt ownership stays intact.

### Existing Sales compatibility

The only canonical repository change is a customer-first lock and fresh identity check in `BookingRepository::create()`. A model loaded before a customer merge, deletion or changed engagement cannot create an orphaned booking. Normal status changes and commission semantics are unchanged.

No autonomous email, WhatsApp, partner notifications or AI extraction runs in these commands. Transaction partner is the recorded partner name; it grants no account capability or tenancy.

## Reference usage

- HTTP requests explicitly map `journey_review` and `journey_booking` fields into the corresponding repositories; no arbitrary submitted readiness/stage or actor IDs are accepted.
- Loader order: finance → `DecisionReviewContextLoader` → `BookingContextLoader`. Locked commands bypass read caches. Queue batches prime canonical booking links once per batch.
- `DecisionWorkspace::present(viewer, context, input, detail, evaluatedResult)` supplies the allowlisted UI contract, including source options, exact eligible Sales engagements, canonical booking choices, gates, manual checklist and exception details. The optional existing result prevents redundant queue calculation; detailed review adds decision-time financial checks.
- File handling follows [MediaService](../media/readMe.md): private uploads outside the customer transaction, then scoped attachment; authenticated reads; durable deletion.

## Related files

- `src/RevenueJourney/Repositories/DecisionReviewRepository.php`
- `src/RevenueJourney/Repositories/DecisionReferenceRepository.php`
- `src/RevenueJourney/Repositories/DecisionReviewContextLoader.php`
- `src/RevenueJourney/Repositories/BookingBridgeRepository.php`
- `src/RevenueJourney/Repositories/BookingFollowUpRepository.php`
- `src/RevenueJourney/Repositories/BookingReceiptRepository.php`
- `src/RevenueJourney/Repositories/BookingContextLoader.php`
- `src/RevenueJourney/Services/DecisionWorkspace.php`
- `app/Http/Requests/Manage/RevenueJourney/{DecisionReview,BookingBridge,BookingReceipt}Request.php`
- `src/Engagement/Repositories/BookingRepository.php`
- `src/Lead/Repositories/LeadRepository.php`
- `src/RevenueJourney/Support/JourneyIdentity.php`
- `tests/Feature/RevenueJourney/DecisionBookingWorkflowTest.php`
- `tests/Unit/RevenueJourney/DecisionReferenceTest.php`

### Unfinished follow-up at booking

When a primary commitment is open, the booking form must explicitly submit `follow_up_disposition: transfer_to_sales`. The same transaction creates one ordinary customer Discussion task for the current executor when their account and Sales customer access remain valid; otherwise it records the fallback to the accountable owner, or to the booking recorder if neither prior staff member can access the customer, retaining the original body, expected outcome, priority and due date. Its text and immutable transfer journal preserve the exact Malaysia-time deadline; the journal also retains original due time and any earlier missed deadline. The original commitment closes as **rescheduled**, its old mirror closes, and the opportunity primary becomes empty. This records no contact attempt, consultation, attendance or readiness confirmation. Booking retries create no duplicate task. Pending and accepted acquisition responsibilities close with an audited booking reason; activated ownership history remains. The transferred task uses the existing Sales/Discussion workflow: its author can edit it and scoped staff can complete/reopen it. Its scheduling field has day precision; exact and earlier missed deadlines remain visible in text and audit.
