Skip to content

Commit 901024b

Browse files
committed
✨ server: forward declined transaction webhook
1 parent 69d0ebd commit 901024b

3 files changed

Lines changed: 52 additions & 0 deletions

File tree

.changeset/bright-panda-decline.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@exactly/server": patch
3+
---
4+
5+
✨ forward declined transaction webhook

server/hooks/panda.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,9 @@ export default new Hono().post(
703703
reason: payload.body.spend.declinedReason ?? "unknown",
704704
},
705705
}).catch((error: unknown) => captureException(error, { level: "error" }));
706+
startSpan({ name: "webhook", op: `panda.webhook.${payload.body.id}` }, () => publish(payload)).catch(
707+
(error: unknown) => captureException(error, { level: "error" }),
708+
);
706709
return c.json({ code: "ok" });
707710
}
708711
if (payload.body.spend.amount < 0) {

server/test/hooks/panda.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2698,6 +2698,50 @@ describe("webhooks", () => {
26982698
expect(createHmac("sha256", secret).update(parse(string(), options?.body)).digest("hex")).toBe(headers.Signature);
26992699
});
27002700

2701+
it("forwards declined transaction webhook", async () => {
2702+
const cardId = `${webhookAccount}-card`;
2703+
const fetch = globalThis.fetch;
2704+
let publish = false;
2705+
const mockFetch = vi.spyOn(globalThis, "fetch").mockImplementation(async (url, init) => {
2706+
if (url === "https://exa.test") {
2707+
publish = true;
2708+
return { ok: true, status: 200, text: () => Promise.resolve("OK") } as Response;
2709+
}
2710+
return fetch(url, init);
2711+
});
2712+
2713+
const response = await appClient.index.$post({
2714+
...authorization,
2715+
json: {
2716+
...authorization.json,
2717+
action: "created",
2718+
body: {
2719+
...authorization.json.body,
2720+
id: "declined-webhook-tx",
2721+
spend: {
2722+
...authorization.json.body.spend,
2723+
cardId,
2724+
userId: webhookAccount,
2725+
status: "declined",
2726+
declinedReason: "webhook declined",
2727+
},
2728+
},
2729+
},
2730+
});
2731+
2732+
expect(response.status).toBe(200);
2733+
await vi.waitUntil(() => publish, 60_000);
2734+
const options = mockFetch.mock.calls.find(([url]) => url === "https://exa.test")?.[1];
2735+
const headers = parse(object({ Signature: string() }), options?.headers);
2736+
expect(createHmac("sha256", secret).update(parse(string(), options?.body)).digest("hex")).toBe(headers.Signature);
2737+
expect(JSON.parse(parse(string(), options?.body))).toMatchObject({
2738+
resource: "transaction",
2739+
action: "created",
2740+
body: { spend: { status: "declined", declinedReason: "webhook declined" } },
2741+
});
2742+
expect(captureException).not.toHaveBeenCalled();
2743+
});
2744+
27012745
it("forwards card updated active", async () => {
27022746
const mockFetch = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({
27032747
ok: true,

0 commit comments

Comments
 (0)