Block preliminary property assessor PDFs
This commit is contained in:
@@ -78,7 +78,8 @@ Current behavior:
|
|||||||
- returns a structured preliminary report payload
|
- returns a structured preliminary report payload
|
||||||
- does not require recipient email(s) for the analysis-only run
|
- does not require recipient email(s) for the analysis-only run
|
||||||
- asks for recipient email(s) only when PDF rendering is explicitly requested
|
- 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:
|
Important limitation:
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ scripts/property-assessor render-report --input "<report-payload-json>" --output
|
|||||||
- complete the analysis without requiring recipient email(s)
|
- 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
|
- 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
|
- 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
|
## Public-record enrichment
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import path from "node:path";
|
|||||||
import { discoverListingSources, type ListingDiscoveryResult } from "./listing-discovery.js";
|
import { discoverListingSources, type ListingDiscoveryResult } from "./listing-discovery.js";
|
||||||
import { extractPhotoData, type PhotoExtractionResult, type PhotoSource } from "./photo-review.js";
|
import { extractPhotoData, type PhotoExtractionResult, type PhotoSource } from "./photo-review.js";
|
||||||
import { resolvePublicRecords, type PublicRecordsResolution } from "./public-records.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 {
|
export interface AssessPropertyOptions {
|
||||||
address: string;
|
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 =
|
const outputPath =
|
||||||
options.output ||
|
options.output ||
|
||||||
path.join(
|
path.join(
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ export interface ReportPayload {
|
|||||||
sourceLinks?: unknown;
|
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[] {
|
function asStringArray(value: unknown): string[] {
|
||||||
if (value == null) return [];
|
if (value == null) return [];
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
@@ -69,6 +77,11 @@ export function validateReportPayload(payload: ReportPayload): string[] {
|
|||||||
if (!address) {
|
if (!address) {
|
||||||
throw new ReportValidationError("The report payload must include subjectProperty.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;
|
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`);
|
const outputPath = path.join(os.tmpdir(), `property-assess-command-${Date.now()}.pdf`);
|
||||||
|
let renderCalls = 0;
|
||||||
const result = await assessProperty(
|
const result = await assessProperty(
|
||||||
{
|
{
|
||||||
address: "4141 Whiteley Dr, Corpus Christi, TX 78418",
|
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."],
|
attempts: ["No listing sources discovered from the address."],
|
||||||
zillowUrl: null,
|
zillowUrl: null,
|
||||||
harUrl: null
|
harUrl: null
|
||||||
})
|
}),
|
||||||
|
renderReportPdfFn: async () => {
|
||||||
|
renderCalls += 1;
|
||||||
|
return outputPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(result.ok, true);
|
assert.equal(result.ok, true);
|
||||||
assert.equal(result.needsAssessmentPurpose, false);
|
assert.equal(result.needsAssessmentPurpose, false);
|
||||||
assert.equal(result.needsRecipientEmails, false);
|
assert.equal(result.needsRecipientEmails, false);
|
||||||
assert.equal(result.outputPath, outputPath);
|
assert.equal(renderCalls, 0);
|
||||||
const stat = await fs.promises.stat(outputPath);
|
assert.equal(result.pdfReady, false);
|
||||||
assert.ok(stat.size > 1000);
|
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 () => {
|
test("assessProperty prioritizes student housing guidance over investment fallback keywords", async () => {
|
||||||
|
|||||||
@@ -68,3 +68,22 @@ test("renderReportPdf requires recipient email", async () => {
|
|||||||
ReportValidationError
|
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