From 56fdfa06e4e047abb25b8303f83eb2717c1e00ca Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 29 May 2026 12:05:53 +0000 Subject: [PATCH] Only let surveyed outcomes override a Do Not Book status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, any HubSpot outcome would override bookingStatus="Do Not Book" and keep the property in the normal pipeline. That was too permissive — outcomes like "Tenant Refusal" or "Not Viable" combined with Do Not Book should classify the property as Removed from Bookings, not lurk in Queries or the survey-issues bucket. Now only completed survey outcomes (Surveyed, Surveyed - Pending Upload, EPC Completed) override Do Not Book. Any other outcome + Do Not Book falls through to Removed from Bookings, surfaces in the Halted or Removed panel, and gets the matching stage badge in the Properties tab. The redundant "Removed from Bookings" chip in the drill-down table is gone since the stage classification now carries that signal cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../your-projects/live/DrillDownTable.tsx | 7 --- .../your-projects/live/SurveyIssuesPanel.tsx | 12 ++-- .../your-projects/live/transforms.test.ts | 57 ++++++++++++++++++- .../your-projects/live/transforms.ts | 6 +- .../(portfolio)/your-projects/live/types.ts | 8 +++ 5 files changed, 77 insertions(+), 13 deletions(-) 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)