# Staff workflow — Step 4

## What it does

Implements the original plan's internal save workflow. A staff command commits its evidence decisions, conversation outcome, next follow-up, existing-task mirror, current readiness/queue snapshot, audit details, receipt and dormant integration event in one database transaction. Failure in any part rolls back all parts. No new migration, live route, frontend activation, provider call or customer send is added.

Step 5 supplies the staff interface and permission-aware controllers. Step 6 supplies external source adapters; Step 7 supplies accepted handoffs, consultation hosts and canonical booking/payment integration; Steps 8/9 supply extraction and sending. This step follows the original ten-step plan, not the later reduced-release recommendation.

## How it works

### Transaction and retry contract

`JourneyCommandRepository` requires the write release flag, reloads the authenticated actor, locks the customer before the context, rechecks access, and delegates receipt handling to the existing `FoundationJournalRepository`. Every new command requires `expected_version`. A stale version returns 409. The caller must reload and preserve the staff member's unsaved text for deliberate reconciliation; it must not silently retry with a newer version.

A request ID is stable for one intentional operation. The identical request returns the existing context without repeating writes, even after its proposed follow-up time has passed. Reusing that request ID with different content conflicts. Payload UUIDs and instants are normalized. Validation requiring the *current* time (a future follow-up/revisit) runs inside the first application, after receipt replay. Receipts never store raw submitted quotes. Detailed immutable events retain the saved decision, reason, actor and source link.

All mutations use the customer lock, so two opportunities reserving one customer's cash serialize together. The active-context unique index remains a second defence against two primaries. Shared funding/contact changes also increment sibling context versions and refresh their snapshots under the customer lock. An outcome cannot commit merely because the snapshot previously looked current.

### Staff commands

`StaffWorkflowRepository::execute($actor, $context, $operation, $input)` accepts only `journey_workflow`, with a UUID `request_id` and integer `expected_version`.

| Operation constant | Data and behavior |
| --- | --- |
| `JourneyEvent::ACTIVATE` | `owner_uuid`, role CJE/Advisor, optional coordinator UUID, and required primary. Initial ownership only. A later owner change must use Step 7's accepted handoff. Owner/coordinator must have existing customer access and workflow capabilities. |
| `COORDINATE` | Nullable coordinator UUID and reason. Owner/scoped manager can change support coordination; ownership and accountable deadline remain the same. |
| `PLAN` | New primary and reason. Closes/replaces the existing action and retains reschedule history. |
| `RESUME` | New primary and reason, explicitly leaves Parked. |
| `OUTCOME` | Current commitment UUID/version; reached/no-answer/rescheduled/cancelled; occurrence time and note; optional reached interaction details; `facts`/`reviews`; disposition `next`, `park` or `lost`. Save is atomic. |
| `EVIDENCE` | Typed facts, each with qualified field key, value, quote, observation instant, optional source UUID/corrected assertion UUID, and explicit `confirm`. A pending note does not confirm itself. |
| `REVIEW` | Assertion UUID, decision, reason when rejecting/invalidating/reconfirming, optional exact expected head and replacement UUID. Conflicting accepted statements cannot silently overwrite one another. |
| `AGREEMENT` | Current commitment UUID/version, agreement state, customer quote and occurrence time. Agreement applies to that exact commitment and due instant. A replacement starts Proposed. |
| `STATEMENT` | Staff-recorded customer statement, current route agreement, current scenario assessment or tradeoff agreement; quote and occurrence time. Advisor assessment requires the advisor capability. |
| `PARTY` | Label, relationship, customer flag, optional existing party event UUID. The original immutable event ID is the stable internal party identity; later descriptions keep that identity. No customer/user account is invented. |
| `BLOCK` | Dimension, active flag and explanatory reason. Intake only supports Need/Relationship. |
| `CONTACT` | Customer-wide do-not-contact flag and reason. Ordinary authorized staff can record an opt-out; clearing it requires a scoped manager. Outbound successors are refused while restricted; an internal review is allowed. |

A primary requires kind, body, expected outcome, exact future due instant and waiting-on state. Optional fields are purpose, gap dimension, named specialist and existing manual-task UUID. A specialist designation does not transfer accountability: accepted specialist handoff clocks arrive in Step 7. Unlinked, open manual tasks can be explicitly adopted with a staff-chosen precise deadline. A legacy date alone never becomes a customer promise.

Reached interaction details are explicitly staff-recorded: channel, conversation/consultation kind, duration, customer presence and advisor attestation. No-answer cannot include attendance/consultation details and does not establish contact. Its default successor is next working day at 10:00 in the configured calendar (Asia/Kuala_Lumpur, weekends excluded by default; holidays can be explicitly configured). Parking creates a dated internal revisit action. Lost requires an allowed reason and explanation; `no_response` also needs 30 days parked and three distinct eligible attempts.

### Confirmations, authorities and references

The request does not accept actor, origin, authority, readiness, visibility or normalized authorization flags. Field capabilities come from `JourneyPolicy` and the registry. Financing reviewers may record/review financing assessments within their visible customer scope without taking ownership. Generic human commands cannot produce measured system facts or select a target. Target selection has its own authorized command and existing project view/group checks.

Corrections append a new assertion with an explicit old assertion reference, accept the replacement and supersede the old accepted head together. Rejection and invalidation append decisions. Reconfirmation records a reason and current dependency bindings in an immutable review event; the original assertion and origin remain unchanged. Repeated confirmation of an already accepted fact requires an explicit reconfirmation reason. Same-context field, source, party, route, target, version and pool references are checked before writing.

The loader accepts only matched internal `journey_manual` sources from this workflow version with a valid content hash. External messages, recordings, AI proposals, secure documents and eligibility jobs are not imported or authorized by this loader. Their integration remains Step 6/8. Manual quote accuracy is a staff attestation, not independent transcript verification.

`relationship.next` is a system-derived *workflow fact*: it verifies the saved staff-recorded customer agreement against the exact current commitment/time. Its source remains visibly staff-recorded. It does not claim independently measured attendance or two-way channel activity. Those measured facts still require the later adapters. Staff sources do not acquire AI/system assessment authority by changing a submitted label.

### Clocks and existing tasks

Every replacement/outcome stores an immutable clock segment with former owner, due time, original promise, closure time and missed-deadline marker. Rescheduling preserves the original promise and inherited miss across subsequent reschedules. A genuinely new follow-up after a completed/no-answer action starts its own due clock; the previous segment stays in history.

Each commitment mirrors an existing `LeadActionItem` through that repository's dedicated synchronization method. Ordinary legacy update/done/reopen/delete paths lock and refuse journey-linked tasks with 409, directing staff to Record outcome/Change next follow-up. Unlinked tasks retain their existing behavior. Manual task mutations take the same customer-first lock order. Guarded deletion commits attachment cleanup intents before the controller touches storage, so a rejected journey deletion leaves every attachment intact. Approved channel-plan tasks cannot be adopted by this workflow; their approval/revision authority remains in their current module. No legacy date precision is silently upgraded.

### Financial commands

`WorkflowFinanceRepository::execute()` accepts `journey_finance`, with the same request/version envelope:

- `POOL`: create a stable named funding pool; creation alone supplies no balance.
- `TARGET`: resolve a local project UUID or federated catalogue project/floor-plan UUID; save immutable quoted price and dated schedule plus staff source quote. The corresponding target assertion/review, current target pointer and dependency versions commit together. Catalogue numeric IDs are caches; UUIDs are retained. Target changes reset dependent gates and require allocation reconfirmation.
- `ROUTE`: a financing reviewer records an immutable current-target loan structure, borrower party IDs, required principal, repayment and source quote. Confirmed `loan.need` must already indicate a loan. A changed route inherits no customer acceptance.
- `ALLOCATION`: reserve, revise or release an explicit amount against a stable pool/tranche/lineage with expected pool and previous allocation versions. Reservations require accepted balance/reserve/availability and a source-recorded customer agreement to the amount, purchase, pool revision and schedule version. The exact dated cash evaluator sees every competing allocation. Incomplete backing or double allocation rolls the command back. Release retains the original reservation in history and can free unspent funds even if its supporting statement later needs reconfirmation.

Acceptance of `loan.need` creates/selects a matching route mode atomically. Acceptance of gross cash amount, reserve or availability advances the pool revision and its sourced normalized records atomically. Net/unspecified declarations remain distinct typed facts; they do not invent a gross pool balance. Corrections to a shared pool's original statement are made explicitly in its originating context. Linking current accepted pool facts into another purchase is an explicit part of its allocation command. Updated/shared invalidated facts cannot survive solely through an older accepted link.

Real consumption and booking transfer commands await Step 7's verified canonical booking/payment bridge. The existing schema/rules already represent those immutable decisions. This step cannot invent a payment or book a property.

### Synchronous projections

`WorkflowContextLoader`/`WorkflowFinanceLoader` build the server-owned input to the Step 3 pure engine. `WorkflowSnapshotRepository` writes current effective heads, resource-specific financial members, queue priority, explanation flags and stage result before commit. Material dependency versions handle target/route/borrower/price/scenario/Need/party changes. Customer-wide cash and contact changes invalidate sibling edit tokens.

The existing scalar `ManualEvidenceRepository` also refreshes its pending-fact snapshot; it still never accepts a note automatically. Snapshot data remains internal and must pass viewer/source authorization in Step 5's read resources. Snapshot refresh in this step occurs on commands. Scheduled reassessment and external-source invalidation workers remain later integration.

## Reference usage

```php
$context = app(StaffWorkflowRepository::class)->execute($request->user(), $context,
    JourneyEvent::OUTCOME, ['journey_workflow' => $mappedValidatedPayload]);
```

Use the dedicated `WorkflowRequest` / `FinanceRequest` from an explicitly mapped future controller. Select the operation server-side; do not let an arbitrary route token become the command allowlist. Root resources use UUIDs; typed internal evidence values use server-resolved record IDs after scoped resolution. Never forward a submitted normalized rules context or serialize the entire internal loader output.

UI success should mean the entire transaction committed. Preserve the request ID on a transport retry; generate a new one after a deliberate edit. On 409, reload the current context and ask staff to reconcile their unsaved change. Source/conversation links open in a new tab. No endpoint or UI is activated by this step.

## Verification and related files

- `tests/Feature/RevenueJourney/StaffWorkflowTest.php`: atomic rollback, delayed idempotent replay, one primary, reached/no-answer, overdue clock preservation, pending/confirmed/corrected/invalidated facts, permissions, DNC, Park/Lost and precise agreement.
- `WorkflowFinanceTest.php`: normalized accepted cash, reservation retry, shared-pool conservation, target-change reconfirmation and release after invalidation.
- `WorkflowConcurrencyTest.php` + `tests/Fixtures/RevenueJourney/workflow-worker.php`: two real PHP processes on MySQL, competing primary saves, identical retries, confirmation vs rejection and two purchases competing for one pool. Fixtures are committed and cleaned in the isolated test database.
- Existing journey foundation, identity, permission, task scheduling, channel-plan approval/revision and source-link regression suites.
- `src/RevenueJourney/Repositories/{JourneyCommandRepository,StaffWorkflowRepository,WorkflowEvidenceRepository,WorkflowFinanceRepository,WorkflowJournalRepository,WorkflowContextLoader,WorkflowFinanceLoader,WorkflowSnapshotRepository}.php`
- `app/Http/Requests/Manage/RevenueJourney/{WorkflowRequest,FinanceRequest}.php`
- `src/Lead/Repositories/LeadActionItemRepository.php`
- `src/RevenueJourney/Support/WorkflowContext.php`
- `tests/Concerns/InteractsWithJourneyFinance.php`

A persistence test exposed nullable booking/payment IDs being passed into a set-key operation in the pure cash evaluator. The evaluator now filters absent references; the new pure regression verifies that ordinary unbooked reservations still evaluate correctly.
