Block preliminary property assessor PDFs
This commit is contained in:
@@ -78,7 +78,8 @@ Current behavior:
|
||||
- returns a structured preliminary report payload
|
||||
- does not require recipient email(s) for the analysis-only run
|
||||
- asks for recipient email(s) only when PDF rendering is explicitly requested
|
||||
- renders the fixed-template PDF when recipient email(s) are present
|
||||
- does not render/send the PDF from a preliminary helper payload with `decision: pending`
|
||||
- only renders the fixed-template PDF after a decision-grade verdict and fair-value range are actually present
|
||||
|
||||
Important limitation:
|
||||
|
||||
|
||||
@@ -96,6 +96,8 @@ scripts/property-assessor render-report --input "<report-payload-json>" --output
|
||||
- complete the analysis without requiring recipient email(s)
|
||||
- only stop and ask for recipient email(s) when the user is explicitly rendering or sending the PDF
|
||||
- render the PDF only after recipient email(s) are known
|
||||
- do **not** render or send a PDF from the helper's preliminary payload while verdict is still `pending` or fair value is not established
|
||||
- if comps, valuation, or decision-grade condition interpretation are still incomplete, return the preliminary payload and say that the PDF/send step must wait
|
||||
|
||||
## Public-record enrichment
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@ import path from "node:path";
|
||||
import { discoverListingSources, type ListingDiscoveryResult } from "./listing-discovery.js";
|
||||
import { extractPhotoData, type PhotoExtractionResult, type PhotoSource } from "./photo-review.js";
|
||||
import { resolvePublicRecords, type PublicRecordsResolution } from "./public-records.js";
|
||||
import { renderReportPdf, type ReportPayload } from "./report-pdf.js";
|
||||
import {
|
||||
isDecisionGradeReportPayload,
|
||||
renderReportPdf,
|
||||
type ReportPayload
|
||||
} from "./report-pdf.js";
|
||||
|
||||
export interface AssessPropertyOptions {
|
||||
address: string;
|
||||
@@ -442,6 +446,20 @@ export async function assessProperty(
|
||||
};
|
||||
}
|
||||
|
||||
if (!isDecisionGradeReportPayload(reportPayload)) {
|
||||
return {
|
||||
ok: true,
|
||||
needsAssessmentPurpose: false,
|
||||
needsRecipientEmails: false,
|
||||
pdfReady: false,
|
||||
message:
|
||||
"The report payload is still preliminary. Do not render or send the PDF until comps, valuation, and a decision-grade verdict are completed.",
|
||||
outputPath: null,
|
||||
reportPayload,
|
||||
publicRecords
|
||||
};
|
||||
}
|
||||
|
||||
const outputPath =
|
||||
options.output ||
|
||||
path.join(
|
||||
|
||||
@@ -25,6 +25,14 @@ export interface ReportPayload {
|
||||
sourceLinks?: unknown;
|
||||
}
|
||||
|
||||
export function isDecisionGradeReportPayload(payload: ReportPayload): boolean {
|
||||
const decision = String(payload.verdict?.decision || "").trim().toLowerCase();
|
||||
const fairValueRange = String(payload.verdict?.fairValueRange || "").trim().toLowerCase();
|
||||
if (!decision || decision === "pending") return false;
|
||||
if (!fairValueRange || fairValueRange === "not established") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function asStringArray(value: unknown): string[] {
|
||||
if (value == null) return [];
|
||||
if (typeof value === "string") {
|
||||
@@ -69,6 +77,11 @@ export function validateReportPayload(payload: ReportPayload): string[] {
|
||||
if (!address) {
|
||||
throw new ReportValidationError("The report payload must include subjectProperty.address.");
|
||||
}
|
||||
if (!isDecisionGradeReportPayload(payload)) {
|
||||
throw new ReportValidationError(
|
||||
"The report payload is still preliminary. Stop and complete the decision-grade analysis before generating or sending the property assessment PDF."
|
||||
);
|
||||
}
|
||||
return recipients;
|
||||
}
|
||||
|
||||
|
||||
@@ -198,8 +198,9 @@ test("assessProperty falls back to HAR when Zillow photo extraction fails", asyn
|
||||
);
|
||||
});
|
||||
|
||||
test("assessProperty renders a PDF when recipient email is present", async () => {
|
||||
test("assessProperty does not render a PDF from a preliminary helper payload even when recipient email is present", async () => {
|
||||
const outputPath = path.join(os.tmpdir(), `property-assess-command-${Date.now()}.pdf`);
|
||||
let renderCalls = 0;
|
||||
const result = await assessProperty(
|
||||
{
|
||||
address: "4141 Whiteley Dr, Corpus Christi, TX 78418",
|
||||
@@ -213,16 +214,21 @@ test("assessProperty renders a PDF when recipient email is present", async () =>
|
||||
attempts: ["No listing sources discovered from the address."],
|
||||
zillowUrl: null,
|
||||
harUrl: null
|
||||
})
|
||||
}),
|
||||
renderReportPdfFn: async () => {
|
||||
renderCalls += 1;
|
||||
return outputPath;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.needsAssessmentPurpose, false);
|
||||
assert.equal(result.needsRecipientEmails, false);
|
||||
assert.equal(result.outputPath, outputPath);
|
||||
const stat = await fs.promises.stat(outputPath);
|
||||
assert.ok(stat.size > 1000);
|
||||
assert.equal(renderCalls, 0);
|
||||
assert.equal(result.pdfReady, false);
|
||||
assert.equal(result.outputPath, null);
|
||||
assert.match(result.message, /preliminary|decision-grade|cannot render/i);
|
||||
});
|
||||
|
||||
test("assessProperty prioritizes student housing guidance over investment fallback keywords", async () => {
|
||||
|
||||
@@ -68,3 +68,22 @@ test("renderReportPdf requires recipient email", async () => {
|
||||
ReportValidationError
|
||||
);
|
||||
});
|
||||
|
||||
test("renderReportPdf rejects a preliminary report with pending verdict", async () => {
|
||||
const outputPath = path.join(os.tmpdir(), `property-assessor-preliminary-${Date.now()}.pdf`);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
renderReportPdf(
|
||||
{
|
||||
...samplePayload,
|
||||
verdict: {
|
||||
decision: "pending",
|
||||
fairValueRange: "Not established",
|
||||
offerGuidance: "Still needs comps and decision-grade analysis."
|
||||
}
|
||||
},
|
||||
outputPath
|
||||
),
|
||||
/decision-grade|preliminary|pending/i
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user