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)