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