@ar-agents/mercadopago 0.15.3 → 0.17.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 +58 -0
- package/README.md +9 -0
- package/bin/mercadopago.js +13 -0
- package/cookbook/10-cross-package-billing.ts +172 -0
- package/cookbook/11-dunning-sequence.ts +305 -0
- package/cookbook/12-reconciliation-pipeline.ts +277 -0
- package/cookbook/README.md +3 -0
- package/dist/cli.cjs +341 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +15 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +338 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.cts +4 -1084
- package/dist/index.d.ts +4 -1084
- package/dist/testing.cjs +281 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +188 -0
- package/dist/testing.d.ts +188 -0
- package/dist/testing.js +270 -0
- package/dist/testing.js.map +1 -0
- package/dist/types-BaOjfcOt.d.cts +1085 -0
- package/dist/types-BaOjfcOt.d.ts +1085 -0
- package/package.json +18 -1
- package/tools.manifest.json +1 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipe 12 — Reconciliation pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Daily batch job that compares MP's settlement records against your internal
|
|
5
|
+
* billing DB and surfaces discrepancies. This is what every finance team
|
|
6
|
+
* eventually asks the dev team to build, and there's no good off-the-shelf
|
|
7
|
+
* tool for AR.
|
|
8
|
+
*
|
|
9
|
+
* # The four discrepancy classes
|
|
10
|
+
*
|
|
11
|
+
* 1. **In MP, not in our DB**: an MP payment exists with no matching
|
|
12
|
+
* internal invoice. Usually means a webhook was missed.
|
|
13
|
+
* → Action: backfill from MP into our DB.
|
|
14
|
+
*
|
|
15
|
+
* 2. **In our DB, not in MP**: we have an invoice marked paid, but no
|
|
16
|
+
* matching MP payment. Usually means we marked something paid
|
|
17
|
+
* manually or had a bug.
|
|
18
|
+
* → Action: flag for human review.
|
|
19
|
+
*
|
|
20
|
+
* 3. **Amount mismatch**: amounts differ between MP and our DB. Could be
|
|
21
|
+
* partial refund, dispute, currency conversion.
|
|
22
|
+
* → Action: classify (refund? dispute?) and update DB.
|
|
23
|
+
*
|
|
24
|
+
* 4. **Fee mismatch**: MP's reported `marketplace_fee` differs from what
|
|
25
|
+
* we calculated. Means our pricing logic is out of sync with the
|
|
26
|
+
* actual MP-side `application_fee`.
|
|
27
|
+
* → Action: recompute and surface for billing audit.
|
|
28
|
+
*
|
|
29
|
+
* # What this recipe shows
|
|
30
|
+
*
|
|
31
|
+
* - Pagination via `paginatePayments` AsyncIterable (Edge-Runtime safe).
|
|
32
|
+
* - Settlement-level reconciliation via `paginateSettlements`.
|
|
33
|
+
* - Composition with the marketplace fee calculator (`computeMarketplaceFee`).
|
|
34
|
+
* - A human-readable "discrepancy report" the agent can email the finance
|
|
35
|
+
* team via @ar-agents/whatsapp or Resend.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
MercadoPagoClient,
|
|
40
|
+
paginatePayments,
|
|
41
|
+
paginateSettlements,
|
|
42
|
+
computeMarketplaceFee,
|
|
43
|
+
type Payment,
|
|
44
|
+
type Settlement,
|
|
45
|
+
} from "@ar-agents/mercadopago";
|
|
46
|
+
|
|
47
|
+
const mp = new MercadoPagoClient({
|
|
48
|
+
accessToken: process.env.MP_ACCESS_TOKEN!,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
// Types — what your internal DB looks like (replace with your schema)
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
type InternalInvoice = {
|
|
56
|
+
externalReference: string; // matches MP's external_reference
|
|
57
|
+
amount: number; // in ARS
|
|
58
|
+
status: "pending" | "paid" | "refunded";
|
|
59
|
+
mpPaymentId?: string;
|
|
60
|
+
expectedFee?: number; // what we expect MP to charge us
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type Discrepancy =
|
|
64
|
+
| { type: "missing_in_db"; mpPayment: Payment }
|
|
65
|
+
| { type: "missing_in_mp"; invoice: InternalInvoice }
|
|
66
|
+
| { type: "amount_mismatch"; mpPayment: Payment; invoice: InternalInvoice }
|
|
67
|
+
| { type: "fee_mismatch"; mpPayment: Payment; expectedFee: number };
|
|
68
|
+
|
|
69
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
70
|
+
// Reconciliation entry point
|
|
71
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
export async function reconcileDay(args: {
|
|
74
|
+
date: Date;
|
|
75
|
+
fetchInternalInvoices: (
|
|
76
|
+
rangeStart: Date,
|
|
77
|
+
rangeEnd: Date,
|
|
78
|
+
) => Promise<InternalInvoice[]>;
|
|
79
|
+
marketplaceFeePct?: number; // e.g. 5 for a 5% platform fee
|
|
80
|
+
}): Promise<{
|
|
81
|
+
matched: number;
|
|
82
|
+
discrepancies: Discrepancy[];
|
|
83
|
+
totals: { mpGross: number; mpNet: number; mpFees: number };
|
|
84
|
+
}> {
|
|
85
|
+
const dayStart = new Date(args.date);
|
|
86
|
+
dayStart.setHours(0, 0, 0, 0);
|
|
87
|
+
const dayEnd = new Date(dayStart.getTime() + 24 * 60 * 60 * 1000);
|
|
88
|
+
|
|
89
|
+
// 1. Pull all approved payments from MP for the day. paginatePayments is
|
|
90
|
+
// an AsyncIterable<Payment> wrapped over MP's search endpoint, with
|
|
91
|
+
// bounded memory regardless of window size.
|
|
92
|
+
const mpPayments: Payment[] = [];
|
|
93
|
+
for await (const p of paginatePayments(mp, {
|
|
94
|
+
status: "approved",
|
|
95
|
+
beginDate: dayStart.toISOString(),
|
|
96
|
+
endDate: dayEnd.toISOString(),
|
|
97
|
+
})) {
|
|
98
|
+
mpPayments.push(p);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2. Pull internal invoices for the same window.
|
|
102
|
+
const invoices = await args.fetchInternalInvoices(dayStart, dayEnd);
|
|
103
|
+
|
|
104
|
+
// 3. Index by external_reference.
|
|
105
|
+
const byRef = new Map<string, InternalInvoice>();
|
|
106
|
+
for (const inv of invoices) byRef.set(inv.externalReference, inv);
|
|
107
|
+
|
|
108
|
+
const discrepancies: Discrepancy[] = [];
|
|
109
|
+
let matched = 0;
|
|
110
|
+
|
|
111
|
+
// 4. Walk MP payments, match against internal.
|
|
112
|
+
for (const p of mpPayments) {
|
|
113
|
+
const ref = p.external_reference;
|
|
114
|
+
if (!ref) continue; // payments without external_reference can't be matched
|
|
115
|
+
const inv = byRef.get(ref);
|
|
116
|
+
|
|
117
|
+
if (!inv) {
|
|
118
|
+
discrepancies.push({ type: "missing_in_db", mpPayment: p });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check 4a: amount.
|
|
123
|
+
if (Math.abs(p.transaction_amount - inv.amount) > 0.01) {
|
|
124
|
+
discrepancies.push({
|
|
125
|
+
type: "amount_mismatch",
|
|
126
|
+
mpPayment: p,
|
|
127
|
+
invoice: inv,
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check 4b: fee, if marketplace.
|
|
133
|
+
const actualFee = (p as Payment & { marketplace_fee?: number }).marketplace_fee;
|
|
134
|
+
if (args.marketplaceFeePct != null && actualFee != null) {
|
|
135
|
+
const expected = computeMarketplaceFee(p.transaction_amount, {
|
|
136
|
+
percent: args.marketplaceFeePct,
|
|
137
|
+
});
|
|
138
|
+
if (Math.abs(actualFee - expected) > 0.01) {
|
|
139
|
+
discrepancies.push({
|
|
140
|
+
type: "fee_mismatch",
|
|
141
|
+
mpPayment: p,
|
|
142
|
+
expectedFee: expected,
|
|
143
|
+
});
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
matched += 1;
|
|
149
|
+
byRef.delete(ref); // remove so we can detect leftover (4c) below
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 4c: any invoice left in byRef has no matching MP payment.
|
|
153
|
+
for (const inv of byRef.values()) {
|
|
154
|
+
if (inv.status !== "paid") continue; // unpaid invoices wouldn't have an MP payment yet
|
|
155
|
+
discrepancies.push({ type: "missing_in_mp", invoice: inv });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 5. Aggregate totals from MP-side settlements (the truth source for
|
|
159
|
+
// what was actually credited to your account today). The Settlement
|
|
160
|
+
// schema only exposes `amount` (the credited net total per settlement);
|
|
161
|
+
// gross/fee breakdowns live on individual payments. We sum payments
|
|
162
|
+
// above for gross + fees, and settlements here for the credited-net
|
|
163
|
+
// cross-check.
|
|
164
|
+
let mpGross = 0;
|
|
165
|
+
let mpFees = 0;
|
|
166
|
+
for (const p of mpPayments) {
|
|
167
|
+
mpGross += p.transaction_amount ?? 0;
|
|
168
|
+
const fee = (p as Payment & { marketplace_fee?: number }).marketplace_fee;
|
|
169
|
+
if (fee) mpFees += fee;
|
|
170
|
+
}
|
|
171
|
+
let mpNet = 0;
|
|
172
|
+
for await (const s of paginateSettlements(mp, {
|
|
173
|
+
date_from: dayStart.toISOString(),
|
|
174
|
+
date_to: dayEnd.toISOString(),
|
|
175
|
+
} as Parameters<typeof paginateSettlements>[1])) {
|
|
176
|
+
mpNet += (s as Settlement).amount ?? 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
matched,
|
|
181
|
+
discrepancies,
|
|
182
|
+
totals: { mpGross, mpNet, mpFees },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
187
|
+
// Discrepancy report formatter
|
|
188
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
export function formatDiscrepancyReport(input: {
|
|
191
|
+
date: Date;
|
|
192
|
+
matched: number;
|
|
193
|
+
discrepancies: Discrepancy[];
|
|
194
|
+
totals: { mpGross: number; mpNet: number; mpFees: number };
|
|
195
|
+
}): string {
|
|
196
|
+
const lines: string[] = [];
|
|
197
|
+
lines.push(`# Reconciliation report — ${input.date.toISOString().slice(0, 10)}`);
|
|
198
|
+
lines.push("");
|
|
199
|
+
lines.push(`**Matched payments:** ${input.matched}`);
|
|
200
|
+
lines.push(`**Discrepancies:** ${input.discrepancies.length}`);
|
|
201
|
+
lines.push("");
|
|
202
|
+
lines.push(`**MP totals:**`);
|
|
203
|
+
lines.push(` - Gross: $${input.totals.mpGross.toLocaleString("es-AR")}`);
|
|
204
|
+
lines.push(` - Fees: $${input.totals.mpFees.toLocaleString("es-AR")}`);
|
|
205
|
+
lines.push(` - Net: $${input.totals.mpNet.toLocaleString("es-AR")}`);
|
|
206
|
+
lines.push("");
|
|
207
|
+
|
|
208
|
+
if (input.discrepancies.length === 0) {
|
|
209
|
+
lines.push("✓ All clean.");
|
|
210
|
+
return lines.join("\n");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
lines.push("## Discrepancies");
|
|
214
|
+
lines.push("");
|
|
215
|
+
for (const d of input.discrepancies) {
|
|
216
|
+
switch (d.type) {
|
|
217
|
+
case "missing_in_db":
|
|
218
|
+
lines.push(
|
|
219
|
+
`- **Missing in DB**: MP payment ${d.mpPayment.id} (ref=${d.mpPayment.external_reference}, $${d.mpPayment.transaction_amount}). Action: backfill.`,
|
|
220
|
+
);
|
|
221
|
+
break;
|
|
222
|
+
case "missing_in_mp":
|
|
223
|
+
lines.push(
|
|
224
|
+
`- **Missing in MP**: internal invoice ${d.invoice.externalReference} marked paid but no MP record. Action: human review.`,
|
|
225
|
+
);
|
|
226
|
+
break;
|
|
227
|
+
case "amount_mismatch":
|
|
228
|
+
lines.push(
|
|
229
|
+
`- **Amount mismatch**: ref=${d.invoice.externalReference}: DB says $${d.invoice.amount}, MP says $${d.mpPayment.transaction_amount}. Action: classify (partial refund? dispute?).`,
|
|
230
|
+
);
|
|
231
|
+
break;
|
|
232
|
+
case "fee_mismatch":
|
|
233
|
+
lines.push(
|
|
234
|
+
`- **Fee mismatch**: payment ${d.mpPayment.id}: expected $${d.expectedFee}, MP charged $${d.mpPayment.marketplace_fee}. Action: audit pricing logic.`,
|
|
235
|
+
);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return lines.join("\n");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
244
|
+
// Cron entry point
|
|
245
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Add to vercel.json:
|
|
249
|
+
*
|
|
250
|
+
* {
|
|
251
|
+
* "crons": [
|
|
252
|
+
* { "path": "/api/cron/reconcile", "schedule": "0 6 * * *" }
|
|
253
|
+
* ]
|
|
254
|
+
* }
|
|
255
|
+
*
|
|
256
|
+
* Runs at 06:00 every day, reconciles the previous day.
|
|
257
|
+
*/
|
|
258
|
+
export async function GET() {
|
|
259
|
+
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
260
|
+
const result = await reconcileDay({
|
|
261
|
+
date: yesterday,
|
|
262
|
+
fetchInternalInvoices: async (start, end) => {
|
|
263
|
+
// Replace with your DB query.
|
|
264
|
+
return [];
|
|
265
|
+
},
|
|
266
|
+
marketplaceFeePct: 0.05,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const report = formatDiscrepancyReport({ date: yesterday, ...result });
|
|
270
|
+
// Send via Resend, Slack, Email, or your channel of choice.
|
|
271
|
+
console.log(report);
|
|
272
|
+
|
|
273
|
+
return new Response(report, {
|
|
274
|
+
status: 200,
|
|
275
|
+
headers: { "content-type": "text/plain" },
|
|
276
|
+
});
|
|
277
|
+
}
|
package/cookbook/README.md
CHANGED
|
@@ -17,6 +17,9 @@ deploy on Vercel as-is.
|
|
|
17
17
|
| 07 | `07-auth-only-order.ts` | `Order` with manual capture → capture later when service completes |
|
|
18
18
|
| 08 | `08-recovery-patterns.ts` | Retry expired subscriptions, recover stuck-pending payments, etc. |
|
|
19
19
|
| 09 | `09-otel-wired.ts` | Full OpenTelemetry wiring — spans + metrics for every MP call + tool |
|
|
20
|
+
| 10 | `10-cross-package-billing.ts` | One agent loop, 5 packages: identity + attest + MP + facturacion + WhatsApp |
|
|
21
|
+
| 11 | `11-dunning-sequence.ts` | Failed-payment recovery loop with multi-step dunning + cancel-with-retention |
|
|
22
|
+
| 12 | `12-reconciliation-pipeline.ts` | Daily MP↔internal-DB reconciliation cron with discrepancy report |
|
|
20
23
|
|
|
21
24
|
## Conventions
|
|
22
25
|
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
4
|
+
// src/cli-doctor.ts
|
|
5
|
+
var C = {
|
|
6
|
+
reset: "\x1B[0m",
|
|
7
|
+
dim: "\x1B[2m",
|
|
8
|
+
red: "\x1B[31m",
|
|
9
|
+
green: "\x1B[32m",
|
|
10
|
+
yellow: "\x1B[33m",
|
|
11
|
+
blue: "\x1B[34m",
|
|
12
|
+
magenta: "\x1B[35m",
|
|
13
|
+
cyan: "\x1B[36m",
|
|
14
|
+
bold: "\x1B[1m"
|
|
15
|
+
};
|
|
16
|
+
var NO_COLOR = process.env.NO_COLOR != null || !process.stdout.isTTY;
|
|
17
|
+
var c = (color, s) => NO_COLOR ? s : `${C[color]}${s}${C.reset}`;
|
|
18
|
+
function fmt(r) {
|
|
19
|
+
const icon = r.kind === "ok" ? c("green", "\u2713") : r.kind === "warn" ? c("yellow", "\u26A0") : c("red", "\u2717");
|
|
20
|
+
const detail = r.detail ? `
|
|
21
|
+
${c("dim", "\u2192 " + r.detail)}` : "";
|
|
22
|
+
return `${icon} ${r.line}${detail}`;
|
|
23
|
+
}
|
|
24
|
+
function checkNode() {
|
|
25
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
26
|
+
if (major >= 20) {
|
|
27
|
+
return {
|
|
28
|
+
kind: "ok",
|
|
29
|
+
line: `Node ${process.versions.node} (>= 20.0.0)`
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
kind: "fail",
|
|
34
|
+
line: `Node ${process.versions.node} too old`,
|
|
35
|
+
detail: `@ar-agents/mercadopago requires Node 20+. Bun and Edge Runtime are also supported.`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function checkAccessToken() {
|
|
39
|
+
const t = process.env.MP_ACCESS_TOKEN?.trim();
|
|
40
|
+
if (!t) {
|
|
41
|
+
return {
|
|
42
|
+
kind: "fail",
|
|
43
|
+
line: "MP_ACCESS_TOKEN not set",
|
|
44
|
+
detail: "Get one at https://www.mercadopago.com.ar/developers/panel/app \u2014 TEST- prefix for sandbox, APP_USR- for production."
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (t.startsWith("TEST-")) {
|
|
48
|
+
return { kind: "ok", line: `MP_ACCESS_TOKEN set (TEST- prefix \u2192 sandbox)` };
|
|
49
|
+
}
|
|
50
|
+
if (t.startsWith("APP_USR-")) {
|
|
51
|
+
return {
|
|
52
|
+
kind: "warn",
|
|
53
|
+
line: `MP_ACCESS_TOKEN set (APP_USR- prefix \u2192 PRODUCTION)`,
|
|
54
|
+
detail: "Live transactions WILL move real money. Use TEST- in development."
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
kind: "fail",
|
|
59
|
+
line: "MP_ACCESS_TOKEN has unexpected prefix",
|
|
60
|
+
detail: `Expected TEST- or APP_USR-, got "${t.slice(0, 8)}\u2026". Common cause: trailing newline in env file (use printf, not echo).`
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function probeToken(token) {
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch("https://api.mercadopago.com/users/me", {
|
|
66
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
67
|
+
});
|
|
68
|
+
if (res.status === 401) {
|
|
69
|
+
return {
|
|
70
|
+
kind: "fail",
|
|
71
|
+
line: "Token rejected by MP API (401 Unauthorized)",
|
|
72
|
+
detail: "Token is invalid, expired, or copy-pasted with extra whitespace."
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
return {
|
|
77
|
+
kind: "fail",
|
|
78
|
+
line: `MP API responded with HTTP ${res.status}`,
|
|
79
|
+
detail: `Unexpected status \u2014 try again or check api.mercadopago.com status.`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const me = await res.json();
|
|
83
|
+
return {
|
|
84
|
+
kind: "ok",
|
|
85
|
+
line: `Authenticated against api.mercadopago.com`,
|
|
86
|
+
detail: `account ${me.id ?? "?"} \xB7 site ${me.site_id ?? "?"} \xB7 country ${me.country_id ?? "?"}`
|
|
87
|
+
};
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return {
|
|
90
|
+
kind: "fail",
|
|
91
|
+
line: "Could not reach api.mercadopago.com",
|
|
92
|
+
detail: err instanceof Error ? err.message : "Unknown network error. Check connectivity / proxy."
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function checkPeerDep(name, required) {
|
|
97
|
+
try {
|
|
98
|
+
await import(name);
|
|
99
|
+
return { kind: "ok", line: `${name} installed` };
|
|
100
|
+
} catch {
|
|
101
|
+
return {
|
|
102
|
+
kind: required ? "fail" : "warn",
|
|
103
|
+
line: `${name} not installed`,
|
|
104
|
+
detail: required ? `Required peer dep \u2014 install with: pnpm add ${name}` : `Optional \u2014 required only if you use the matching subpath.`
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function checkBackUrl() {
|
|
109
|
+
const url = process.env.NEXT_PUBLIC_BACK_URL ?? process.env.BACK_URL;
|
|
110
|
+
if (!url) {
|
|
111
|
+
return {
|
|
112
|
+
kind: "warn",
|
|
113
|
+
line: "NEXT_PUBLIC_BACK_URL not set",
|
|
114
|
+
detail: "Required by mercadoPagoTools({ backUrl }) for create_subscription / create_payment_preference. Must be HTTPS (localhost rejected by MP in production)."
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (!url.startsWith("https://")) {
|
|
118
|
+
return {
|
|
119
|
+
kind: "fail",
|
|
120
|
+
line: `NEXT_PUBLIC_BACK_URL must be HTTPS (got: ${url.slice(0, 40)}\u2026)`,
|
|
121
|
+
detail: "MP rejects localhost and http:// URLs server-side."
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return { kind: "ok", line: `NEXT_PUBLIC_BACK_URL set (${url})` };
|
|
125
|
+
}
|
|
126
|
+
function checkWebhookSecret() {
|
|
127
|
+
const s = process.env.MP_WEBHOOK_SECRET?.trim();
|
|
128
|
+
if (!s) {
|
|
129
|
+
return {
|
|
130
|
+
kind: "warn",
|
|
131
|
+
line: "MP_WEBHOOK_SECRET not set",
|
|
132
|
+
detail: "verifyWebhookSignature() needs this. Copy from https://www.mercadopago.com.ar/developers/panel/app \u2192 notificaciones."
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (s.length < 16) {
|
|
136
|
+
return {
|
|
137
|
+
kind: "warn",
|
|
138
|
+
line: `MP_WEBHOOK_SECRET set but suspiciously short (${s.length} chars)`,
|
|
139
|
+
detail: "MP secrets are typically 32+ chars. Double-check the paste."
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return { kind: "ok", line: `MP_WEBHOOK_SECRET set (${s.length} chars)` };
|
|
143
|
+
}
|
|
144
|
+
async function loadManifest() {
|
|
145
|
+
const fs = await import('fs/promises');
|
|
146
|
+
for (const rel of ["../tools.manifest.json", "../../tools.manifest.json"]) {
|
|
147
|
+
try {
|
|
148
|
+
const url = new URL(rel, (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
|
|
149
|
+
const text = await fs.readFile(url, "utf-8");
|
|
150
|
+
return JSON.parse(text);
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function summarizeManifest(m) {
|
|
157
|
+
const lines = [];
|
|
158
|
+
lines.push(c("bold", `Tools registered: ${m.tools.length}`));
|
|
159
|
+
const groups = /* @__PURE__ */ new Map();
|
|
160
|
+
for (const tool of m.tools) {
|
|
161
|
+
const cat = tool.category ?? inferCategory(tool.name);
|
|
162
|
+
if (!groups.has(cat)) groups.set(cat, []);
|
|
163
|
+
groups.get(cat).push(tool.name);
|
|
164
|
+
}
|
|
165
|
+
for (const [cat, names] of [...groups.entries()].sort()) {
|
|
166
|
+
lines.push(` ${c("dim", "\u2192")} ${cat} (${names.length})`);
|
|
167
|
+
}
|
|
168
|
+
return lines;
|
|
169
|
+
}
|
|
170
|
+
function inferCategory(toolName) {
|
|
171
|
+
if (toolName.includes("subscription") || toolName.includes("preapproval")) return "Subscriptions";
|
|
172
|
+
if (toolName.includes("payment") && !toolName.includes("preference")) return "Payments";
|
|
173
|
+
if (toolName.includes("refund")) return "Refunds";
|
|
174
|
+
if (toolName.includes("preference") || toolName.includes("checkout")) return "Checkout Pro";
|
|
175
|
+
if (toolName.includes("oauth") || toolName.includes("marketplace")) return "Marketplace OAuth";
|
|
176
|
+
if (toolName.includes("merchant_order") || toolName.includes("order")) return "Order Management";
|
|
177
|
+
if (toolName.includes("customer") && !toolName.includes("card")) return "Customers";
|
|
178
|
+
if (toolName.includes("customer_card") || toolName.includes("card")) return "Saved cards";
|
|
179
|
+
if (toolName.includes("installment") || toolName.includes("cuotas") || toolName.includes("promo")) return "Cuotas";
|
|
180
|
+
if (toolName.includes("qr")) return "QR";
|
|
181
|
+
if (toolName.includes("3ds") || toolName.includes("challenge")) return "3DS";
|
|
182
|
+
if (toolName.includes("point")) return "Point devices";
|
|
183
|
+
if (toolName.includes("store") || toolName.includes("pos")) return "Stores+POS";
|
|
184
|
+
if (toolName.includes("balance") || toolName.includes("settlement") || toolName.includes("account")) return "Account/Balance";
|
|
185
|
+
if (toolName.includes("dispute")) return "Disputes";
|
|
186
|
+
if (toolName.includes("webhook")) return "Webhooks";
|
|
187
|
+
if (toolName.includes("bank_account")) return "Bank Accounts";
|
|
188
|
+
if (toolName.includes("lookup") || toolName.includes("validate") || toolName.includes("explain") || toolName.includes("compute")) return "Lookups";
|
|
189
|
+
return "Other";
|
|
190
|
+
}
|
|
191
|
+
var HITL_TOOLS = [
|
|
192
|
+
"refund_payment",
|
|
193
|
+
"cancel_subscription",
|
|
194
|
+
"pause_subscription",
|
|
195
|
+
"cancel_payment_preference",
|
|
196
|
+
"delete_customer_card",
|
|
197
|
+
"cancel_qr_dynamic",
|
|
198
|
+
"delete_pos",
|
|
199
|
+
"revoke_marketplace_token"
|
|
200
|
+
];
|
|
201
|
+
async function runDoctor(args = { probe: false }) {
|
|
202
|
+
const lines = [];
|
|
203
|
+
lines.push("");
|
|
204
|
+
lines.push(c("bold", `${c("cyan", "@ar-agents/mercadopago")} doctor`));
|
|
205
|
+
lines.push(c("dim", " diagnosing your environment"));
|
|
206
|
+
lines.push("");
|
|
207
|
+
lines.push(fmt(checkNode()));
|
|
208
|
+
const tokenCheck = checkAccessToken();
|
|
209
|
+
lines.push(fmt(tokenCheck));
|
|
210
|
+
if (tokenCheck.kind === "ok" || tokenCheck.kind === "warn") {
|
|
211
|
+
const probe = await probeToken(process.env.MP_ACCESS_TOKEN.trim());
|
|
212
|
+
lines.push(fmt(probe));
|
|
213
|
+
}
|
|
214
|
+
lines.push(fmt(checkBackUrl()));
|
|
215
|
+
lines.push(fmt(checkWebhookSecret()));
|
|
216
|
+
lines.push(fmt(await checkPeerDep("ai", true)));
|
|
217
|
+
lines.push(fmt(await checkPeerDep("zod", true)));
|
|
218
|
+
lines.push(fmt(await checkPeerDep("@vercel/kv", false)));
|
|
219
|
+
lines.push(fmt(await checkPeerDep("@opentelemetry/api", false)));
|
|
220
|
+
lines.push("");
|
|
221
|
+
const manifest = await loadManifest();
|
|
222
|
+
if (manifest) {
|
|
223
|
+
lines.push(...summarizeManifest(manifest));
|
|
224
|
+
lines.push("");
|
|
225
|
+
lines.push(
|
|
226
|
+
c(
|
|
227
|
+
"yellow",
|
|
228
|
+
`${HITL_TOOLS.length} irreversible ops behind requireConfirmation():`
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
lines.push(" " + c("dim", HITL_TOOLS.join(" \xB7 ")));
|
|
232
|
+
} else {
|
|
233
|
+
lines.push(
|
|
234
|
+
fmt({
|
|
235
|
+
kind: "warn",
|
|
236
|
+
line: "Could not load tools.manifest.json",
|
|
237
|
+
detail: "The package shipping is incomplete \u2014 reinstall."
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
lines.push("");
|
|
242
|
+
if (args.probe && tokenCheck.kind === "ok") {
|
|
243
|
+
lines.push(c("bold", "Probe mode: dry-calling validate_tax_id\u2026"));
|
|
244
|
+
try {
|
|
245
|
+
const res = await fetch("https://api.mercadopago.com/v1/identification_types", {
|
|
246
|
+
headers: { Authorization: `Bearer ${process.env.MP_ACCESS_TOKEN.trim()}` }
|
|
247
|
+
});
|
|
248
|
+
if (res.ok) {
|
|
249
|
+
const types = await res.json();
|
|
250
|
+
lines.push(
|
|
251
|
+
fmt({
|
|
252
|
+
kind: "ok",
|
|
253
|
+
line: `validate_tax_id reachable \u2014 ${types.length} ID types available`
|
|
254
|
+
})
|
|
255
|
+
);
|
|
256
|
+
} else {
|
|
257
|
+
lines.push(
|
|
258
|
+
fmt({
|
|
259
|
+
kind: "warn",
|
|
260
|
+
line: `validate_tax_id probe returned HTTP ${res.status}`
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
lines.push(
|
|
266
|
+
fmt({
|
|
267
|
+
kind: "warn",
|
|
268
|
+
line: "Probe failed",
|
|
269
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
270
|
+
})
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
lines.push("");
|
|
274
|
+
}
|
|
275
|
+
lines.push(
|
|
276
|
+
c(
|
|
277
|
+
"dim",
|
|
278
|
+
args.probe ? "All probes done. Pass without --probe for the lighter check." : "Run with --probe to also dry-call validate_tax_id (no charge)."
|
|
279
|
+
)
|
|
280
|
+
);
|
|
281
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
282
|
+
const hasFail = lines.some((l) => l.includes("\u2717"));
|
|
283
|
+
return hasFail ? 1 : 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/cli.ts
|
|
287
|
+
var HELP = `@ar-agents/mercadopago CLI
|
|
288
|
+
|
|
289
|
+
Commands:
|
|
290
|
+
doctor Diagnose your environment (Node, token, peer deps, tools)
|
|
291
|
+
doctor --probe Same as doctor but also dry-calls validate_tax_id
|
|
292
|
+
help Print this message
|
|
293
|
+
version Print the installed package version
|
|
294
|
+
|
|
295
|
+
Environment:
|
|
296
|
+
MP_ACCESS_TOKEN Required for any live calls. TEST- for sandbox, APP_USR- for prod.
|
|
297
|
+
NEXT_PUBLIC_BACK_URL Required HTTPS URL for create_subscription / create_payment_preference.
|
|
298
|
+
MP_WEBHOOK_SECRET Required for verifyWebhookSignature.
|
|
299
|
+
|
|
300
|
+
Docs: https://github.com/ar-agents/ar-agents/tree/main/packages/mercadopago
|
|
301
|
+
`;
|
|
302
|
+
async function readVersion() {
|
|
303
|
+
for (const rel of ["../package.json", "../../package.json"]) {
|
|
304
|
+
try {
|
|
305
|
+
const url = new URL(rel, (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
|
|
306
|
+
const fs = await import('fs/promises');
|
|
307
|
+
const text = await fs.readFile(url, "utf-8");
|
|
308
|
+
const pkg = JSON.parse(text);
|
|
309
|
+
if (pkg.version) return pkg.version;
|
|
310
|
+
} catch {
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return "unknown";
|
|
314
|
+
}
|
|
315
|
+
async function runCli(argv) {
|
|
316
|
+
const [, , cmd, ...rest] = argv;
|
|
317
|
+
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
318
|
+
process.stdout.write(HELP);
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
if (cmd === "version" || cmd === "--version" || cmd === "-v") {
|
|
322
|
+
process.stdout.write(`@ar-agents/mercadopago ${await readVersion()}
|
|
323
|
+
`);
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
if (cmd === "doctor") {
|
|
327
|
+
const probe = rest.includes("--probe");
|
|
328
|
+
return runDoctor({ probe });
|
|
329
|
+
}
|
|
330
|
+
process.stderr.write(
|
|
331
|
+
`Unknown command: ${cmd}
|
|
332
|
+
|
|
333
|
+
Run \`mercadopago help\` for usage.
|
|
334
|
+
`
|
|
335
|
+
);
|
|
336
|
+
return 2;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
exports.runCli = runCli;
|
|
340
|
+
//# sourceMappingURL=cli.cjs.map
|
|
341
|
+
//# sourceMappingURL=cli.cjs.map
|