79 lines
4.5 KiB
TypeScript
79 lines
4.5 KiB
TypeScript
import {
|
|
buildImportTable,
|
|
buildRecipientImportPreview,
|
|
defaultColumnMappings,
|
|
materializeRecipientImport,
|
|
parseDelimitedText,
|
|
type RecipientColumnMapping
|
|
} from "../src/features/campaigns/utils/bulkImport";
|
|
|
|
function assert(condition: unknown, message = "assertion failed"): void {
|
|
if (!condition) throw new Error(message);
|
|
}
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
}
|
|
|
|
function mappingFor(mappings: RecipientColumnMapping[], columnIndex: number): RecipientColumnMapping {
|
|
return mappings.find((mapping) => mapping.columnIndex === columnIndex) ?? { columnIndex, kind: "ignore" };
|
|
}
|
|
|
|
const parsed = parseDelimitedText('email;name;field.note\n"alice@example.org";"Alice; A";"hello"', ";");
|
|
assert(parsed[1][1] === "Alice; A", "quoted delimiters are preserved");
|
|
|
|
const csv = [
|
|
"email;name;customer_id;field.contract_no;attachment_pattern;active",
|
|
"alice@example.org;Alice;C-1;CN-1;${customer_id}.pdf|archive/*.pdf;yes",
|
|
"bad-address;Broken;C-2;CN-2;broken.pdf;yes",
|
|
";Missing;C-3;CN-3;;yes"
|
|
].join("\n");
|
|
const existingFields = [{ name: "customer_id", label: "Customer ID", type: "string", required: false, can_override: true }];
|
|
const table = buildImportTable(csv, { delimiter: "auto", headerRows: 1, quoted: true });
|
|
const mappings = defaultColumnMappings(table.headers, existingFields);
|
|
const preview = buildRecipientImportPreview(table, mappings, {
|
|
existingFields,
|
|
existingEntries: [{ id: "alice" }],
|
|
valueSeparators: ",;|"
|
|
});
|
|
|
|
assert(table.delimiter === ";", "semicolon delimiter is detected");
|
|
assert(table.headerRows.length === 1 && table.dataRows.length === 3, "header and data rows are separated");
|
|
assert(mappingFor(mappings, 0).kind === "to", "email columns map to To");
|
|
assert(mappingFor(mappings, 2).kind === "field" && mappingFor(mappings, 2).fieldName === "customer_id", "existing fields are detected");
|
|
assert(mappingFor(mappings, 3).kind === "new_field" && mappingFor(mappings, 3).newFieldName === "contract_no", "explicit new field columns are detected");
|
|
assert(preview.validCount === 1, "one valid row is detected");
|
|
assert(preview.invalidCount === 2, "invalid rows are detected");
|
|
assert(preview.patternCount === 3, "all pattern cells are counted before invalid rows are skipped");
|
|
assert(preview.fieldNamesToCreate.length === 1 && preview.fieldNamesToCreate[0] === "contract_no", "mapped new fields are created");
|
|
assert(preview.rows[0].id === "alice-2", "import ids avoid existing entries");
|
|
assert(preview.rows[0].patterns.length === 2, "pattern cells split on configured separators");
|
|
|
|
const multiTable = buildImportTable('to;name\n"Alice <alice@example.org>|bob@example.org";Team', { delimiter: "auto", headerRows: 1, quoted: true });
|
|
const multiPreview = buildRecipientImportPreview(multiTable, defaultColumnMappings(multiTable.headers, []), {
|
|
existingFields: [],
|
|
valueSeparators: "|"
|
|
});
|
|
assert(multiPreview.validCount === 1, "multi-address cells can produce a valid recipient");
|
|
assert(multiPreview.rows[0].addresses.to.length === 2, "To columns can contain multiple addresses");
|
|
assert(multiPreview.rows[0].addresses.to[0].name === "Alice", "display names are parsed from address cells");
|
|
|
|
const draft = {
|
|
fields: [{ name: "customer_id", label: "Customer ID", type: "string", required: false, can_override: true }],
|
|
entries: { inline: [{ id: "alice", to: [{ email: "old@example.org" }] }], source: { type: "csv" }, mapping: { id: "id" } }
|
|
};
|
|
const imported = materializeRecipientImport(draft, preview, { mode: "append", attachmentBasePath: { id: "bp-1", path: "invoices" } });
|
|
const entries = asRecord(imported.entries);
|
|
const inline = entries.inline as Record<string, unknown>[];
|
|
const fields = imported.fields as Record<string, unknown>[];
|
|
const importedEntry = inline[1];
|
|
const attachments = importedEntry.attachments as Record<string, unknown>[];
|
|
|
|
assert(inline.length === 2, "append keeps existing entries");
|
|
assert(entries.source === undefined && entries.mapping === undefined, "external source metadata is removed for inline imports");
|
|
assert(fields.some((field) => field.name === "contract_no"), "new field definition is added");
|
|
assert(importedEntry.email === "alice@example.org", "entry email is materialized");
|
|
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");
|