diff --git a/.changeset/nine-hands-shave.md b/.changeset/nine-hands-shave.md new file mode 100644 index 0000000000..4424f71750 --- /dev/null +++ b/.changeset/nine-hands-shave.md @@ -0,0 +1,5 @@ +--- +"@exactly/server": patch +--- + +✨ add card provisioning webhook fields diff --git a/docs/src/content/docs/webhooks.md b/docs/src/content/docs/webhooks.md index 6cb41bbe1e..8e8a1645ca 100644 --- a/docs/src/content/docs/webhooks.md +++ b/docs/src/content/docs/webhooks.md @@ -641,7 +641,7 @@ This webhook is sent whenever a user's compliance status is updated. No response ### Card updated -This webhook is currently triggered when a user adds their card to a digital wallet. +This webhook is triggered when a card is updated, including when it is replaced or added to a digital wallet. | field | type | description | example | | --- | --- | --- | --- | @@ -649,9 +649,10 @@ This webhook is currently triggered when a user adds their card to a digital wal | timestamp | string | time when the event was triggered in ISO 8601 format | 2025-08-12T18:47:33.687Z | | resource | "card" | | card | | action | "updated" | | updated | +| statusChangeReason? | "card_replaced" \| "wallet_provisioned" | reason for the card update | wallet_provisioned | | body.id | string | card identifier | e874583f-47d9-4211-8ea6-3b92e450821b | | body.last4 | string | last 4 digits of the card | 7392 | | body.limit.amount | number | spending limit amount | 1000000 | | body.limit.frequency | "per24HourPeriod" \| "per7DayPeriod" \| "per30DayPeriod" \| "perYearPeriod" | frequency of the spending limit | per7DayPeriod | | body.status | "ACTIVE" \| "FROZEN" \| "DELETED" \| "INACTIVE" | current status of the card | ACTIVE | -| body.tokenWallets | ["Apple"] \| ["Google Pay"] \| undefined | array of token wallets | ["Apple"] | +| body.tokenWallets? | ["Apple"] \| ["Google Pay"] | array of token wallets | ["Apple"] | diff --git a/server/test/hooks/panda.test.ts b/server/test/hooks/panda.test.ts index 337ec0ef9f..df8467e061 100644 --- a/server/test/hooks/panda.test.ts +++ b/server/test/hooks/panda.test.ts @@ -3389,7 +3389,14 @@ describe("webhooks", () => { it("enqueues card updated webhooks", async () => { const response = await appClient.index.$post({ ...cardUpdated, - json: { ...cardUpdated.json, body: { ...cardUpdated.json.body, tokenWallets: ["Apple"] } }, + json: { + ...cardUpdated.json, + body: { + ...cardUpdated.json.body, + tokenWallets: ["Apple"], + }, + statusChangeReason: "wallet_provisioned", + }, }); expect(response.status).toBe(200); diff --git a/server/test/workers/hook.test.ts b/server/test/workers/hook.test.ts index 28b2a0ecd3..15a6287180 100644 --- a/server/test/workers/hook.test.ts +++ b/server/test/workers/hook.test.ts @@ -333,6 +333,16 @@ describe("hook worker", () => { ]); }); + it("delivers card updates with provisioning details", async () => { + card("wk-card-provisioning", "active", "webhook-user", "wallet_provisioned"); + const mockFetch = vi.spyOn(globalThis, "fetch").mockImplementation(() => Promise.resolve(new Response("OK"))); + + await jobFinished("wk-card-provisioning"); + + const body = parse(string(), mockFetch.mock.calls[0]?.[1]?.body); + expect(JSON.parse(body)).toMatchObject({ statusChangeReason: "wallet_provisioned" }); + }); + it("delivers user updates with credential ids", async () => { user("wk-user", "webhook-user"); const mockFetch = vi.spyOn(globalThis, "fetch").mockImplementation(() => Promise.resolve(new Response("OK"))); @@ -730,7 +740,7 @@ function transaction(id: string, override: Record, action = "cr }); } -function card(id: string, status: string, userId = "webhook-user") { +function card(id: string, status: string, userId = "webhook-user", statusChangeReason?: string) { webhooks.set(id, { id, requestBody: { @@ -745,6 +755,7 @@ function card(id: string, status: string, userId = "webhook-user") { status, tokenWallets: ["Apple"], }, + ...(statusChangeReason !== undefined && { statusChangeReason }), }, requestSentAt: "2026-01-01T00:00:00.000Z", }); diff --git a/server/utils/panda.ts b/server/utils/panda.ts index e23eafa7c2..0ae516b96a 100644 --- a/server/utils/panda.ts +++ b/server/utils/panda.ts @@ -502,6 +502,7 @@ const Card = variant("action", [ id: string(), resource: literal("card"), action: literal("updated"), + statusChangeReason: optional(picklist(["card_replaced", "wallet_provisioned"])), body: object({ expirationMonth: pipe(string(), minLength(1), maxLength(2)), expirationYear: pipe(string(), length(4)), diff --git a/server/workers/hook/worker.ts b/server/workers/hook/worker.ts index 8358b52114..2f07711455 100644 --- a/server/workers/hook/worker.ts +++ b/server/workers/hook/worker.ts @@ -310,6 +310,7 @@ const Webhook = v.variant("resource", [ timestamp: v.pipe(v.string(), v.isoTimestamp()), resource: v.literal("card"), action: v.literal("updated"), + statusChangeReason: v.nullish(v.picklist(["card_replaced", "wallet_provisioned"])), body: v.object({ id: v.string(), last4: v.pipe(v.string(), v.length(4)),