mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
add domna survey fields to deal-properties registry
Plug `domna_survey_type` and `domna_survey_date` into DEAL_PROPERTY_FIELDS, gated on the approver capability and reusing the shared string-or-null + ISO date schemas. Drops the now-unused boolean column types from the field registry. Vitest cases cover validation, capability gating, HubSpot mapping, independent setting and clear-to-null. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
73a365468a
commit
1336d90c8d
2 changed files with 186 additions and 11 deletions
|
|
@ -40,6 +40,27 @@ describe("DEAL_PROPERTY_FIELDS registry", () => {
|
|||
).toBe(false);
|
||||
});
|
||||
|
||||
it("exposes the domna survey fields gated on the approver capability", () => {
|
||||
expect(isDealPropertyField("domna_survey_type")).toBe(true);
|
||||
expect(isDealPropertyField("domna_survey_date")).toBe(true);
|
||||
// Plain roles never satisfy the approver gate on their own.
|
||||
for (const role of ["read", "write", "admin", "creator"]) {
|
||||
expect(roleAllowedForField("domna_survey_type", role)).toBe(false);
|
||||
expect(roleAllowedForField("domna_survey_date", role)).toBe(false);
|
||||
}
|
||||
// Approver capability unlocks both.
|
||||
expect(
|
||||
roleAllowedForField("domna_survey_type", "read", ["approver"]),
|
||||
).toBe(true);
|
||||
expect(
|
||||
roleAllowedForField("domna_survey_date", "read", ["approver"]),
|
||||
).toBe(true);
|
||||
// Other capabilities do not unlock the fields.
|
||||
expect(
|
||||
roleAllowedForField("domna_survey_type", "write", ["contractor"]),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("maps each registered field to the matching HubSpot property", () => {
|
||||
expect(DEAL_PROPERTY_FIELDS.pibi_order_date.hubspotProperty).toBe(
|
||||
"pibi_order_date",
|
||||
|
|
@ -53,6 +74,12 @@ describe("DEAL_PROPERTY_FIELDS registry", () => {
|
|||
expect(DEAL_PROPERTY_FIELDS.property_halted_reason.hubspotProperty).toBe(
|
||||
"property_halted_reason",
|
||||
);
|
||||
expect(DEAL_PROPERTY_FIELDS.domna_survey_type.hubspotProperty).toBe(
|
||||
"domna_survey_type",
|
||||
);
|
||||
expect(DEAL_PROPERTY_FIELDS.domna_survey_date.hubspotProperty).toBe(
|
||||
"domna_survey_date",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects unknown fields", () => {
|
||||
|
|
@ -288,6 +315,148 @@ describe("applyDealPropertyUpdate", () => {
|
|||
expect("property_halted_reason" in props).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects domna fields when the caller lacks approver capability", async () => {
|
||||
const updateDb = vi.fn();
|
||||
const pushHubspot = vi.fn();
|
||||
const out = await applyDealPropertyUpdate({
|
||||
dealId: "deal-d1",
|
||||
fields: {
|
||||
domna_survey_type: "Standard",
|
||||
domna_survey_date: "2025-07-15T00:00:00.000Z",
|
||||
},
|
||||
// Even creator role alone is not enough — capability is orthogonal.
|
||||
role: "creator",
|
||||
capabilities: [],
|
||||
deps: { updateDb, pushHubspot },
|
||||
});
|
||||
expect(out.results.domna_survey_type).toEqual({
|
||||
ok: false,
|
||||
error: "Insufficient permissions",
|
||||
});
|
||||
expect(out.results.domna_survey_date).toEqual({
|
||||
ok: false,
|
||||
error: "Insufficient permissions",
|
||||
});
|
||||
expect(out.hubspotSync).toBe("skipped");
|
||||
expect(updateDb).not.toHaveBeenCalled();
|
||||
expect(pushHubspot).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("persists domna survey type + date for an approver and pushes them to HubSpot", async () => {
|
||||
const updateDb = vi.fn().mockResolvedValue(undefined);
|
||||
const pushHubspot = vi.fn().mockResolvedValue({ ok: true });
|
||||
const surveyType = "Detailed";
|
||||
const surveyIso = "2025-07-15T00:00:00.000Z";
|
||||
|
||||
const out = await applyDealPropertyUpdate({
|
||||
dealId: "deal-d2",
|
||||
fields: {
|
||||
domna_survey_type: surveyType,
|
||||
domna_survey_date: surveyIso,
|
||||
},
|
||||
role: "read",
|
||||
capabilities: ["approver"],
|
||||
deps: { updateDb, pushHubspot },
|
||||
});
|
||||
|
||||
expect(out.results.domna_survey_type).toEqual({ ok: true });
|
||||
expect(out.results.domna_survey_date).toEqual({ ok: true });
|
||||
expect(out.hubspotSync).toBe("ok");
|
||||
|
||||
const dbValues = updateDb.mock.calls[0][1];
|
||||
expect(dbValues.domnaSurveyType).toBe(surveyType);
|
||||
expect(dbValues.domnaSurveyDate).toBeInstanceOf(Date);
|
||||
expect((dbValues.domnaSurveyDate as Date).toISOString()).toBe(surveyIso);
|
||||
|
||||
const props = pushHubspot.mock.calls[0][0].properties;
|
||||
expect(props.domna_survey_type).toBe(surveyType);
|
||||
expect(props.domna_survey_date).toBe(
|
||||
String(new Date(surveyIso).getTime()),
|
||||
);
|
||||
});
|
||||
|
||||
it("validates the domna survey date format", async () => {
|
||||
const updateDb = vi.fn();
|
||||
const pushHubspot = vi.fn();
|
||||
const out = await applyDealPropertyUpdate({
|
||||
dealId: "deal-d3",
|
||||
fields: { domna_survey_date: "definitely-not-a-date" },
|
||||
role: "read",
|
||||
capabilities: ["approver"],
|
||||
deps: { updateDb, pushHubspot },
|
||||
});
|
||||
expect(out.results.domna_survey_date.ok).toBe(false);
|
||||
expect(updateDb).not.toHaveBeenCalled();
|
||||
expect(pushHubspot).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("lets domna survey type and date be settable independently", async () => {
|
||||
// Setting only the type — date column is untouched.
|
||||
const updateDbType = vi.fn().mockResolvedValue(undefined);
|
||||
const pushHubspotType = vi.fn().mockResolvedValue({ ok: true });
|
||||
const outType = await applyDealPropertyUpdate({
|
||||
dealId: "deal-d4",
|
||||
fields: { domna_survey_type: "Standard" },
|
||||
role: "read",
|
||||
capabilities: ["approver"],
|
||||
deps: { updateDb: updateDbType, pushHubspot: pushHubspotType },
|
||||
});
|
||||
expect(outType.results.domna_survey_type).toEqual({ ok: true });
|
||||
expect(outType.results.domna_survey_date).toBeUndefined();
|
||||
const typeOnlyDb = updateDbType.mock.calls[0][1];
|
||||
expect(typeOnlyDb.domnaSurveyType).toBe("Standard");
|
||||
expect("domnaSurveyDate" in typeOnlyDb).toBe(false);
|
||||
const typeOnlyProps = pushHubspotType.mock.calls[0][0].properties;
|
||||
expect(typeOnlyProps.domna_survey_type).toBe("Standard");
|
||||
expect("domna_survey_date" in typeOnlyProps).toBe(false);
|
||||
|
||||
// Setting only the date — type column is untouched.
|
||||
const updateDbDate = vi.fn().mockResolvedValue(undefined);
|
||||
const pushHubspotDate = vi.fn().mockResolvedValue({ ok: true });
|
||||
const surveyIso = "2025-08-20T00:00:00.000Z";
|
||||
const outDate = await applyDealPropertyUpdate({
|
||||
dealId: "deal-d4b",
|
||||
fields: { domna_survey_date: surveyIso },
|
||||
role: "read",
|
||||
capabilities: ["approver"],
|
||||
deps: { updateDb: updateDbDate, pushHubspot: pushHubspotDate },
|
||||
});
|
||||
expect(outDate.results.domna_survey_date).toEqual({ ok: true });
|
||||
expect(outDate.results.domna_survey_type).toBeUndefined();
|
||||
const dateOnlyDb = updateDbDate.mock.calls[0][1];
|
||||
expect(dateOnlyDb.domnaSurveyDate).toBeInstanceOf(Date);
|
||||
expect("domnaSurveyType" in dateOnlyDb).toBe(false);
|
||||
const dateOnlyProps = pushHubspotDate.mock.calls[0][0].properties;
|
||||
expect(dateOnlyProps.domna_survey_date).toBe(
|
||||
String(new Date(surveyIso).getTime()),
|
||||
);
|
||||
expect("domna_survey_type" in dateOnlyProps).toBe(false);
|
||||
});
|
||||
|
||||
it("clears both domna fields to null when explicitly cleared", async () => {
|
||||
const updateDb = vi.fn().mockResolvedValue(undefined);
|
||||
const pushHubspot = vi.fn().mockResolvedValue({ ok: true });
|
||||
const out = await applyDealPropertyUpdate({
|
||||
dealId: "deal-d5",
|
||||
fields: {
|
||||
// empty string collapses to null (mirrors the date schema)
|
||||
domna_survey_type: "",
|
||||
domna_survey_date: null,
|
||||
},
|
||||
role: "read",
|
||||
capabilities: ["approver"],
|
||||
deps: { updateDb, pushHubspot },
|
||||
});
|
||||
expect(out.results.domna_survey_type).toEqual({ ok: true });
|
||||
expect(out.results.domna_survey_date).toEqual({ ok: true });
|
||||
const dbValues = updateDb.mock.calls[0][1];
|
||||
expect(dbValues.domnaSurveyType).toBeNull();
|
||||
expect(dbValues.domnaSurveyDate).toBeNull();
|
||||
const props = pushHubspot.mock.calls[0][0].properties;
|
||||
expect(props.domna_survey_type).toBe("");
|
||||
expect(props.domna_survey_date).toBe("");
|
||||
});
|
||||
|
||||
it("surfaces HubSpot push failures back to the caller", async () => {
|
||||
const updateDb = vi.fn().mockResolvedValue(undefined);
|
||||
const pushHubspot = vi
|
||||
|
|
|
|||
|
|
@ -76,15 +76,12 @@ const stringOrNullSchema = z
|
|||
|
||||
type DateColumn = typeof hubspotDealData.pibiOrderDate;
|
||||
type TextColumn = typeof hubspotDealData.propertyHaltedReason;
|
||||
type BoolColumn = typeof hubspotDealData.domnaSurveyRequired;
|
||||
|
||||
type ColumnFor<T> = T extends Date | null
|
||||
? DateColumn
|
||||
: T extends string | null
|
||||
? TextColumn
|
||||
: T extends boolean | null
|
||||
? BoolColumn
|
||||
: never;
|
||||
: never;
|
||||
|
||||
interface DealPropertyFieldDef<TParsed = unknown> {
|
||||
/** Schema used to parse the incoming JSON value. */
|
||||
|
|
@ -113,9 +110,6 @@ const dateToHubspot = (d: Date | null): string =>
|
|||
|
||||
const stringToHubspot = (s: string | null): string => (s === null ? "" : s);
|
||||
|
||||
const booleanToHubspot = (b: boolean | null): string =>
|
||||
b === null ? "" : b ? "true" : "false";
|
||||
|
||||
// -- Registry ----------------------------------------------------------------
|
||||
// Slice 5 ships PIBI dates only. Halted (#255) and Domna (#256) reuse the
|
||||
// same registry by adding entries here — no other code path needs to change.
|
||||
|
|
@ -150,12 +144,24 @@ export const DEAL_PROPERTY_FIELDS = {
|
|||
dbColumn: hubspotDealData.propertyHaltedReason,
|
||||
toHubspot: stringToHubspot,
|
||||
} satisfies DealPropertyFieldDef<string | null>,
|
||||
// -- Slot for issue #256 (Domna survey type / date) ----------------------
|
||||
// domna_survey_type / domna_survey_date will plug in here.
|
||||
// -- Domna survey (issue #256) -------------------------------------------
|
||||
// Approver capability gates these — write role alone is not sufficient.
|
||||
domna_survey_type: {
|
||||
schema: stringOrNullSchema,
|
||||
allowedRoles: APPROVER_ROLES,
|
||||
hubspotProperty: "domna_survey_type",
|
||||
dbColumn: hubspotDealData.domnaSurveyType,
|
||||
toHubspot: stringToHubspot,
|
||||
} satisfies DealPropertyFieldDef<string | null>,
|
||||
domna_survey_date: {
|
||||
schema: isoDateSchema,
|
||||
allowedRoles: APPROVER_ROLES,
|
||||
hubspotProperty: "domna_survey_date",
|
||||
dbColumn: hubspotDealData.domnaSurveyDate,
|
||||
toHubspot: dateToHubspot,
|
||||
} satisfies DealPropertyFieldDef<Date | null>,
|
||||
} as const;
|
||||
|
||||
void booleanToHubspot;
|
||||
|
||||
export type DealPropertyFieldKey = keyof typeof DEAL_PROPERTY_FIELDS;
|
||||
|
||||
export function isDealPropertyField(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue