mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-07-27 22:45:03 +00:00
fix(live): flag major condition issues by the issue, not a retired stage / S3 URL
24 Charlesworth Street (deal 507693469922) has a major condition issue recorded (major_condition_issue_description = "Damp in bathroom") but didn't appear in the live tracker's condition-issue surfaces. Two causes, both now keyed on the issue itself via a shared hasMajorConditionIssue(deal) predicate: - majorConditionDeals filtered on dealstage === "3061261536" — a HubSpot stage that was retired, so the set was always empty. (This set is also not yet rendered in any card — noted for follow-up.) - The rendered "Damp, Mould & Other Condition Issues → Flagged at Survey" count (computeDampMouldRisk.surveyFlagDeals) keyed on major_condition_issue_evidence_s3_url, which is null for this deal (its photos are in the raw HubSpot field). This is the count the property was actually missing from. There is no yes/no column; the description is the "Yes" signal (only filled when an issue exists). Removed the now-dead MAJOR_CONDITION_STAGE_ID. TDD: red→green→refactor; 66 live-tracker tests, full suite (502) + typecheck + lint green. Also gitignore .sandcastle/ (untracked agent worktree scratch). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fe77a2e74e
commit
ae6a6ebf68
4 changed files with 75 additions and 19 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -52,3 +52,6 @@ docs/wip/**
|
|||
.chip-preview.*
|
||||
.shot.cjs
|
||||
scratch-*.cjs
|
||||
|
||||
# Sandcastle agent worktrees (local scratch — never commit)
|
||||
.sandcastle/
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
computeFunnelStages,
|
||||
computeProjectProgress,
|
||||
computeLiveTrackerData,
|
||||
hasMajorConditionIssue,
|
||||
} from "./transforms";
|
||||
import type { HubspotDeal, ClassifiedDeal } from "./types";
|
||||
|
||||
|
|
@ -408,15 +409,28 @@ describe("classifyDeals", () => {
|
|||
// -----------------------------------------------------------------------
|
||||
|
||||
describe("computeDampMouldRisk", () => {
|
||||
it("counts survey flags from majorConditionIssuePhotosS3", () => {
|
||||
it("counts survey flags from a flagged major condition issue", () => {
|
||||
const deals = [
|
||||
makeClassified({ majorConditionIssuePhotosS3: "s3://bucket/file.jpg" }),
|
||||
makeClassified({ majorConditionIssuePhotosS3: null }),
|
||||
makeClassified({ majorConditionIssueDescription: "Damp in bathroom" }),
|
||||
makeClassified({ majorConditionIssueDescription: null }),
|
||||
];
|
||||
const result = computeDampMouldRisk(deals);
|
||||
expect(result.surveyFlagCount).toBe(1);
|
||||
});
|
||||
|
||||
it("counts a survey flag even when no S3 evidence URL was mirrored", () => {
|
||||
// Regression: a flagged issue whose photos live in the raw HubSpot field
|
||||
// (not the S3 mirror) must still count — keying on the S3 URL missed these.
|
||||
const deals = [
|
||||
makeClassified({
|
||||
majorConditionIssueDescription: "Damp in bathroom",
|
||||
majorConditionIssuePhotos: "https://hubspot/uploaded/photo.jpeg",
|
||||
majorConditionIssuePhotosS3: null,
|
||||
}),
|
||||
];
|
||||
expect(computeDampMouldRisk(deals).surveyFlagCount).toBe(1);
|
||||
});
|
||||
|
||||
it("counts coordinator flags from dampMouldFlag", () => {
|
||||
const deals = [
|
||||
makeClassified({ dampMouldFlag: "Yes" }),
|
||||
|
|
@ -461,9 +475,9 @@ describe("computeDampMouldRisk", () => {
|
|||
|
||||
it("counts deals flagged at both stages independently", () => {
|
||||
const deals = [
|
||||
makeClassified({ majorConditionIssuePhotosS3: "s3://x", dampMouldFlag: "Yes" }),
|
||||
makeClassified({ majorConditionIssuePhotosS3: "s3://y", dampMouldFlag: null }),
|
||||
makeClassified({ majorConditionIssuePhotosS3: null, dampMouldFlag: "Yes" }),
|
||||
makeClassified({ majorConditionIssueDescription: "Damp", dampMouldFlag: "Yes" }),
|
||||
makeClassified({ majorConditionIssueDescription: "Mould", dampMouldFlag: null }),
|
||||
makeClassified({ majorConditionIssueDescription: null, dampMouldFlag: "Yes" }),
|
||||
];
|
||||
const result = computeDampMouldRisk(deals);
|
||||
expect(result.surveyFlagCount).toBe(2);
|
||||
|
|
@ -643,14 +657,26 @@ describe("computeLiveTrackerData", () => {
|
|||
expect(computeLiveTrackerData(deals).totalDeals).toBe(3);
|
||||
});
|
||||
|
||||
it("filters majorConditionDeals by the major condition stage ID", () => {
|
||||
it("selects majorConditionDeals by a flagged issue, regardless of dealstage", () => {
|
||||
// Regression: the major-condition stage (3061261536) was retired in HubSpot,
|
||||
// so a flagged deal now sits in some other stage. It must still be selected.
|
||||
const deals = [
|
||||
makeDeal({ dealstage: "3061261536" }), // MAJOR_CONDITION_STAGE_ID
|
||||
makeDeal({ dealstage: "1617223910" }),
|
||||
makeDeal({
|
||||
dealstage: "1617223910", // any current stage, not the retired one
|
||||
majorConditionIssueDescription: "Damp in bathroom",
|
||||
}),
|
||||
makeDeal({ dealstage: "1617223910" }), // no issue flagged
|
||||
];
|
||||
const result = computeLiveTrackerData(deals);
|
||||
expect(result.majorConditionDeals).toHaveLength(1);
|
||||
expect(result.majorConditionDeals[0].dealstage).toBe("3061261536");
|
||||
expect(result.majorConditionDeals[0].majorConditionIssueDescription).toBe(
|
||||
"Damp in bathroom",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not select a deal on the retired stage id alone (no issue flagged)", () => {
|
||||
const deals = [makeDeal({ dealstage: "3061261536" })];
|
||||
expect(computeLiveTrackerData(deals).majorConditionDeals).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("groups deals without a projectCode under Unknown Project", () => {
|
||||
|
|
@ -666,3 +692,17 @@ describe("computeLiveTrackerData", () => {
|
|||
expect(result.majorConditionDeals).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasMajorConditionIssue", () => {
|
||||
it("is true when a major condition issue is described", () => {
|
||||
expect(
|
||||
hasMajorConditionIssue(makeDeal({ majorConditionIssueDescription: "Damp in bathroom" })),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("is false when the description is null, empty, or whitespace only", () => {
|
||||
expect(hasMajorConditionIssue(makeDeal({ majorConditionIssueDescription: null }))).toBe(false);
|
||||
expect(hasMajorConditionIssue(makeDeal({ majorConditionIssueDescription: "" }))).toBe(false);
|
||||
expect(hasMajorConditionIssue(makeDeal({ majorConditionIssueDescription: " " }))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import type {
|
|||
|
||||
import {
|
||||
STAGE_ORDER,
|
||||
MAJOR_CONDITION_STAGE_ID,
|
||||
SUCCESSFUL_SURVEY_OUTCOMES,
|
||||
} from "./types";
|
||||
|
||||
|
|
@ -170,7 +169,10 @@ function isCoordinatorFlagged(d: ClassifiedDeal): boolean {
|
|||
}
|
||||
|
||||
export function computeDampMouldRisk(deals: ClassifiedDeal[]): DampMouldRiskData {
|
||||
const surveyFlagDeals = deals.filter((d) => !!d.majorConditionIssuePhotosS3);
|
||||
// Survey-stage flag = the assessor recorded a major condition issue. Keyed on
|
||||
// the issue itself, not the S3 evidence-photo URL — that mirror is often empty
|
||||
// (photos land in the raw HubSpot field), which silently dropped flagged homes.
|
||||
const surveyFlagDeals = deals.filter(hasMajorConditionIssue);
|
||||
const coordinatorFlagDeals = deals.filter(isCoordinatorFlagged);
|
||||
const bothFlaggedCount = surveyFlagDeals.filter(isCoordinatorFlagged).length;
|
||||
|
||||
|
|
@ -261,6 +263,18 @@ export function computeProjectProgress(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a deal has a major condition issue flagged (Awaab's Law). The signal
|
||||
* is the assessor's recorded description — there is no yes/no HubSpot field; a
|
||||
* description is only filled in when an issue exists. Whitespace-only counts as
|
||||
* empty.
|
||||
*/
|
||||
export function hasMajorConditionIssue(deal: {
|
||||
majorConditionIssueDescription: string | null;
|
||||
}): boolean {
|
||||
return !!deal.majorConditionIssueDescription?.trim();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Top-level function called by page.tsx
|
||||
// Orchestrates all transformations: classify, group by project, compute stats
|
||||
|
|
@ -271,10 +285,11 @@ export function computeLiveTrackerData(
|
|||
// Classify all deals (add displayStage field)
|
||||
const classified = classifyDeals(rawDeals);
|
||||
|
||||
// Filter for major condition deals (Awaab's Law)
|
||||
const majorConditionDeals = classified.filter(
|
||||
(d) => d.dealstage === MAJOR_CONDITION_STAGE_ID
|
||||
);
|
||||
// Filter for major condition deals (Awaab's Law). Keyed on the flagged issue,
|
||||
// not the deal's pipeline stage: the old "Major condition issue" HubSpot stage
|
||||
// was retired, so flagged deals now sit in whatever stage they've progressed
|
||||
// to. A deal counts when the assessor recorded a major condition issue.
|
||||
const majorConditionDeals = classified.filter(hasMajorConditionIssue);
|
||||
|
||||
// Group deals by projectCode
|
||||
const grouped: Record<string, ClassifiedDeal[]> = {};
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ export type StageProgressItem = {
|
|||
// Damp & mould risk comparison (survey-stage vs coordination-stage flags)
|
||||
// -----------------------------------------------------------------------
|
||||
export type DampMouldRiskData = {
|
||||
surveyFlagCount: number; // majorConditionIssuePhotosS3 not null
|
||||
surveyFlagCount: number; // major condition issue flagged (description present)
|
||||
coordinatorFlagCount: number; // dampMouldFlag not null/non-empty
|
||||
bothFlaggedCount: number; // flagged at both stages (highest risk)
|
||||
totalDeals: number;
|
||||
|
|
@ -336,8 +336,6 @@ export const SUCCESSFUL_SURVEY_OUTCOMES: ReadonlySet<string> = new Set([
|
|||
"EPC Completed",
|
||||
]);
|
||||
|
||||
export const MAJOR_CONDITION_STAGE_ID = "3061261536" as const;
|
||||
|
||||
// Order of stages for grouping/display (queries excluded from this list)
|
||||
export const STAGE_ORDER: DisplayStage[] = [
|
||||
"Scope & Planning",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue