facturas 0.10.0 → 0.11.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/README.md CHANGED
@@ -4,889 +4,137 @@
4
4
  [![CI](https://github.com/LaPyme/facturas/actions/workflows/ci.yml/badge.svg)](https://github.com/LaPyme/facturas/actions/workflows/ci.yml)
5
5
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://github.com/LaPyme/facturas/blob/main/LICENSE)
6
6
 
7
- Node.js SDK for ARCA / AFIP invoicing, credit notes and Padrón, with direct WSFE and WSMTXCA integration. [Inicio rápido en español](./docs/inicio-rapido.md).
8
-
9
- - **ESM-only**, Node.js **>= 20**
10
- - **Direct ARCA integration** with no proxy or hosted dependency
11
- - **WSAA login handling** with in-memory ticket cache, optional durable session stores, in-flight deduplication, and recovery for `coe.alreadyAuthenticated`
12
- - **Strict TypeScript** public API with JS-style field names mapped to SOAP internally
13
- - **Common ARCA reference data** exported as constants so examples and app code do not need magic numbers
14
- - **Copy-pasteable examples** designed to be readable by humans and coding agents
15
-
16
- ## Install
7
+ SDK de Node.js para ARCA / AFIP: facturas, notas de crédito y Padrón, con
8
+ integración directa a WSFE y WSMTXCA.
9
+
10
+ - **Solo ESM**, Node.js **>= 20**
11
+ - **CLI incluido**: `npx facturas init` genera la clave y el CSR, y
12
+ `npx facturas check` nombra la capa de ARCA que falla
13
+ - **Integración directa con ARCA**, sin proxy ni dependencia alojada
14
+ - **Login WSAA resuelto**: caché de tickets en memoria, stores de sesión
15
+ durables opcionales, deduplicación de logins en vuelo y recuperación de
16
+ `coe.alreadyAuthenticated`
17
+ - **API pública en TypeScript estricto**, con nombres al estilo JS mapeados a
18
+ SOAP internamente
19
+ - **Datos de referencia comunes de ARCA** exportados como constantes, para que
20
+ los ejemplos y tu código no necesiten números mágicos
21
+ - **Ejemplos copiables**, escritos para que los lean personas y agentes de
22
+ código
23
+
24
+ La documentación está en castellano, en [docs/](./docs/README.md).
25
+ [English summary](./docs/en/README.md).
26
+
27
+ ## Instalación
17
28
 
18
29
  ```bash
19
30
  pnpm add facturas
20
- ```
21
-
22
- ```bash
31
+ # o
23
32
  npm install facturas
24
33
  ```
25
34
 
26
- ## Issue an invoice
35
+ ## Emití tu primera factura
27
36
 
28
- Set the [environment variables](#configuration), provision the store table below,
29
- and use the sale's stable ID as the key. The example assumes `venta` is your sale.
37
+ Definí las [variables de entorno](./docs/configuracion.md#variables-de-entorno)
38
+ con tu CUIT, certificado y clave. No hace falta nada más: ni base de datos, ni
39
+ tabla, ni servicio externo.
30
40
 
31
41
  ```ts
32
- import { createArcaClient, createPostgresStore } from "facturas";
33
- import { sql } from "@vercel/postgres";
34
-
35
- const arca = createArcaClient({
36
- store: createPostgresStore({ query: (text, params) => sql.query(text, params) }),
37
- });
38
-
39
- const factura = await arca.issue(
40
- {
41
- issuer: "monotributo",
42
- salesPoint: 3,
43
- to: { condition: "consumidor_final" },
44
- items: [{ amount: 150_000 }], // ARS 1.500,00 en centavos
45
- },
46
- { idempotencyKey: venta.id },
47
- );
48
- ```
49
-
50
- Without `idempotencyKey`, a retry after a crash
51
- can issue the invoice twice. Configure a `store` and pass the key so retries
52
- are safe.
53
-
54
- A key is 1–255 characters. Use the sale or order ID, never a new UUID for each
55
- attempt. Do not put CUIT, DNI or other personal data in keys. Reuse the same
56
- key and input on retries; changed input throws `ARCA_INPUT_IDEMPOTENCY_MISMATCH`.
57
- Keys are scoped to the client's CUIT and environment. `representedTaxId` is
58
- also checked as part of the input identity. A key without a store throws before
59
- provider I/O. Different keys identify different business operations; a key does
60
- not reserve the entire point-of-sale sequence against other writers.
61
-
62
- ### Check before you send
42
+ import { createArcaClient } from "facturas";
63
43
 
64
- `preview()` derives exactly what `issue()` would send, with no I/O at all: no
65
- store, no WSAA, no SOAP and no next-number read. It throws the same input
66
- errors `issue()` throws before its first call, so an input that previews
67
- cleanly raises no new local error at issuance.
44
+ const arca = createArcaClient();
68
45
 
69
- ```ts
70
- const input = {
71
- issuer: "monotributo" as const,
46
+ const factura = await arca.issue({
47
+ issuer: "monotributo",
72
48
  salesPoint: 3,
73
- to: { condition: "consumidor_final" as const },
74
- items: [{ amount: 150_000 }],
75
- };
76
-
77
- const previsualizacion = arca.preview(input);
78
- if (previsualizacion.amounts.sentTotal !== venta.totalEnCentavos) {
79
- throw new Error("The derived invoice does not match the sale total.");
80
- }
81
- const factura = await arca.issue(input, { idempotencyKey: venta.id });
82
- ```
83
-
84
- `preview()` is synchronous and returns the derived `voucherClass`,
85
- `voucherType`, the same `amounts` an authorized outcome reports, and `request`,
86
- the exact `WsfeVoucherInput` that `issue()` would send. The voucher number is
87
- absent because it is only known when the number is reserved at issuance.
88
- Previewing a credit note needs the original and is not offered.
89
-
90
- ## Credit notes
91
-
92
- ARCA has no cancellation. A correction is a credit note, and both modes of
93
- `issueCreditNote()` write a real fiscal document that stays in ARCA's records.
94
-
95
- A credit note names the invoice it corrects. The SDK does not offer associated
96
- periods; use `wsfe.issue()` if you need one.
97
-
98
- The common case is partial: a refund or a price correction that credits chosen
99
- lines rather than the whole invoice.
100
-
101
- ```ts
102
- const nota = await arca.issueCreditNote(
103
- {
104
- for: { salesPoint: 3, voucherType: 11, number: 41 },
105
- items: [{ amount: 50_000 }], // ARS 500,00 of an ARS 1.500,00 invoice.
106
- },
107
- { idempotencyKey: `nc:${devolucion.id}` },
108
- );
109
- ```
110
-
111
- `all: true` credits the whole original instead, mirroring its amounts and VAT
112
- rates line by line:
113
-
114
- ```ts
115
- const notaTotal = await arca.issueCreditNote(
116
- { for: { salesPoint: 3, voucherType: 11, number: 41 }, all: true },
117
- { idempotencyKey: `nc-total:${venta.id}` },
118
- );
119
- ```
120
-
121
- The mode is explicit and required: an input with neither `items` nor
122
- `all: true`, or with both, is rejected before any I/O, so a forgotten field
123
- cannot credit the whole invoice.
124
-
125
- From the original the SDK takes the class and therefore the note type
126
- (1 → 3, 6 → 8, 11 → 13), the receiver document type, number and VAT condition,
127
- the currency and exchange rate, the concept and, for concepts 2 and 3, the
128
- service dates with the due date raised to the note date. The caller provides
129
- `for`, the mode (`items`, with an optional `total`, or `all: true`) and, at
130
- most, the note's own `salesPoint`, which defaults to the original's, and
131
- `date`, which defaults to today in Buenos Aires. There is no `issuer`, `to` or
132
- `currency` field: a note to a different receiver or in a different currency is
133
- a different document and belongs to the exact layer.
134
-
135
- Item shapes follow the original's class. A class C note accepts `{ amount }`
136
- items; class A and B notes accept `{ gross | net, vat }` items with the same
137
- rates and reconciliation as `issue()`. The class is evidence from the original,
138
- so a shape that contradicts it is rejected after the single lookup of the
139
- original and before any write.
140
-
141
- The note may not exceed the original's total. The SDK does not track earlier
142
- notes against an original; preventing several notes from adding up beyond the
143
- invoice is the application's job.
144
-
145
- Only authorized invoices of type 1, 6 or 11 in ARS or USD can be corrected.
146
- Originals with tributes, optional fields, buyers, activities or an associated
147
- period, debit-note targets, and FCE all require the exact API.
148
-
149
- `idempotencyKey` and `include` work exactly as for `issue()`; give the note its
150
- own stable key, such as `nc:${devolucion.id}`. A keyed replay consults only the
151
- reserved note and reports `amounts` from the stored request, so on that path
152
- `computedTotal` equals `sentTotal` and `vatAdjustment` is `0`.
153
-
154
- A production smoke test is an ARS 1 invoice followed by a full note. **Both
155
- documents are real and remain in ARCA's records.** The note is a separate
156
- operation; a failure leaves the invoice outstanding. Match `issuer` to your
157
- actual tax condition.
158
-
159
- ## Stores
160
-
161
- One `store` persists WSAA tickets and immutable invoice/credit-note reservations.
162
- The SDK adds no database or Redis driver dependency. Store failures throw
163
- `ArcaConfigurationError` with their cause attached and a content-free message.
164
-
165
- ### Postgres
166
-
167
- Use your application's existing client. Neon, Supabase Postgres, Vercel Postgres,
168
- `pg` and `postgres` can provide the parameterized query function. Results can
169
- be an array of rows or `{ rows }`. With `postgres`, adapt `sql.unsafe(text, params)`.
170
- Provision the default table once:
171
-
172
- ```sql
173
- CREATE TABLE arca_store (
174
- key text PRIMARY KEY,
175
- value text NOT NULL,
176
- updated_at timestamptz NOT NULL DEFAULT now()
177
- );
178
- ```
179
-
180
- ```ts
181
- const store = createPostgresStore({
182
- query: (text, params) => sql.query(text, params),
183
- table: "arca_store", // Optional simple SQL identifier.
49
+ to: { condition: "consumidor_final" },
50
+ items: [{ amount: 150_000 }], // ARS 1.500,00 en centavos
184
51
  });
185
52
  ```
186
53
 
187
- Atomic creation uses `INSERT ... ON CONFLICT DO NOTHING RETURNING key`.
188
- The adapter does not create the table or hold a database lock.
189
-
190
- ### Redis
191
-
192
- ```ts
193
- import { createRedisStore } from "facturas";
194
- const store = createRedisStore(redis);
195
- // Optional override: createRedisStore(redis, { flavor: "upstash" });
196
- ```
197
-
198
- A client with `call` uses ioredis `SET key value NX`; otherwise the adapter
199
- uses Upstash `set(key, value, { nx: true })`. Use a durable Redis deployment
200
- without eviction of reservation keys. Neither flavor applies a TTL or lock.
201
-
202
- ### Files
203
-
204
- ```ts
205
- import { createFileStore } from "facturas";
206
- const store = createFileStore("/private/durable/arca");
207
- ```
208
-
209
- Use a private durable volume on a single server. Keys are hashed to filenames;
210
- creation is exclusive, replacement uses a temporary file and rename. Files
211
- have mode `0600`, new directories `0700`. No process lock is provided.
212
-
213
- ### Memory
214
-
215
- ```ts
216
- import { createMemoryStore } from "facturas";
217
- const store = createMemoryStore();
218
- ```
219
-
220
- For tests and examples. It serializes ticket refreshes within the shared object,
221
- but **does not survive a restart**. It does not make serverless retries durable.
222
-
223
- ### Custom store and record lifetime
224
-
225
- ```ts
226
- type ArcaStore = {
227
- get(key: string): Promise<string | null>;
228
- set(key: string, value: string): Promise<void>;
229
- add(key: string, value: string): Promise<boolean>;
230
- delete?(key: string): Promise<void>;
231
- withLock?<T>(key: string, fn: () => Promise<T>): Promise<T>;
232
- };
233
- ```
234
-
235
- `add` must atomically return false without changing an existing value. Optional
236
- `withLock` coordinates WSAA ticket refreshes. Explicit `wsaaSessionStore` wins
237
- for tickets when both options are provided.
238
-
239
- Keys use `arca:v1:wsaa:{environment}:{service}:{fingerprint}` and
240
- `arca:v1:attempt:{environment}:{taxId}:{idempotencyKey}`. Reservation records
241
- contain the input hash, operation, reserved coordinates and exact sent input.
242
- They contain fiscal/customer data: restrict access and protect backups.
243
-
244
- **Do not prune, expire or rewrite reservation records.** The SDK only creates
245
- them, never saves outcomes over them, and always consults ARCA on replay.
246
- Deleting a reservation can let a later retry issue another invoice.
247
-
248
- ## Invoice inputs
249
-
250
- The issuer is your legal assertion on each call; the SDK never infers it from
251
- items or Padrón. An RI issuer produces A for RI or Monotributo receivers and B
252
- for the other supported conditions. Monotributo, Exento and No Alcanzado issuers
253
- produce C and use `items: [{ amount: 10_000 }]`. ARCA validates actual eligibility.
254
- `to` is the fiscal receiver (the exact layer's receiver document/condition fields),
255
- not a customer record.
54
+ Tratá siempre los cuatro resultados:
256
55
 
257
- Amounts are integer minor units. For RI items, choose `net` or `gross` on each
258
- item and one of `0 | 2.5 | 5 | 10.5 | 21 | 27 | "exempt" | "untaxed"` for `vat`.
259
- Numeric zero is a VAT rate; exempt and untaxed amounts have separate fiscal
260
- fields. Items are grouped by rate before Round Half Even rounding.
261
-
262
- `total`, when supplied, asserts the sent total. The SDK adjusts header VAT only
263
- within one cent per emitted numeric rate, while keeping VAT non-negative.
264
- Class C totals must match exactly. Authorized results expose `computedTotal`,
265
- `sentTotal` and `vatAdjustment` in `voucher.amounts`, all in minor units.
266
-
267
- The defaults are today's date in Buenos Aires, goods (concept 1), and `ARS` at
268
- exchange rate `1`. Use `currency: "USD"` with a positive decimal-string
269
- `exchangeRate`, or `service: { from, to, dueDate }` for concept 2. Dates accept
270
- `YYYY-MM-DD` or `YYYYMMDD`; service end must be on or after its start and the payment due
271
- date must be on or after the invoice date.
272
-
273
- Non-final-consumer receivers require an 11-digit `cuit`. A final consumer accepts
274
- one `cuit` or `dni`, or neither below the identification threshold. At or above
275
- ARS 10,000,000 (including USD converted at the supplied rate), identification is
276
- required under [RG 5866/2026](https://www.argentina.gob.ar/normativa/nacional/norma-427092/texto).
277
- When the customer requests a CUIT for an income-tax deduction, supply it regardless
278
- of amount. Document shape checks do not verify provider registration.
279
-
280
- ### Facade fiscal contract
281
-
282
- Without a key, a call reads one next number and authorizes once, with at most
283
- one identity lookup after an indeterminate response. This is the v0.8 behavior.
284
- A first keyed call reserves that number before writing. A keyed replay looks
285
- up the reservation: only `not_found` allows one authorization of the stored
286
- number. A found voucher is never resubmitted. Indeterminate writes and keyed
287
- 10016 rejections can add one lookup; a 10016 without a complete match remains
288
- rejected. `issueCreditNote()` adds the original's lookup only when creating a new
289
- reservation.
290
-
291
- | Outcome | Meaning and caller action |
56
+ | Resultado | Significado y acción del llamador |
292
57
  | --- | --- |
293
- | `authorized` | Save the voucher and CAE. `recoveredByMatch: true` means the stored input matched the consulted identity; it proves consistency, not authorship. |
294
- | `rejected` | Review ARCA's `issues`. A key remains bound to its input even after rejection. |
295
- | `indeterminate` | Preserve the number and evidence. Reconcile or retry the identical input with its existing key. |
296
- | `conflict` | A different voucher occupies the reserved number. Stop and investigate. |
297
-
298
- The second argument accepts `idempotencyKey`, `representedTaxId`, `forceRefresh`
299
- and `include: { raw: true, exactInput: true }`. Outcomes are raw-free by default;
300
- `sent` is included only on authorized outcomes when requested. Replay without
301
- an observed write outcome uses an indeterminate attempt with
302
- `reason: "incomplete_response"`; the lookup provides the authorization evidence.
303
-
304
- The identity matcher compares coordinates, date, concept, receiver, currency,
305
- all header amounts, VAT rates, service dates, and note associations. Missing
306
- fields stay incomplete; differences are conflicts. Unsupported exact extensions
307
- remain incomplete. Use exact APIs for tributes, FCE, other receiver conditions,
308
- same-currency foreign cancellation and WSMTXCA.
309
-
310
- ## Exact control
311
-
312
- This example mirrors [examples/factura-b-consumidor-final.ts](./examples/factura-b-consumidor-final.ts).
313
-
314
- ```ts
315
- import { buildFacturaB, createArcaClient } from "facturas";
316
- import {
317
- ARCA_CONCEPT_TYPES,
318
- ARCA_DOCUMENT_TYPES,
319
- ARCA_RECEIVER_VAT_CONDITIONS,
320
- } from "facturas/constants";
321
-
322
- const client = createArcaClient({
323
- taxId: "20123456789",
324
- certificatePem:
325
- "-----BEGIN CERTIFICATE-----\nREPLACE_WITH_YOUR_CERTIFICATE\n-----END CERTIFICATE-----",
326
- privateKeyPem:
327
- "-----BEGIN PRIVATE KEY-----\nREPLACE_WITH_YOUR_PRIVATE_KEY\n-----END PRIVATE KEY-----",
328
- environment: "test",
329
- });
330
-
331
- const data = buildFacturaB({
332
- salesPoint: 1,
333
- concept: ARCA_CONCEPT_TYPES.PRODUCTOS,
334
- documentType: ARCA_DOCUMENT_TYPES.CONSUMIDOR_FINAL,
335
- documentNumber: 0,
336
- receiverVatConditionId: ARCA_RECEIVER_VAT_CONDITIONS.CONSUMIDOR_FINAL,
337
- // Deterministic example date; use an ARCA-allowed current date in homologation.
338
- voucherDate: "2026-09-02",
339
- taxableAmount: 10_000, // Integer minor units: ARS 100.00.
340
- vatRate: 21,
341
- // currency is omitted, so the builder defaults to ISO ARS.
342
- });
343
-
344
- // Exact layer: reserve the number yourself, then issue it exactly once.
345
- const voucherNumber = await client.wsfe.getNextVoucherNumber({
346
- salesPoint: data.salesPoint,
347
- voucherType: data.voucherType,
348
- });
349
- const issued = await client.wsfe.issue({ voucherNumber, data });
350
-
351
- if (issued.kind === "authorized") {
352
- console.log(issued.cae, issued.caeExpiry, issued.voucherNumber);
353
- }
354
- ```
355
-
356
- `buildFacturaB()` derives the Factura B type, net amount, IVA detail, IVA
357
- amount, zero-value fields, and total without floating-point tax arithmetic.
358
- `buildFacturaC()` separately builds the zero-IVA Factura C shape. Both builders
359
- accept integer currency minor units and support ISO `ARS` (the default) and
360
- `USD`. Factura B requires a positive `taxableAmount`; when `vatRate` is
361
- positive, the amount must produce at least one currency minor unit of IVA after
362
- rounding. IVA uses the Round Half Even criterion documented by ARCA, so an
363
- exact half-cent is rounded to the even cent.
364
-
365
- For a USD invoice, pass a decimal-string exchange rate:
366
-
367
- ```ts
368
- const usdData = buildFacturaB({
369
- salesPoint: 1,
370
- concept: ARCA_CONCEPT_TYPES.PRODUCTOS,
371
- documentType: ARCA_DOCUMENT_TYPES.CONSUMIDOR_FINAL,
372
- documentNumber: 0,
373
- receiverVatConditionId: ARCA_RECEIVER_VAT_CONDITIONS.CONSUMIDOR_FINAL,
374
- voucherDate: "2026-09-02",
375
- taxableAmount: 10_000, // USD 100.00.
376
- vatRate: 21,
377
- currency: "USD",
378
- exchangeRate: "1095.500000",
379
- });
380
- ```
381
-
382
- ## What You Can Do Today
383
-
384
- ### WSFE
385
-
386
- - Build A/B/C invoices from explicit assertions with the facade above
387
- - Issue invoices and credit notes with the existing exact WSFE methods
388
- - Query voucher numbers and voucher details
389
- - Read ARCA catalogs with methods like `getVoucherTypes()` and `getVatRates()`
390
- - Check backend health with `getServerStatus()`
391
-
392
- ### Padrón
393
-
394
- - Look up taxpayer data with `client.padron.getTaxpayerDetails(...)`
395
- - Resolve CUITs from document numbers with `client.padron.getTaxIdByDocument(...)`
396
-
397
- ### WSMTXCA
398
-
399
- WSMTXCA remains supported and exported, but this package currently puts most editorial focus on WSFE and Padrón. If you need `issue`, `getLastAuthorizedVoucher`, `lookupVoucher`, `getVoucher`, or `getSalesPoints`, the runtime API is available and covered by tests.
400
-
401
- ## Exact issuance and recovery evidence
402
-
403
- `client.issue()` derives the WSFE request, reserves the number and recovers
404
- after a crash for you. When you need something it does not derive (tributes,
405
- FCE, an associated period, a note in another currency or to another receiver)
406
- or your application owns the
407
- numbering, use the exact layer: `client.wsfe.issue(...)` sends one
408
- FECAESolicitar for a caller-owned, durably reserved voucher number and tells
409
- you whether that exact attempt was authorized, rejected, or left
410
- indeterminate. It preserves every structured error and observation with its
411
- service, operation, code, source, and result level.
412
-
413
- ```ts
414
- const outcome = await client.wsfe.issue({
415
- voucherNumber: reservedVoucherNumber,
416
- data,
417
- });
418
-
419
- if (outcome.kind === "authorized") {
420
- console.log(outcome.cae, outcome.voucherNumber);
421
- } else if (outcome.kind === "rejected") {
422
- console.error(outcome.errors, outcome.observations);
423
- } else {
424
- if (outcome.reason === "authentication_rejected") {
425
- console.error(outcome.authentication?.reason);
426
- }
427
- // Consult the same number before any new authorization attempt.
428
- const lookup = await client.wsfe.lookupVoucher({
429
- number: reservedVoucherNumber,
430
- salesPoint: data.salesPoint,
431
- voucherType: data.voucherType,
432
- });
433
- console.log(lookup.kind);
434
- }
435
- ```
436
-
437
- `wsfe.issue()` and `wsmtxca.issue()` force one SOAP transport attempt, even when the client has general transport retries configured. They never refresh credentials and resubmit automatically. An explicit provider authentication rejection is returned as `reason: "authentication_rejected"` with safe typed `authentication` evidence; a timeout, connection failure, invalid response, or incomplete/contradictory result remains indeterminate without resubmission. This prevents uncertain fiscal work from causing a hidden second authorization.
438
-
439
- Authenticated read, catalog, and lookup operations may repeat once with a
440
- forced credential refresh after an explicit typed authentication rejection.
441
- Passing `forceRefresh: true` disables any further authentication recovery
442
- attempt.
443
-
444
- Exact lookup absence is operation-specific:
445
-
446
- - WSFE `FECompConsultar` code 602 returns `not_found`.
447
- - WSMTXCA `consultarComprobante` code 1503 returns `not_found`.
448
- - WSMTXCA `consultarUltimoComprobanteAutorizado` code 1502 returns voucher number `0`.
449
- - WSMTXCA code 602 is not exact-voucher absence and remains an error.
450
-
451
- The SDK normalizes provider protocol evidence only. Your application remains responsible for persisting the exact request, owning its sequence or lane, and deciding when a retry is safe.
452
-
453
- For tributes, notes, FCE, or other advanced cases, use the exact
454
- `WsfeVoucherInput` escape hatch. It also continues to support exemptions,
455
- non-taxable amounts and multiple IVA rates. Exact
456
- amounts remain major-unit numbers, are validated locally, and are serialized as
457
- canonical two-decimal strings:
458
-
459
- ```ts
460
- import type { WsfeVoucherInput } from "facturas/wsfe";
461
- import {
462
- ARCA_CONCEPT_TYPES,
463
- ARCA_CURRENCY_IDS,
464
- ARCA_DOCUMENT_TYPES,
465
- ARCA_RECEIVER_VAT_CONDITIONS,
466
- ARCA_VAT_RATES,
467
- ARCA_VOUCHER_TYPES,
468
- } from "facturas/constants";
469
-
470
- const exactData: WsfeVoucherInput = {
471
- salesPoint: 1,
472
- voucherType: ARCA_VOUCHER_TYPES.FACTURA_B,
473
- concept: ARCA_CONCEPT_TYPES.PRODUCTOS,
474
- documentType: ARCA_DOCUMENT_TYPES.CONSUMIDOR_FINAL,
475
- documentNumber: 0,
476
- receiverVatConditionId: ARCA_RECEIVER_VAT_CONDITIONS.CONSUMIDOR_FINAL,
477
- voucherDate: "2026-09-02",
478
- totalAmount: 121,
479
- nonTaxableAmount: 0,
480
- netAmount: 100,
481
- exemptAmount: 0,
482
- taxAmount: 0,
483
- vatAmount: 21,
484
- currencyId: ARCA_CURRENCY_IDS.ARS,
485
- exchangeRate: "1",
486
- vatRates: [{ id: ARCA_VAT_RATES.IVA_21, baseAmount: 100, amount: 21 }],
487
- };
488
- ```
489
-
490
- Exact inputs and live catalog responses use ARCA protocol identifiers such as
491
- `PES` and `DOL`; the facade and high-level builders accept ISO `ARS` and `USD`.
492
-
493
- ### Migrating amount and currency inputs
494
-
495
- Applications that previously rounded decimal major-unit values and translated
496
- currencies to ARCA IDs can move that provider-boundary work into a builder:
497
-
498
- ```ts
499
- // Before: caller-owned decimal rounding and provider vocabulary.
500
- const exactAmount = Number(sourceAmount.toFixed(2));
501
- const exactCurrencyId = sourceCurrency === "ARS" ? "PES" : "DOL";
502
-
503
- // After: integer minor units and ISO currency input.
504
- const data = buildFacturaB({
505
- salesPoint: 1,
506
- concept: ARCA_CONCEPT_TYPES.PRODUCTOS,
507
- documentType: ARCA_DOCUMENT_TYPES.CONSUMIDOR_FINAL,
508
- documentNumber: 0,
509
- receiverVatConditionId: ARCA_RECEIVER_VAT_CONDITIONS.CONSUMIDOR_FINAL,
510
- voucherDate: "2026-09-02",
511
- taxableAmount: 10_000, // 100.00 in the selected currency.
512
- vatRate: 21,
513
- currency: "USD",
514
- exchangeRate: "1095.5",
515
- });
516
- ```
517
-
518
- Keep using `WsfeVoucherInput` when you need advanced exact fields. Its amounts
519
- remain decimal major-unit values and its `currencyId` remains an ARCA ID.
520
-
521
- ## Examples
522
-
523
- - [Keyed invoice](./examples/issue-invoice.ts)
524
- - [Preview before issuing](./examples/preview.ts)
525
- - [Partial credit note](./examples/nota-de-credito-parcial.ts)
526
- - [Full credit note](./examples/nota-de-credito-total.ts)
527
-
528
- Examples live in [examples/](./examples) and are intentionally complete, hardcoded, and readable so they can be adapted quickly by a developer or a coding agent.
529
- Issuance examples use deterministic dates for compilation; replace them with an
530
- ARCA-allowed current date before a homologation request.
531
-
532
- - [factura-b-consumidor-final.ts](./examples/factura-b-consumidor-final.ts)
533
- - [factura-a-responsable-inscripto.ts](./examples/factura-a-responsable-inscripto.ts)
534
- - [nota-de-credito-asociada.ts](./examples/nota-de-credito-asociada.ts)
535
- - [factura-servicios-con-periodo.ts](./examples/factura-servicios-con-periodo.ts)
536
- - [consultar-comprobante.ts](./examples/consultar-comprobante.ts)
537
- - [consultar-contribuyente.ts](./examples/consultar-contribuyente.ts)
538
-
539
- ## Manual Setup Reality
540
-
541
- This package does **not** provision ARCA credentials for you. You still need to do the official certificate and service setup outside the SDK.
542
-
543
- Before using the SDK:
544
-
545
- 1. Obtain a valid CUIT.
546
- 2. Generate or receive a certificate and matching private key in PEM format.
547
- 3. Authorize the certificate for the target service and environment.
548
- 4. Start with `environment: "test"` and move to production only after end-to-end validation.
58
+ | `authorized` | Guardá el comprobante y el CAE. `recoveredByMatch: true` significa que el input guardado coincidió con la identidad consultada; prueba consistencia, no autoría. |
59
+ | `rejected` | Revisá los `issues` de ARCA. Una clave queda ligada a su input incluso después de un rechazo. |
60
+ | `indeterminate` | Conservá el número y la evidencia. Conciliá o repetí el input idéntico con su clave existente. |
61
+ | `conflict` | Hay otro comprobante en el número reservado. Detené el flujo e investigá. |
549
62
 
550
- Official ARCA / AFIP references:
63
+ El paso a paso está en [Inicio rápido](./docs/inicio-rapido.md); el detalle, en
64
+ [Facturas](./docs/facturas.md).
551
65
 
552
- - [WSAA documentation](https://www.afip.gob.ar/ws/documentacion/wsaa.asp)
553
- - [Certificates for testing / homologation](https://www.afip.gob.ar/ws/documentacion/certificados.asp)
554
- - [WSAA developer manual](https://www.afip.gob.ar/ws/WSAA/WSAAmanualDev.pdf)
555
- - [WSASS service onboarding](https://www.afip.gob.ar/ws/WSASS/WSASS_como_adherirse.pdf)
556
- - [WSFE developer manual](https://www.afip.gob.ar/ws/documentacion/manuales/manual-desarrollador-ARCA-COMPG.pdf)
66
+ ## Reintentos seguros
557
67
 
558
- ## Reference Data
559
-
560
- The package exports a small, stable set of common ARCA codes from `facturas/constants`.
68
+ Recomendado en toda aplicación real, opcional para empezar. Sin
69
+ `idempotencyKey`, un reintento después de una caída puede emitir la factura dos
70
+ veces. Con un `store` y el ID estable de la venta como clave, el reintento
71
+ consulta el número reservado y nunca vuelve a emitir.
561
72
 
562
73
  ```ts
563
- import {
564
- ARCA_CONCEPT_TYPES,
565
- ARCA_CURRENCY_IDS,
566
- ARCA_CURRENCIES,
567
- ARCA_DOCUMENT_TYPES,
568
- ARCA_RECEIVER_VAT_CONDITIONS,
569
- ARCA_VAT_RATES,
570
- ARCA_VOUCHER_TYPES,
571
- ISO_CURRENCIES,
572
- } from "facturas/constants";
573
-
574
- ARCA_VOUCHER_TYPES.FACTURA_A; // 1
575
- ARCA_VOUCHER_TYPES.FACTURA_B; // 6
576
- ARCA_DOCUMENT_TYPES.CUIT; // 80
577
- ARCA_DOCUMENT_TYPES.DNI; // 96
578
- ARCA_DOCUMENT_TYPES.CONSUMIDOR_FINAL; // 99
579
- ARCA_RECEIVER_VAT_CONDITIONS.RESPONSABLE_INSCRIPTO; // 1
580
- ARCA_RECEIVER_VAT_CONDITIONS.CONSUMIDOR_FINAL; // 5
581
- ARCA_CONCEPT_TYPES.SERVICIOS; // 2
582
- ARCA_VAT_RATES.IVA_21; // 5
583
- ISO_CURRENCIES.ARS; // "ARS"
584
- ARCA_CURRENCY_IDS.USD; // "DOL"
585
- ARCA_CURRENCIES.PES; // "PES"
586
- ARCA_CURRENCIES.DOL; // "DOL"
587
- ```
588
-
589
- The constants cover the most common values used by the README and examples:
590
-
591
- - voucher types for invoice A/B/C, debit note A/B/C, and credit note A/B/C
592
- - document types for CUIT, DNI, and final consumers
593
- - common receiver IVA conditions, subject to voucher-class and live-catalog rules
594
- - concept types for products, services, and products + services
595
- - IVA rates for `0`, `2.5`, `5`, `10.5`, `21`, and `27`
596
- - builder ISO currencies `ARS` and `USD`, with explicit ARCA mappings to `PES`
597
- and `DOL`
598
-
599
- `ARCA_CURRENCIES` remains a deprecated compatibility alias with its existing
600
- `PES` and `DOL` values. If you need broader catalogs at runtime, WSFE methods
601
- such as `getVoucherTypes()`, `getDocumentTypes()`, `getCurrencyTypes()`, and
602
- `getVatRates()` are still available. `getCurrencyTypes()` returns live ARCA
603
- identifiers, not ISO codes.
604
-
605
- ## Configuration
606
-
607
- ### Environment variables
608
-
609
- `createArcaClient()` discovers missing fields using the same rules as
610
- `createArcaClientConfigFromEnv()`. Explicit fields win; `process.env` is not changed:
611
-
612
- | Variable | Required | Notes |
613
- | --- | --- | --- |
614
- | `ARCA_TAX_ID` | Yes | 11-digit CUIT |
615
- | `ARCA_CERTIFICATE_PEM` | Yes | PEM certificate |
616
- | `ARCA_PRIVATE_KEY_PEM` | Yes | PEM private key |
617
- | `ARCA_ENVIRONMENT` | Yes | `test` or `production`; there is no default |
618
-
619
- For logging without code changes, set `ARCA_LOG_LEVEL` to `debug`, `info`, `warn`, or `error`.
620
-
621
- Pass a config object to `createArcaClient`:
622
-
623
- ```ts
624
- import { createArcaClient } from "facturas";
625
-
626
- const client = createArcaClient({
627
- taxId: "20123456789",
628
- certificatePem: process.env.ARCA_CERTIFICATE_PEM!,
629
- privateKeyPem: process.env.ARCA_PRIVATE_KEY_PEM!,
630
- environment: "test",
631
- timeout: 30_000,
632
- retries: 2,
633
- retryDelay: 500,
634
- logger: { level: "debug" },
635
- // Optional: share WSAA login tickets across workers.
636
- // wsaaSessionStore,
637
- });
638
- ```
639
-
640
- | Field | Default | Description |
641
- | --- | --- | --- |
642
- | `taxId` | `ARCA_TAX_ID` | 11-digit CUIT |
643
- | `certificatePem` | `ARCA_CERTIFICATE_PEM` | PEM certificate |
644
- | `privateKeyPem` | `ARCA_PRIVATE_KEY_PEM` | PEM private key |
645
- | `environment` | `test` | `test` or `production` |
646
- | `timeout` | `30000` | HTTP request timeout in milliseconds |
647
- | `retries` | `0` | Extra attempts after transport failures only |
648
- | `retryDelay` | `500` | Delay between transport retries in milliseconds |
649
- | `logger` | — | Optional structured logger config |
650
- | `store` | — | Unified durable tickets and reservations |
651
- | `wsaaSessionStore` | — | Optional WSAA ticket store for multi-worker deployments |
652
-
653
- ### WSAA session stores
654
-
655
- By default, WSAA login tickets are cached in the current process only. That keeps scripts and single-process apps zero-config:
74
+ import { createArcaClient, createPostgresStore } from "facturas";
75
+ import { sql } from "@vercel/postgres";
656
76
 
657
- ```ts
658
- const client = createArcaClient({
659
- taxId: "20123456789",
660
- certificatePem: process.env.ARCA_CERTIFICATE_PEM!,
661
- privateKeyPem: process.env.ARCA_PRIVATE_KEY_PEM!,
662
- environment: "production",
77
+ const arca = createArcaClient({
78
+ store: createPostgresStore({ query: (text, params) => sql.query(text, params) }),
663
79
  });
664
- ```
665
-
666
- A configured `store` supplies durable WSAA tickets automatically. An explicit
667
- `wsaaSessionStore` remains supported and takes precedence for tickets only.
668
-
669
- ```ts
670
- import {
671
- type ArcaAuthCredentials,
672
- type ArcaWsaaSessionKey,
673
- createArcaClient,
674
- } from "facturas";
675
80
 
676
- const wsaaSessionStore = {
677
- async get(key: ArcaWsaaSessionKey): Promise<ArcaAuthCredentials | null> {
678
- // Read from Postgres, Redis, or another shared store.
679
- return null;
680
- },
681
- async set(
682
- key: ArcaWsaaSessionKey,
683
- credentials: ArcaAuthCredentials
684
- ): Promise<void> {
685
- // Persist token, sign, and expiresAt for the key.
686
- },
687
- async withLock<T>(
688
- key: ArcaWsaaSessionKey,
689
- fn: () => Promise<T>
690
- ): Promise<T> {
691
- // Optional but recommended: serialize cold-start refreshes.
692
- return await fn();
693
- },
694
- };
695
-
696
- const client = createArcaClient({
697
- taxId: "20123456789",
698
- certificatePem: process.env.ARCA_CERTIFICATE_PEM!,
699
- privateKeyPem: process.env.ARCA_PRIVATE_KEY_PEM!,
700
- environment: "production",
701
- wsaaSessionStore,
702
- });
81
+ const factura = await arca.issue(input, { idempotencyKey: venta.id });
703
82
  ```
704
83
 
705
- The store key is scoped by environment, WSAA service, and certificate fingerprint. Store reads are still checked with the SDK's expiration safety margin. A production store should share data across all workers, encrypt or rely on encrypted storage, enforce expiration on read, and implement locking with advisory locks, Redis locks, or equivalent.
84
+ Hay adaptadores para Postgres, Redis, archivos y memoria, y podés escribir el
85
+ tuyo. Ver [Stores](./docs/stores.md).
706
86
 
707
- For tests and local coordination through one shared object, the package also exports `createMemoryWsaaSessionStore()`.
87
+ ## Nota de crédito
708
88
 
709
- ## Logging
710
-
711
- Default minimum level is `warn`. At `debug`, the SDK logs SOAP requests, response timings, WSAA login source (`cached` vs `fresh`), and retry attempts.
89
+ ARCA no anula comprobantes: una corrección es una nota de crédito y también es
90
+ un documento real que queda en los registros de ARCA. Lo habitual es la nota
91
+ parcial, que acredita las líneas que elegís.
712
92
 
713
93
  ```ts
714
- const client = createArcaClient({
715
- taxId: "20123456789",
716
- certificatePem: "...",
717
- privateKeyPem: "...",
718
- environment: "test",
719
- logger: { level: "debug" },
720
- });
721
- ```
722
-
723
- Custom logger sinks receive `(level, message, ...args)`:
724
-
725
- ```ts
726
- const client = createArcaClient({
727
- taxId: "20123456789",
728
- certificatePem: "...",
729
- privateKeyPem: "...",
730
- environment: "production",
731
- logger: {
732
- level: "info",
733
- log(level, message, ...args) {
734
- // forward to your logger
735
- },
94
+ const nota = await arca.issueCreditNote(
95
+ {
96
+ for: { salesPoint: 3, voucherType: 11, number: 41 },
97
+ items: [{ amount: 50_000 }], // ARS 500,00 de una factura de ARS 1.500,00.
736
98
  },
737
- });
738
- ```
739
-
740
- Disable logging entirely with `logger: { disabled: true }`.
741
-
742
- ## Retries and timeouts
743
-
744
- Configured transport retries apply only to `ArcaTransportError`: timeouts, connection failures, and non-XML HTTP error responses. XML responses, including HTTP 500 SOAP faults, are parsed and surfaced as SOAP or service errors instead of being retried blindly.
745
-
746
- Separately, authenticated WSFE and WSMTXCA convenience operations may perform
747
- one forced-refresh retry only after `ArcaAuthenticationError`. Timeouts,
748
- connection loss, invalid SOAP, incomplete evidence, contradictory evidence, and
749
- generic service rejections never unlock this recovery path. `wsfe.issue()` and
750
- `wsmtxca.issue()` always perform one exact authorization attempt, and each
751
- authorization SOAP attempt has transport retries set to zero.
752
-
753
- ## Service Surface
754
-
755
- ### `client.wsfe`
756
-
757
- WSFE electronic invoicing. Inputs use JS-style names and the SDK maps them to AFIP / ARCA SOAP fields internally.
758
-
759
- - Date fields accept `YYYY-MM-DD` or `YYYYMMDD`.
760
- - `issue({ voucherNumber, data })` sends one exact authorization and returns `authorized`, `rejected` or `indeterminate` evidence.
761
- - `getNextVoucherNumber({ salesPoint, voucherType })` reads the next number to reserve.
762
- - `getVoucherInfo({ number, salesPoint, voucherType })` returns voucher details or `null`.
763
- - Catalog methods are available for live reference data when you do not want to hardcode values.
764
- - Authenticated methods accept `forceRefresh: true` to discard the cached WSAA TA and request a fresh Token Authorization for the same service.
765
-
766
- ### `client.padron`
767
-
768
- - `getTaxpayerDetails(taxId)` returns taxpayer data or `null`
769
- - `getTaxIdByDocument(documentNumber)` returns CUIT candidates or `null`
770
-
771
- Padron "not found" handling currently depends on SOAP fault message text from ARCA and is therefore more fragile than WSFE code-based flows.
772
-
773
- ### `client.wsmtxca`
774
-
775
- - `issue({ data })`
776
- - `getLastAuthorizedVoucher({ voucherType, salesPoint })`
777
- - `getVoucher({ voucherType, salesPoint, voucherNumber })`
778
- - Authenticated methods accept `forceRefresh: true` to renew the WSMTXCA WSAA TA before the call.
779
-
780
- The runtime support is stable and public. It is simply not the main documentation path in this SDK-focused pass.
781
-
782
- ## Error handling
783
-
784
- All errors extend `ArcaError` and expose a stable `code` string.
785
-
786
- | Class | When |
787
- | --- | --- |
788
- | `ArcaConfigurationError` | Invalid client config |
789
- | `ArcaInputError` | Invalid caller input such as a malformed date |
790
- | `ArcaAuthenticationError` | Explicit provider authentication rejection |
791
- | `ArcaTransportError` | HTTP or transport failure |
792
- | `ArcaSoapFaultError` | SOAP fault returned by ARCA |
793
- | `ArcaServiceError` | Business-level service rejection, especially WSFE-style errors |
794
-
795
- ```ts
796
- import {
797
- ArcaAuthenticationError,
798
- ArcaServiceError,
799
- ArcaSoapFaultError,
800
- ArcaTransportError,
801
- } from "facturas";
802
-
803
- try {
804
- await client.wsfe.getNextVoucherNumber({ salesPoint: 1, voucherType: 6 });
805
- } catch (error) {
806
- if (error instanceof ArcaAuthenticationError) {
807
- console.error(
808
- error.reason,
809
- error.service,
810
- error.operation,
811
- error.providerCode
812
- );
813
- } else if (error instanceof ArcaServiceError) {
814
- console.error(error.serviceCode, error.message);
815
- } else if (error instanceof ArcaSoapFaultError) {
816
- console.error(error.faultCode, error.message);
817
- } else if (error instanceof ArcaTransportError) {
818
- console.error(error.statusCode, error.message);
819
- }
820
- throw error;
821
- }
822
- ```
823
-
824
- Import error classes from `facturas` or `facturas/errors`.
825
- `isArcaAuthenticationError(error)` is also exported for predicate-style
826
- routing. Authentication errors expose only the stable code
827
- `ARCA_AUTHENTICATION_ERROR`, a typed `reason`, service, operation, and a safe
828
- provider code when available; raw provider bodies and credential values are not
829
- attached.
830
-
831
- ## Troubleshooting
832
-
833
- - `coe.alreadyAuthenticated`: the SDK deduplicates in-flight WSAA logins and reuses valid cached tickets. In serverless, queue workers, or any multi-process deployment, configure a durable `wsaaSessionStore` so cold workers can reuse the TA obtained by another process. Memory-only caching cannot recover across processes.
834
- - `dh key too small`: WSFE production requests already use a legacy OpenSSL security level where needed. If you still see this, confirm you are not bypassing the SDK transport or terminating TLS in another layer.
835
- - Expired certificate: replace the PEM certificate with a renewed one that matches the same private key expectations, then redeploy or restart the process.
836
- - Unauthorized service: your certificate may be valid but not authorized for the target service or environment. Re-check WSASS / homologation setup for test and service relationships for production.
837
- - WSFE `10015`: usually means the `DocTipo` / `DocNro` combination is inconsistent for the voucher type and amount. For example, Factura B has special receiver-document rules depending on the total amount.
838
- - WSFE `10016`: the voucher number sent in `CbteDesde` is not the next valid one for that point of sale and voucher type. Call `getNextVoucherNumber()` immediately before authorizing when your numbering may have moved.
839
-
840
- When an error is unclear, check these in order:
841
-
842
- 1. Certificate and private key match.
843
- 2. Environment is correct (`test` vs `production`).
844
- 3. Service authorization was done for that environment.
845
- 4. The voucher type, document type, and amount combination is valid.
846
- 5. Your process is not reusing stale assumptions about the next voucher number.
847
-
848
- ## Public API (semver)
849
-
850
- Documented entrypoints:
851
-
852
- - `facturas`
853
- - `facturas/constants`
854
- - `facturas/wsfe`
855
- - `facturas/wsmtxca`
856
- - `facturas/padron`
857
- - `facturas/errors`
858
- - `facturas/types`
859
-
860
- Low-level SOAP, HTTP, and WSAA internals are not part of the semver contract.
861
-
862
- Subpath example:
863
-
864
- ```ts
865
- import { createWsfeService } from "facturas/wsfe";
866
- import { ARCA_VOUCHER_TYPES } from "facturas/constants";
867
- import { ArcaServiceError } from "facturas/errors";
868
- ```
869
-
870
- ## Security
871
-
872
- - Treat certificates and private keys as secrets.
873
- - By default, WSAA tickets are cached in memory only.
874
- - The SDK persists WSAA tickets when you provide `store` or `wsaaSessionStore`. Keep their storage private; certificate and private-key configuration is never stored by the bundled adapters.
875
- - Production `wsaaSessionStore` implementations should encrypt credentials at rest or use a backend that provides encryption at rest.
876
-
877
- ## Development
878
-
879
- ```bash
880
- pnpm install
881
- pnpm typecheck
882
- pnpm typecheck:examples
883
- pnpm test
884
- pnpm test:coverage
885
- pnpm pack:check
99
+ { idempotencyKey: `nc:${devolucion.id}` },
100
+ );
886
101
  ```
887
102
 
888
- Optional for local DX: install Turbo globally with `pnpm add --global turbo`. The repo scripts still use the local workspace version.
889
-
890
- ## License
103
+ Con `all: true` acreditás el original completo. El modo es explícito y
104
+ obligatorio. La misma fachada emite [notas de débito y por
105
+ período](./docs/notas-de-credito.md), tributos, FCE, [detalle de ítems por
106
+ WSMTXCA](./docs/wsmtxca.md) y `recover()`, que concilia sin emitir.
107
+
108
+ ## Documentación
109
+
110
+ - [Inicio rápido](./docs/inicio-rapido.md): de cero a la primera factura y su
111
+ nota de crédito.
112
+ - [Habilitación en ARCA](./docs/habilitacion-arca.md): CUIT, certificado, punto
113
+ de venta y referencias oficiales.
114
+ - [CLI](./docs/cli.md): `init`, `check` e `issue`, con la tabla de diagnósticos.
115
+ - [Facturas](./docs/facturas.md): `issue()`, `preview()`, datos de la factura y
116
+ contrato fiscal de la fachada.
117
+ - [Notas de crédito](./docs/notas-de-credito.md): `issueCreditNote()`, modo
118
+ parcial y modo total.
119
+ - [Stores](./docs/stores.md): Postgres, Redis, archivos, memoria, store propio
120
+ y vida de los registros.
121
+ - [Configuración](./docs/configuracion.md): variables de entorno, opciones del
122
+ cliente, sesiones WSAA, logging, reintentos y timeouts.
123
+ - [Capa exacta](./docs/capa-exacta.md): builders, superficie de servicios,
124
+ emisión exacta y evidencia de recuperación.
125
+ - [Errores](./docs/errores.md): clases de error y diagnóstico.
126
+ - [Referencia](./docs/referencia.md): constantes, API pública con semver y
127
+ seguridad.
128
+ - [Ejemplos](./docs/ejemplos.md): índice de [examples/](./examples).
129
+
130
+ ## Estado del proyecto
131
+
132
+ Pre-1.0. Mientras la versión empiece en `0.`, un minor puede cambiar o quitar
133
+ partes de la API pública. Fijá la versión exacta y leé el
134
+ [changelog](./packages/arca/CHANGELOG.md) antes de actualizar.
135
+
136
+ Para contribuir, mirá [CONTRIBUTING.md](./CONTRIBUTING.md).
137
+
138
+ ## Licencia
891
139
 
892
140
  Apache-2.0