arca-sdk 0.1.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/dist/index.js ADDED
@@ -0,0 +1,725 @@
1
+ // src/constants/endpoints.ts
2
+ var WSAA_ENDPOINTS = {
3
+ homologacion: "https://wsaahomo.afip.gov.ar/ws/services/LoginCms",
4
+ produccion: "https://wsaa.afip.gov.ar/ws/services/LoginCms"
5
+ };
6
+ var WSFE_ENDPOINTS = {
7
+ homologacion: "https://wswhomo.afip.gov.ar/wsfev1/service.asmx",
8
+ produccion: "https://servicios1.afip.gov.ar/wsfev1/service.asmx"
9
+ };
10
+ function getWsaaEndpoint(environment) {
11
+ return WSAA_ENDPOINTS[environment];
12
+ }
13
+ function getWsfeEndpoint(environment) {
14
+ return WSFE_ENDPOINTS[environment];
15
+ }
16
+
17
+ // src/types/common.ts
18
+ var ArcaError = class extends Error {
19
+ constructor(message, code, details) {
20
+ super(message);
21
+ this.code = code;
22
+ this.details = details;
23
+ this.name = "ArcaError";
24
+ }
25
+ };
26
+ var ArcaAuthError = class extends ArcaError {
27
+ constructor(message, details) {
28
+ super(message, "AUTH_ERROR", details);
29
+ this.name = "ArcaAuthError";
30
+ }
31
+ };
32
+ var ArcaValidationError = class extends ArcaError {
33
+ constructor(message, details) {
34
+ super(message, "VALIDATION_ERROR", details);
35
+ this.name = "ArcaValidationError";
36
+ }
37
+ };
38
+
39
+ // src/utils/xml.ts
40
+ import { XMLBuilder, XMLParser } from "fast-xml-parser";
41
+ function buildTRA(service, cuit) {
42
+ const now = /* @__PURE__ */ new Date();
43
+ const expirationTime = new Date(now.getTime() + 12 * 60 * 60 * 1e3);
44
+ const tra = {
45
+ loginTicketRequest: {
46
+ "@_version": "1.0",
47
+ header: {
48
+ uniqueId: Math.floor(now.getTime() / 1e3),
49
+ generationTime: now.toISOString(),
50
+ expirationTime: expirationTime.toISOString()
51
+ },
52
+ service
53
+ }
54
+ };
55
+ const builder = new XMLBuilder({
56
+ ignoreAttributes: false,
57
+ format: true
58
+ });
59
+ const xml = builder.build(tra);
60
+ return `<?xml version="1.0" encoding="UTF-8"?>
61
+ ${xml}`;
62
+ }
63
+ function parseWsaaResponse(xml) {
64
+ try {
65
+ const parser = new XMLParser({
66
+ ignoreAttributes: false,
67
+ parseAttributeValue: true,
68
+ removeNSPrefix: true
69
+ // Sugerencia: más robusto contra cambios de prefijos soapenv
70
+ });
71
+ const result = parser.parse(xml);
72
+ const credentials = result?.Envelope?.Body?.loginCmsResponse?.loginCmsReturn;
73
+ if (!credentials) {
74
+ const fault = result?.Envelope?.Body?.Fault;
75
+ if (fault) {
76
+ throw new ArcaAuthError(
77
+ `Error ARCA: ${fault.faultstring || "Error desconocido"}`,
78
+ { faultCode: fault.faultcode, detail: fault.detail }
79
+ );
80
+ }
81
+ throw new ArcaAuthError(
82
+ "Respuesta WSAA inv\xE1lida: estructura no reconocida",
83
+ { receivedStructure: result }
84
+ );
85
+ }
86
+ return {
87
+ token: credentials.token,
88
+ sign: credentials.sign,
89
+ generationTime: new Date(credentials.header.generationTime),
90
+ expirationTime: new Date(credentials.header.expirationTime)
91
+ };
92
+ } catch (error) {
93
+ if (error instanceof ArcaAuthError) throw error;
94
+ throw new ArcaAuthError(
95
+ "Error al parsear respuesta WSAA",
96
+ { originalError: error }
97
+ );
98
+ }
99
+ }
100
+ function parseXml(xml) {
101
+ const parser = new XMLParser({
102
+ ignoreAttributes: false,
103
+ parseAttributeValue: true,
104
+ removeNSPrefix: true
105
+ });
106
+ return parser.parse(xml);
107
+ }
108
+ function validateCUIT(cuit) {
109
+ return /^\d{11}$/.test(cuit);
110
+ }
111
+
112
+ // src/utils/crypto.ts
113
+ import * as forge from "node-forge";
114
+ function signCMS(xml, certPem, keyPem) {
115
+ try {
116
+ const cert = forge.pki.certificateFromPem(certPem);
117
+ const privateKey = forge.pki.privateKeyFromPem(keyPem);
118
+ const p7 = forge.pkcs7.createSignedData();
119
+ p7.content = forge.util.createBuffer(xml, "utf8");
120
+ p7.addCertificate(cert);
121
+ p7.addSigner({
122
+ key: privateKey,
123
+ certificate: cert,
124
+ digestAlgorithm: forge.pki.oids.sha256,
125
+ authenticatedAttributes: [
126
+ {
127
+ type: forge.pki.oids.contentType,
128
+ value: forge.pki.oids.data
129
+ },
130
+ {
131
+ type: forge.pki.oids.messageDigest
132
+ // El valor será calculado automáticamente
133
+ },
134
+ {
135
+ type: forge.pki.oids.signingTime,
136
+ // node-forge expects a Date object, but its typings might be missing or expect string/any
137
+ value: /* @__PURE__ */ new Date()
138
+ }
139
+ ]
140
+ });
141
+ p7.sign();
142
+ const derBytes = forge.asn1.toDer(p7.toAsn1()).getBytes();
143
+ const base64 = forge.util.encode64(derBytes);
144
+ return base64;
145
+ } catch (error) {
146
+ if (error instanceof ArcaAuthError) throw error;
147
+ throw new ArcaAuthError(
148
+ "Error al firmar XML con certificado usando PKCS#7",
149
+ {
150
+ originalError: error,
151
+ hint: "Verificar que el certificado y la clave privada sean v\xE1lidos y correspondan entre s\xED"
152
+ }
153
+ );
154
+ }
155
+ }
156
+ function validateCertificate(cert) {
157
+ return cert.includes("-----BEGIN CERTIFICATE-----") && cert.includes("-----END CERTIFICATE-----");
158
+ }
159
+ function validatePrivateKey(key) {
160
+ return key.includes("-----BEGIN PRIVATE KEY-----") && key.includes("-----END PRIVATE KEY-----") || key.includes("-----BEGIN RSA PRIVATE KEY-----") && key.includes("-----END RSA PRIVATE KEY-----");
161
+ }
162
+
163
+ // src/auth/ticket.ts
164
+ var TicketManager = class {
165
+ ticket = null;
166
+ /**
167
+ * Guarda un ticket en cache
168
+ */
169
+ setTicket(ticket) {
170
+ this.ticket = ticket;
171
+ }
172
+ /**
173
+ * Obtiene el ticket actual si es válido
174
+ * @returns Ticket válido o null si expiró
175
+ */
176
+ getTicket() {
177
+ if (!this.ticket) return null;
178
+ const now = /* @__PURE__ */ new Date();
179
+ const expiresIn = this.ticket.expirationTime.getTime() - now.getTime();
180
+ const BUFFER_MS = 5 * 60 * 1e3;
181
+ if (expiresIn < BUFFER_MS) {
182
+ this.ticket = null;
183
+ return null;
184
+ }
185
+ return this.ticket;
186
+ }
187
+ /**
188
+ * Verifica si hay un ticket válido
189
+ */
190
+ hasValidTicket() {
191
+ return this.getTicket() !== null;
192
+ }
193
+ /**
194
+ * Limpia el ticket en cache
195
+ */
196
+ clearTicket() {
197
+ this.ticket = null;
198
+ }
199
+ };
200
+
201
+ // src/auth/wsaa.ts
202
+ var WsaaService = class {
203
+ config;
204
+ ticketManager;
205
+ constructor(config) {
206
+ this.validateConfig(config);
207
+ this.config = config;
208
+ this.ticketManager = new TicketManager();
209
+ }
210
+ /**
211
+ * Valida la configuración
212
+ */
213
+ validateConfig(config) {
214
+ if (!validateCUIT(config.cuit)) {
215
+ throw new ArcaValidationError(
216
+ "CUIT inv\xE1lido: debe tener 11 d\xEDgitos sin guiones",
217
+ { cuit: config.cuit }
218
+ );
219
+ }
220
+ if (!validateCertificate(config.cert)) {
221
+ throw new ArcaValidationError(
222
+ "Certificado inv\xE1lido: debe estar en formato PEM",
223
+ { hint: "Debe contener -----BEGIN CERTIFICATE-----" }
224
+ );
225
+ }
226
+ if (!validatePrivateKey(config.key)) {
227
+ throw new ArcaValidationError(
228
+ "Clave privada inv\xE1lida: debe estar en formato PEM",
229
+ { hint: "Debe contener -----BEGIN PRIVATE KEY----- o -----BEGIN RSA PRIVATE KEY-----" }
230
+ );
231
+ }
232
+ if (!config.service || config.service.trim() === "") {
233
+ throw new ArcaValidationError(
234
+ "Servicio ARCA no especificado",
235
+ { hint: 'Ejemplos: "wsfe", "wsmtxca"' }
236
+ );
237
+ }
238
+ }
239
+ /**
240
+ * Obtiene un ticket de acceso válido
241
+ * Usa cache si el ticket actual es válido
242
+ *
243
+ * @returns Ticket de acceso
244
+ */
245
+ async login() {
246
+ const cachedTicket = this.ticketManager.getTicket();
247
+ if (cachedTicket) {
248
+ return cachedTicket;
249
+ }
250
+ const ticket = await this.requestNewTicket();
251
+ this.ticketManager.setTicket(ticket);
252
+ return ticket;
253
+ }
254
+ /**
255
+ * Solicita un nuevo ticket a WSAA
256
+ */
257
+ async requestNewTicket() {
258
+ const tra = buildTRA(this.config.service, this.config.cuit);
259
+ let cms;
260
+ try {
261
+ cms = signCMS(tra, this.config.cert, this.config.key);
262
+ } catch (error) {
263
+ throw new ArcaAuthError(
264
+ "Error al firmar TRA con certificado",
265
+ { originalError: error }
266
+ );
267
+ }
268
+ const endpoint = getWsaaEndpoint(this.config.environment);
269
+ const response = await fetch(endpoint, {
270
+ method: "POST",
271
+ headers: {
272
+ "Content-Type": "text/xml; charset=utf-8",
273
+ "SOAPAction": ""
274
+ },
275
+ body: this.buildSoapRequest(cms)
276
+ });
277
+ if (!response.ok) {
278
+ throw new ArcaAuthError(
279
+ `Error HTTP al comunicarse con WSAA: ${response.status} ${response.statusText}`,
280
+ { status: response.status, statusText: response.statusText }
281
+ );
282
+ }
283
+ const responseXml = await response.text();
284
+ return parseWsaaResponse(responseXml);
285
+ }
286
+ /**
287
+ * Construye el SOAP request para WSAA
288
+ */
289
+ buildSoapRequest(cms) {
290
+ return `<?xml version="1.0" encoding="UTF-8"?>
291
+ <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
292
+ xmlns:wsaa="http://wsaa.view.sua.dvadac.desein.afip.gov">
293
+ <soapenv:Header/>
294
+ <soapenv:Body>
295
+ <wsaa:loginCms>
296
+ <wsaa:in0>${cms}</wsaa:in0>
297
+ </wsaa:loginCms>
298
+ </soapenv:Body>
299
+ </soapenv:Envelope>`;
300
+ }
301
+ /**
302
+ * Limpia el ticket en cache (forzar renovación)
303
+ */
304
+ clearCache() {
305
+ this.ticketManager.clearTicket();
306
+ }
307
+ };
308
+
309
+ // src/types/wsfe.ts
310
+ var TipoComprobante = /* @__PURE__ */ ((TipoComprobante2) => {
311
+ TipoComprobante2[TipoComprobante2["FACTURA_A"] = 1] = "FACTURA_A";
312
+ TipoComprobante2[TipoComprobante2["FACTURA_B"] = 6] = "FACTURA_B";
313
+ TipoComprobante2[TipoComprobante2["FACTURA_C"] = 11] = "FACTURA_C";
314
+ TipoComprobante2[TipoComprobante2["TICKET_A"] = 81] = "TICKET_A";
315
+ TipoComprobante2[TipoComprobante2["TICKET_B"] = 82] = "TICKET_B";
316
+ TipoComprobante2[TipoComprobante2["TICKET_C"] = 83] = "TICKET_C";
317
+ return TipoComprobante2;
318
+ })(TipoComprobante || {});
319
+ var Concepto = /* @__PURE__ */ ((Concepto2) => {
320
+ Concepto2[Concepto2["PRODUCTOS"] = 1] = "PRODUCTOS";
321
+ Concepto2[Concepto2["SERVICIOS"] = 2] = "SERVICIOS";
322
+ Concepto2[Concepto2["PRODUCTOS_Y_SERVICIOS"] = 3] = "PRODUCTOS_Y_SERVICIOS";
323
+ return Concepto2;
324
+ })(Concepto || {});
325
+ var TipoDocumento = /* @__PURE__ */ ((TipoDocumento2) => {
326
+ TipoDocumento2[TipoDocumento2["CUIT"] = 80] = "CUIT";
327
+ TipoDocumento2[TipoDocumento2["CUIL"] = 86] = "CUIL";
328
+ TipoDocumento2[TipoDocumento2["CDI"] = 87] = "CDI";
329
+ TipoDocumento2[TipoDocumento2["LE"] = 89] = "LE";
330
+ TipoDocumento2[TipoDocumento2["LC"] = 90] = "LC";
331
+ TipoDocumento2[TipoDocumento2["CI_EXTRANJERA"] = 91] = "CI_EXTRANJERA";
332
+ TipoDocumento2[TipoDocumento2["PASAPORTE"] = 94] = "PASAPORTE";
333
+ TipoDocumento2[TipoDocumento2["CI_BUENOS_AIRES"] = 95] = "CI_BUENOS_AIRES";
334
+ TipoDocumento2[TipoDocumento2["CI_POLICIA_FEDERAL"] = 96] = "CI_POLICIA_FEDERAL";
335
+ TipoDocumento2[TipoDocumento2["DNI"] = 96] = "DNI";
336
+ TipoDocumento2[TipoDocumento2["CONSUMIDOR_FINAL"] = 99] = "CONSUMIDOR_FINAL";
337
+ return TipoDocumento2;
338
+ })(TipoDocumento || {});
339
+
340
+ // src/utils/calculations.ts
341
+ function calcularSubtotal(items) {
342
+ return items.reduce((sum, item) => {
343
+ return sum + item.cantidad * item.precioUnitario;
344
+ }, 0);
345
+ }
346
+ function calcularIVA(items) {
347
+ return items.reduce((sum, item) => {
348
+ const subtotal = item.cantidad * item.precioUnitario;
349
+ const alicuota = item.alicuotaIva || 0;
350
+ return sum + subtotal * alicuota / 100;
351
+ }, 0);
352
+ }
353
+ function calcularTotal(items) {
354
+ const subtotal = calcularSubtotal(items);
355
+ const iva = calcularIVA(items);
356
+ return subtotal + iva;
357
+ }
358
+ function redondear(valor) {
359
+ return Math.round(valor * 100) / 100;
360
+ }
361
+
362
+ // src/services/wsfe.ts
363
+ var WsfeService = class {
364
+ config;
365
+ constructor(config) {
366
+ this.validateConfig(config);
367
+ this.config = config;
368
+ }
369
+ validateConfig(config) {
370
+ if (!config.ticket || !config.ticket.token) {
371
+ throw new ArcaValidationError(
372
+ "Ticket WSAA requerido. Ejecut\xE1 wsaa.login() primero.",
373
+ { hint: "El ticket se obtiene del servicio WSAA" }
374
+ );
375
+ }
376
+ if (!config.puntoVenta || config.puntoVenta < 1 || config.puntoVenta > 9999) {
377
+ throw new ArcaValidationError(
378
+ "Punto de venta inv\xE1lido: debe ser un n\xFAmero entre 1 y 9999",
379
+ { puntoVenta: config.puntoVenta }
380
+ );
381
+ }
382
+ }
383
+ /**
384
+ * Emite un Ticket C de forma simple (solo total)
385
+ * Tipo de comprobante: 83
386
+ */
387
+ async emitirTicketCSimple(params) {
388
+ return this.emitirComprobante({
389
+ tipo: 83 /* TICKET_C */,
390
+ concepto: params.concepto || 1 /* PRODUCTOS */,
391
+ total: params.total,
392
+ fecha: params.fecha,
393
+ comprador: {
394
+ tipoDocumento: 99 /* CONSUMIDOR_FINAL */,
395
+ nroDocumento: "0"
396
+ }
397
+ });
398
+ }
399
+ /**
400
+ * Emite un Ticket C completo (con detalle de items)
401
+ * Los items no se envían a ARCA, pero se retornan en la respuesta.
402
+ */
403
+ async emitirTicketC(params) {
404
+ const total = redondear(calcularTotal(params.items));
405
+ const cae = await this.emitirComprobante({
406
+ tipo: 83 /* TICKET_C */,
407
+ concepto: params.concepto || 1 /* PRODUCTOS */,
408
+ total,
409
+ fecha: params.fecha,
410
+ comprador: {
411
+ tipoDocumento: 99 /* CONSUMIDOR_FINAL */,
412
+ nroDocumento: "0"
413
+ }
414
+ });
415
+ return {
416
+ ...cae,
417
+ items: params.items
418
+ };
419
+ }
420
+ /**
421
+ * Emite una Factura B (monotributo a responsable inscripto)
422
+ * REQUIERE detalle de items con IVA discriminado
423
+ */
424
+ async emitirFacturaB(params) {
425
+ this.validateItemsWithIVA(params.items);
426
+ const ivaData = this.calcularIVAPorAlicuota(params.items);
427
+ return this.emitirComprobante({
428
+ tipo: 6 /* FACTURA_B */,
429
+ concepto: params.concepto || 1 /* PRODUCTOS */,
430
+ items: params.items,
431
+ comprador: params.comprador,
432
+ fecha: params.fecha,
433
+ ivaData
434
+ });
435
+ }
436
+ /**
437
+ * Emite una Factura A (RI a RI)
438
+ * REQUIERE detalle de items con IVA discriminado
439
+ */
440
+ async emitirFacturaA(params) {
441
+ this.validateItemsWithIVA(params.items);
442
+ const ivaData = this.calcularIVAPorAlicuota(params.items);
443
+ return this.emitirComprobante({
444
+ tipo: 1 /* FACTURA_A */,
445
+ concepto: params.concepto || 1 /* PRODUCTOS */,
446
+ items: params.items,
447
+ comprador: params.comprador,
448
+ fecha: params.fecha,
449
+ ivaData
450
+ });
451
+ }
452
+ /**
453
+ * Valida que todos los items tengan alícuota IVA definida
454
+ */
455
+ validateItemsWithIVA(items) {
456
+ const sinIva = items.filter(
457
+ (item) => item.alicuotaIva === void 0 || item.alicuotaIva === null
458
+ );
459
+ if (sinIva.length > 0) {
460
+ throw new ArcaValidationError(
461
+ "Esta operaci\xF3n requiere IVA discriminado en todos los items",
462
+ {
463
+ itemsSinIva: sinIva.map((i) => i.descripcion),
464
+ hint: "Agreg\xE1 alicuotaIva a cada item (21, 10.5, 27, o 0)"
465
+ }
466
+ );
467
+ }
468
+ }
469
+ /**
470
+ * Calcula IVA agrupado por alícuota
471
+ * ARCA requiere esto para Factura B/A
472
+ */
473
+ calcularIVAPorAlicuota(items) {
474
+ const porAlicuota = /* @__PURE__ */ new Map();
475
+ items.forEach((item) => {
476
+ const alicuota = item.alicuotaIva || 0;
477
+ const base = item.cantidad * item.precioUnitario;
478
+ const importe = base * alicuota / 100;
479
+ const actual = porAlicuota.get(alicuota) || { base: 0, importe: 0 };
480
+ porAlicuota.set(alicuota, {
481
+ base: actual.base + base,
482
+ importe: actual.importe + importe
483
+ });
484
+ });
485
+ return Array.from(porAlicuota.entries()).map(([alicuota, valores]) => ({
486
+ alicuota,
487
+ baseImponible: redondear(valores.base),
488
+ importe: redondear(valores.importe)
489
+ }));
490
+ }
491
+ /**
492
+ * Emite una Factura C (consumidor final)
493
+ * Forma simplificada sin especificar comprador
494
+ */
495
+ async emitirFacturaC(params) {
496
+ const total = redondear(calcularTotal(params.items));
497
+ return this.emitirComprobante({
498
+ tipo: 11 /* FACTURA_C */,
499
+ concepto: params.concepto || 1 /* PRODUCTOS */,
500
+ total,
501
+ fecha: params.fecha,
502
+ comprador: {
503
+ tipoDocumento: 99 /* CONSUMIDOR_FINAL */,
504
+ nroDocumento: "0"
505
+ },
506
+ items: params.items
507
+ });
508
+ }
509
+ /**
510
+ * Emite un comprobante (método genérico interno)
511
+ */
512
+ async emitirComprobante(request) {
513
+ const nroComprobante = await this.obtenerProximoNumero(request.tipo);
514
+ let total = request.total || 0;
515
+ let subtotal = total;
516
+ let iva = 0;
517
+ if (request.items && request.items.length > 0) {
518
+ subtotal = redondear(calcularSubtotal(request.items));
519
+ iva = redondear(calcularIVA(request.items));
520
+ total = redondear(calcularTotal(request.items));
521
+ }
522
+ if (total <= 0) {
523
+ throw new ArcaValidationError("El monto total debe ser mayor a 0");
524
+ }
525
+ const soapRequest = this.buildCAESolicitarRequest({
526
+ tipo: request.tipo,
527
+ puntoVenta: this.config.puntoVenta,
528
+ nroComprobante,
529
+ concepto: request.concepto,
530
+ fecha: request.fecha || /* @__PURE__ */ new Date(),
531
+ comprador: request.comprador,
532
+ subtotal,
533
+ iva,
534
+ total,
535
+ ivaData: request.ivaData
536
+ });
537
+ const endpoint = getWsfeEndpoint(this.config.environment);
538
+ const response = await fetch(endpoint, {
539
+ method: "POST",
540
+ headers: {
541
+ "Content-Type": "text/xml; charset=utf-8",
542
+ "SOAPAction": "http://ar.gov.afip.dif.FEV1/FECAESolicitar"
543
+ },
544
+ body: soapRequest
545
+ });
546
+ if (!response.ok) {
547
+ throw new ArcaError(
548
+ `Error HTTP al comunicarse con WSFE: ${response.status}`,
549
+ "HTTP_ERROR",
550
+ { status: response.status }
551
+ );
552
+ }
553
+ const responseXml = await response.text();
554
+ const result = await this.parseCAEResponse(responseXml);
555
+ return {
556
+ ...result,
557
+ items: request.items,
558
+ iva: request.ivaData
559
+ };
560
+ }
561
+ /**
562
+ * Obtiene el próximo número de comprobante disponible
563
+ */
564
+ async obtenerProximoNumero(tipo) {
565
+ const soapRequest = this.buildProximoNumeroRequest(tipo);
566
+ const endpoint = getWsfeEndpoint(this.config.environment);
567
+ const response = await fetch(endpoint, {
568
+ method: "POST",
569
+ headers: {
570
+ "Content-Type": "text/xml; charset=utf-8",
571
+ "SOAPAction": "http://ar.gov.afip.dif.FEV1/FECompUltimoAutorizado"
572
+ },
573
+ body: soapRequest
574
+ });
575
+ if (!response.ok) {
576
+ throw new ArcaError(`Error HTTP al consultar pr\xF3ximo n\xFAmero: ${response.status}`, "HTTP_ERROR");
577
+ }
578
+ const responseXml = await response.text();
579
+ const result = parseXml(responseXml);
580
+ const data = result?.Envelope?.Body?.FECompUltimoAutorizadoResponse?.FECompUltimoAutorizadoResult;
581
+ if (data?.Errors) {
582
+ const error = Array.isArray(data.Errors.Err) ? data.Errors.Err[0] : data.Errors.Err;
583
+ throw new ArcaError(`Error ARCA: ${error?.Msg || "Error desconocido"}`, "ARCA_ERROR", data.Errors);
584
+ }
585
+ const nro = data?.CbteNro;
586
+ return typeof nro === "number" ? nro + 1 : 1;
587
+ }
588
+ buildCAESolicitarRequest(params) {
589
+ const fechaStr = params.fecha.toISOString().split("T")[0].replace(/-/g, "");
590
+ let ivaXml = "";
591
+ if (params.ivaData && params.ivaData.length > 0) {
592
+ ivaXml = "<ar:Iva>";
593
+ params.ivaData.forEach((aliquot) => {
594
+ ivaXml += `
595
+ <ar:AlicIva>
596
+ <ar:Id>${this.getCodigoAlicuota(aliquot.alicuota)}</ar:Id>
597
+ <ar:BaseImp>${aliquot.baseImponible.toFixed(2)}</ar:BaseImp>
598
+ <ar:Importe>${aliquot.importe.toFixed(2)}</ar:Importe>
599
+ </ar:AlicIva>`;
600
+ });
601
+ ivaXml += "\n </ar:Iva>";
602
+ }
603
+ return `<?xml version="1.0" encoding="UTF-8"?>
604
+ <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
605
+ xmlns:ar="http://ar.gov.afip.dif.FEV1/">
606
+ <soapenv:Header/>
607
+ <soapenv:Body>
608
+ <ar:FECAESolicitar>
609
+ <ar:Auth>
610
+ <ar:Token>${this.config.ticket.token}</ar:Token>
611
+ <ar:Sign>${this.config.ticket.sign}</ar:Sign>
612
+ <ar:Cuit>${this.config.cuit}</ar:Cuit>
613
+ </ar:Auth>
614
+ <ar:FeCAEReq>
615
+ <ar:FeCabReq>
616
+ <ar:CantReg>1</ar:CantReg>
617
+ <ar:PtoVta>${params.puntoVenta}</ar:PtoVta>
618
+ <ar:CbteTipo>${params.tipo}</ar:CbteTipo>
619
+ </ar:FeCabReq>
620
+ <ar:FeDetReq>
621
+ <ar:FECAEDetRequest>
622
+ <ar:Concepto>${params.concepto}</ar:Concepto>
623
+ <ar:DocTipo>${params.comprador?.tipoDocumento || 99}</ar:DocTipo>
624
+ <ar:DocNro>${params.comprador?.nroDocumento || 0}</ar:DocNro>
625
+ <ar:CbteDesde>${params.nroComprobante}</ar:CbteDesde>
626
+ <ar:CbteHasta>${params.nroComprobante}</ar:CbteHasta>
627
+ <ar:CbteFch>${fechaStr}</ar:CbteFch>
628
+ <ar:ImpTotal>${params.total.toFixed(2)}</ar:ImpTotal>
629
+ <ar:ImpTotConc>0.00</ar:ImpTotConc>
630
+ <ar:ImpNeto>${params.subtotal.toFixed(2)}</ar:ImpNeto>
631
+ <ar:ImpOpEx>0.00</ar:ImpOpEx>
632
+ <ar:ImpIVA>${params.iva.toFixed(2)}</ar:ImpIVA>
633
+ <ar:ImpTrib>0.00</ar:ImpTrib>
634
+ <ar:MonId>PES</ar:MonId>
635
+ <ar:MonCotiz>1</ar:MonCotiz>
636
+ ${ivaXml}
637
+ </ar:FECAEDetRequest>
638
+ </ar:FeDetReq>
639
+ </ar:FeCAEReq>
640
+ </ar:FECAESolicitar>
641
+ </soapenv:Body>
642
+ </soapenv:Envelope>`;
643
+ }
644
+ /**
645
+ * Mapea alícuota % a código ARCA
646
+ */
647
+ getCodigoAlicuota(porcentaje) {
648
+ const mapa = {
649
+ 0: 3,
650
+ 10.5: 4,
651
+ 21: 5,
652
+ 27: 6
653
+ };
654
+ const codigo = mapa[porcentaje];
655
+ if (!codigo) {
656
+ throw new ArcaValidationError(
657
+ `Al\xEDcuota IVA inv\xE1lida: ${porcentaje}%`,
658
+ {
659
+ alicuotasValidas: [0, 10.5, 21, 27],
660
+ hint: "Usar una de las al\xEDcuotas oficiales de Argentina"
661
+ }
662
+ );
663
+ }
664
+ return codigo;
665
+ }
666
+ buildProximoNumeroRequest(tipo) {
667
+ return `<?xml version="1.0" encoding="UTF-8"?>
668
+ <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
669
+ xmlns:ar="http://ar.gov.afip.dif.FEV1/">
670
+ <soapenv:Header/>
671
+ <soapenv:Body>
672
+ <ar:FECompUltimoAutorizado>
673
+ <ar:Auth>
674
+ <ar:Token>${this.config.ticket.token}</ar:Token>
675
+ <ar:Sign>${this.config.ticket.sign}</ar:Sign>
676
+ <ar:Cuit>${this.config.cuit}</ar:Cuit>
677
+ </ar:Auth>
678
+ <ar:PtoVta>${this.config.puntoVenta}</ar:PtoVta>
679
+ <ar:CbteTipo>${tipo}</ar:CbteTipo>
680
+ </ar:FECompUltimoAutorizado>
681
+ </soapenv:Body>
682
+ </soapenv:Envelope>`;
683
+ }
684
+ async parseCAEResponse(xml) {
685
+ const result = parseXml(xml);
686
+ const data = result?.Envelope?.Body?.FECAESolicitarResponse?.FECAESolicitarResult;
687
+ if (!data) {
688
+ throw new ArcaError("Respuesta WSFE inv\xE1lida: estructura no reconocida", "PARSE_ERROR", { xml });
689
+ }
690
+ if (data.Errors) {
691
+ const error = Array.isArray(data.Errors.Err) ? data.Errors.Err[0] : data.Errors.Err;
692
+ throw new ArcaError(`Error ARCA: ${error?.Msg || "Error desconocido"}`, "ARCA_ERROR", data.Errors);
693
+ }
694
+ const cab = data.FeCabResp;
695
+ const det = Array.isArray(data.FeDetResp.FECAEDetResponse) ? data.FeDetResp.FECAEDetResponse[0] : data.FeDetResp.FECAEDetResponse;
696
+ if (!det) {
697
+ throw new ArcaError("Respuesta WSFE incompleta: falta detalle del comprobante", "PARSE_ERROR");
698
+ }
699
+ const observaciones = [];
700
+ if (det.Observaciones) {
701
+ const obsArray = Array.isArray(det.Observaciones.Obs) ? det.Observaciones.Obs : [det.Observaciones.Obs];
702
+ obsArray.forEach((o) => observaciones.push(o.Msg));
703
+ }
704
+ return {
705
+ tipoComprobante: cab.CbteTipo,
706
+ puntoVenta: cab.PtoVta,
707
+ nroComprobante: det.CbteDesde,
708
+ fecha: det.CbteFch,
709
+ cae: det.CAE,
710
+ vencimientoCae: det.CAEFchVto,
711
+ resultado: det.Resultado,
712
+ observaciones: observaciones.length > 0 ? observaciones : void 0
713
+ };
714
+ }
715
+ };
716
+ export {
717
+ ArcaAuthError,
718
+ ArcaError,
719
+ ArcaValidationError,
720
+ Concepto,
721
+ TipoComprobante,
722
+ TipoDocumento,
723
+ WsaaService,
724
+ WsfeService
725
+ };