diff --git a/src/lib/intake-blocks.ts b/src/lib/intake-blocks.ts index 4b542e4..e591477 100644 --- a/src/lib/intake-blocks.ts +++ b/src/lib/intake-blocks.ts @@ -12,7 +12,7 @@ import { platformLabel } from "@/lib/intake-rules"; export type Block = Record; -export type Candidate = { transaction_id: number; description: string | null; transaction_date: string; amount: string | number }; +export type Candidate = { transaction_id: number; description: string | null; transaction_date: string; amount: string | number; currency?: string | null }; export function financePublicUrl(): string { return (process.env.FINANCE_PUBLIC_URL || "https://finance.bosecamp.com").replace(/\/$/, ""); @@ -42,7 +42,9 @@ export function esc(value: string | null | undefined): string { */ const money = (n: number | string, currency?: string | null) => { const amount = Number(n).toFixed(2); - return !currency || currency === "AUD" ? `$${amount}` : `${currency} ${amount}`; + // The currency comes off the model (`parseExtraction` accepts any string), so + // an unescaped `<@U123>` here would be a mention on every card. + return !currency || currency === "AUD" ? `$${amount}` : `${esc(currency)} ${amount}`; }; const section = (text: string): Block => ({ type: "section", text: { type: "mrkdwn", text } }); const button = (action_id: string, text: string, value: unknown, style?: "primary" | "danger"): Block => ({ @@ -103,15 +105,17 @@ export function candidatesCard(c: CardInput, candidates: Candidate[]): { blocks: for (const cand of candidates) { blocks.push({ type: "section", - text: { type: "mrkdwn", text: `${esc(cand.transaction_date)} · ${esc(cand.description) || "—"} · ${money(cand.amount)}` }, + text: { type: "mrkdwn", text: `${esc(cand.transaction_date)} · ${esc(cand.description) || "—"} · ${money(cand.amount, cand.currency)}` }, accessory: button("intake_use_candidate", "Use this", { batch_id: c.batchId, transaction_id: cand.transaction_id }), }); } blocks.push({ type: "actions", block_id: "intake_create", - // A button's `text` is plain_text, never mrkdwn — the raw merchant belongs here. - elements: [button("intake_create", `Create new ${c.total !== null ? money(c.total, c.currency) + " " : ""}${c.merchant ?? ""} transaction`.replace(/\s+/g, " ").trim(), { batch_id: c.batchId }, "primary")], + // plain_text disables *formatting*, not entity decoding: `&`, `<` and `>` + // are control characters everywhere in a Slack payload, so the label is + // escaped like any other value we did not write. + elements: [button("intake_create", `Create new ${c.total !== null ? money(c.total, c.currency) + " " : ""}${esc(c.merchant)} transaction`.replace(/\s+/g, " ").trim(), { batch_id: c.batchId }, "primary")], }); return { blocks, text }; }