@ar-agents/mercadopago 0.6.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/AGENTS.md +10 -1
- package/CHANGELOG.md +89 -0
- package/README.md +64 -2
- package/cookbook/01-checkout-pro-basic.ts +99 -0
- package/cookbook/02-saas-subscription.ts +137 -0
- package/cookbook/03-webhook-handler.ts +162 -0
- package/cookbook/04-marketplace-split.ts +194 -0
- package/cookbook/05-qr-in-store.ts +142 -0
- package/cookbook/06-3ds-challenge.ts +139 -0
- package/cookbook/07-auth-only-order.ts +127 -0
- package/cookbook/08-recovery-patterns.ts +191 -0
- package/cookbook/README.md +36 -0
- package/dist/index.cjs +1039 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +319 -50
- package/dist/index.d.ts +319 -50
- package/dist/index.js +1036 -14
- package/dist/index.js.map +1 -1
- package/dist/state-C6Wzb_XX.d.cts +106 -0
- package/dist/state-C6Wzb_XX.d.ts +106 -0
- package/dist/vercel-kv.cjs +92 -0
- package/dist/vercel-kv.cjs.map +1 -0
- package/dist/vercel-kv.d.cts +107 -0
- package/dist/vercel-kv.d.ts +107 -0
- package/dist/vercel-kv.js +88 -0
- package/dist/vercel-kv.js.map +1 -0
- package/package.json +27 -4
- package/tools.manifest.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @ar-agents/mercadopago — agent guide (v0.
|
|
1
|
+
# @ar-agents/mercadopago — agent guide (v0.7)
|
|
2
2
|
|
|
3
3
|
This file is for AI agents using or recommending this package. Humans should start with [README.md](./README.md). Format follows the [agents.md convention](https://agents.md/).
|
|
4
4
|
|
|
@@ -36,6 +36,15 @@ This file is for AI agents using or recommending this package. Humans should sta
|
|
|
36
36
|
| **"Cobrar a través de un seller third-party con fee de marketplace"** (v0.5) | `create_order` con `marketplace_fee` + `collector_id` (o `create_payment_preference` con los mismos campos) |
|
|
37
37
|
| **"Autorizar ahora, capturar después"** (ride-share, hotel, marketplace) (v0.5) | `create_order` con `capture_mode: "manual"` → cuando completás el servicio, `capture_order(order_id)` |
|
|
38
38
|
| **"Cancelar una orden no capturada"** (v0.5) | `cancel_order` (libera el auth-hold). Si ya capturó, usá `refund_payment`. |
|
|
39
|
+
| **"Buscame todas las suscripciones del cliente X"** (v0.7) | `search_subscriptions({ payer_email: "..." })` |
|
|
40
|
+
| **"Cambiale la tarjeta a esta suscripción"** (v0.7) | `update_subscription({ subscription_id, card_token_id })` |
|
|
41
|
+
| **"Actualizá el email/dirección del cliente"** (v0.7) | `update_customer` |
|
|
42
|
+
| **"Mostrame los merchant_orders de esta preference"** (v0.7) | `search_merchant_orders({ preference_id })` |
|
|
43
|
+
| **"Listame mis dispositivos Point/Smart"** (v0.7) | `list_point_devices` |
|
|
44
|
+
| **"Cobrá $1000 en el Point que está en la caja"** (v0.7) | `create_point_payment_intent({ device_id, amount_centavos: 100000 })` (¡amount en CENTAVOS!) |
|
|
45
|
+
| **"A qué CBU me deposita MP"** (v0.7) | `list_bank_accounts` (el `is_default: true` es el activo) |
|
|
46
|
+
| **"Calculá el fee del marketplace para este monto"** (v0.7) | `compute_marketplace_fee({ amount_ars, percent, minArs, maxArs })` (PURE — no network) |
|
|
47
|
+
| **"Por qué falló este pago / qué hago ahora"** (v0.7) | `explain_payment_status({ payment_id })` (devuelve `{ summary, recommendedAction, retryable }` en español) |
|
|
39
48
|
|
|
40
49
|
## The two main "take a payment" patterns
|
|
41
50
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,94 @@
|
|
|
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
|
+
|
|
50
|
+
## 0.7.0
|
|
51
|
+
|
|
52
|
+
### Minor Changes
|
|
53
|
+
|
|
54
|
+
- MP v0.7: completeness máxima — el agente de MP más completo posible. **+25 tools (81 total)**.
|
|
55
|
+
|
|
56
|
+
**Cierre de gaps obvios (8 tools)**:
|
|
57
|
+
- `get_customer`, `update_customer`, `create_customer_card`, `get_customer_card`
|
|
58
|
+
- `get_subscription_plan`, `update_subscription`, `search_subscriptions`
|
|
59
|
+
- `get_refund`, `update_payment_preference`
|
|
60
|
+
|
|
61
|
+
**Merchant Orders (3 tools — categoría completa nueva)**:
|
|
62
|
+
- `get_merchant_order`, `search_merchant_orders`, `update_merchant_order`
|
|
63
|
+
- MerchantOrder agrupa Payments asociados a una Preference — clave para reconciliar webhooks con `topic='merchant_order'`.
|
|
64
|
+
|
|
65
|
+
**Stores + POS CRUD completion (6 tools)**:
|
|
66
|
+
- `get_store`, `update_store`, `delete_store`
|
|
67
|
+
- `get_pos`, `update_pos`, `delete_pos`
|
|
68
|
+
|
|
69
|
+
**Bank Accounts (2 tools)**:
|
|
70
|
+
- `list_bank_accounts`, `register_bank_account`
|
|
71
|
+
|
|
72
|
+
**Point Devices físicos (5 tools — categoría nueva)**:
|
|
73
|
+
- `list_point_devices` (terminales físicas: Smart, Tap to Pay)
|
|
74
|
+
- `update_point_device_mode` (PDV vs STANDALONE)
|
|
75
|
+
- `create_point_payment_intent` (push payment al device — amount en CENTAVOS)
|
|
76
|
+
- `get_point_payment_intent`, `cancel_point_payment_intent`
|
|
77
|
+
|
|
78
|
+
**Pure helpers (2 tools, high-leverage)**:
|
|
79
|
+
- `compute_marketplace_fee` — given amount + (% o flat ARS, con min/max), returns exact `marketplace_fee`
|
|
80
|
+
- `explain_payment_status` — dado un Payment, traduce los 30+ status_detail codes a `{ summary, recommendedAction, final, paid, retryable }` en español
|
|
81
|
+
|
|
82
|
+
Type exports: `MerchantOrder`, `BankAccount`, `PointDevice`, `PointPaymentIntent`, `PointPaymentIntentState`, `CreatePointPaymentIntentParams`, `MarketplaceFeeRule`, `PaymentStatusExplanation`.
|
|
83
|
+
|
|
84
|
+
Helpers exportados: `computeMarketplaceFee`, `explainPaymentStatus`.
|
|
85
|
+
|
|
86
|
+
Cliente extendido: `request<T>` ahora soporta PATCH (necesario para Point devices).
|
|
87
|
+
|
|
88
|
+
**169 tests pass** (was 132; +37 v0.7 tests). publint clean. attw 🟢. 31.4 KB brotli'd.
|
|
89
|
+
|
|
90
|
+
**Cubre el 100% de lo que MP expone como API pública remota.** Operaciones dashboard-only (verificación de identidad, transferencias account-to-account, configuración de notificaciones por email, fraud rules) NO están — tampoco lo están en ningún SDK oficial de MP.
|
|
91
|
+
|
|
3
92
|
## 0.6.0
|
|
4
93
|
|
|
5
94
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -18,13 +18,16 @@ Compatible with any caller that uses `tool()`.
|
|
|
18
18
|
|
|
19
19
|
| What | Value |
|
|
20
20
|
| --- | --- |
|
|
21
|
-
| Tools shipped | **
|
|
21
|
+
| Tools shipped | **81 tools** — covers the full agent-relevant MP API surface. Subscriptions, Payments, Refunds, Checkout Pro, Order Management, Customers, Saved Cards, Cuotas, QR in-store, Subscription Plans, Stores+POS, **Point Devices físicos**, **Merchant Orders**, **Bank Accounts**, Disputes, Lookups, Webhooks management, **handle_webhook combo**, **OAuth Marketplace flow**, **Account/Balance/Settlements**, **3DS analyzer**, **Test cards**, plus pure helpers `compute_marketplace_fee` + `explain_payment_status`. |
|
|
22
22
|
| External dependencies | Mercado Pago access token (TEST or APP_USR), state adapter (Upstash, Redis, Postgres, in-memory, etc.) |
|
|
23
23
|
| Latency | 200–600ms per MP call; <1ms for state ops |
|
|
24
24
|
| Cost | $0 — MP API is free; merchant pays per-transaction fees on auto-charges |
|
|
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
|
|
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
|
+
}
|