Consume governed recipients and add operational checks

This commit is contained in:
2026-07-31 22:48:07 +02:00
parent fa4eb39e0b
commit 82ddc0c34c
15 changed files with 354 additions and 14 deletions
+17 -1
View File
@@ -256,6 +256,18 @@ export type CampaignRecipientSnapshotItem = {
provenance: Record<string, unknown>;
};
export type CampaignRecipientSnapshotExcludedItem = {
contact_id: string;
display_name: string;
channel: string;
target: string;
contact_point_id?: string | null;
status: string;
reason_code?: string | null;
explanation?: string | null;
provenance: Record<string, unknown>;
};
export type CampaignRecipientAddressSourceSnapshot = {
source_id: string;
source_label: string;
@@ -263,6 +275,10 @@ export type CampaignRecipientAddressSourceSnapshot = {
source_revision: string;
generated_at: string;
recipients: CampaignRecipientSnapshotItem[];
excluded: CampaignRecipientSnapshotExcludedItem[];
included_count: number;
excluded_count: number;
purpose: string;
provenance: Record<string, unknown>;
};
@@ -714,7 +730,7 @@ sourceId: string)
: Promise<CampaignRecipientAddressSourceSnapshot> {
return apiFetch<CampaignRecipientAddressSourceSnapshot>(settings, `/api/v1/campaigns/${campaignId}/recipient-address-sources/snapshot`, {
method: "POST",
body: JSON.stringify({ source_id: sourceId })
body: JSON.stringify({ source_id: sourceId, purpose: "campaign_delivery" })
});
}
@@ -74,6 +74,14 @@ export default function AddressSourceImportDialog({
() => sources.find((source) => source.source_id === selectedSourceId) ?? null,
[selectedSourceId, sources]
);
const exclusionCounts = useMemo(() => {
const counts = new Map<string, number>();
for (const item of snapshot?.excluded ?? []) {
const reason = item.reason_code || item.status || "excluded";
counts.set(reason, (counts.get(reason) ?? 0) + 1);
}
return [...counts.entries()].sort((left, right) => right[1] - left[1]);
}, [snapshot]);
useEffect(() => {
if (filteredSources.some((source) => source.source_id === selectedSourceId)) {
@@ -276,14 +284,26 @@ export default function AddressSourceImportDialog({
</dd>
</div>
<div>
<dt>Recipients</dt>
<dd>{snapshot.recipients.length}</dd>
<dt>Included</dt>
<dd>{snapshot.included_count}</dd>
</div>
<div>
<dt>Excluded</dt>
<dd>{snapshot.excluded_count}</dd>
</div>
<div>
<dt>Revision</dt>
<dd className="mono-small">{snapshot.source_revision}</dd>
</div>
</dl>
{snapshot.excluded_count > 0 && (
<DismissibleAlert tone="warning" compact dismissible={false}>
{snapshot.excluded_count} contact point{snapshot.excluded_count === 1 ? " was" : "s were"} excluded by effective communication-governance facts.
{exclusionCounts.length > 0 && (
<span> {exclusionCounts.map(([reason, count]) => `${reason}: ${count}`).join(" · ")}</span>
)}
</DismissibleAlert>
)}
<AddressSourceRecipientPreviewGrid recipients={snapshot.recipients.slice(0, 20)} />
{snapshot.recipients.length > 20 && (
<p className="muted small-note">
@@ -98,7 +98,27 @@ export function createAddressSourceImportProvenance(
source_id: snapshot.source_id,
source_label: snapshot.source_label,
source_revision: snapshot.source_revision,
source_provenance: snapshot.provenance,
source_provenance: {
...snapshot.provenance,
generated_at: snapshot.generated_at,
purpose: snapshot.purpose,
included_count: snapshot.included_count,
excluded_count: snapshot.excluded_count,
included_decisions: snapshot.recipients.map((recipient) => ({
contact_id: recipient.contact_id,
email: recipient.email,
provenance: recipient.provenance
})),
exclusions: snapshot.excluded.map((item) => ({
contact_id: item.contact_id,
channel: item.channel,
target: item.target,
status: item.status,
reason_code: item.reason_code,
explanation: item.explanation,
provenance: item.provenance
}))
},
filename: null,
sheet_name: null,
encoding: null,