Integrate Distribution Lists with Campaign recipients

This commit is contained in:
2026-08-02 11:53:35 +02:00
parent 4eeba62bbc
commit b38597f2be
15 changed files with 1552 additions and 3 deletions
+107
View File
@@ -12,6 +12,11 @@ import {
xlsxSheetsFromArrayBuffer,
type RecipientColumnMapping
} from "../src/features/campaigns/utils/bulkImport";
import {
distributionListDrift,
materializeDistributionListExpansion,
type DistributionListExpansionSnapshot
} from "../src/features/campaigns/utils/distributionListImport";
function assert(condition: unknown, message = "assertion failed"): void {
if (!condition) throw new Error(message);
@@ -130,6 +135,108 @@ assert(attachments.length === 2, "valid row patterns become attachment rules");
assert(attachments[0].base_path_id === "bp-1" && attachments[0].base_dir === "invoices", "attachment rules use the selected source");
assert(attachments[0].file_filter === "${customer_id}.pdf", "field placeholders remain in imported patterns");
const distributionExpansion: DistributionListExpansionSnapshot = {
source: {
id: "list-1",
tenant_id: "tenant-1",
name: "Residents",
revision_id: "revision-3",
revision: 3,
definition_hash: "definition-hash-3",
definition_kind: "static",
status: "active",
entry_count: 2,
read_only: false,
stale: false,
parameters: [],
provenance: {},
metadata: {}
},
request: { requested_channels: ["email", "postal"] },
recipients: [
{
recipient_key: "contact:ada",
display_name: "Ada Lovelace",
status: "usable",
channels: [
{
channel: "email",
target: "ada@example.org",
target_key: "email:ada@example.org",
status: "usable",
preferred: true,
decision_provenance: { policy: "allow" }
},
{
channel: "postal",
target: "Example Street 1",
target_key: "postal:ada",
status: "usable",
preferred: false,
decision_provenance: { policy: "fallback" }
}
],
source_entry_ids: ["entry-addresses"],
explanations: [],
attributes: { district: "north" },
provenance: { provider: "addresses" }
},
{
recipient_key: "contact:postal",
display_name: "Postal only",
status: "usable",
channels: [
{
channel: "postal",
target: "Example Street 2",
target_key: "postal:only",
status: "usable",
preferred: true,
decision_provenance: { policy: "allow" }
}
],
source_entry_ids: ["entry-postal"],
explanations: [],
attributes: {},
provenance: {}
}
],
excluded: [
{
recipient_key: "contact:suppressed",
display_name: "Suppressed",
status: "suppressed",
channels: [],
source_entry_ids: ["entry-addresses"],
explanations: [{ code: "opt_out", message: "Recipient opted out.", severity: "warning", provenance: {} }],
attributes: {},
provenance: {}
}
],
diagnostics: [],
provider_evidence: [],
expansion_hash: "expansion-hash-3",
generated_at: "2026-08-02T10:00:00Z",
snapshot_id: "snapshot-3",
stale: false,
truncated: false
};
const distributionDraft = materializeDistributionListExpansion(draft, distributionExpansion, "replace");
const distributionEntries = asRecord(distributionDraft.entries);
const distributionInline = distributionEntries.inline as Record<string, unknown>[];
const firstDistributionSource = asRecord(distributionInline[0].distribution_source);
const secondDistributionSource = asRecord(distributionInline[1].distribution_source);
const distributionImports = distributionEntries.imports as Record<string, unknown>[];
assert(distributionInline.length === 2, "all included Distribution List recipients are frozen into Campaign");
assert(distributionInline[0].active === true && distributionInline[0].email === "ada@example.org", "one preferred route is selected without duplicating channels");
assert(distributionInline[1].active === false, "postal-only recipients are retained but not sent through the mail-only path");
assert(firstDistributionSource.snapshot_id === "snapshot-3" && firstDistributionSource.list_revision_id === "revision-3", "recipient rows retain snapshot and list revision evidence");
assert((secondDistributionSource.source_entry_ids as string[])[0] === "entry-postal", "recipient rows retain source entry references");
assert(distributionImports[0].source_type === "distribution_list", "Campaign stores Distribution List import provenance");
assert((asRecord(distributionImports[0].source_provenance).exclusions as unknown[]).length === 1, "excluded recipients remain in immutable import evidence");
assert(distributionListDrift(distributionEntries.imports, [{ id: "list-1", revision: 4, revision_id: "revision-4", definition_hash: "definition-hash-4" }]).length === 1, "list revision drift is detected without changing frozen recipients");
void runXlsxImportAssertions();
async function runXlsxImportAssertions(): Promise<void> {