@ar-agents/mercadopago 0.17.2 → 0.18.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 +33 -0
- package/cookbook/18-usa-llc-self-incorporates-ar.ts +208 -0
- package/cookbook/19-forensic-compliance-dashboard.ts +320 -0
- package/cookbook/20-multi-tenant-marketplace.ts +274 -0
- package/cookbook/21-cross-jurisdictional-ap2.ts +298 -0
- package/cookbook/22-mp-webhook-afip-reconciliation.ts +374 -0
- package/cookbook/23-astro-arg-reference-customer.ts +187 -0
- package/cookbook/24-sociedad-ia-disaster-recovery.ts +350 -0
- package/cookbook/25-sociedad-ia-quarterly-compliance.ts +545 -0
- package/cookbook/26-certify-by-fetch.ts +536 -0
- package/cookbook/27-live-conformance-monitoring.ts +260 -0
- package/cookbook/28-operator-onboarding-checklist.ts +315 -0
- package/cookbook/29-publish-your-keys.ts +193 -0
- package/cookbook/30-submit-to-registry.ts +257 -0
- package/dist/index.cjs +27 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -5
- package/dist/index.d.ts +21 -5
- package/dist/index.js +27 -14
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipe 20 — Multi-tenant marketplace spawning vendor sociedades-IA.
|
|
3
|
+
*
|
|
4
|
+
* # Pattern
|
|
5
|
+
*
|
|
6
|
+
* A platform (think: Tienda Nube, Mercado Shops, an SaaS marketplace)
|
|
7
|
+
* onboards vendors who each need a thin AR-side legal entity to operate
|
|
8
|
+
* within Argentina's jurisdiction. Pre-sociedad-IA, this was a manual
|
|
9
|
+
* escribono job per vendor. With `@ar-agents/incorporate`, the platform
|
|
10
|
+
* can spawn a fresh sociedad-IA spec on-demand at vendor sign-up — one
|
|
11
|
+
* API call, the vendor's deploy URL is materialized, the platform
|
|
12
|
+
* tracks the audit log per tenant.
|
|
13
|
+
*
|
|
14
|
+
* # When to use
|
|
15
|
+
*
|
|
16
|
+
* - Vertical SaaS where each vendor needs their own AR fiscal identity
|
|
17
|
+
* (factura emission under their CUIT, not the platform's).
|
|
18
|
+
* - Marketplaces that scale to hundreds of vendors and can't manually
|
|
19
|
+
* coordinate escribano + contador per spawn.
|
|
20
|
+
* - Cross-jurisdictional plays: a USA platform onboarding AR sellers who
|
|
21
|
+
* need a properly-incorporated sociedad to receive Mercado Pago.
|
|
22
|
+
*
|
|
23
|
+
* # Architecture
|
|
24
|
+
*
|
|
25
|
+
* Platform /api/auto-incorporate
|
|
26
|
+
* ┌──────────┐ POST ┌────────────────┐
|
|
27
|
+
* │ vendor │ ─────────────▶ │ ar-agents │
|
|
28
|
+
* │ signup │ with vendor │ generates spec │
|
|
29
|
+
* │ flow │ details + a │ + signs audit │
|
|
30
|
+
* └──────────┘ per-vendor │ entry under │
|
|
31
|
+
* │ sessionId │ vendor's id │
|
|
32
|
+
* ▼ └────────────────┘
|
|
33
|
+
* ┌──────────┐ │
|
|
34
|
+
* │ Vercel │ one-click deploy URL ◀───┘
|
|
35
|
+
* │ deploy │
|
|
36
|
+
* └──────────┘
|
|
37
|
+
* │
|
|
38
|
+
* ▼
|
|
39
|
+
* ┌──────────┐ GET /api/play/audit/{tenantId}?verify=1
|
|
40
|
+
* │ ops │ ◀──────────────────────────────────────┐
|
|
41
|
+
* │ dash │ │
|
|
42
|
+
* └──────────┘ │
|
|
43
|
+
* │ │
|
|
44
|
+
* ▼ (recipe 19 cron) │
|
|
45
|
+
* compliance digest per tenant ──────────────────────┘
|
|
46
|
+
*
|
|
47
|
+
* # Idempotency
|
|
48
|
+
*
|
|
49
|
+
* Calling /api/auto-incorporate with the same input + sessionId twice
|
|
50
|
+
* is idempotent on the spec output but writes two audit entries (one per
|
|
51
|
+
* call). For exact-once semantics, dedupe on the platform side keyed by
|
|
52
|
+
* (vendor_id, sociedad_denominacion). The audit log will show both
|
|
53
|
+
* attempts which is the more honest signal anyway.
|
|
54
|
+
*
|
|
55
|
+
* # Edge Runtime
|
|
56
|
+
*
|
|
57
|
+
* Yes — the @ar-agents/incorporate client is fetch-only.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
import {
|
|
61
|
+
incorporate,
|
|
62
|
+
fetchAudit,
|
|
63
|
+
type IncorporateInput,
|
|
64
|
+
type IncorporateSuccess,
|
|
65
|
+
} from "@ar-agents/incorporate";
|
|
66
|
+
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
// Platform's vendor model
|
|
69
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
interface Vendor {
|
|
72
|
+
id: string; // platform's internal vendor id
|
|
73
|
+
legalName: string;
|
|
74
|
+
representanteName: string;
|
|
75
|
+
representanteCuit: string;
|
|
76
|
+
contactEmail: string;
|
|
77
|
+
/** What the vendor sells. Used as the seed for objeto social. */
|
|
78
|
+
category: "software" | "services" | "products" | "ecommerce";
|
|
79
|
+
expectedMonthlyRevenueArs: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
// Spawn a sociedad-IA for a vendor at sign-up
|
|
84
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
const OBJETO_BY_CATEGORY: Record<Vendor["category"], string> = {
|
|
87
|
+
software:
|
|
88
|
+
"Desarrollo y comercialización de software propio para empresas y consumidores en Argentina, incluyendo pero no limitándose a aplicaciones web, móviles y servicios cloud.",
|
|
89
|
+
services:
|
|
90
|
+
"Prestación de servicios profesionales digitales (consultoría, desarrollo, marketing, atención al cliente) a empresas y consumidores en territorio argentino.",
|
|
91
|
+
products:
|
|
92
|
+
"Comercialización mayorista y minorista de productos físicos y digitales propios y de terceros, incluyendo importación, almacenamiento, marketing y logística en Argentina.",
|
|
93
|
+
ecommerce:
|
|
94
|
+
"Operación de tiendas online y marketplaces para la venta de productos y servicios al consumidor final argentino, incluyendo procesamiento de pagos y logística.",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The platform's tenantId for this vendor's sociedad-IA. Becomes the
|
|
99
|
+
* sessionId for the entire forensic timeline (incorporation + ongoing
|
|
100
|
+
* tool calls). Pick something stable + opaque — UUID v4 or a hashed
|
|
101
|
+
* concatenation of vendor_id + a per-platform secret.
|
|
102
|
+
*/
|
|
103
|
+
function tenantSessionIdFor(vendor: Vendor): string {
|
|
104
|
+
// For the cookbook, derive deterministically. In production, prefer
|
|
105
|
+
// crypto.randomUUID() per tenant to avoid leaking enumerable ids.
|
|
106
|
+
return `tenant-${vendor.id.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 56)}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function spawnVendorSociedad(vendor: Vendor): Promise<IncorporateSuccess> {
|
|
110
|
+
const sessionId = tenantSessionIdFor(vendor);
|
|
111
|
+
|
|
112
|
+
// Pieza selection driven by vendor profile.
|
|
113
|
+
const piezas: IncorporateInput["piezas"] = [
|
|
114
|
+
"identity",
|
|
115
|
+
"gde-tad",
|
|
116
|
+
"mercadopago",
|
|
117
|
+
"banking",
|
|
118
|
+
"facturacion",
|
|
119
|
+
"boletin-oficial",
|
|
120
|
+
"igj",
|
|
121
|
+
];
|
|
122
|
+
if (vendor.category === "ecommerce" || vendor.category === "products") {
|
|
123
|
+
piezas.push("shipping");
|
|
124
|
+
}
|
|
125
|
+
if (vendor.expectedMonthlyRevenueArs > 1_000_000) {
|
|
126
|
+
// Large-revenue tenants need WhatsApp customer-comms + ACP for LLM-buyer
|
|
127
|
+
// checkout. Smaller tenants can stay lean.
|
|
128
|
+
piezas.push("whatsapp", "agentic-commerce-bridge", "ap2");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Capital social: SAS minimum is 100k. Bump for large-revenue vendors so
|
|
132
|
+
// BCRA and downstream banks don't flag the disparity later.
|
|
133
|
+
const capitalSocial =
|
|
134
|
+
vendor.expectedMonthlyRevenueArs > 1_000_000 ? 500_000 : 200_000;
|
|
135
|
+
|
|
136
|
+
const input: IncorporateInput = {
|
|
137
|
+
denominacion: `${vendor.legalName} SAS`,
|
|
138
|
+
tipo: "SAS", // SOCIEDAD-IA when the regime ships
|
|
139
|
+
capitalSocial,
|
|
140
|
+
objeto: OBJETO_BY_CATEGORY[vendor.category],
|
|
141
|
+
representante: {
|
|
142
|
+
nombre: vendor.representanteName,
|
|
143
|
+
cuit: vendor.representanteCuit,
|
|
144
|
+
},
|
|
145
|
+
emailContacto: vendor.contactEmail,
|
|
146
|
+
piezas,
|
|
147
|
+
sessionId,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const result = await incorporate(input);
|
|
151
|
+
|
|
152
|
+
if (!result.ok) {
|
|
153
|
+
// Pre-flight failure — surface the findings + reject the signup.
|
|
154
|
+
const errors = result.validation.findings
|
|
155
|
+
.filter((f) => f.severity === "error")
|
|
156
|
+
.map((f) => `${f.field}: ${f.message}`);
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Vendor signup rejected at IGJ pre-flight: ${errors.join("; ")}`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
165
|
+
// Platform's spawn flow: handle a fresh signup
|
|
166
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
interface SignupRecord {
|
|
169
|
+
vendorId: string;
|
|
170
|
+
tenantSessionId: string;
|
|
171
|
+
slug: string;
|
|
172
|
+
deployUrl: string;
|
|
173
|
+
auditDashboardUrl: string;
|
|
174
|
+
auditVerifyUrl: string;
|
|
175
|
+
badgeSvgUrl: string;
|
|
176
|
+
createdAt: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Persist this in the platform's vendor table (Postgres / KV / whatever).
|
|
181
|
+
* The badge SVG URL is what you embed in the vendor's profile page so any
|
|
182
|
+
* visitor sees their "verified · N/M" forensic-clean status.
|
|
183
|
+
*/
|
|
184
|
+
function recordFor(
|
|
185
|
+
vendor: Vendor,
|
|
186
|
+
result: IncorporateSuccess,
|
|
187
|
+
): SignupRecord {
|
|
188
|
+
return {
|
|
189
|
+
vendorId: vendor.id,
|
|
190
|
+
tenantSessionId: result.audit.sessionId,
|
|
191
|
+
slug: result.sociedad.slug,
|
|
192
|
+
deployUrl: result.deploy.oneClickUrl,
|
|
193
|
+
auditDashboardUrl: result.audit.dashboardUrl,
|
|
194
|
+
auditVerifyUrl: result.audit.verifyUrl,
|
|
195
|
+
badgeSvgUrl: `https://ar-agents.ar/api/badge/${result.audit.sessionId}`,
|
|
196
|
+
createdAt: new Date().toISOString(),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
201
|
+
// Periodic compliance digest per tenant (recipe 19 in a fan-out loop)
|
|
202
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
interface TenantHealth {
|
|
205
|
+
vendorId: string;
|
|
206
|
+
totalEvents: number;
|
|
207
|
+
tampered: number;
|
|
208
|
+
hmacWired: boolean;
|
|
209
|
+
alert: string | null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function platformComplianceSweep(
|
|
213
|
+
records: SignupRecord[],
|
|
214
|
+
): Promise<TenantHealth[]> {
|
|
215
|
+
const results: TenantHealth[] = [];
|
|
216
|
+
for (const r of records) {
|
|
217
|
+
const audit = (await fetchAudit(r.tenantSessionId, { verify: true })) as {
|
|
218
|
+
count: number;
|
|
219
|
+
verification?: { tampered: number; verified: number; total: number; hmacWired: boolean };
|
|
220
|
+
};
|
|
221
|
+
const v = audit.verification ?? { tampered: 0, verified: 0, total: 0, hmacWired: false };
|
|
222
|
+
let alert: string | null = null;
|
|
223
|
+
if (!v.hmacWired) alert = "HMAC not wired in deploy — log unverified.";
|
|
224
|
+
else if (v.tampered > 0)
|
|
225
|
+
alert = `Tampering detected on tenant ${r.vendorId}: ${v.tampered} entries.`;
|
|
226
|
+
results.push({
|
|
227
|
+
vendorId: r.vendorId,
|
|
228
|
+
totalEvents: audit.count,
|
|
229
|
+
tampered: v.tampered,
|
|
230
|
+
hmacWired: v.hmacWired,
|
|
231
|
+
alert,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return results;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
238
|
+
// Example: signup + initial audit verify
|
|
239
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
async function main() {
|
|
242
|
+
// 1. New vendor signs up on the platform.
|
|
243
|
+
const vendor: Vendor = {
|
|
244
|
+
id: "v_42",
|
|
245
|
+
legalName: "Berreta Software",
|
|
246
|
+
representanteName: "Pérez, Juan",
|
|
247
|
+
representanteCuit: "20-12345678-9",
|
|
248
|
+
contactEmail: "ops@berreta.example",
|
|
249
|
+
category: "software",
|
|
250
|
+
expectedMonthlyRevenueArs: 800_000,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// 2. Spawn the sociedad-IA spec.
|
|
254
|
+
const result = await spawnVendorSociedad(vendor);
|
|
255
|
+
const record = recordFor(vendor, result);
|
|
256
|
+
|
|
257
|
+
console.log("Vendor onboarded:", record);
|
|
258
|
+
|
|
259
|
+
// 3. Verify the signup audit entry was actually written + signed.
|
|
260
|
+
const sweep = await platformComplianceSweep([record]);
|
|
261
|
+
console.log("Initial compliance sweep:", sweep);
|
|
262
|
+
|
|
263
|
+
// 4. Embed the badge in the vendor's profile page in the platform UI.
|
|
264
|
+
console.log("Badge URL for vendor profile:", record.badgeSvgUrl);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (typeof require !== "undefined" && require.main === module) {
|
|
268
|
+
main().catch((err) => {
|
|
269
|
+
console.error("Recipe 20 failed:", err);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export { main };
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipe 21 — Cross-jurisdictional commerce w/ AP2 mandate verification.
|
|
3
|
+
*
|
|
4
|
+
* # Pattern
|
|
5
|
+
*
|
|
6
|
+
* A USA-LLC agent (e.g., a Wyoming DAO LLC operating an Etsy-style
|
|
7
|
+
* marketplace) wants to sell to AR consumers via MP, but doesn't have AR
|
|
8
|
+
* fiscal residency. The AR-side is a sociedad-IA (or pre-launch SAS) that
|
|
9
|
+
* (a) issues factura A/B/C to the AR consumer, (b) cobra MP, (c) settles
|
|
10
|
+
* inter-entity to the USA-LLC weekly.
|
|
11
|
+
*
|
|
12
|
+
* Each cross-entity transaction is gated by an AP2 (Google Agent Payments
|
|
13
|
+
* Protocol) mandate the USA-LLC signs and the AR sociedad verifies before
|
|
14
|
+
* acting. This is what RFC-001 § 7 sketches as the contract surface for
|
|
15
|
+
* "agent commerce that crosses jurisdictions".
|
|
16
|
+
*
|
|
17
|
+
* The mandate has 3 critical fields:
|
|
18
|
+
* - issuer: USA-LLC's stable identifier (DID, DAO LLC EIN, etc.)
|
|
19
|
+
* - subject: the AR sociedad's CUIT
|
|
20
|
+
* - claims: { action: "factura.emit", amount, currency: "ARS",
|
|
21
|
+
* consumer: { dni, email }, allowance: { capPerMonth, capPerOp } }
|
|
22
|
+
*
|
|
23
|
+
* Verification:
|
|
24
|
+
* 1. AP2 verify chain — JWS ES256 signature against the issuer's pinned key.
|
|
25
|
+
* 2. Mandate hasn't expired (`exp` claim).
|
|
26
|
+
* 3. Operation respects the cap (cumulative against the audit log).
|
|
27
|
+
* 4. Consumer-side checks (CUIT validity, BCRA situation if amount large).
|
|
28
|
+
*
|
|
29
|
+
* If any check fails, the AR sociedad refuses + logs the refusal in the
|
|
30
|
+
* audit log. RFC-001 § 9.2 makes that audit entry probative — the USA-LLC
|
|
31
|
+
* can later challenge "you said no, prove the rule".
|
|
32
|
+
*
|
|
33
|
+
* # Edge Runtime
|
|
34
|
+
*
|
|
35
|
+
* Yes — @ar-agents/ap2 + @ar-agents/incorporate are both fetch-only.
|
|
36
|
+
* Verification uses Web Crypto's verify() against the issuer's pinned JWK.
|
|
37
|
+
*
|
|
38
|
+
* # Production caveats
|
|
39
|
+
*
|
|
40
|
+
* - The AR sociedad MUST emit the factura under its own CUIT, not the
|
|
41
|
+
* USA-LLC's. Cross-CUIT factura is not legal in AR.
|
|
42
|
+
* - Settlement between entities is an SLA between the operator and the
|
|
43
|
+
* USA-LLC. The toolkit doesn't dictate the cadence; recipe 22 (planned)
|
|
44
|
+
* covers nightly reconciliation.
|
|
45
|
+
* - For amounts > certain BCRA thresholds, AR sociedad needs to hold
|
|
46
|
+
* funds for ~2 days while AFIP IBPP processes. The agent should surface
|
|
47
|
+
* this to the consumer at checkout.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import { fetchAudit } from "@ar-agents/incorporate";
|
|
51
|
+
import { verifyMandate, type Mandate } from "@ar-agents/ap2";
|
|
52
|
+
import {
|
|
53
|
+
identityTools,
|
|
54
|
+
UnconfiguredAfipPadronAdapter,
|
|
55
|
+
} from "@ar-agents/identity";
|
|
56
|
+
import { facturacionTools } from "@ar-agents/facturacion";
|
|
57
|
+
import { bankingTools } from "@ar-agents/banking";
|
|
58
|
+
|
|
59
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
60
|
+
// Mandate the USA-LLC sends to the AR sociedad
|
|
61
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
interface CrossJurisdictionalClaims {
|
|
64
|
+
action: "factura.emit" | "mp.charge" | "shipment.create";
|
|
65
|
+
amountArs: number;
|
|
66
|
+
currency: "ARS";
|
|
67
|
+
consumer: { cuit: string; email: string };
|
|
68
|
+
allowance: {
|
|
69
|
+
capPerOpArs: number;
|
|
70
|
+
capPerMonthArs: number;
|
|
71
|
+
};
|
|
72
|
+
/** ISO 8601. Mandate expires here. */
|
|
73
|
+
exp: string;
|
|
74
|
+
/** USA-LLC's transaction reference for reconciliation. */
|
|
75
|
+
externalId: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
// AR sociedad's verify-then-act handler
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
interface VerifyAndActOptions {
|
|
83
|
+
/**
|
|
84
|
+
* The AR sociedad's audit-log session id. All operations under this
|
|
85
|
+
* mandate land in the same forensic timeline.
|
|
86
|
+
*/
|
|
87
|
+
sessionId: string;
|
|
88
|
+
/**
|
|
89
|
+
* The USA-LLC's JWK that issued the mandate. In production: pinned
|
|
90
|
+
* via the platform's vendor table.
|
|
91
|
+
*/
|
|
92
|
+
issuerJwk: JsonWebKey;
|
|
93
|
+
/** The mandate the USA-LLC delivered. */
|
|
94
|
+
mandate: Mandate<CrossJurisdictionalClaims>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface ActResult {
|
|
98
|
+
ok: boolean;
|
|
99
|
+
reason?: string;
|
|
100
|
+
facturaCae?: string;
|
|
101
|
+
auditDashboardUrl: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function verifyAndAct(
|
|
105
|
+
options: VerifyAndActOptions,
|
|
106
|
+
): Promise<ActResult> {
|
|
107
|
+
const { sessionId, issuerJwk, mandate } = options;
|
|
108
|
+
|
|
109
|
+
// ─── 1. Verify the AP2 mandate ──────────────────────────────────────
|
|
110
|
+
const verification = await verifyMandate(mandate, { issuerJwk });
|
|
111
|
+
if (!verification.valid) {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
reason: `Mandate verification failed: ${verification.reason}`,
|
|
115
|
+
auditDashboardUrl: `https://ar-agents.ar/dashboard/${sessionId}`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── 2. Expiry check ────────────────────────────────────────────────
|
|
120
|
+
if (Date.parse(mandate.claims.exp) < Date.now()) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
reason: "Mandate expired (exp claim in the past)",
|
|
124
|
+
auditDashboardUrl: `https://ar-agents.ar/dashboard/${sessionId}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── 3. Per-op cap ──────────────────────────────────────────────────
|
|
129
|
+
if (mandate.claims.amountArs > mandate.claims.allowance.capPerOpArs) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
reason: `Amount $${mandate.claims.amountArs} exceeds per-op cap $${mandate.claims.allowance.capPerOpArs}.`,
|
|
133
|
+
auditDashboardUrl: `https://ar-agents.ar/dashboard/${sessionId}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── 4. Cumulative cap (querying the audit log) ─────────────────────
|
|
138
|
+
// Pull this month's prior emissions on the same sessionId + sum.
|
|
139
|
+
const audit = (await fetchAudit(sessionId, { verify: false })) as {
|
|
140
|
+
entries: Array<{
|
|
141
|
+
tool: string;
|
|
142
|
+
ts: string;
|
|
143
|
+
input: { amountArs?: number; externalId?: string };
|
|
144
|
+
}>;
|
|
145
|
+
};
|
|
146
|
+
const monthStart = new Date();
|
|
147
|
+
monthStart.setDate(1);
|
|
148
|
+
monthStart.setHours(0, 0, 0, 0);
|
|
149
|
+
const cumulative = audit.entries
|
|
150
|
+
.filter(
|
|
151
|
+
(e) =>
|
|
152
|
+
e.tool === "cross_jurisdictional_factura_emit" &&
|
|
153
|
+
Date.parse(e.ts) >= monthStart.getTime(),
|
|
154
|
+
)
|
|
155
|
+
.reduce((sum, e) => sum + (e.input.amountArs ?? 0), 0);
|
|
156
|
+
|
|
157
|
+
if (cumulative + mandate.claims.amountArs > mandate.claims.allowance.capPerMonthArs) {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
reason: `Cumulative monthly emissions ($${cumulative} + $${mandate.claims.amountArs}) exceed cap $${mandate.claims.allowance.capPerMonthArs}.`,
|
|
161
|
+
auditDashboardUrl: `https://ar-agents.ar/dashboard/${sessionId}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── 5. Idempotency: did we already process this externalId? ────────
|
|
166
|
+
if (
|
|
167
|
+
audit.entries.some(
|
|
168
|
+
(e) =>
|
|
169
|
+
e.tool === "cross_jurisdictional_factura_emit" &&
|
|
170
|
+
e.input.externalId === mandate.claims.externalId,
|
|
171
|
+
)
|
|
172
|
+
) {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
reason: `externalId ${mandate.claims.externalId} already processed (idempotency).`,
|
|
176
|
+
auditDashboardUrl: `https://ar-agents.ar/dashboard/${sessionId}`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─── 6. Validate the consumer's CUIT ────────────────────────────────
|
|
181
|
+
// (Real prod would have an AfipPadronAdapter wired — using the
|
|
182
|
+
// unconfigured shim here just to demonstrate the flow.)
|
|
183
|
+
const idTools = identityTools({ afip: new UnconfiguredAfipPadronAdapter() });
|
|
184
|
+
const cuitCheck = await idTools.validate_cuit.execute({
|
|
185
|
+
cuit: mandate.claims.consumer.cuit,
|
|
186
|
+
});
|
|
187
|
+
if (!cuitCheck.valid) {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
reason: `Consumer CUIT ${mandate.claims.consumer.cuit} is malformed.`,
|
|
191
|
+
auditDashboardUrl: `https://ar-agents.ar/dashboard/${sessionId}`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── 7. For amounts above $500k ARS, BCRA credit-situation check ────
|
|
196
|
+
if (mandate.claims.amountArs > 500_000) {
|
|
197
|
+
const bk = bankingTools();
|
|
198
|
+
// In production this hits BCRA Central de Deudores for real; mock here.
|
|
199
|
+
const credit = await bk.lookup_credit_situation.execute({
|
|
200
|
+
cuit: mandate.claims.consumer.cuit,
|
|
201
|
+
});
|
|
202
|
+
if (
|
|
203
|
+
credit.available === false ||
|
|
204
|
+
(credit.worstSituation && credit.worstSituation > 2)
|
|
205
|
+
) {
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
reason: `Consumer has BCRA situation ${credit.worstSituation} (>2 = high risk).`,
|
|
209
|
+
auditDashboardUrl: `https://ar-agents.ar/dashboard/${sessionId}`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── 8. Emit the factura ────────────────────────────────────────────
|
|
215
|
+
// (Real prod requires AFIP cert; using mock returns CAE here.)
|
|
216
|
+
const fact = facturacionTools();
|
|
217
|
+
// Note: in real code, you'd select cbteTipo ("FACTURA_B" for
|
|
218
|
+
// CONSUMIDOR_FINAL etc.) based on AFIP padron lookup of the consumer.
|
|
219
|
+
// Demo just hardcodes Factura B + 21% IVA.
|
|
220
|
+
const result = await fact.crear_factura.execute({
|
|
221
|
+
cbteTipo: "FACTURA_B",
|
|
222
|
+
docTipo: "CUIT",
|
|
223
|
+
docNro: mandate.claims.consumer.cuit,
|
|
224
|
+
impTotal: mandate.claims.amountArs,
|
|
225
|
+
impNeto: Math.round(mandate.claims.amountArs / 1.21),
|
|
226
|
+
impIVA: mandate.claims.amountArs - Math.round(mandate.claims.amountArs / 1.21),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ─── 9. Audit log entry happens automatically via the tool wrapper.
|
|
230
|
+
// Surface the result + dashboard URL for the USA-LLC's records.
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
ok: true,
|
|
234
|
+
facturaCae: result.cae ?? undefined,
|
|
235
|
+
auditDashboardUrl: `https://ar-agents.ar/dashboard/${sessionId}`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
240
|
+
// Example: handle a cross-jurisdictional factura request
|
|
241
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
async function main() {
|
|
244
|
+
const sessionId = "tenant-clawbank-llc-ar"; // pinned per USA-LLC tenant
|
|
245
|
+
|
|
246
|
+
// Mandate the USA-LLC produced (signed JWS; pretend it's already verified
|
|
247
|
+
// structurally — in real code, JWS parsing happens inside verifyMandate).
|
|
248
|
+
const mandate = {
|
|
249
|
+
issuer: "wyoming-dao-llc:claw-bank",
|
|
250
|
+
subject: "ar-sociedad:30123456789",
|
|
251
|
+
claims: {
|
|
252
|
+
action: "factura.emit" as const,
|
|
253
|
+
amountArs: 75_000,
|
|
254
|
+
currency: "ARS" as const,
|
|
255
|
+
consumer: { cuit: "20-12345678-9", email: "consumidor@example.com" },
|
|
256
|
+
allowance: {
|
|
257
|
+
capPerOpArs: 100_000,
|
|
258
|
+
capPerMonthArs: 2_000_000,
|
|
259
|
+
},
|
|
260
|
+
exp: new Date(Date.now() + 3600 * 1000).toISOString(),
|
|
261
|
+
externalId: "claw-bank:tx_42",
|
|
262
|
+
},
|
|
263
|
+
signature: "<JWS-ES256-signature>", // produced by USA-LLC
|
|
264
|
+
} as unknown as Mandate<CrossJurisdictionalClaims>;
|
|
265
|
+
|
|
266
|
+
// The USA-LLC's pinned issuer key. In production: pulled from the
|
|
267
|
+
// platform's vendor table at signup time.
|
|
268
|
+
const issuerJwk: JsonWebKey = {
|
|
269
|
+
kty: "EC",
|
|
270
|
+
crv: "P-256",
|
|
271
|
+
x: "<base64url-x>",
|
|
272
|
+
y: "<base64url-y>",
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const result = await verifyAndAct({ sessionId, issuerJwk, mandate });
|
|
276
|
+
|
|
277
|
+
if (!result.ok) {
|
|
278
|
+
console.error("Refused:", result.reason);
|
|
279
|
+
console.error("Audit dashboard:", result.auditDashboardUrl);
|
|
280
|
+
process.exit(0);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
console.log("Factura emitted:", result.facturaCae);
|
|
284
|
+
console.log("Audit dashboard:", result.auditDashboardUrl);
|
|
285
|
+
console.log(
|
|
286
|
+
"Settlement reference:",
|
|
287
|
+
`clawbank-llc:${mandate.claims.externalId}`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (typeof require !== "undefined" && require.main === module) {
|
|
292
|
+
main().catch((err) => {
|
|
293
|
+
console.error("Recipe 21 failed:", err);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export { main };
|