@ramiidv/arca-empleados 0.1.0 → 0.3.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.
@@ -1,304 +1,327 @@
1
- import { ArcaSerializationError } from './errors.js';
2
- import { TipoOperacion } from './constants.js';
3
- import { normalizeCuil } from './validators.js';
4
- // ---------------------------------------------------------------------------
5
- // Formatting helpers
6
- // ---------------------------------------------------------------------------
7
1
  /**
8
- * Formats a Date as DDMMYYYY.
2
+ * Serializador del archivo TXT de ingreso masivo de relaciones laborales de
3
+ * Simplificación Registral: registros de ancho fijo de 130 posiciones, tipo `01`.
4
+ *
5
+ * Diseño de registro: Manual de Ayuda de Simplificación Registral, ARCA, feb. 2025, pág. 24.
6
+ * https://www.arca.gob.ar/simplificacionregistral/ayuda/documentos/SR_Manual-de-Ayuda_20250218_definitivo.pdf
7
+ *
8
+ * Lo que el manual NO define (relleno, decimales de la retribución, codificación, fin
9
+ * de línea, valor de los campos vacíos) sigue la convención habitual de los diseños
10
+ * de registro de ARCA y es configurable con {@link OpcionesSerializacion}; ver README.
9
11
  */
10
- export function formatDate(date) {
11
- const day = String(date.getDate()).padStart(2, '0');
12
- const month = String(date.getMonth() + 1).padStart(2, '0');
13
- const year = String(date.getFullYear());
14
- return `${day}${month}${year}`;
12
+ import { ArcaValidationError, formatDate, normalizeCuit } from '@ramiidv/arca-common';
13
+ import { ArcaSerializationError } from './errors.js';
14
+ import { CODIGOS_MOVIMIENTO, LONGITUD_REGISTRO, TIPO_REGISTRO } from './constants.js';
15
+ function campo(campo, descripcion, desde, hasta, tipo) {
16
+ return Object.freeze({ campo, descripcion, desde, hasta, longitud: hasta - desde + 1, tipo });
15
17
  }
16
- /**
17
- * Converts a boolean to 'S' or 'N'.
18
- */
19
- export function boolToStr(val) {
20
- return val ? 'S' : 'N';
18
+ /** Diseño de registro `01` (manual de Simplificación Registral, pág. 24). */
19
+ export const DISENO_REGISTRO = Object.freeze([
20
+ campo('tipoRegistro', 'Tipo de registro', 1, 2, 'N'),
21
+ campo('movimiento', 'Código de movimiento', 3, 4, 'A'),
22
+ campo('cuil', 'Cuil del empleado', 5, 15, 'N'),
23
+ campo('trabajadorAgropecuario', 'Marca de trabajador agropecuario', 16, 16, 'A'),
24
+ campo('modalidadContrato', 'Modalidad de contrato', 17, 19, 'N'),
25
+ campo('fechaInicio', 'Fecha de inicio de rel. laboral', 20, 29, 'F'),
26
+ campo('fechaFin', 'Fecha de fin de rel. laboral', 30, 39, 'F'),
27
+ campo('obraSocial', 'Código obra social', 40, 45, 'N'),
28
+ campo('situacionRevista', 'Código de situación de revista', 46, 47, 'N'),
29
+ campo('fechaTelegramaRenuncia', 'Fecha recepción telegrama de renuncia', 48, 57, 'F'),
30
+ campo('retribucion', 'Retribución pactada', 58, 72, 'N'),
31
+ campo('modalidadLiquidacion', 'Modalidad de liquidación', 73, 73, 'N'),
32
+ campo('sucursal', 'Sucursal - Domicilio de desempeño', 74, 78, 'N'),
33
+ campo('actividad', 'Actividad en el domicilio de desempeño', 79, 84, 'N'),
34
+ campo('puesto', 'Puesto desempeñado', 85, 88, 'A'),
35
+ campo('rectificacion', 'Rectificación', 89, 90, 'N'),
36
+ campo('cct', 'Código de Convenio Colectivo de Trabajo', 91, 100, 'A'),
37
+ campo('categoriaProfesional', 'Categoría profesional', 101, 106, 'N'),
38
+ campo('tipoServicio', 'Tipo de servicio', 107, 109, 'N'),
39
+ campo('fechaSuspensionServiciosTemporarios', 'Fecha de suspensión servicios temporarios', 110, 119, 'F'),
40
+ campo('numeroFormularioAgropecuario', 'Número formulario agropecuario', 120, 129, 'N'),
41
+ campo('marcaCovidTipoContratoCCG', 'Marca de trabajador licenciado por COVID / Tipo de contrato CCG', 130, 130, 'N'),
42
+ ]);
43
+ /** Campos numéricos con default `0` según el manual (en lugar de "vacío"). */
44
+ const DEFAULT_CERO = new Set(['rectificacion', 'marcaCovidTipoContratoCCG']);
45
+ const CONTROL_CHARS = /[\u0000-\u001F\u007F-\u009F]/;
46
+ function resolverOpciones(opts = {}) {
47
+ const r = {
48
+ eol: opts.eol ?? '\r\n',
49
+ eolFinal: opts.eolFinal ?? true,
50
+ encoding: opts.encoding ?? 'latin1',
51
+ noRepresentables: opts.noRepresentables ?? 'error',
52
+ numericoVacio: opts.numericoVacio ?? 'ceros',
53
+ };
54
+ if (r.eol !== '\r\n' && r.eol !== '\n')
55
+ throw new TypeError(`eol inválido: ${JSON.stringify(opts.eol)}`);
56
+ if (r.encoding !== 'latin1' && r.encoding !== 'ascii') {
57
+ throw new TypeError(`encoding inválido: ${JSON.stringify(opts.encoding)} (usar 'latin1' o 'ascii')`);
58
+ }
59
+ if (r.noRepresentables !== 'error' && r.noRepresentables !== 'transliterar') {
60
+ throw new TypeError(`noRepresentables inválido: ${JSON.stringify(opts.noRepresentables)}`);
61
+ }
62
+ if (r.numericoVacio !== 'ceros' && r.numericoVacio !== 'espacios') {
63
+ throw new TypeError(`numericoVacio inválido: ${JSON.stringify(opts.numericoVacio)}`);
64
+ }
65
+ return r;
21
66
  }
22
- /**
23
- * Formats a number with 2 decimal places as a string (e.g. 1500.50 -> "1500.50").
24
- */
25
- function formatMonto(val) {
26
- if (val === undefined || val === null)
27
- return '';
28
- return val.toFixed(2);
67
+ function maxCodePoint(encoding) {
68
+ return encoding === 'ascii' ? 0x7f : 0xff;
29
69
  }
30
- /**
31
- * Returns the string representation of a value, or empty string if undefined/null.
32
- */
33
- function str(val) {
34
- if (val === undefined || val === null)
35
- return '';
36
- return String(val);
70
+ function fail(c, value, motivo, cause) {
71
+ throw new ArcaSerializationError(`Campo "${c.campo}" (posiciones ${c.desde}-${c.hasta}, ${c.descripcion}): ${motivo}`, { field: c.campo, desde: c.desde, hasta: c.hasta, value, cause });
37
72
  }
73
+ // ---------------------------------------------------------------------------
74
+ // Fechas
75
+ // ---------------------------------------------------------------------------
76
+ const FECHA_STRING_RE = /^(\d{4})([-/]?)(\d{2})\2(\d{2})$/;
38
77
  /**
39
- * Escapes a string field for the pipe-delimited format.
40
- * Replaces pipe characters (|), newlines, and carriage returns to prevent
41
- * corruption of the file format.
78
+ * Formatea una fecha como `AAAA/MM/DD` (formato de las fechas del archivo de
79
+ * Simplificación Registral). Distinto del `formatDate` de common (`AAAAMMDD`).
80
+ *
81
+ * - `Date`: fecha calendario en Argentina (`America/Argentina/Buenos_Aires`),
82
+ * independiente de la zona horaria del proceso.
83
+ * - string `AAAA-MM-DD`, `AAAAMMDD` o `AAAA/MM/DD`: se toma literal.
84
+ *
85
+ * @throws ArcaValidationError si el formato es otro, la fecha no existe o el `Date` es inválido.
42
86
  */
43
- function escapeField(val) {
44
- if (!val)
45
- return '';
46
- return val
47
- .replace(/\\/g, '\\\\')
48
- .replace(/\|/g, '\\|')
49
- .replace(/\n/g, '\\n')
50
- .replace(/\r/g, '\\r');
87
+ export function formatFechaSR(fecha) {
88
+ let input;
89
+ if (typeof fecha === 'string') {
90
+ const m = FECHA_STRING_RE.exec(fecha);
91
+ if (!m) {
92
+ throw new ArcaValidationError(`Fecha inválida: "${fecha}". Formato esperado: AAAA-MM-DD, AAAAMMDD o AAAA/MM/DD.`, [{ field: 'fecha', message: 'Formato esperado: AAAA-MM-DD, AAAAMMDD o AAAA/MM/DD', value: fecha }], 'fecha');
93
+ }
94
+ input = `${m[1]}-${m[3]}-${m[4]}`;
95
+ }
96
+ else if (fecha instanceof Date) {
97
+ input = fecha;
98
+ }
99
+ else {
100
+ throw new ArcaValidationError(`Fecha inválida: se esperaba Date o string, se recibió ${tipoDe(fecha)}.`, [{ field: 'fecha', message: 'Se esperaba Date o string', value: fecha }], 'fecha');
101
+ }
102
+ const yyyymmdd = formatDate(input);
103
+ return `${yyyymmdd.slice(0, 4)}/${yyyymmdd.slice(4, 6)}/${yyyymmdd.slice(6, 8)}`;
51
104
  }
52
- /**
53
- * Returns the escaped string representation of a value, or empty string if undefined/null.
54
- * Use this for free-text fields (calle, localidad, razon social, nombres, etc.).
55
- */
56
- function strEsc(val) {
57
- if (val === undefined || val === null)
58
- return '';
59
- return escapeField(val);
105
+ function tipoDe(v) {
106
+ if (v === null)
107
+ return 'null';
108
+ if (Array.isArray(v))
109
+ return 'array';
110
+ if (typeof v === 'number' && Number.isNaN(v))
111
+ return 'NaN';
112
+ return typeof v;
60
113
  }
61
114
  // ---------------------------------------------------------------------------
62
- // Record serializers
115
+ // Campos
63
116
  // ---------------------------------------------------------------------------
64
- /**
65
- * Serializes the file header line.
66
- * Format: 00|CUIT|RAZON_SOCIAL|FECHA_GENERACION|CANTIDAD_REGISTROS|
67
- */
68
- export function serializeHeader(archivo) {
69
- const cuit = normalizeCuil(archivo.cuitEmpleador);
70
- const razon = strEsc(archivo.razonSocial ?? '');
71
- const fecha = formatDate(archivo.fechaGeneracion);
72
- const cantidad = String(archivo.cantidadRegistros);
73
- return `00|${cuit}|${razon}|${fecha}|${cantidad}|`;
117
+ function vacio(v) {
118
+ return v === undefined || v === null || v === '';
74
119
  }
75
- /**
76
- * Serializes an alta (employee registration) record.
77
- * Format: 01|CUIT_EMPLEADOR|TIPO_OP|CUIL|FECHA_INICIO|MODALIDAD|OBRA_SOCIAL|ACTIVIDAD|AGROPECUARIO|FECHA_FIN|CALLE|NUMERO|PISO|DEPTO|LOCALIDAD|CP|PROVINCIA|TELEFONO|
78
- */
79
- export function serializeAlta(cuitEmpleador, alta) {
80
- const cuit = normalizeCuil(cuitEmpleador);
81
- const cuil = normalizeCuil(alta.cuil);
82
- const dom = alta.domicilioExplotacion;
83
- const fields = [
84
- '01',
85
- cuit,
86
- String(TipoOperacion.ALTA),
87
- cuil,
88
- formatDate(alta.fechaInicio),
89
- String(alta.modalidadContratacion),
90
- alta.obraSocial,
91
- alta.actividadEconomica,
92
- boolToStr(alta.trabajadorAgropecuario),
93
- alta.fechaFin ? formatDate(alta.fechaFin) : '',
94
- strEsc(dom.calle),
95
- strEsc(dom.numero),
96
- strEsc(dom.piso),
97
- strEsc(dom.depto),
98
- strEsc(dom.localidad),
99
- dom.codigoPostal,
100
- String(dom.provincia),
101
- strEsc(dom.telefono),
102
- ];
103
- return fields.join('|') + '|';
120
+ function formatearNumerico(c, value, o) {
121
+ if (vacio(value)) {
122
+ if (DEFAULT_CERO.has(c.campo))
123
+ return '0'.repeat(c.longitud);
124
+ return (o.numericoVacio === 'ceros' ? '0' : ' ').repeat(c.longitud);
125
+ }
126
+ let digits;
127
+ if (typeof value === 'number') {
128
+ if (!Number.isSafeInteger(value) || value < 0) {
129
+ fail(c, value, `debe ser un entero no negativo (se recibió ${Number.isNaN(value) ? 'NaN' : value}).`);
130
+ }
131
+ digits = String(value);
132
+ }
133
+ else if (typeof value === 'string') {
134
+ if (!/^\d+$/.test(value))
135
+ fail(c, value, `debe contener solo dígitos (se recibió "${value}").`);
136
+ digits = value;
137
+ }
138
+ else {
139
+ fail(c, value, `se esperaba number o string de dígitos, se recibió ${tipoDe(value)}.`);
140
+ }
141
+ if (digits.length > c.longitud) {
142
+ fail(c, value, `"${digits}" tiene ${digits.length} dígitos y el campo admite ${c.longitud}.`);
143
+ }
144
+ return digits.padStart(c.longitud, '0');
104
145
  }
105
- /**
106
- * Serializes a baja (employee termination) record.
107
- * Format: 01|CUIT_EMPLEADOR|TIPO_OP|CUIL|FECHA_BAJA|MOTIVO_BAJA|
108
- */
109
- export function serializeBaja(cuitEmpleador, baja) {
110
- const cuit = normalizeCuil(cuitEmpleador);
111
- const cuil = normalizeCuil(baja.cuil);
112
- const fields = [
113
- '01',
114
- cuit,
115
- String(TipoOperacion.BAJA),
116
- cuil,
117
- formatDate(baja.fechaBaja),
118
- String(baja.motivoBaja),
119
- ];
120
- return fields.join('|') + '|';
146
+ function formatearRetribucion(c, value, o) {
147
+ if (vacio(value))
148
+ return formatearNumerico(c, undefined, o);
149
+ if (typeof value !== 'number')
150
+ fail(c, value, `se esperaba number, se recibió ${tipoDe(value)}.`);
151
+ if (!Number.isFinite(value) || value < 0) {
152
+ fail(c, value, `debe ser un importe finito y no negativo (se recibió ${value}).`);
153
+ }
154
+ const centavosExactos = value * 100;
155
+ const centavos = Math.round(centavosExactos);
156
+ if (Math.abs(centavosExactos - centavos) > 1e-6 * Math.max(1, Math.abs(centavosExactos))) {
157
+ fail(c, value, `admite como máximo 2 decimales (se recibió ${value}).`);
158
+ }
159
+ if (!Number.isSafeInteger(centavos))
160
+ fail(c, value, 'importe demasiado grande.');
161
+ const digits = String(centavos);
162
+ if (digits.length > c.longitud) {
163
+ fail(c, value, `${value} ocupa ${digits.length} posiciones (2 decimales implícitos) y el campo admite ${c.longitud}.`);
164
+ }
165
+ return digits.padStart(c.longitud, '0');
121
166
  }
122
- /**
123
- * Serializes a modificacion (employee modification) record.
124
- * Format: 01|CUIT_EMPLEADOR|TIPO_OP|CUIL|FECHA_INICIO|MODALIDAD|OBRA_SOCIAL|ACTIVIDAD|AGROPECUARIO|FECHA_FIN|CALLE|NUMERO|PISO|DEPTO|LOCALIDAD|CP|PROVINCIA|TELEFONO|
125
- */
126
- export function serializeModificacion(cuitEmpleador, mod) {
127
- const cuit = normalizeCuil(cuitEmpleador);
128
- const cuil = normalizeCuil(mod.cuil);
129
- const dom = mod.domicilioExplotacion;
130
- const fields = [
131
- '01',
132
- cuit,
133
- String(TipoOperacion.MODIFICACION),
134
- cuil,
135
- formatDate(mod.fechaInicio),
136
- str(mod.modalidadContratacion),
137
- str(mod.obraSocial),
138
- str(mod.actividadEconomica),
139
- mod.trabajadorAgropecuario !== undefined ? boolToStr(mod.trabajadorAgropecuario) : '',
140
- mod.fechaFin ? formatDate(mod.fechaFin) : '',
141
- dom ? strEsc(dom.calle) : '',
142
- dom ? strEsc(dom.numero) : '',
143
- dom ? strEsc(dom.piso) : '',
144
- dom ? strEsc(dom.depto) : '',
145
- dom ? strEsc(dom.localidad) : '',
146
- dom ? dom.codigoPostal : '',
147
- dom ? String(dom.provincia) : '',
148
- dom ? strEsc(dom.telefono) : '',
149
- ];
150
- return fields.join('|') + '|';
167
+ function transliterar(s) {
168
+ return s.normalize('NFD').replace(/[̀-ͯ]/g, '').normalize('NFC');
151
169
  }
152
- /**
153
- * Serializes a datos complementarios record.
154
- * Format: 02|CUIT_EMPLEADOR|CUIL|FECHA_INICIO|SITUACION_REVISTA|NIVEL_EDUCACION|PUESTO|CCT|TIPO_SERVICIO|MOD_LIQUIDACION|ESTADO_CIVIL|REMUNERACION|HORAS|APORTE_ADICIONAL|CONTRIB_TAREAS_DIF|COD_SINIESTRADO|MARCA_PREAVISO|FECHA_PREAVISO|CONCEPTO_LIQUIDACION|
155
- */
156
- export function serializeDatosComplementarios(cuitEmpleador, datos) {
157
- const cuit = normalizeCuil(cuitEmpleador);
158
- const cuil = normalizeCuil(datos.cuil);
159
- const fields = [
160
- '02',
161
- cuit,
162
- cuil,
163
- formatDate(datos.fechaInicio),
164
- String(datos.situacionRevista),
165
- str(datos.nivelEducacion),
166
- strEsc(datos.puestoDesempenado),
167
- strEsc(datos.convenioColectivo),
168
- str(datos.tipoServicio),
169
- str(datos.modalidadLiquidacion),
170
- str(datos.estadoCivil),
171
- formatMonto(datos.remuneracion),
172
- str(datos.cantidadHorasTrabajadas),
173
- formatMonto(datos.porcentajeAporteAdicionalSS),
174
- str(datos.contribucionTareasDiferenciales),
175
- str(datos.codigoSiniestrado),
176
- datos.marcaPreaviso !== undefined ? boolToStr(datos.marcaPreaviso) : '',
177
- datos.fechaPreaviso ? formatDate(datos.fechaPreaviso) : '',
178
- str(datos.conceptoLiquidacion),
179
- ];
180
- return fields.join('|') + '|';
170
+ function formatearAlfanumerico(c, value, o) {
171
+ if (vacio(value))
172
+ return ' '.repeat(c.longitud);
173
+ if (typeof value !== 'string')
174
+ fail(c, value, `se esperaba string, se recibió ${tipoDe(value)}.`);
175
+ if (CONTROL_CHARS.test(value))
176
+ fail(c, value, 'contiene caracteres de control (saltos de línea, tabulaciones, etc.).');
177
+ const max = maxCodePoint(o.encoding);
178
+ let s = value.normalize('NFC');
179
+ const noRepresentable = (t) => [...t].find((ch) => ch.codePointAt(0) > max);
180
+ if (noRepresentable(s) !== undefined && o.noRepresentables === 'transliterar')
181
+ s = transliterar(s);
182
+ const bad = noRepresentable(s);
183
+ if (bad !== undefined) {
184
+ fail(c, value, `el caracter "${bad}" no es representable en ${o.encoding === 'ascii' ? 'ASCII' : 'ISO-8859-1'}.`);
185
+ }
186
+ if (s.length > c.longitud) {
187
+ fail(c, value, `"${s}" tiene ${s.length} caracteres y el campo admite ${c.longitud}.`);
188
+ }
189
+ return s.padEnd(c.longitud, ' ');
181
190
  }
182
- /**
183
- * Serializes a CBU record.
184
- * Format: 03|CUIT_EMPLEADOR|CUIL|FECHA_INICIO|CBU|TIPO_CUENTA|
185
- */
186
- export function serializeCBU(cuitEmpleador, cbuData) {
187
- const cuit = normalizeCuil(cuitEmpleador);
188
- const cuil = normalizeCuil(cbuData.cuil);
189
- const fields = [
190
- '03',
191
- cuit,
192
- cuil,
193
- formatDate(cbuData.fechaInicio),
194
- cbuData.cbu,
195
- str(cbuData.tipoCuenta),
196
- ];
197
- return fields.join('|') + '|';
191
+ function formatearFecha(c, value) {
192
+ if (vacio(value))
193
+ return ' '.repeat(c.longitud);
194
+ try {
195
+ return formatFechaSR(value);
196
+ }
197
+ catch (e) {
198
+ if (e instanceof ArcaValidationError)
199
+ fail(c, value, e.message, e);
200
+ throw e;
201
+ }
198
202
  }
199
- /**
200
- * Serializes a relacion familiar record.
201
- * Format: 04|CUIT_EMPLEADOR|CUIL|FECHA_INICIO|TIPO_RELACION|CUIL_FAMILIAR|DNI_FAMILIAR|NOMBRE|APELLIDO|FECHA_NACIMIENTO|DISCAPACITADO|ESCOLARIDAD|OBRA_SOCIAL_FAMILIAR|CUIL_TITULAR_OS|
202
- */
203
- export function serializeRelacionFamiliar(cuitEmpleador, rel) {
204
- const cuit = normalizeCuil(cuitEmpleador);
205
- const cuil = normalizeCuil(rel.cuil);
206
- const fields = [
207
- '04',
208
- cuit,
209
- cuil,
210
- formatDate(rel.fechaInicio),
211
- String(rel.tipoRelacion),
212
- rel.cuilFamiliar ? normalizeCuil(rel.cuilFamiliar) : '',
213
- str(rel.dniFamiliar),
214
- strEsc(rel.nombreFamiliar),
215
- strEsc(rel.apellidoFamiliar),
216
- formatDate(rel.fechaNacimiento),
217
- rel.discapacitado !== undefined ? boolToStr(rel.discapacitado) : '',
218
- str(rel.escolaridad),
219
- str(rel.obraSocialFamiliar),
220
- rel.cuilTitularObraSocial ? normalizeCuil(rel.cuilTitularObraSocial) : '',
221
- ];
222
- return fields.join('|') + '|';
203
+ function formatearCuil(c, value) {
204
+ if (typeof value !== 'string' && typeof value !== 'number') {
205
+ fail(c, value, `es obligatorio (string o number de 11 dígitos), se recibió ${tipoDe(value)}.`);
206
+ }
207
+ const n = normalizeCuit(value);
208
+ if (!/^\d{11}$/.test(n))
209
+ fail(c, value, `debe tener 11 dígitos (se recibió "${String(value)}").`);
210
+ return n;
223
211
  }
224
- /**
225
- * Serializes a domicilio de explotacion record.
226
- * Format: 05|CUIT_EMPLEADOR|CUIL|CALLE|NUMERO|PISO|DEPTO|LOCALIDAD|CP|PROVINCIA|TELEFONO|ACTIVIDAD_ECONOMICA|
227
- */
228
- export function serializeDomicilio(cuitEmpleador, dom) {
229
- const cuit = normalizeCuil(cuitEmpleador);
230
- const cuil = normalizeCuil(dom.cuil);
231
- const fields = [
232
- '05',
233
- cuit,
234
- cuil,
235
- strEsc(dom.calle),
236
- strEsc(dom.numero),
237
- strEsc(dom.piso),
238
- strEsc(dom.depto),
239
- strEsc(dom.localidad),
240
- dom.codigoPostal,
241
- String(dom.provincia),
242
- strEsc(dom.telefono),
243
- str(dom.actividadEconomica),
244
- ];
245
- return fields.join('|') + '|';
212
+ function formatearCampo(c, registro, o) {
213
+ if (c.campo === 'tipoRegistro')
214
+ return TIPO_REGISTRO;
215
+ const value = registro[c.campo];
216
+ switch (c.campo) {
217
+ case 'movimiento':
218
+ if (typeof value !== 'string' || !CODIGOS_MOVIMIENTO.includes(value)) {
219
+ fail(c, value, `debe ser uno de ${CODIGOS_MOVIMIENTO.join(', ')} (se recibió ${JSON.stringify(value) ?? tipoDe(value)}).`);
220
+ }
221
+ return value;
222
+ case 'cuil':
223
+ return formatearCuil(c, value);
224
+ case 'retribucion':
225
+ return formatearRetribucion(c, value, o);
226
+ }
227
+ switch (c.tipo) {
228
+ case 'N': return formatearNumerico(c, value, o);
229
+ case 'A': return formatearAlfanumerico(c, value, o);
230
+ case 'F': return formatearFecha(c, value);
231
+ }
232
+ }
233
+ function assertObjeto(registro) {
234
+ if (typeof registro !== 'object' || registro === null || Array.isArray(registro)) {
235
+ throw new ArcaSerializationError(`El registro debe ser un objeto con los datos de la relación laboral, se recibió ${tipoDe(registro)}.`, { value: registro });
236
+ }
246
237
  }
247
238
  // ---------------------------------------------------------------------------
248
- // Full file serializer
239
+ // API pública
249
240
  // ---------------------------------------------------------------------------
250
241
  /**
251
- * Serializes a complete F935Archivo to a string.
252
- * Returns the full file content with header and all record lines.
253
- * @throws ArcaSerializationError if serialization fails.
242
+ * Serializa un registro `01` a una línea de exactamente 130 caracteres (sin fin de línea).
243
+ * @throws ArcaSerializationError si algún campo no se puede escribir (nunca trunca).
244
+ */
245
+ export function serializarRegistro(registro, opciones) {
246
+ const o = resolverOpciones(opciones);
247
+ assertObjeto(registro);
248
+ const linea = DISENO_REGISTRO.map((c) => formatearCampo(c, registro, o)).join('');
249
+ if (linea.length !== LONGITUD_REGISTRO) {
250
+ // Invariante: cada formateador devuelve exactamente `longitud` caracteres.
251
+ throw new ArcaSerializationError(`Registro de ${linea.length} caracteres (se esperaban ${LONGITUD_REGISTRO}).`);
252
+ }
253
+ return linea;
254
+ }
255
+ /**
256
+ * Devuelve todos los errores de formato de un registro (sin lanzar), uno por campo.
257
+ * @internal Lo usa `validateRegistro`.
254
258
  */
255
- export function serializeArchivo(archivo) {
259
+ export function erroresDeFormato(registro, opciones) {
260
+ const o = resolverOpciones(opciones);
256
261
  try {
257
- const lines = [];
258
- // Header
259
- lines.push(serializeHeader(archivo));
260
- // Records
261
- for (const registro of archivo.registros) {
262
- lines.push(serializeRegistro(archivo.cuitEmpleador, registro));
263
- }
264
- return lines.join('\n');
262
+ assertObjeto(registro);
263
+ }
264
+ catch (e) {
265
+ return [{ field: 'registro', message: e.message, value: registro }];
265
266
  }
266
- catch (error) {
267
- if (error instanceof ArcaSerializationError) {
268
- throw error;
267
+ const errores = [];
268
+ for (const c of DISENO_REGISTRO) {
269
+ try {
270
+ formatearCampo(c, registro, o);
271
+ }
272
+ catch (e) {
273
+ if (!(e instanceof ArcaSerializationError))
274
+ throw e;
275
+ errores.push({ field: c.campo, message: e.message, value: e.value });
269
276
  }
270
- const message = error instanceof Error ? error.message : String(error);
271
- throw new ArcaSerializationError(`Error al serializar el archivo F935: ${message}`);
272
277
  }
278
+ return errores;
273
279
  }
274
280
  /**
275
- * Serializes a single F935Registro based on its type.
281
+ * Serializa varios registros al contenido completo del archivo (string).
282
+ * Por defecto: líneas separadas por CRLF y CRLF final.
283
+ * @throws ArcaSerializationError con `indice` del registro que falló y `cause` = error original.
276
284
  */
277
- function serializeRegistro(cuitEmpleador, registro) {
278
- switch (registro.tipo) {
279
- case 1:
280
- switch (registro.operacion) {
281
- case 0:
282
- return serializeAlta(cuitEmpleador, registro.data);
283
- case 1:
284
- return serializeBaja(cuitEmpleador, registro.data);
285
- case 2:
286
- return serializeModificacion(cuitEmpleador, registro.data);
287
- default:
288
- throw new ArcaSerializationError(`Tipo de operacion desconocido en registro tipo 1`);
289
- }
290
- case 2:
291
- return serializeDatosComplementarios(cuitEmpleador, registro.data);
292
- case 3:
293
- return serializeCBU(cuitEmpleador, registro.data);
294
- case 4:
295
- return serializeRelacionFamiliar(cuitEmpleador, registro.data);
296
- case 5:
297
- return serializeDomicilio(cuitEmpleador, registro.data);
298
- default: {
299
- const _exhaustive = registro;
300
- throw new ArcaSerializationError(`Tipo de registro desconocido: ${_exhaustive.tipo}`);
285
+ export function serializarArchivo(registros, opciones) {
286
+ const o = resolverOpciones(opciones);
287
+ if (!Array.isArray(registros)) {
288
+ throw new ArcaSerializationError(`Se esperaba un array de registros, se recibió ${tipoDe(registros)}.`);
289
+ }
290
+ const lineas = registros.map((r, i) => {
291
+ try {
292
+ return serializarRegistro(r, o);
293
+ }
294
+ catch (e) {
295
+ if (!(e instanceof ArcaSerializationError))
296
+ throw e;
297
+ throw new ArcaSerializationError(`Registro ${i}: ${e.message}`, {
298
+ field: e.field, desde: e.desde, hasta: e.hasta, value: e.value, indice: i, cause: e,
299
+ });
300
+ }
301
+ });
302
+ return unirLineas(lineas, o);
303
+ }
304
+ /** @internal */
305
+ export function unirLineas(lineas, opciones) {
306
+ const o = resolverOpciones(opciones);
307
+ if (lineas.length === 0)
308
+ return '';
309
+ return lineas.join(o.eol) + (o.eolFinal ? o.eol : '');
310
+ }
311
+ /**
312
+ * Codifica el contenido del archivo a `Buffer` (ISO-8859-1 por defecto, un byte por caracter).
313
+ * @throws ArcaSerializationError si hay un caracter no representable en la codificación.
314
+ */
315
+ export function codificarArchivo(contenido, encoding = 'latin1') {
316
+ if (encoding !== 'latin1' && encoding !== 'ascii') {
317
+ throw new TypeError(`encoding inválido: ${JSON.stringify(encoding)} (usar 'latin1' o 'ascii')`);
318
+ }
319
+ const max = maxCodePoint(encoding);
320
+ for (let i = 0; i < contenido.length; i++) {
321
+ if (contenido.charCodeAt(i) > max) {
322
+ throw new ArcaSerializationError(`El caracter "${contenido[i]}" (posición ${i} del contenido) no es representable en ${encoding}.`, { value: contenido[i] });
301
323
  }
302
324
  }
325
+ return Buffer.from(contenido, 'latin1');
303
326
  }
304
327
  //# sourceMappingURL=serializer.js.map