@albertomarturelo/sii-core 0.13.0 → 0.14.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/auth/auth.d.ts.map +1 -1
  3. package/dist/auth/auth.js +7 -5
  4. package/dist/auth/auth.js.map +1 -1
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/portal/f22/index.d.ts +1 -0
  10. package/dist/portal/f22/index.d.ts.map +1 -1
  11. package/dist/portal/f22/index.js +1 -0
  12. package/dist/portal/f22/index.js.map +1 -1
  13. package/dist/portal/f22/shared.d.ts +10 -1
  14. package/dist/portal/f22/shared.d.ts.map +1 -1
  15. package/dist/portal/f22/shared.js +13 -11
  16. package/dist/portal/f22/shared.js.map +1 -1
  17. package/dist/portal/f22/terceros.d.ts +58 -0
  18. package/dist/portal/f22/terceros.d.ts.map +1 -0
  19. package/dist/portal/f22/terceros.js +323 -0
  20. package/dist/portal/f22/terceros.js.map +1 -0
  21. package/dist/portal/f29-rfi-form.d.ts +40 -0
  22. package/dist/portal/f29-rfi-form.d.ts.map +1 -0
  23. package/dist/portal/f29-rfi-form.js +627 -0
  24. package/dist/portal/f29-rfi-form.js.map +1 -0
  25. package/dist/portal/f29-rfi.d.ts +13 -0
  26. package/dist/portal/f29-rfi.d.ts.map +1 -0
  27. package/dist/portal/f29-rfi.js +37 -0
  28. package/dist/portal/f29-rfi.js.map +1 -0
  29. package/dist/portal/gwt.d.ts +99 -0
  30. package/dist/portal/gwt.d.ts.map +1 -1
  31. package/dist/portal/gwt.js +618 -2
  32. package/dist/portal/gwt.js.map +1 -1
  33. package/dist/portal/rfi-schema.d.ts +7 -0
  34. package/dist/portal/rfi-schema.d.ts.map +1 -0
  35. package/dist/portal/rfi-schema.js +429 -0
  36. package/dist/portal/rfi-schema.js.map +1 -0
  37. package/dist/tasks/f22.d.ts +38 -2
  38. package/dist/tasks/f22.d.ts.map +1 -1
  39. package/dist/tasks/f22.js +126 -6
  40. package/dist/tasks/f22.js.map +1 -1
  41. package/package.json +1 -1
@@ -0,0 +1,627 @@
1
+ // The F29 form interpreter: fills a decoded rfiInternet layout the way SII's own client does
2
+ // before validation (ADR-029 amendment, #160). Without it SII validated a borrador missing the
3
+ // remanente (77) and the total de créditos (537) as "sin errores".
4
+ //
5
+ // Everything here is read first-hand from the client's compiled code, permutation
6
+ // 543D42AD0EF9E262804F560CE81AF5A6, observed 2026-10-19 (ADR-004). No tax logic is written by
7
+ // hand: the formulas are SII's, shipped as data in the layout (`CampoMetadata.n`, an `Expresion`
8
+ // AST), and this module only evaluates them.
9
+ // - the evaluator is the client fn `PG(expr, formulario, seccion)`;
10
+ // - a campo with a formula always reads as its formula's result (`rC`), formatted by its type
11
+ // (`jC` → `tJ` ENTERO*, `sJ` DECIMAL*), so a stale stored value never survives;
12
+ // - numbers go through SII's own decimal class (`rK`/`sK`), whose quirks are kept: a result
13
+ // always prints at least one decimal ("5.0"), rounding is half-up on the magnitude, and a
14
+ // division keeps 2 decimals;
15
+ // - the propuesta screen (`QRb`, mode PPTA-F29-COMPLETA) sets the 773 and 778 checkboxes to
16
+ // FALSE when the layout shows them;
17
+ // - a rule (`Regla`) fails when its expression is not TRUE, except kind 1 (`GC`).
18
+ // An operator, campo type or shape this module does not know throws "scraper roto"; nothing
19
+ // falls back to a silent 0.
20
+ import { F29Error } from '../errors/index.js';
21
+ const ROTO = 'No se pudo interpretar el formulario F29 de SII (scraper roto)';
22
+ const roto = (detail) => new F29Error(`${ROTO}: ${detail}`);
23
+ /** Parse like SII's `jK`: an optional sign, digits, at most one '.'. A bare integer gets one
24
+ * decimal ("5" → 5.0). Anything else is a value the formula cannot compute with. */
25
+ export function parseSiiNumber(text) {
26
+ if (text.length === 0)
27
+ throw roto('número vacío');
28
+ let s = text;
29
+ let neg = false;
30
+ if (s.startsWith('-')) {
31
+ neg = true;
32
+ s = s.slice(1);
33
+ }
34
+ else if (s.startsWith('+'))
35
+ s = s.slice(1);
36
+ if (!/^(\d*)(\.(\d*))?$/.test(s) || s === '' || s === '.')
37
+ throw roto(`número inválido "${text.slice(0, 20)}"`);
38
+ const [int = '', frac = ''] = s.split('.');
39
+ const f = frac === '' ? '0' : frac;
40
+ return { neg, units: BigInt((int === '' ? '0' : int) + f), scale: f.length };
41
+ }
42
+ const pow10 = (n) => 10n ** BigInt(n);
43
+ const rescale = (x, scale) => x.units * pow10(scale - x.scale);
44
+ const fromSigned = (v, scale) => ({
45
+ neg: v < 0n,
46
+ units: v < 0n ? -v : v,
47
+ scale,
48
+ });
49
+ const abs = (x) => ({ ...x, neg: false });
50
+ /** `new sK(…, -1)`: SII forces the sign negative, even on a zero. */
51
+ const forceNeg = (x) => ({ ...x, neg: true });
52
+ /** |r| - |s| with SII's sign `neg`; the callers guarantee |r| ≥ |s|. */
53
+ function magnitudeDiff(r, s, neg) {
54
+ const scale = Math.max(r.scale, s.scale);
55
+ return { neg, units: rescale(r, scale) - rescale(s, scale), scale };
56
+ }
57
+ /** `_J`: mixed signs become a subtraction; otherwise the magnitudes add, and the result is
58
+ * negative only when both operands are (so a zero result keeps that sign, as in SII). */
59
+ function add(a, b) {
60
+ if (a.neg !== b.neg)
61
+ return a.neg ? add(b, a) : subtract(a, abs(b));
62
+ const scale = Math.max(a.scale, b.scale);
63
+ return { neg: a.neg && b.neg, units: rescale(a, scale) + rescale(b, scale), scale };
64
+ }
65
+ /** `oK`, branch by branch, so the sign of a zero result matches SII's (`-5 - -5` is `-0.0`). */
66
+ function subtract(a, b) {
67
+ if (b.neg && !a.neg)
68
+ return add(a, abs(b));
69
+ if (a.neg && !b.neg)
70
+ return forceNeg(add(b, abs(a)));
71
+ if (a.neg && b.neg) {
72
+ // SII's digit subtraction assumes |r| ≥ |s|, which two negatives of different magnitude
73
+ // never satisfy in `oK`, so its result wraps around. Return the mathematically correct
74
+ // value instead; equal magnitudes give SII's `-0.0`.
75
+ const scale = Math.max(a.scale, b.scale);
76
+ const d = rescale(b, scale) - rescale(a, scale);
77
+ return d === 0n ? { neg: true, units: 0n, scale } : fromSigned(d, scale);
78
+ }
79
+ return compare(a, b) < 0 ? magnitudeDiff(b, a, true) : magnitudeDiff(a, b, false);
80
+ }
81
+ function multiply(a, b) {
82
+ const v = a.units * b.units;
83
+ // SII multiplies magnitudes and multiplies the signs, so a zero can come out negative.
84
+ return { neg: a.neg !== b.neg, units: v, scale: a.scale + b.scale };
85
+ }
86
+ /** `eK(a, b, 2)`: half-up on the magnitude to 2 decimals. A zero dividend is returned as is. */
87
+ function divide(a, b, decimals = 2) {
88
+ if (a.units === 0n)
89
+ return a;
90
+ if (b.units === 0n)
91
+ throw roto('división por cero');
92
+ const num = a.units * pow10(b.scale + decimals);
93
+ const den = b.units * pow10(a.scale);
94
+ const q = num / den;
95
+ const units = (num % den) * 2n >= den ? q + 1n : q;
96
+ return { neg: a.neg !== b.neg, units, scale: decimals };
97
+ }
98
+ /** `hK`: the sign first (a negative zero sorts below zero, as in SII), then the magnitude. */
99
+ function compare(a, b) {
100
+ if (a.neg !== b.neg)
101
+ return a.neg ? -1 : 1;
102
+ const scale = Math.max(a.scale, b.scale);
103
+ const d = rescale(a, scale) - rescale(b, scale);
104
+ const m = d === 0n ? 0 : d > 0n ? 1 : -1;
105
+ return a.neg ? -m : m;
106
+ }
107
+ /** `lK(x, n)`: half-up on the magnitude to `n` decimals; fewer decimals are left as they are. */
108
+ function setScale(x, n) {
109
+ if (x.scale <= n)
110
+ return x;
111
+ const den = pow10(x.scale - n);
112
+ const q = x.units / den;
113
+ return { ...x, units: (x.units % den) * 2n >= den ? q + 1n : q, scale: n };
114
+ }
115
+ function digits(x) {
116
+ const s = x.units.toString().padStart(x.scale + 1, '0');
117
+ return { int: s.slice(0, s.length - x.scale), frac: s.slice(s.length - x.scale) };
118
+ }
119
+ /** `pK`: SII's string form. The trailing zeros of the decimals go, but one decimal always
120
+ * stays ("5.0", "12.5"); the sign stays even on zero. */
121
+ export function formatSiiNumber(x) {
122
+ const { int, frac } = digits(x);
123
+ const f = frac.replace(/0+$/, '');
124
+ return `${x.neg ? '-' : ''}${int}.${f === '' ? '0' : f}`;
125
+ }
126
+ // ── Layout access (positions read from the deserializers, 2026-10-19) ──────────
127
+ // CampoMetadata: c=1 a long TC resets, f=4 código, n=9 formula, p=11 key, r=13 max length, v=17 decimals, x=19 type,
128
+ // B=23 ("0" keeps a computed zero visible).
129
+ const MD = {
130
+ c: 1,
131
+ codigo: 4,
132
+ formula: 9,
133
+ key: 11,
134
+ maxLen: 13,
135
+ decimales: 17,
136
+ tipo: 19,
137
+ ceroVisible: 23,
138
+ };
139
+ // Campo: d=0 metadata, g=2 stored value. Expresion: c=1 operator/name, d=2 left, e=3 right,
140
+ // f=4 type. Regla: b=0 expression, c=1 name, d=2 groups, e=3 message, f=4 kind.
141
+ // Formulario: d=2 campos by key, v=17 metadata, A=21 parameter maps, K=31 multivalued sections.
142
+ // FormularioMetadata: g=5 rules by name. layout.Cell: id at 6.
143
+ const CAMPO = { meta: 0, value: 2 };
144
+ const EXPR = { op: 1, left: 2, right: 3, type: 4 };
145
+ const REGLA = { expr: 0, nombre: 1, grupos: 2, mensaje: 3, tipo: 4 };
146
+ const FORM = { campos: 2, meta: 17, params: 21, multivalor: 31 };
147
+ const FORM_META_RULES = 5;
148
+ // layout.Cell: e=3 bound reference, f=4 widget, i=6 id, j=7 visible, n=9 css class, o=10 width.
149
+ const CELL = { ref: 3, widget: 4, id: 6, visible: 7, css: 9, width: 10 };
150
+ const CLS = {
151
+ formulario: 'cl.sii.sdi.difm.rcs.web.client.formulario.instance.Formulario',
152
+ campo: 'cl.sii.sdi.difm.rcs.web.client.formulario.instance.Campo',
153
+ expresion: 'cl.sii.sdi.difm.rcs.web.client.formulario.metadata.Expresion',
154
+ cell: 'cl.sii.sdi.difm.rcs.web.client.formulario.layout.Cell',
155
+ };
156
+ function isNode(v) {
157
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
158
+ }
159
+ function node(v, cls, what) {
160
+ if (!isNode(v) || v.cls !== cls)
161
+ throw roto(`${what} no es ${cls.split('.').pop()}`);
162
+ return v;
163
+ }
164
+ function str(v) {
165
+ if (v === null)
166
+ return null;
167
+ if (typeof v === 'string')
168
+ return v;
169
+ if (isNode(v) && v.cls === 'java.lang.String' && typeof v.values[0] === 'string')
170
+ return v.values[0];
171
+ throw roto('se esperaba un string');
172
+ }
173
+ function num(v, what) {
174
+ if (typeof v === 'number')
175
+ return v;
176
+ if (isNode(v) && typeof v.values[0] === 'number')
177
+ return v.values[0]; // a boxed Integer
178
+ throw roto(`${what} no es un número`);
179
+ }
180
+ /** A `java.util.HashMap` node's entries as a Map keyed by string. */
181
+ function hashMap(v, what) {
182
+ if (!isNode(v) || v.cls !== 'java.util.HashMap' || !Array.isArray(v.values[0])) {
183
+ throw roto(`${what} no es un HashMap`);
184
+ }
185
+ const out = new Map();
186
+ for (const e of v.values[0]) {
187
+ if (!Array.isArray(e) || e.length !== 2)
188
+ throw roto(`${what}: entrada inválida`);
189
+ const k = str(e[0]);
190
+ if (k !== null)
191
+ out.set(k, e[1]);
192
+ }
193
+ return out;
194
+ }
195
+ function findAll(root, cls) {
196
+ const out = [];
197
+ const seen = new Set();
198
+ const walk = (v) => {
199
+ if (v === null || typeof v !== 'object' || seen.has(v))
200
+ return;
201
+ seen.add(v);
202
+ if (Array.isArray(v))
203
+ return v.forEach(walk);
204
+ if (v.cls === cls)
205
+ out.push(v);
206
+ v.values.forEach(walk);
207
+ };
208
+ walk(root);
209
+ return out;
210
+ }
211
+ // ── The evaluator (PG) ───────────────────────────────────────────────────────
212
+ const TRUE = 'TRUE';
213
+ const FALSE = 'FALSE';
214
+ // The node's type `f`: 64 + the data type marks a campo reference (`$cod502`); 1 ENTERO,
215
+ // 2 DECIMAL, 4 STRING, 8 BOOLEAN, 16/32 dates (OG/NG compare by these).
216
+ const CAMPO_REF = new Set([64, 65, 66, 68, 72, 80, 96]);
217
+ const isNumType = (f) => f === 1 || f === 65 || f === 2 || f === 66;
218
+ const isStrType = (f) => f === 4 || f === 68;
219
+ const isBoolType = (f) => f === 8 || f === 72 || f === 128;
220
+ const isDateType = (f) => f === 16 || f === 32 || f === 80 || f === 96;
221
+ // The widgets whose refresh stores the campo's value (observed: TEXTBOX `jL`, CHECKBOX `GK`/`QL`).
222
+ // A cell bound to a campo with any other widget is a shape we have not observed: scraper roto.
223
+ const WIDGETS = new Set(['TEXTBOX', 'CHECKBOX']);
224
+ const UNBOUND_WIDGETS = new Set(['LABEL', 'IMAGE']);
225
+ /** `TNf`: trims only when the string starts or ends with whitespace. */
226
+ const trim = (s) => s.trim();
227
+ class F29Form {
228
+ formulario;
229
+ hoy;
230
+ campos;
231
+ params;
232
+ computed = new Map();
233
+ computing = new Set();
234
+ constructor(formulario, hoy) {
235
+ this.formulario = formulario;
236
+ this.hoy = hoy;
237
+ this.campos = new Map([...hashMap(formulario.values[FORM.campos], 'Formulario.d')].map(([k, v]) => [
238
+ k,
239
+ node(v, CLS.campo, `campo ${k}`),
240
+ ]));
241
+ this.params = hashMap(formulario.values[FORM.params], 'Formulario.A');
242
+ const multi = formulario.values[FORM.multivalor];
243
+ const sections = isNode(multi) && Array.isArray(multi.values[0]) ? multi.values[0] : [];
244
+ if (sections.length > 0) {
245
+ // Multivalued sections evaluate formulas per row (`cE`/`VD`); the F29 has none.
246
+ throw roto('el formulario tiene secciones multivalor');
247
+ }
248
+ }
249
+ campo(key) {
250
+ const c = this.campos.get(key);
251
+ if (!c)
252
+ throw roto(`no existe el campo ${key}`);
253
+ return c;
254
+ }
255
+ meta(campo) {
256
+ const m = campo.values[CAMPO.meta];
257
+ if (!isNode(m))
258
+ throw roto('campo sin metadata');
259
+ return m;
260
+ }
261
+ stored(campo) {
262
+ return str(campo.values[CAMPO.value]) ?? '';
263
+ }
264
+ set(campo, value) {
265
+ campo.values[CAMPO.value] = value;
266
+ this.computed.clear();
267
+ }
268
+ /** `rC`: a campo with a formula reads as its formula's formatted result. */
269
+ value(campo) {
270
+ const f = this.meta(campo).values[MD.formula];
271
+ if (!isNode(f))
272
+ return this.stored(campo);
273
+ const hit = this.computed.get(campo);
274
+ if (hit !== undefined)
275
+ return hit;
276
+ if (this.computing.has(campo))
277
+ throw roto(`fórmula circular en ${this.code(campo)}`);
278
+ this.computing.add(campo);
279
+ try {
280
+ const v = this.format(campo, this.eval(f));
281
+ this.computed.set(campo, v);
282
+ return v;
283
+ }
284
+ finally {
285
+ this.computing.delete(campo);
286
+ }
287
+ }
288
+ code(campo) {
289
+ return str(this.meta(campo).values[MD.codigo]) ?? '?';
290
+ }
291
+ /** `jC`: format a formula's result by the campo's type. */
292
+ format(campo, raw) {
293
+ if (raw === '')
294
+ return '';
295
+ const md = this.meta(campo);
296
+ const tipo = str(md.values[MD.tipo]) ?? '';
297
+ const maxLen = num(md.values[MD.maxLen], 'largo máximo');
298
+ if (/^ENTERO[01]?$/.test(tipo)) {
299
+ if (raw.indexOf('-') > 0)
300
+ return raw.slice(0, raw.indexOf('-')); // vJ, as SII does
301
+ const r = setScale(parseSiiNumber(raw), 0);
302
+ const { int } = digits(r);
303
+ if (int.length > maxLen)
304
+ throw roto(`el campo calculado ${this.code(campo)} excede su largo`);
305
+ return (r.neg ? '-' : '') + int;
306
+ }
307
+ if (/^DECIMAL[01]?$/.test(tipo)) {
308
+ const v = num(md.values[MD.decimales], 'decimales');
309
+ const r = setScale(parseSiiNumber(raw), v !== 0 ? v : 2);
310
+ if (digits(r).int.length > maxLen)
311
+ throw roto(`el campo calculado ${this.code(campo)} excede su largo`);
312
+ return formatSiiNumber(r);
313
+ }
314
+ return raw;
315
+ }
316
+ /** `jL`: what a widget's refresh stores — a numeric zero becomes '' unless B is "0". */
317
+ widgetValue(campo, v) {
318
+ const md = this.meta(campo);
319
+ const tipo = str(md.values[MD.tipo]) ?? '';
320
+ if (v.length > 0 &&
321
+ /^(ENTERO|DECIMAL)[01]?$/.test(tipo) &&
322
+ str(md.values[MD.ceroVisible]) !== '0') {
323
+ const n = Number(v);
324
+ if (Number.isFinite(n) && n === 0)
325
+ return '';
326
+ }
327
+ return v;
328
+ }
329
+ ref(expr) {
330
+ const name = str(expr.values[EXPR.op]) ?? '';
331
+ return this.campo(name.slice(1)); // `$cod502` → `cod502`
332
+ }
333
+ eval(expr) {
334
+ if (!isNode(expr) || expr.cls !== CLS.expresion)
335
+ throw roto('expresión inválida');
336
+ const op = str(expr.values[EXPR.op]) ?? '';
337
+ const left = expr.values[EXPR.left] ?? null;
338
+ const right = expr.values[EXPR.right] ?? null;
339
+ const type = num(expr.values[EXPR.type], 'tipo de expresión');
340
+ const sub = (v) => {
341
+ if (!isNode(v))
342
+ throw roto(`${op} sin operando`);
343
+ return v;
344
+ };
345
+ const isTrue = (v) => trim(this.eval(v)) === TRUE;
346
+ if (op === '?') {
347
+ const branches = sub(right);
348
+ return isTrue(left)
349
+ ? trim(this.eval(branches.values[EXPR.left]))
350
+ : trim(this.eval(branches.values[EXPR.right]));
351
+ }
352
+ if (CAMPO_REF.has(type))
353
+ return trim(this.value(this.ref(expr)));
354
+ if (type === 128)
355
+ throw roto('referencia a regla en una fórmula');
356
+ if (left === null && right === null) {
357
+ if (op === 'FECHA_MES_ACTUAL')
358
+ return mesActual(this.hoy); // R4f: the browser's YYYYMM
359
+ if (op === 'POS' || op === 'FECHA_DIA_ACTUAL')
360
+ throw roto(`función sin soporte: ${op}`);
361
+ return type === 4 ? op.slice(1, -1) : trim(op); // a constant ('…' strings lose the quotes)
362
+ }
363
+ switch (op) {
364
+ case '+':
365
+ case '-':
366
+ case '*':
367
+ case '/': {
368
+ const q = this.eval(left);
369
+ const t = right === null ? null : this.eval(right);
370
+ if (q === '' && t === '')
371
+ return '';
372
+ if (t === null)
373
+ throw roto(`${op} sin segundo operando`);
374
+ const a = q === '' ? parseSiiNumber('0') : parseSiiNumber(q);
375
+ const b = t === '' ? parseSiiNumber('0') : parseSiiNumber(t);
376
+ const r = op === '+'
377
+ ? add(a, b)
378
+ : op === '-'
379
+ ? subtract(a, b)
380
+ : op === '*'
381
+ ? multiply(a, b)
382
+ : divide(a, b);
383
+ return formatSiiNumber(r);
384
+ }
385
+ case '==':
386
+ case '!=': {
387
+ // NG answers FALSE when the right operand is missing.
388
+ const eq = isNode(right) && this.equal(sub(left), right, this.eval(left), this.eval(right));
389
+ return op === '==' ? (eq ? TRUE : FALSE) : eq ? FALSE : TRUE;
390
+ }
391
+ case '<':
392
+ case '>':
393
+ case '<=':
394
+ case '>=': {
395
+ const l = sub(left);
396
+ const r = sub(right);
397
+ const q = this.eval(l);
398
+ const t = this.eval(r);
399
+ const lt = this.less(l, r, q, t);
400
+ if (op === '<')
401
+ return lt ? TRUE : FALSE;
402
+ if (op === '>=')
403
+ return lt ? FALSE : TRUE;
404
+ const eq = this.equal(l, r, q, t);
405
+ if (op === '<=')
406
+ return lt || eq ? TRUE : FALSE;
407
+ return !lt && !eq ? TRUE : FALSE;
408
+ }
409
+ case 'AND':
410
+ return isTrue(left) && isTrue(right) ? TRUE : FALSE;
411
+ case 'OR':
412
+ return isTrue(left) || isTrue(right) ? TRUE : FALSE;
413
+ case '->':
414
+ return !isTrue(left) || isTrue(right) ? TRUE : FALSE;
415
+ case 'NOT':
416
+ return isTrue(left) ? FALSE : TRUE;
417
+ case 'PSTV': {
418
+ const q = this.eval(left);
419
+ return q.indexOf('-') === 0 ? '0' : q;
420
+ }
421
+ case 'ABS': {
422
+ const q = this.eval(left);
423
+ return q.indexOf('-') === 0 ? q.slice(1) : q;
424
+ }
425
+ case 'NEG': {
426
+ const q = this.eval(left);
427
+ return q.indexOf('-') === 0 ? q.slice(1) : '-' + q;
428
+ }
429
+ case 'ISNULO':
430
+ return trim(this.value(this.ref(sub(left)))) === '' ? TRUE : FALSE;
431
+ case 'EXISTE_DATO': {
432
+ // qJ: present when something other than leading zeros is left
433
+ const k = trim(this.eval(left)).replace(/^0+/, '');
434
+ return k !== '' ? TRUE : FALSE;
435
+ }
436
+ case 'ROUND': {
437
+ // SII rounds the fraction alone with Math.round and adds it to the integer part, so a
438
+ // negative x.5 goes up (-12.5 → -11). Kept as observed.
439
+ const q = trim(this.eval(left));
440
+ const k = q.indexOf('.');
441
+ if (k === -1)
442
+ return q;
443
+ const head = q.slice(0, k);
444
+ if (!/^[+-]?\d*$/.test(head) || !/^\.\d*$/.test(q.slice(k))) {
445
+ throw roto(`ROUND de un valor no numérico "${q.slice(0, 20)}"`);
446
+ }
447
+ const whole = BigInt(/\d/.test(head) ? head : '0');
448
+ return (whole + BigInt(Math.round(Number('0' + q.slice(k))))).toString();
449
+ }
450
+ case 'MINL':
451
+ case 'MAXL': {
452
+ if (right === null)
453
+ return this.eval(left);
454
+ const q = trim(this.eval(left));
455
+ const rn = sub(right);
456
+ if ((str(rn.values[EXPR.op]) ?? '') === ':')
457
+ throw roto(`${op} con lista`);
458
+ const t = trim(this.eval(rn));
459
+ const c = compare(q === '' ? parseSiiNumber('0') : parseSiiNumber(q), t === '' ? parseSiiNumber('0') : parseSiiNumber(t));
460
+ return op === 'MINL' ? (c < 0 ? q : t) : c > 0 ? q : t;
461
+ }
462
+ case 'GET_VALUE': {
463
+ const table = this.params.get(trim(this.eval(left)));
464
+ if (table === undefined)
465
+ return '';
466
+ const v = hashMap(table, 'tabla de parámetros').get(trim(this.eval(right)));
467
+ return v === undefined ? '' : (str(v) ?? '');
468
+ }
469
+ case 'PROX_PERIODO':
470
+ return shiftPeriodo(trim(this.value(this.ref(sub(left)))), +1);
471
+ case 'AANT_PERIODO':
472
+ return shiftPeriodo(trim(this.value(this.ref(sub(left)))), -2);
473
+ default:
474
+ throw roto(`operador desconocido "${op}"`);
475
+ }
476
+ }
477
+ /** `NG`: equality by the operands' types. */
478
+ equal(l, r, q, t) {
479
+ const lf = num(l.values[EXPR.type], 'tipo');
480
+ const rf = num(r.values[EXPR.type], 'tipo');
481
+ if (isBoolType(lf) || isBoolType(rf) || isStrType(lf) || isStrType(rf))
482
+ return q === t;
483
+ if (isNumType(lf) || isNumType(rf)) {
484
+ return (compare(q === '' ? parseSiiNumber('0') : parseSiiNumber(q), t === '' ? parseSiiNumber('0') : parseSiiNumber(t)) === 0);
485
+ }
486
+ if (isDateType(lf) && isDateType(rf)) {
487
+ if (q.length === 0 || t.length === 0)
488
+ return q.length === t.length;
489
+ return q === t;
490
+ }
491
+ return false;
492
+ }
493
+ /** `OG`: "less than" by the operands' types. */
494
+ less(l, r, q, t) {
495
+ const lf = num(l.values[EXPR.type], 'tipo');
496
+ const rf = num(r.values[EXPR.type], 'tipo');
497
+ if (isStrType(lf) || isStrType(rf))
498
+ return q < t;
499
+ if (isNumType(lf) || isNumType(rf)) {
500
+ return (compare(q === '' ? parseSiiNumber('0') : parseSiiNumber(q), t === '' ? parseSiiNumber('0') : parseSiiNumber(t)) < 0);
501
+ }
502
+ if (isDateType(lf) && isDateType(rf)) {
503
+ if (q.length === 0 || t.length === 0)
504
+ return false;
505
+ return q < t;
506
+ }
507
+ return false;
508
+ }
509
+ }
510
+ /** `FECHA_MES_ACTUAL`: the client builds YYYYMM from the browser's clock; SII's users are in
511
+ * Chile, so the month is taken in America/Santiago. */
512
+ function mesActual(hoy) {
513
+ const parts = new Intl.DateTimeFormat('en-CA', {
514
+ timeZone: 'America/Santiago',
515
+ year: 'numeric',
516
+ month: '2-digit',
517
+ }).formatToParts(hoy);
518
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? '';
519
+ return get('year') + get('month');
520
+ }
521
+ /** `xJ` (+1 month) and `$I` (-2 months) on a `YYYYMM` or `YYYYMMDD` período; '' if invalid. */
522
+ function shiftPeriodo(p, months) {
523
+ if (!/^\d{6}(\d{2})?$/.test(p))
524
+ return '';
525
+ const y = Number(p.slice(0, 4));
526
+ const m = Number(p.slice(4, 6));
527
+ if (m < 1 || m > 12)
528
+ return '';
529
+ const total = y * 12 + (m - 1) + months;
530
+ return `${Math.floor(total / 12)}${String((total % 12) + 1).padStart(2, '0')}`;
531
+ }
532
+ /** Fill the layout `findVersionBorradorById` returned, IN PLACE, as SII's client does before
533
+ * `getBorradorRfiValidado` in mode PPTA-F29-COMPLETA: the 773/778 checkboxes the screen shows
534
+ * go to FALSE, then every campo with a formula takes its formula's value. Returns what changed
535
+ * and the rules that fail. `hoy` is the moment of the fill (a few rules read the current
536
+ * month); pass the `Clock`'s now. The values are taxpayer data: never log them. */
537
+ export function fillF29Form(layout, hoy) {
538
+ const form = new F29Form(theFormulario(layout), hoy);
539
+ const before = new Map();
540
+ for (const [, c] of form.campos)
541
+ before.set(c, form.stored(c));
542
+ const after = (c) => form.stored(c);
543
+ // QRb: the propuesta screen sets these checkboxes to FALSE when it shows them.
544
+ const cells = findAll(layout, CLS.cell);
545
+ const cellIds = new Set(cells.map((c) => c.values[CELL.id]));
546
+ for (const [codigo, cell] of [
547
+ ['773', 'celdaCod773_1'],
548
+ ['778', 'celdaCod778_1'],
549
+ ]) {
550
+ if (!cellIds.has(cell))
551
+ continue;
552
+ const c = form.campo(`cod${codigo}`);
553
+ form.set(c, FALSE);
554
+ // TC also resets the metadata's long `c` to 0 (base64 "A" on the wire).
555
+ form.meta(c).values[MD.c] = 'A';
556
+ }
557
+ // The serializer sends a campo's STORED value. The client stores a formula's result only
558
+ // through the widget that shows the campo: its refresh (`jL` TEXTBOX, `GK`/`QL` CHECKBOX)
559
+ // writes the getter's value, and a numeric zero is stored as '' unless the metadata's B is "0".
560
+ // A formula campo no cell shows keeps what the layout brought.
561
+ const shown = new Set();
562
+ for (const cell of cells) {
563
+ const ref = cell.values[CELL.ref];
564
+ const m = typeof ref === 'string' ? /^\$(cod\d+)$/.exec(ref) : null;
565
+ if (!m)
566
+ continue;
567
+ const widget = String(cell.values[CELL.widget]);
568
+ if (UNBOUND_WIDGETS.has(widget))
569
+ continue;
570
+ if (!WIDGETS.has(widget))
571
+ throw roto(`widget desconocido ${widget} en ${m[1]}`);
572
+ shown.add(form.campo(m[1]));
573
+ if (widget === 'TEXTBOX') {
574
+ // wM builds a read-only TEXTBOX (the propuesta view) as a hidden-value field.
575
+ cell.values[CELL.visible] = FALSE;
576
+ cell.values[CELL.css] = 'fw-campoOculto';
577
+ if (cell.values[CELL.width] === '')
578
+ cell.values[CELL.width] = '125px';
579
+ }
580
+ }
581
+ const values = new Map();
582
+ for (const c of shown) {
583
+ if (!isNode(form.meta(c).values[MD.formula]))
584
+ continue;
585
+ values.set(c, form.widgetValue(c, form.value(c)));
586
+ }
587
+ for (const [c, v] of values)
588
+ c.values[CAMPO.value] = v;
589
+ const cambios = [...form.campos.values()]
590
+ .filter((c) => (before.get(c) ?? '') !== after(c))
591
+ .map((c) => ({ codigo: form.code(c), antes: before.get(c) ?? '', despues: after(c) }));
592
+ // GC with no group: rules with no group or the DEFAULT group, kind ≠ 1, fail unless TRUE.
593
+ // Assumed to be the call the validation makes (consistent with run6: no rule fails there);
594
+ // #161 confirms it against the client's "Enviar" handler.
595
+ const meta = node(form.formulario.values[FORM.meta], 'cl.sii.sdi.difm.rcs.web.client.formulario.metadata.FormularioMetadata', 'Formulario.v');
596
+ const reglasFallidas = [];
597
+ for (const [nombre, r] of hashMap(meta.values[FORM_META_RULES], 'reglas')) {
598
+ if (!isNode(r))
599
+ throw roto('regla inválida');
600
+ const tipo = num(r.values[REGLA.tipo], 'tipo de regla');
601
+ if (tipo === 1)
602
+ continue;
603
+ const grupos = r.values[REGLA.grupos];
604
+ const gs = Array.isArray(grupos)
605
+ ? grupos.map((g) => str(g))
606
+ : isNode(grupos) && Array.isArray(grupos.values[0])
607
+ ? grupos.values[0].map((g) => str(g))
608
+ : [];
609
+ if (gs.length > 0 && !gs.includes('DEFAULT'))
610
+ continue;
611
+ if (trim(form.eval(r.values[REGLA.expr])) !== TRUE) {
612
+ reglasFallidas.push({ nombre, mensaje: str(r.values[REGLA.mensaje]) ?? '', tipo });
613
+ }
614
+ }
615
+ return { cambios, reglasFallidas };
616
+ }
617
+ /** Evaluate one expression against a filled layout (tests, and the submit's safety checks). */
618
+ export function evaluateF29Expresion(layout, expr, hoy) {
619
+ return new F29Form(theFormulario(layout), hoy).eval(expr);
620
+ }
621
+ function theFormulario(layout) {
622
+ const formularios = findAll(layout, CLS.formulario);
623
+ if (formularios.length !== 1)
624
+ throw roto(`se esperaba un Formulario, hay ${formularios.length}`);
625
+ return formularios[0];
626
+ }
627
+ //# sourceMappingURL=f29-rfi-form.js.map