intake: escape the currency and the button label too
money() interpolated the extracted currency raw, and parseExtraction accepts any string for it — so "<@U123>" would have been a live mention on every card the batch produced. And plain_text disables *formatting*, not entity decoding: &, < and > are control characters everywhere in a Slack payload, so the Create button's label is escaped like any other value we did not write. My earlier note claiming the opposite was wrong. Candidate rows also carry their transaction's currency (NATIVE_CURRENCY) rather than always printing "$" under a header reading "USD 22.40".
This commit is contained in:
@@ -12,7 +12,7 @@ import { platformLabel } from "@/lib/intake-rules";
|
||||
|
||||
export type Block = Record<string, unknown>;
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user