@ar-agents/mercadopago 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes — Edge Runtime + Vercel KV + Cookbook
6
+
7
+ **Edge Runtime support (was: Node-only)**
8
+
9
+ - Replaced `node:crypto` with the universal Web Crypto API across all crypto helpers.
10
+ - The toolkit now runs in **Vercel Edge Runtime, Cloudflare Workers, Deno, browsers, and Node 18+** with zero changes.
11
+ - New module `./crypto` exposes `hmacSha256Hex`, `sha256Hex`, `timingSafeEqualHex`.
12
+
13
+ **Webhook signature verify is now async + replay-attack protected**
14
+
15
+ - `verifyWebhookSignature(...)` returns `Promise<boolean>` (was `boolean`). All call sites in `handle_webhook` tool already awaited.
16
+ - New default 5-minute replay window: signatures with `ts` more than `replayToleranceSeconds` (default 300) old are rejected as replay attempts.
17
+ - Override the window per-call with the new `replayToleranceSeconds` option.
18
+ - **Breaking**: callers using the exported `verifyWebhookSignature` directly need to add `await`.
19
+
20
+ **Vercel KV adapters via subpath `@ar-agents/mercadopago/vercel-kv`**
21
+
22
+ - `VercelKVSubscriptionStateAdapter` — drop-in `SubscriptionStateAdapter` backed by Vercel KV (Upstash Redis).
23
+ - `VercelKVOAuthTokenStore` — persists per-seller OAuth tokens for marketplace flows. Key namespace `mp:oauth:{userId}`.
24
+ - `VercelKVIdempotencyCache` — TTL-aware cache for short-circuiting agent retries.
25
+ - `@vercel/kv` is an **optional** peer dependency — only consumers who use the subpath install it. Main bundle untouched.
26
+ - All three adapters work in Edge Runtime.
27
+
28
+ **New state adapter interfaces in main package**
29
+
30
+ - `OAuthTokenStore` + `InMemoryOAuthTokenStore` — token bundle persistence for marketplace OAuth.
31
+ - `IdempotencyCache` + `InMemoryIdempotencyCache` — agent-retry deduplication layer on top of MP's server-side dedup.
32
+
33
+ **Cookbook (8 recipes)**
34
+
35
+ - `cookbook/01-checkout-pro-basic.ts` — first-time hosted checkout
36
+ - `cookbook/02-saas-subscription.ts` — reusable plan + first payment + card swap on rejection
37
+ - `cookbook/03-webhook-handler.ts` — production-grade Edge handler with HMAC verify
38
+ - `cookbook/04-marketplace-split.ts` — OAuth seller link → preference with fee → reconciliation
39
+ - `cookbook/05-qr-in-store.ts` — QR generation → buyer scan → WhatsApp notify
40
+ - `cookbook/06-3ds-challenge.ts` — detect → redirect → recover via webhook
41
+ - `cookbook/07-auth-only-order.ts` — Order with manual capture (ride-share / hotel pattern)
42
+ - `cookbook/08-recovery-patterns.ts` — recover stuck-pending, card-swap on rejected sub, idempotent upsert via search, cron-driven monitoring
43
+
44
+ **Quality**
45
+
46
+ - 185 tests pass (was 169; +16 for KV adapters + 2 for replay protection).
47
+ - publint clean, attw all 🟢 across both subpaths.
48
+ - Bundle: main 31.9 KB brotli'd; vercel-kv subpath 0.6 KB brotli'd.
49
+
3
50
  ## 0.7.0
4
51
 
5
52
  ### Minor Changes
package/README.md CHANGED
@@ -25,6 +25,9 @@ Compatible with any caller that uses `tool()`.
25
25
  | Side effects | `create_subscription` creates a preapproval. `cancel`/`pause`/`resume` mutate state. `get_status` is read-only. |
26
26
  | Agent safety | `cancel_subscription` description triggers confirm-before-call in Claude Sonnet 4.6+ |
27
27
  | Sites supported | MLA (Argentina) verified end-to-end. Other LATAM sites should work but aren't exercised by tests. |
28
+ | Runtime | **Edge Runtime + Node 18+** — Web Crypto under the hood, no `node:crypto`. Drops into Vercel Edge Functions, Cloudflare Workers, Deno deploy, or any modern Node. |
29
+ | Vercel-native | First-class adapters for **Vercel KV** (subscription state, OAuth tokens, idempotency cache) via `@ar-agents/mercadopago/vercel-kv` subpath. |
30
+ | Cookbook | 8 production-grade recipes shipped in `cookbook/` — checkout, subscriptions, webhook handler, marketplace OAuth, QR in-store, 3DS challenge, auth-only Order, recovery patterns. |
28
31
 
29
32
  ## Why this exists
30
33
 
@@ -361,11 +364,70 @@ and `mpResponse` for inspection. Specific subclasses:
361
364
  - `MercadoPagoAuthorizeForbiddenError` — see gotcha #6
362
365
  - `MercadoPagoRateLimitError` — 429 from MP
363
366
 
367
+ ## Vercel-native (v0.8+)
368
+
369
+ The toolkit ships first-class adapters for Vercel infrastructure via the
370
+ `@ar-agents/mercadopago/vercel-kv` subpath. `@vercel/kv` is an **optional**
371
+ peer dep — only install it if you use the subpath.
372
+
373
+ ```ts
374
+ import { mercadoPagoTools, MercadoPagoClient } from "@ar-agents/mercadopago";
375
+ import {
376
+ VercelKVSubscriptionStateAdapter,
377
+ VercelKVOAuthTokenStore,
378
+ VercelKVIdempotencyCache,
379
+ } from "@ar-agents/mercadopago/vercel-kv";
380
+
381
+ const tools = mercadoPagoTools(
382
+ new MercadoPagoClient({ accessToken: process.env.MP_ACCESS_TOKEN! }),
383
+ {
384
+ state: new VercelKVSubscriptionStateAdapter(),
385
+ backUrl: "https://yourapp.com/done",
386
+ webhookSecret: process.env.MP_WEBHOOK_SECRET,
387
+ oauth: {
388
+ clientId: process.env.MP_CLIENT_ID!,
389
+ clientSecret: process.env.MP_CLIENT_SECRET!,
390
+ },
391
+ },
392
+ );
393
+ ```
394
+
395
+ ### Edge Runtime
396
+
397
+ The toolkit (including HMAC webhook verification) is fully Edge-Runtime
398
+ compatible. Add `export const runtime = "edge"` to any Vercel route handler
399
+ that uses MP tools — sub-100ms global cold starts.
400
+
401
+ ### Vercel Cron + Blob + Functions
402
+
403
+ See `cookbook/08-recovery-patterns.ts` for a Vercel Cron Job example that
404
+ monitors stuck-pending payments. For label/invoice PDF storage, the
405
+ `crear_envio` tool (in `@ar-agents/shipping`) returns label URLs you can
406
+ mirror to [Vercel Blob](https://vercel.com/docs/storage/vercel-blob).
407
+
408
+ ## Cookbook
409
+
410
+ Production-grade recipes shipped in [`cookbook/`](./cookbook):
411
+
412
+ | Recipe | What it shows |
413
+ |---|---|
414
+ | `01-checkout-pro-basic.ts` | First-time hosted checkout sale via the agent |
415
+ | `02-saas-subscription.ts` | Reusable plan + first payment + card swap on rejection |
416
+ | `03-webhook-handler.ts` | Edge Runtime webhook handler with HMAC verify + dispatch |
417
+ | `04-marketplace-split.ts` | OAuth seller link + preference with `marketplace_fee` + reconciliation |
418
+ | `05-qr-in-store.ts` | QR generation → buyer scan → cashier WhatsApp notify |
419
+ | `06-3ds-challenge.ts` | Detect → redirect to challenge → recover via webhook |
420
+ | `07-auth-only-order.ts` | Order with `capture_mode: "manual"` (ride-share / hotel pattern) |
421
+ | `08-recovery-patterns.ts` | Card swap on subscription, stuck-pending recovery, idempotent upsert via search, Vercel Cron monitoring |
422
+
423
+ Each recipe is copy-pasteable into a Next.js route handler.
424
+
364
425
  ## Compatibility
365
426
 
366
- - Node.js 20+
427
+ - **Node.js 18+** (Web Crypto required) or **Vercel Edge Runtime** / **Cloudflare Workers** / **Deno**
367
428
  - Vercel AI SDK 6+
368
429
  - Zod 3+
430
+ - Optional: `@vercel/kv >=2` for the `vercel-kv` subpath
369
431
  - Pairs cleanly with [Vercel AI Gateway](https://vercel.com/ai-gateway) for model routing.
370
432
 
371
433
  ## License
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Recipe 01 — First-time Checkout Pro sale via an agent.
3
+ *
4
+ * # Pattern
5
+ *
6
+ * 1. Agent receives the user's intent ("comprar 3 unidades a $X")
7
+ * 2. Agent calls `create_payment_preference` to get a hosted checkout URL
8
+ * 3. Agent surfaces the `init_point_url` to the user (or sends via WhatsApp)
9
+ * 4. The buyer completes payment on MP's hosted form (no PCI scope for you)
10
+ * 5. MP fires a `payment` webhook to your endpoint (see recipe 03)
11
+ *
12
+ * # When to use
13
+ *
14
+ * - Single one-off purchase (not recurring → use recipe 02 for that)
15
+ * - You only have a payer email; no card token from MP frontend SDK
16
+ * - You want PCI-out-of-scope (buyer enters card data on MP's form)
17
+ *
18
+ * # Edge Runtime
19
+ *
20
+ * Fully Edge-compatible. Uncomment `export const runtime = "edge"` to deploy
21
+ * as a Vercel Edge Function for sub-100ms global cold starts.
22
+ */
23
+
24
+ import { Experimental_Agent as Agent, stepCountIs } from "ai";
25
+ import {
26
+ InMemoryStateAdapter,
27
+ MercadoPagoClient,
28
+ mercadoPagoTools,
29
+ } from "@ar-agents/mercadopago";
30
+
31
+ // export const runtime = "edge"; // Uncomment for Edge deployment
32
+
33
+ const mp = new MercadoPagoClient({
34
+ accessToken: process.env.MP_ACCESS_TOKEN!,
35
+ // Production robustez defaults: 30s timeout, 1 retry on 5xx, exponential backoff
36
+ requestTimeoutMs: 30_000,
37
+ maxRetries: 1,
38
+ });
39
+
40
+ const agent = new Agent({
41
+ model: "anthropic/claude-sonnet-4-6",
42
+ instructions: `Sos el asistente de checkout de un e-commerce argentino.
43
+ Cuando el cliente quiere comprar:
44
+ 1. Confirmá el monto y la descripción del producto.
45
+ 2. Llamá a create_payment_preference con back_urls de éxito/error.
46
+ 3. Devolvele el init_point_url (Checkout Pro) al cliente.
47
+ 4. NO pidas datos de tarjeta — los cargan en MP.`,
48
+ tools: mercadoPagoTools(mp, {
49
+ state: new InMemoryStateAdapter(),
50
+ backUrl: "https://yourapp.com/payment-result",
51
+ notificationUrl: "https://yourapp.com/api/mp/webhook",
52
+ }),
53
+ stopWhen: stepCountIs(5),
54
+ });
55
+
56
+ // In a Next.js route handler:
57
+ export async function POST(req: Request) {
58
+ const { prompt } = (await req.json()) as { prompt: string };
59
+ const result = await agent.generate({ prompt });
60
+ return Response.json({ text: result.text });
61
+ }
62
+
63
+ // ─────────────────────────────────────────────────────────────────────────────
64
+ // Manual / non-agent path (for direct API use)
65
+ // ─────────────────────────────────────────────────────────────────────────────
66
+
67
+ export async function createCheckoutPreference(input: {
68
+ customerEmail: string;
69
+ productTitle: string;
70
+ unitPriceArs: number;
71
+ quantity: number;
72
+ externalReference: string;
73
+ }) {
74
+ const preference = await mp.createPreference({
75
+ items: [
76
+ {
77
+ title: input.productTitle,
78
+ quantity: input.quantity,
79
+ unit_price: input.unitPriceArs,
80
+ currency_id: "ARS",
81
+ },
82
+ ],
83
+ payer: { email: input.customerEmail },
84
+ backUrls: {
85
+ success: "https://yourapp.com/payment-success",
86
+ failure: "https://yourapp.com/payment-failure",
87
+ pending: "https://yourapp.com/payment-pending",
88
+ },
89
+ autoReturn: "approved",
90
+ externalReference: input.externalReference,
91
+ notificationUrl: "https://yourapp.com/api/mp/webhook",
92
+ });
93
+
94
+ return {
95
+ preferenceId: preference.id,
96
+ initPoint: preference.init_point,
97
+ sandboxInitPoint: preference.sandbox_init_point, // Use this in TEST mode
98
+ };
99
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Recipe 02 — SaaS subscription with reusable plan + first payment + card swap.
3
+ *
4
+ * # Pattern
5
+ *
6
+ * **One-time setup**: create a `Plan` (price + frequency) — re-use across customers.
7
+ *
8
+ * **Per-customer**:
9
+ * 1. `subscribe_to_plan` → returns init_point for first-payment authorization
10
+ * 2. Buyer pays first installment with card+CVV (MP requirement, can't bypass)
11
+ * 3. `subscription_preapproval` webhook fires → status flips to `authorized`
12
+ * 4. MP auto-charges at the configured frequency thereafter
13
+ *
14
+ * **Card swap on failure** (when buyer's card expires):
15
+ * - You receive a `subscription_authorized_payment` webhook with rejection
16
+ * - Generate a fresh card token via MP frontend SDK on the buyer's side
17
+ * - Call `update_subscription({ card_token_id })` to swap without recreating
18
+ *
19
+ * # When to use
20
+ *
21
+ * - Monthly/quarterly SaaS billing (Básico/Pro/Enterprise tiers)
22
+ * - You want one Plan definition shared across all subscribers
23
+ * - You need the option to update price for new subscribers without
24
+ * touching existing ones
25
+ */
26
+
27
+ import {
28
+ InMemoryStateAdapter,
29
+ MercadoPagoClient,
30
+ } from "@ar-agents/mercadopago";
31
+
32
+ const mp = new MercadoPagoClient({
33
+ accessToken: process.env.MP_ACCESS_TOKEN!,
34
+ });
35
+
36
+ const state = new InMemoryStateAdapter();
37
+ // In production, swap for VercelKVSubscriptionStateAdapter:
38
+ // import { VercelKVSubscriptionStateAdapter } from "@ar-agents/mercadopago/vercel-kv";
39
+ // const state = new VercelKVSubscriptionStateAdapter();
40
+
41
+ // ─────────────────────────────────────────────────────────────────────────────
42
+ // Step 1 — One-time setup: create the plan
43
+ // ─────────────────────────────────────────────────────────────────────────────
44
+
45
+ export async function createPlanProMonthly() {
46
+ const plan = await mp.createSubscriptionPlan({
47
+ reason: "Plan Pro mensual",
48
+ backUrl: "https://yourapp.com/subscription-result",
49
+ frequency: 1,
50
+ frequencyType: "months",
51
+ amount: 25_000,
52
+ currency: "ARS",
53
+ });
54
+ return plan; // persist plan.id in your DB
55
+ }
56
+
57
+ // ─────────────────────────────────────────────────────────────────────────────
58
+ // Step 2 — Per-customer: subscribe to the plan
59
+ // ─────────────────────────────────────────────────────────────────────────────
60
+
61
+ export async function subscribeUserToProPlan(input: {
62
+ planId: string;
63
+ customerEmail: string;
64
+ externalReference: string; // your-system user id
65
+ }) {
66
+ const sub = await mp.subscribeToPlan({
67
+ planId: input.planId,
68
+ payerEmail: input.customerEmail,
69
+ externalReference: input.externalReference,
70
+ });
71
+
72
+ // Persist locally for fast lookups + webhook routing
73
+ await state.set(sub.id, {
74
+ payerEmail: input.customerEmail,
75
+ initPoint: sub.init_point,
76
+ externalReference: input.externalReference,
77
+ createdAt: new Date().toISOString(),
78
+ status: sub.status,
79
+ });
80
+
81
+ return {
82
+ subscriptionId: sub.id,
83
+ initPoint: sub.init_point,
84
+ nextStep:
85
+ "Send init_point to the customer. They must complete the first payment with card+CVV. Listen for subscription_preapproval webhook to confirm activation.",
86
+ };
87
+ }
88
+
89
+ // ─────────────────────────────────────────────────────────────────────────────
90
+ // Step 3 — Webhook: subscription_preapproval activation
91
+ // ─────────────────────────────────────────────────────────────────────────────
92
+
93
+ export async function handlePreapprovalWebhook(subscriptionId: string) {
94
+ const sub = await mp.getPreapproval(subscriptionId);
95
+ await state.set(sub.id, {
96
+ status: sub.status,
97
+ lastWebhookStatus: sub.status,
98
+ lastWebhookAt: new Date().toISOString(),
99
+ });
100
+ if (sub.status === "authorized") {
101
+ // First payment cleared — provision the user's plan in your DB
102
+ // await db.users.update({ where: { externalReference: sub.external_reference }, data: { plan: "pro", status: "active" } });
103
+ }
104
+ return sub;
105
+ }
106
+
107
+ // ─────────────────────────────────────────────────────────────────────────────
108
+ // Step 4 — Card swap (when buyer's card is rejected on a recurring charge)
109
+ // ─────────────────────────────────────────────────────────────────────────────
110
+
111
+ export async function swapCardOnSubscription(input: {
112
+ subscriptionId: string;
113
+ newCardToken: string; // from MP frontend SDK / Cardform on buyer's side
114
+ }) {
115
+ const sub = await mp.updatePreapproval(input.subscriptionId, {
116
+ card_token_id: input.newCardToken,
117
+ });
118
+ await state.set(sub.id, {
119
+ status: sub.status,
120
+ lastWebhookStatus: "card_swapped",
121
+ lastWebhookAt: new Date().toISOString(),
122
+ });
123
+ return sub;
124
+ }
125
+
126
+ // ─────────────────────────────────────────────────────────────────────────────
127
+ // Step 5 — Cancel
128
+ // ─────────────────────────────────────────────────────────────────────────────
129
+
130
+ export async function cancelSubscription(subscriptionId: string) {
131
+ const sub = await mp.cancelPreapproval(subscriptionId);
132
+ await state.set(sub.id, {
133
+ status: sub.status,
134
+ cancelledAt: new Date().toISOString(),
135
+ });
136
+ return sub;
137
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Recipe 03 — Production-grade webhook handler.
3
+ *
4
+ * # The 3-line summary
5
+ *
6
+ * - Verify HMAC-SHA256 signature → reject with 401 if invalid (replay protection included)
7
+ * - Parse the event (topic + dataId) from query/body (MP sends in both)
8
+ * - Auto-fetch the underlying resource (Payment / Preapproval / Order)
9
+ * - Dispatch by topic to your business logic
10
+ *
11
+ * Without HMAC verify, ANYONE can POST to your webhook URL and forge
12
+ * payments/cancellations. The lib's `verifyWebhookSignature` rejects
13
+ * stale signatures (>5min old) too — replay attack protection.
14
+ *
15
+ * # Why use the agent's `handle_webhook` tool vs calling primitives manually
16
+ *
17
+ * The tool consolidates verify + parse + auto-fetch + dispatch into one
18
+ * call. Saves ~30 lines per webhook handler vs the manual chain.
19
+ *
20
+ * # Edge Runtime
21
+ *
22
+ * This recipe is fully Edge-compatible. Webhook handlers benefit from Edge
23
+ * (lower cold-start = faster MP-acked, fewer 500s during traffic spikes).
24
+ */
25
+
26
+ import {
27
+ MercadoPagoClient,
28
+ parseWebhookEvent,
29
+ verifyWebhookSignature,
30
+ } from "@ar-agents/mercadopago";
31
+
32
+ export const runtime = "edge";
33
+
34
+ const mp = new MercadoPagoClient({
35
+ accessToken: process.env.MP_ACCESS_TOKEN!,
36
+ });
37
+
38
+ const WEBHOOK_SECRET = process.env.MP_WEBHOOK_SECRET!;
39
+
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+ // Approach A — Manual primitives (more control, more code)
42
+ // ─────────────────────────────────────────────────────────────────────────────
43
+
44
+ export async function POST(req: Request) {
45
+ // 1. Read the RAW body — DO NOT use req.json() before HMAC verify, as
46
+ // JSON.stringify changes whitespace and breaks the signature.
47
+ const rawBody = await req.text();
48
+
49
+ const signatureHeader = req.headers.get("x-signature");
50
+ const requestId = req.headers.get("x-request-id");
51
+ const url = new URL(req.url);
52
+
53
+ // 2. Parse the event from body or query (MP sends in both).
54
+ let parsedBody: unknown;
55
+ try {
56
+ parsedBody = JSON.parse(rawBody);
57
+ } catch {
58
+ return new Response("invalid JSON", { status: 400 });
59
+ }
60
+ const event = parseWebhookEvent(parsedBody, url.searchParams);
61
+ if (!event) {
62
+ return new Response("unrecognized webhook shape", { status: 400 });
63
+ }
64
+
65
+ // 3. Verify HMAC + replay-tolerance window.
66
+ const verified = await verifyWebhookSignature({
67
+ requestId,
68
+ dataId: event.dataId,
69
+ signatureHeader,
70
+ secret: WEBHOOK_SECRET,
71
+ });
72
+ if (!verified) {
73
+ return new Response("unauthorized", { status: 401 });
74
+ }
75
+
76
+ // 4. Dispatch by topic.
77
+ try {
78
+ switch (event.topic) {
79
+ case "payment":
80
+ case "payment.created":
81
+ case "payment.updated": {
82
+ const payment = await mp.getPayment(event.dataId);
83
+ await handlePayment(payment);
84
+ break;
85
+ }
86
+ case "subscription_preapproval":
87
+ case "preapproval": {
88
+ const sub = await mp.getPreapproval(event.dataId);
89
+ await handleSubscription(sub);
90
+ break;
91
+ }
92
+ case "subscription_authorized_payment": {
93
+ // The dataId IS the authorized_payment id — list under parent
94
+ // preapproval to get full context.
95
+ await handleRecurringCharge(event.dataId);
96
+ break;
97
+ }
98
+ case "merchant_order": {
99
+ const mo = await mp.getMerchantOrder(event.dataId);
100
+ await handleMerchantOrder(mo);
101
+ break;
102
+ }
103
+ case "point_integration_wh": {
104
+ const intent = await mp.getPointPaymentIntent(event.dataId);
105
+ await handlePointPaymentIntent(intent);
106
+ break;
107
+ }
108
+ default:
109
+ // Unknown topic — log and acknowledge so MP doesn't retry forever.
110
+ console.warn(`Unhandled webhook topic: ${event.topic}`);
111
+ }
112
+ } catch (err) {
113
+ // Return 5xx so MP retries (it has built-in exponential backoff).
114
+ console.error("webhook handler failed:", err);
115
+ return new Response("internal error", { status: 500 });
116
+ }
117
+
118
+ return new Response("ok", { status: 200 });
119
+ }
120
+
121
+ // ─────────────────────────────────────────────────────────────────────────────
122
+ // Approach B — Agent tool (let the agent dispatch)
123
+ // ─────────────────────────────────────────────────────────────────────────────
124
+
125
+ /**
126
+ * Alternative: pass everything to an agent + the `handle_webhook` tool.
127
+ * Useful when your business logic varies by webhook content and an LLM
128
+ * makes the decision (e.g., "if this payment is for an old SKU, refund it").
129
+ *
130
+ * Note: this is HIGHER LATENCY than approach A and uses LLM tokens. Only
131
+ * use when LLM reasoning is genuinely required.
132
+ */
133
+ // export async function POST_via_agent(req: Request) {
134
+ // const rawBody = await req.text();
135
+ // const result = await agent.generate({
136
+ // prompt: `Procesá este webhook de MP. Topic + body:\n${rawBody}`,
137
+ // toolChoice: "required",
138
+ // tools: mercadoPagoTools(mp, {
139
+ // state, backUrl, webhookSecret: WEBHOOK_SECRET, oauth: {...}
140
+ // }),
141
+ // });
142
+ // }
143
+
144
+ // ─────────────────────────────────────────────────────────────────────────────
145
+ // Business logic stubs
146
+ // ─────────────────────────────────────────────────────────────────────────────
147
+
148
+ async function handlePayment(payment: unknown) {
149
+ // Update your DB, fire shipping flow, send notification, etc.
150
+ }
151
+ async function handleSubscription(sub: unknown) {
152
+ /* ... */
153
+ }
154
+ async function handleRecurringCharge(authorizedPaymentId: string) {
155
+ /* ... */
156
+ }
157
+ async function handleMerchantOrder(mo: unknown) {
158
+ /* ... */
159
+ }
160
+ async function handlePointPaymentIntent(intent: unknown) {
161
+ /* ... */
162
+ }