docs(uat): SHIPPED annotations for PR8 (qualification rework)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 17:48:31 +02:00
parent 51ca875665
commit 535ff69fc4

View File

@@ -379,14 +379,14 @@ _Component refactors, multi-file edits, single-service tweaks, new validators._
> - **Current-stage milestone hidden under "Upcoming milestones" when its sub-steps are already checked off (active phase mislabelled)** — _src/components/interests/interest-tabs.tsx:611-624_ (`milestoneCompletion` map + `firstIncompleteKey` derivation) — the phase classifier marks a milestone as `'past'` whenever ALL its sub-steps are complete, so when the interest is at Reservation stage with reservation-agreement-signed already ticked (via the manual stage-jump), the Reservation milestone is `past` and EOI (which still has gaps because the rep hasn't backfilled) becomes the `firstIncompleteKey` → flagged as "NEXT STEP". Net effect (image 23): EOI shows as "NEXT STEP" + Reservation gets buried in the "Upcoming milestones" accordion even though it's the actual current stage.
> - **Fix:** introduce a third concept besides `past | current | future` — the milestone that owns the CURRENT pipeline stage (regardless of completion) should always be `current` and never be collapsed into the past-strip nor the upcoming-accordion. Compute the rep's "true current" milestone by mapping `interest.pipelineStage` → milestone key (eoi/eoi_sent/eoi_signed → 'eoi'; reservation → 'reservation'; deposit_paid → 'deposit'; contract_sent/contract_signed → 'contract'). The `firstIncompleteKey` rule still works for nurturing / qualified stages where no milestone naturally owns the stage. Past-but-fully-done milestones BEFORE the current stage go in the past-strip; future milestones go in the upcoming-accordion. Pair with the backfill-controls fix above so a "current" milestone with missing dates still has the affordances to fill them.
> - **Effort:** ~30-45 min. Captured 2026-05-21 from UAT.
> - **Qualification auto-confirm "intent confirmed" once stage ≥ EOI (extend `computeAutoSatisfied`)** — _src/lib/services/qualification.service.ts:342-360_ (`computeAutoSatisfied` only branches on `'dimensions'` — `'intent_confirmed'` falls through to `false`) + the call-site context build at lines 296-316 (needs `pipelineStage` added) — when a rep manually advances the deal past Qualified to EOI/Reservation/Deposit/Contract, "Intent confirmed" still requires an explicit tick. The act of signing an EOI is itself the strongest signal of intent — leaving the row unchecked makes the checklist feel like noise. Extend the auto-satisfaction context with `pipelineStage`, add an `if (key === 'intent_confirmed') return stageIdx > qualifiedIdx;` branch, and `computeEvidence` returns "Stage advanced past Qualified" when triggered. Rep can still untick to overrule.
> - **Qualification auto-confirm "intent confirmed" once stage ≥ EOI (extend `computeAutoSatisfied`)** — _src/lib/services/qualification.service.ts:342-360_ (`computeAutoSatisfied` only branches on `'dimensions'` — `'intent_confirmed'` falls through to `false`) + the call-site context build at lines 296-316 (needs `pipelineStage` added) — when a rep manually advances the deal past Qualified to EOI/Reservation/Deposit/Contract, "Intent confirmed" still requires an explicit tick. The act of signing an EOI is itself the strongest signal of intent — leaving the row unchecked makes the checklist feel like noise. Extend the auto-satisfaction context with `pipelineStage`, add an `if (key === 'intent_confirmed') return stageIdx > qualifiedIdx;` branch, and `computeEvidence` returns "Stage advanced past Qualified" when triggered. Rep can still untick to overrule. **SHIPPED in 51ca875.**
> - **Effort:** ~30 min including the evidence string + an integration test. Captured 2026-05-21 from UAT.
> - **Qualification: stale explicit-tick survives removal of underlying auto-evidence (esp. dimensions)** — _src/lib/services/qualification.service.ts:296-334_ (`confirmed: explicit || autoSatisfied`) — `autoSatisfied` is recomputed at fetch time, but `explicit` persists in `interestQualifications.confirmed` once a rep has manually ticked the row. Result: if dims were present at one point, the rep clicked the box (or the auto-tick happened alongside an explicit click somewhere in the flow), then dims are later removed, the row STAYS ticked because `explicit=true` covers for `autoSatisfied=false`. The `AUTO` badge disappears so it now looks like a manual confirmation — but the rep may have no memory of making it. Footgun: checklist claims "Dimensions confirmed" with no underlying data.
> - **Fix (recommended — strict for derived-only criteria):** for keys where there's no rep judgement involved (`dimensions` today; future similar "does X data exist" checks), make the row purely derived — ignore `explicit`, return `confirmed: autoSatisfied`. Removing dims always unticks. Keep `explicit || autoSatisfied` for judgement-based keys like `intent_confirmed`. Implement by marking each criterion with a `derivedOnly: boolean` flag (lives next to the auto-rule) and branching in the merge.
> - **Alt (lenient with warning):** keep the OR but surface an `inconsistent` flag (`explicit && !autoSatisfied`) — UI renders the row with an amber "Evidence missing — re-verify" annotation, lets the rep re-confirm or untick.
> - **Effort:** ~45 min for strict (incl. integration test covering the remove-dims-after-tick flow); ~1h for lenient (annotation + amber styling). Captured 2026-05-21 from UAT.
> - **Effort:** ~45 min for strict (incl. integration test covering the remove-dims-after-tick flow); ~1h for lenient (annotation + amber styling). Captured 2026-05-21 from UAT. **SHIPPED (strict variant) in 51ca875:** `DERIVED_ONLY_KEYS` Set sentinel; merge branches on `isDerivedOnly(key)` to ignore explicit ticks for `dimensions`.
> - **Qualification checklist: collapse to one-line summary once "All confirmed"** — _src/components/interests/qualification-checklist.tsx_ — once every row is confirmed (explicit + auto combined), the full card stops being a gate and just occupies prime Overview real estate. Replace the expanded card with a single-row summary: `✓ Qualification — all confirmed (dimensions · intent)` + a chevron to expand on demand. Audit trail stays one click away. While expanded the rep can still untick or add notes; on next page load the card re-collapses if fully confirmed. Pairs naturally with the auto-confirm-on-stage-advance change above — deals at Reservation+ stage land with a collapsed Qualification block instead of a full card. Don't redesign the checklist content per stage (cognitive load); just change the visual weight once it's no longer informationally hot.
> - **Effort:** ~30 min. Captured 2026-05-21 from UAT.
> - **Effort:** ~30 min. Captured 2026-05-21 from UAT. **SHIPPED in 51ca875:** card header is now a button-style toggle; aria-expanded; when fully confirmed it collapses to "✓ All confirmed (label · label)" + chevron; rep clicks header to inspect/untick.
> - **Yacht Ownership History tab: flesh out the controls; don't remove (carries real semantic load)** — _src/components/yachts/yacht-ownership-history.tsx_ + _src/components/yachts/yacht-tabs.tsx:333_ + _src/components/yachts/yacht-form.tsx:337-345_ (existing Transfer affordance) + _src/lib/services/yachts.service.ts:215_ (`transferOwnership` service) + _src/lib/db/schema/yachts.ts:72-96_ (`yachtOwnershipHistory` table with partial unique index `(yacht_id) WHERE end_date IS NULL`).
> - **Why keep:** the table isn't decorative — (i) partial unique index enforces one active owner at a time; (ii) berth reservation logic (`berth-reservations.service.ts`) gates "active company_membership on the owning company", so the yacht's ownership chain materially affects berth standing; (iii) the data is **already auto-populated** by `createYacht`, `transferOwnership`, and `public-interest.service.ts` — no rep effort required to maintain; (iv) audit trail value for disputed deals, EOIs generated under prior ownership, etc. Removing the tab AND/OR the table would lose audit fidelity and force reservation logic to derive ownership some other way. The "no way to enter/change" perception is a UI gap, not a missing concept.
> - **Flesh-out scope (recommended):**