diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx index a20476a..38a05ce 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/DrillDownTable.tsx @@ -60,7 +60,6 @@ function RemovalFlagChip({ label, tooltip }: { label: string; tooltip: string }) function DealNameCell({ deal }: { deal: ClassifiedDeal }) { const name = deal.dealname; - const isDoNotBook = deal.bookingStatus === "Do Not Book"; const isRemovedFromProgram = deal.batch === "Removed from Program"; return ( @@ -68,12 +67,6 @@ function DealNameCell({ deal }: { deal: ClassifiedDeal }) { {name ?? } - {isDoNotBook && ( - - )} {isRemovedFromProgram && ( d.outcome && !SUCCESSFUL_OUTCOMES.has(d.outcome), + (d) => + d.outcome && + !SUCCESSFUL_SURVEY_OUTCOMES.has(d.outcome) && + d.displayStage !== "Removed from Bookings", ); if (issueDeals.length === 0) return null; diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.test.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.test.ts index 3d76862..a60db64 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.test.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.test.ts @@ -144,13 +144,68 @@ describe("resolveDisplayStage — Removed from Bookings", () => { ).toBe("Removed from Bookings"); }); - it("falls back to normal stage resolution when bookingStatus is Do Not Book but an outcome is set", () => { + it("classifies as Removed from Bookings when bookingStatus is Do Not Book and outcome is a non-successful survey outcome", () => { + // Tenant Refusal is a non-successful outcome; with DNB the property is removed from bookings, + // not lingering in the normal pipeline. expect( resolveDisplayStage(makeDeal({ dealstage: "1617223910", bookingStatus: "Do Not Book", outcome: "Tenant Refusal", })) + ).toBe("Removed from Bookings"); + }); + + it("falls back to normal stage resolution when bookingStatus is Do Not Book but outcome is Surveyed", () => { + // Real survey history overrides DNB — the property stays in the normal pipeline. + expect( + resolveDisplayStage(makeDeal({ + dealstage: "1617223910", + bookingStatus: "Do Not Book", + outcome: "Surveyed", + })) + ).toBe("Scope & Planning"); + }); + + it("falls back to normal stage resolution when bookingStatus is Do Not Book but outcome is Surveyed - Pending Upload", () => { + expect( + resolveDisplayStage(makeDeal({ + dealstage: "1617223910", + bookingStatus: "Do Not Book", + outcome: "Surveyed - Pending Upload", + })) + ).toBe("Scope & Planning"); + }); + + it("falls back to normal stage resolution when bookingStatus is Do Not Book but outcome is EPC Completed", () => { + expect( + resolveDisplayStage(makeDeal({ + dealstage: "1617223910", + bookingStatus: "Do Not Book", + outcome: "EPC Completed", + })) + ).toBe("Scope & Planning"); + }); + + it("classifies as Removed from Bookings for Not Viable + Do Not Book even when the dealstage maps to Queries", () => { + // "1887735998" is the Not Viable dealstage, which normally maps to "Queries". + // With DNB + a non-successful outcome, Removed from Bookings takes precedence over the Queries mapping. + expect( + resolveDisplayStage(makeDeal({ + dealstage: "1887735998", + bookingStatus: "Do Not Book", + outcome: "Not Viable", + })) + ).toBe("Removed from Bookings"); + }); + + it("does not classify as Removed from Bookings when an outcome is set but bookingStatus is null", () => { + expect( + resolveDisplayStage(makeDeal({ + dealstage: "1617223910", + bookingStatus: null, + outcome: "Surveyed", + })) ).toBe("Scope & Planning"); }); }); diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts index 6171f20..d36cda5 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/transforms.ts @@ -18,6 +18,7 @@ import type { import { STAGE_ORDER, MAJOR_CONDITION_STAGE_ID, + SUCCESSFUL_SURVEY_OUTCOMES, } from "./types"; // Terminal stages that exit the pipeline by design — excluded from funnel @@ -116,7 +117,10 @@ export function resolveDisplayStage(deal: HubspotDeal): DisplayStage { return "Removed from Program"; } - if (deal.bookingStatus === "Do Not Book" && !deal.outcome) { + if ( + deal.bookingStatus === "Do Not Book" && + !(deal.outcome && SUCCESSFUL_SURVEY_OUTCOMES.has(deal.outcome)) + ) { return "Removed from Bookings"; } diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts index 1f4b989..f53d71a 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/types.ts @@ -316,6 +316,14 @@ export const SURVEYOR_OUTCOMES = [ export type SurveyorOutcome = (typeof SURVEYOR_OUTCOMES)[number]; +// Outcomes that represent real completed survey history. Take precedence over +// a "Do Not Book" booking status — the property stays in the normal pipeline. +export const SUCCESSFUL_SURVEY_OUTCOMES: ReadonlySet = new Set([ + "Surveyed", + "Surveyed - Pending Upload", + "EPC Completed", +]); + export const MAJOR_CONDITION_STAGE_ID = "3061261536" as const; // Order of stages for grouping/display (queries excluded from this list)