Add follow-up status tracking slice
This commit is contained in:
34
tests/outreach-follow-up-source.test.ts
Normal file
34
tests/outreach-follow-up-source.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const outreachSource = readFileSync(
|
||||
join(process.cwd(), "convex", "outreach.ts"),
|
||||
"utf8",
|
||||
);
|
||||
const schemaSource = readFileSync(
|
||||
join(process.cwd(), "convex", "schema.ts"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("outreach schema stores follow-up due and do-not-contact recheck dates as optional migration-safe fields", () => {
|
||||
assert.match(schemaSource, /followUpDueAt:\s*v\.optional\(v\.number\(\)\)/);
|
||||
assert.match(schemaSource, /parentOutreachId:\s*v\.optional\(v\.id\("outreachRecords"\)\)/);
|
||||
assert.match(schemaSource, /doNotContactUntil:\s*v\.optional\(v\.number\(\)\)/);
|
||||
});
|
||||
|
||||
test("successful initial sends create one follow-up draft for manual approval", () => {
|
||||
assert.match(outreachSource, /createFollowUpDraftAfterInitialSend/);
|
||||
assert.match(outreachSource, /followUpDueAt:\s*sentAt\s*\+\s*FOLLOW_UP_DUE_DELAY_MS/);
|
||||
assert.match(outreachSource, /approvalStatus:\s*"draft"[\s\S]*sendStatus:\s*"not_sent"/);
|
||||
assert.match(outreachSource, /parentOutreachId:\s*outreach\._id/);
|
||||
});
|
||||
|
||||
test("manual sales status mutation updates lead suppression states", () => {
|
||||
assert.match(outreachSource, /export const updateManualSalesStatus = mutation/);
|
||||
assert.match(outreachSource, /salesStatus:\s*manualSalesStatus/);
|
||||
assert.match(outreachSource, /reply_received[\s\S]*leadPatch\.contactStatus\s*=\s*"replied"/);
|
||||
assert.match(outreachSource, /not_interested[\s\S]*outreachPatch\.responseStatus\s*=\s*"no_interest"/);
|
||||
assert.match(outreachSource, /do_not_pursue[\s\S]*outreachPatch\.doNotContactUntil\s*=\s*now\s*\+\s*DO_NOT_CONTACT_RECHECK_MS/);
|
||||
});
|
||||
94
tests/outreach-follow-up.test.ts
Normal file
94
tests/outreach-follow-up.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
DO_NOT_CONTACT_RECHECK_MS,
|
||||
FOLLOW_UP_DUE_DELAY_MS,
|
||||
getManualSalesStatusLabel,
|
||||
getFollowUpPromptState,
|
||||
getDoNotContactRecheckState,
|
||||
shouldCreateFollowUpDraftAfterSend,
|
||||
} from "../lib/outreach-follow-up";
|
||||
|
||||
test("manual sales statuses expose the German MVP labels", () => {
|
||||
assert.equal(getManualSalesStatusLabel("reply_received"), "Antwort erhalten");
|
||||
assert.equal(getManualSalesStatusLabel("not_interested"), "Kein Interesse");
|
||||
assert.equal(getManualSalesStatusLabel("later"), "Später wieder melden");
|
||||
assert.equal(getManualSalesStatusLabel("meeting_scheduled"), "Gespräch vereinbart");
|
||||
assert.equal(getManualSalesStatusLabel("proposal_requested"), "Angebot angefragt");
|
||||
assert.equal(getManualSalesStatusLabel("proposal_sent"), "Angebot gesendet");
|
||||
assert.equal(getManualSalesStatusLabel("won"), "Auftrag gewonnen");
|
||||
assert.equal(getManualSalesStatusLabel("lost"), "Auftrag verloren");
|
||||
assert.equal(getManualSalesStatusLabel("do_not_pursue"), "Nicht weiter verfolgen");
|
||||
assert.equal(getManualSalesStatusLabel("follow_up_planned"), "Follow-up geplant");
|
||||
assert.equal(getManualSalesStatusLabel("follow_up_sent"), "Follow-up gesendet");
|
||||
});
|
||||
|
||||
test("initial send creates exactly one pending follow-up window", () => {
|
||||
const sentAt = Date.UTC(2026, 5, 5);
|
||||
|
||||
assert.equal(
|
||||
shouldCreateFollowUpDraftAfterSend({
|
||||
existingFollowUpOutreachCount: 0,
|
||||
followUpDraft: "Kurze Nachfrage",
|
||||
salesStatus: "follow_up_planned",
|
||||
sendStatus: "sent",
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
getFollowUpPromptState({
|
||||
followUpDueAt: sentAt + FOLLOW_UP_DUE_DELAY_MS,
|
||||
responseStatus: "none",
|
||||
salesStatus: "follow_up_planned",
|
||||
now: sentAt + FOLLOW_UP_DUE_DELAY_MS,
|
||||
}),
|
||||
"due",
|
||||
);
|
||||
});
|
||||
|
||||
test("answers and no-interest statuses suppress pending follow-up prompts", () => {
|
||||
const dueAt = Date.UTC(2026, 5, 12);
|
||||
|
||||
for (const salesStatus of ["reply_received", "not_interested"] as const) {
|
||||
assert.equal(
|
||||
getFollowUpPromptState({
|
||||
followUpDueAt: dueAt,
|
||||
responseStatus: "none",
|
||||
salesStatus,
|
||||
now: dueAt + 1,
|
||||
}),
|
||||
"suppressed",
|
||||
);
|
||||
}
|
||||
|
||||
assert.equal(
|
||||
getFollowUpPromptState({
|
||||
followUpDueAt: dueAt,
|
||||
responseStatus: "manual_reply_recorded",
|
||||
salesStatus: "follow_up_planned",
|
||||
now: dueAt + 1,
|
||||
}),
|
||||
"suppressed",
|
||||
);
|
||||
});
|
||||
|
||||
test("do-not-contact blocks outreach for twelve months before recheck", () => {
|
||||
const markedAt = Date.UTC(2026, 0, 1);
|
||||
const recheckAt = markedAt + DO_NOT_CONTACT_RECHECK_MS;
|
||||
|
||||
assert.deepEqual(
|
||||
getDoNotContactRecheckState({
|
||||
doNotContactUntil: recheckAt,
|
||||
now: recheckAt - 1,
|
||||
}),
|
||||
{ status: "blocked", label: "Nicht erneut kontaktieren" },
|
||||
);
|
||||
assert.deepEqual(
|
||||
getDoNotContactRecheckState({
|
||||
doNotContactUntil: recheckAt,
|
||||
now: recheckAt,
|
||||
}),
|
||||
{ status: "recheck", label: "Erneut prüfen" },
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user