Add SMTP send flow for approved outreach

This commit is contained in:
Matthias
2026-06-05 21:05:59 +02:00
parent 42a3ea64a5
commit b2f7348ef0
10 changed files with 1531 additions and 56 deletions

View File

@@ -19,8 +19,10 @@ const outreachWorkspacePath = join(
);
function extractConstFunction(source: string, name: string) {
const declaration = `const ${name} = async`;
const start = source.indexOf(declaration);
const declaration = new RegExp(`const ${name}\\s*=\\s*(?:async\\s+)?\\(`);
const match = source.match(declaration);
assert.ok(match, `${name} handler should exist.`);
const start = match.index ?? -1;
assert.ok(start >= 0, `${name} handler should exist.`);
const firstBrace = source.indexOf("{", start);
@@ -95,13 +97,13 @@ test("OutreachReviewWorkspace separates audit publication from email approval",
assert.match(source, /Audit veröffentlichen/);
assert.match(source, /Änderungen speichern/);
assert.match(source, /E-Mail freigeben/);
assert.doesNotMatch(source, /E-Mail freigeben und senden/);
assert.doesNotMatch(source, /api\.outreach\.(send|sendEmail|sendDraft)/);
assert.match(source, /E-Mail freigeben und senden/);
assert.match(source, /useAction/);
assert.match(source, /outreachSendAction[\s\S]*sendApprovedEmail/);
const auditPublishIndex = source.indexOf("Audit veröffentlichen");
const auditSaveIndex = source.indexOf("Änderungen speichern");
const emailApprovalIndex = source.indexOf("E-Mail freigeben");
const emailApprovalIndex = source.indexOf("E-Mail freigeben und senden");
assert.ok(auditPublishIndex >= 0);
assert.ok(auditSaveIndex >= 0);
@@ -113,6 +115,117 @@ test("OutreachReviewWorkspace separates audit publication from email approval",
);
});
test("OutreachReviewWorkspace disables draft/approval/final controls for queued send", async () => {
const source = await readFile(outreachWorkspacePath, "utf8");
assert.match(source, /outreach\.sendStatus\s*===\s*"queued"/);
assert.match(source, /const isQueuedSend = outreach\?\.sendStatus === "queued"/);
assert.match(
source,
/disabled=\{[\s\S]*busyAction === `\$?\{record\.id\}:outreach-save`[\s\S]*\|\|\s*isQueuedSend[\s\S]*\}/,
"Outreach save control should be disabled while outreach is queued.",
);
assert.match(
source,
/disabled=\{[\s\S]*busyAction === `\$?\{record\.id\}:email-approval`[\s\S]*\|\|\s*isQueuedSend[\s\S]*\}/,
"Email approval control should be disabled while outreach is queued.",
);
assert.match(
source,
/disabled=\{[\s\S]*busyAction === `\$?\{pendingEmailConfirmation\.id\}:email-send`[\s\S]*\|\|\s*isQueuedSendForConfirmation[\s\S]*\}/,
"Final send control should be disabled while confirmed outreach is queued.",
);
});
test("OutreachReviewWorkspace prevents draft mutation handlers for queued outreach", async () => {
const source = await readFile(outreachWorkspacePath, "utf8");
const saveOutreachHandler = extractConstFunction(source, "saveOutreach");
const approveEmailHandler = extractConstFunction(source, "approveEmail");
assert.match(saveOutreachHandler, /outreach\.sendStatus\s*===\s*"queued"/);
assert.match(saveOutreachHandler, /[aA]ufgrund des laufenden Sendevorgangs/);
assert.match(approveEmailHandler, /outreach\.sendStatus\s*===\s*"queued"/);
assert.match(approveEmailHandler, /[aA]ufgrund des laufenden Sendevorgangs/);
});
test("OutreachReviewWorkspace prevents final send when confirmed outreach is queued", async () => {
const source = await readFile(outreachWorkspacePath, "utf8");
const sendHandler = extractConstFunction(source, "sendApprovedEmailFromConfirmation");
assert.match(
sendHandler,
/const isQueuedSend = rows\.some\(/,
"Final send handler should check current list records for queued status before sending.",
);
assert.match(
sendHandler,
/\.sendStatus\s*===\s*"queued"/,
"Final send handler should guard queued status.",
);
assert.match(
sendHandler,
/setNotice\(\s*"(?:.*(?:[aA]ufgrund[^"\r\n]*Sendevorgang|.*bereits[^"\r\n]*Vorgang|.*bereits[^"\r\n]*im Gange)[^"]*)"/,
);
});
test("OutreachReviewWorkspace useAction receives a typed send action ref", async () => {
const source = await readFile(outreachWorkspacePath, "utf8");
assert.match(source, /useAction\(api\.outreachSendAction\.sendApprovedEmail\)/);
assert.doesNotMatch(
source,
/as \{\s*outreachSendAction:\s*\{\s*sendApprovedEmail:\s*unknown/,
);
});
test("OutreachReviewWorkspace includes final confirmation UI fields", async () => {
const source = await readFile(outreachWorkspacePath, "utf8");
assert.match(source, /Dialog/);
assert.match(source, /Empfänger/);
assert.match(source, /Betreff/);
assert.match(source, /Absender/);
assert.match(source, /Audit-Link/);
assert.match(source, /sender/);
assert.doesNotMatch(source, /Konfigurierter SMTP-Absender/);
});
test("approveEmail opens confirmation after save and approval", async () => {
const source = await readFile(outreachWorkspacePath, "utf8");
const handler = extractConstFunction(source, "approveEmail");
const draftIndex = handler.indexOf("const draft = drafts[record.id] ?? getDraft(record)");
const saveIndex = handler.indexOf("await saveReviewDraft");
const approveIndex = handler.indexOf("await approveEmailDraft");
const confirmationIndex = handler.indexOf("setPendingEmailConfirmation");
assert.ok(draftIndex >= 0, "Approval should read the current local draft.");
assert.ok(saveIndex >= 0, "Approval should persist the current draft first.");
assert.ok(approveIndex >= 0, "Approval should still call approveEmailDraft.");
assert.ok(confirmationIndex >= 0, "Approval should open a confirmation dialog.");
assert.ok(draftIndex < saveIndex, "Approval should read draft before save.");
assert.ok(saveIndex < approveIndex, "Approval should save before approve.");
assert.ok(approveIndex < confirmationIndex, "Confirmation should open after approval.");
assert.equal(
/sendApprovedEmail/.test(handler),
false,
"Approval should not call sendApprovedEmail.",
);
assert.match(handler, /emailSubject:\s*draft\.emailSubject/);
assert.match(handler, /emailBody:\s*draft\.emailBody/);
assert.match(handler, /followUpDraft:\s*draft\.followUpDraft/);
assert.match(
handler,
/approvalData\.sender/,
);
assert.ok(
!/approvalData\.sender\s*\?\?\s*SMTP_SENDER_PLACEHOLDER/.test(handler),
"Sender should come from approvalResult without placeholder fallback.",
);
});
test("OutreachReviewWorkspace gates phone scripts to call-first or missing-contact leads with phone numbers", async () => {
const source = await readFile(outreachWorkspacePath, "utf8");
@@ -143,6 +256,38 @@ test("approveEmail saves the visible outreach draft before approving it", async
assert.match(handler, /followUpDraft:\s*draft\.followUpDraft/);
});
test("final email send handler calls sendApprovedEmail with outreach id", async () => {
const source = await readFile(outreachWorkspacePath, "utf8");
const handler = extractConstFunction(source, "sendApprovedEmailFromConfirmation");
assert.match(handler, /await sendApprovedEmail\(\{\s*id:\s*confirmation\.id/);
assert.ok(
/await sendApprovedEmail\([\s\S]*id:\s*confirmation\.id[\s\S]*\}/.test(handler),
"Final handler should pass the currently confirmed outreach id.",
);
assert.ok(
handler.indexOf("E-Mail gesendet.") >= 0,
"Final send should show a success notice.",
);
assert.ok(
/Retry|erneut|nochmal|nicht versendet|nicht gesendet/.test(handler),
"Final send should surface a retry-oriented failure notice.",
);
assert.equal(
handler.indexOf("setPendingEmailConfirmation(null)") >= 0,
true,
"Final handler should clear confirmation on success.",
);
});
test("canceling confirmation does not send", async () => {
const source = await readFile(outreachWorkspacePath, "utf8");
const handler = extractConstFunction(source, "closeEmailConfirmation");
assert.ok(handler.includes("setPendingEmailConfirmation(null)"));
assert.equal(/sendApprovedEmail/.test(handler), false);
});
test("publishAudit saves the visible audit draft before publishing it", async () => {
const source = await readFile(outreachWorkspacePath, "utf8");
const handler = extractConstFunction(source, "publishAudit");