parche-cli 0.1.1 → 0.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parche-cli",
3
- "version": "0.1.1",
3
+ "version": "0.5.1",
4
4
  "description": "Dependabot para APIs: detecta cambios de APIs de terceros en tu código y abre el PR con el arreglo.",
5
5
  "keywords": [
6
6
  "api",
@@ -29,7 +29,9 @@
29
29
  "bin",
30
30
  "src",
31
31
  "packs",
32
- "templates"
32
+ "data",
33
+ "templates",
34
+ "CHANGELOG.md"
33
35
  ],
34
36
  "scripts": {
35
37
  "test": "node --test test/*.test.js",
@@ -40,6 +42,8 @@
40
42
  },
41
43
  "dependencies": {
42
44
  "@babel/parser": "^7.28.0",
43
- "jscodeshift": "^17.3.0"
45
+ "jscodeshift": "^17.3.0",
46
+ "php-parser": "^3.7.0",
47
+ "typescript": "~6.0.3"
44
48
  }
45
49
  }
@@ -1,38 +1,90 @@
1
1
  {
2
2
  "id": "ai.model-retirements",
3
- "version": "2026.10.15",
3
+ "version": "2026.10.16",
4
4
  "provider": "Modelos de IA",
5
5
  "title": "Modelos y endpoints de IA retirados o por retirarse",
6
6
  "kind": "retirement",
7
7
  "severity": "high",
8
8
  "deadline": null,
9
- "last_verified": "2026-10-15",
9
+ "last_verified": "2026-10-16",
10
10
  "summary_es": "Tu código usa modelos o APIs de IA que ya no responden o tienen fecha de retiro.",
11
- "why_es": "Los proveedores de modelos retiran IDs de modelo y endpoints con algunas semanas o meses de aviso. Después de la fecha, las llamadas fallan. Parche no reemplaza el modelo automáticamente porque cambiar de modelo cambia el comportamiento: te avisa y te dice a qué migrar.",
11
+ "why_es": "Los proveedores de modelos retiran IDs de modelo y endpoints con algunas semanas o meses de aviso. Después de la fecha, las llamadas fallan. Parche no reemplaza el modelo automáticamente porque cambiar de modelo cambia el comportamiento: te avisa, te dice la fecha y el reemplazo que recomienda el proveedor.",
12
12
  "sources": [
13
13
  { "title": "Anthropic: Model deprecations", "url": "https://platform.claude.com/docs/en/about-claude/model-deprecations" },
14
- { "title": "OpenAI Assistants API: cierre el 26/08/2026", "url": "https://dev.to/mr_manushukla/openai-assistants-api-shuts-down-26-august-2026-a-3-week-migration-sprint-plan-596b" }
14
+ { "title": "OpenAI: Deprecations", "url": "https://developers.openai.com/api/docs/deprecations" }
15
15
  ],
16
16
  "detect": {
17
17
  "packages": ["@anthropic-ai/sdk", "openai"],
18
18
  "retired_models": {
19
- "claude-3-opus-20240229": "2026-01-05",
20
- "claude-3-7-sonnet-20250219": "2026-02-19",
21
- "claude-3-5-haiku-20241022": "2026-02-19",
22
- "claude-3-haiku-20240307": "2026-04-20",
23
- "claude-sonnet-4-20250514": "2026-06-15",
24
- "claude-opus-4-20250514": "2026-06-15",
25
- "claude-opus-4-1-20250805": "2026-08-05"
19
+ "claude-sonnet-4-5-20250929": { "date": "2026-11-30", "replacement": "claude-sonnet-5-5" },
20
+ "claude-opus-4-1-20250805": { "date": "2026-08-05", "replacement": "claude-opus-4-8" },
21
+ "claude-sonnet-4-20250514": { "date": "2026-06-15", "replacement": "claude-sonnet-5-5" },
22
+ "claude-opus-4-20250514": { "date": "2026-06-15", "replacement": "claude-opus-4-8" },
23
+ "claude-3-haiku-20240307": { "date": "2026-04-20", "replacement": "claude-haiku-4-5-20251001" },
24
+ "claude-3-7-sonnet-20250219": { "date": "2026-02-19", "replacement": "claude-sonnet-5-5" },
25
+ "claude-3-5-haiku-20241022": { "date": "2026-02-19", "replacement": "claude-haiku-4-5-20251001" },
26
+ "claude-3-opus-20240229": { "date": "2026-01-05", "replacement": "claude-opus-4-8" },
27
+ "claude-3-5-sonnet-20241022": { "date": "2025-10-28", "replacement": "claude-sonnet-5-5" },
28
+ "claude-3-5-sonnet-20240620": { "date": "2025-10-28", "replacement": "claude-sonnet-5-5" },
29
+ "claude-3-sonnet-20240229": { "date": "2025-07-21", "replacement": "claude-sonnet-5-5" },
30
+ "claude-2.1": { "date": "2025-07-21", "replacement": "claude-opus-4-8" },
31
+ "claude-2.0": { "date": "2025-07-21", "replacement": "claude-opus-4-8" },
32
+ "claude-instant-1.2": { "date": "2024-11-06", "replacement": "claude-haiku-4-5-20251001" },
33
+ "claude-instant-1.1": { "date": "2024-11-06", "replacement": "claude-haiku-4-5-20251001" },
34
+ "claude-instant-1.0": { "date": "2024-11-06", "replacement": "claude-haiku-4-5-20251001" },
35
+ "claude-1.3": { "date": "2024-11-06", "replacement": "claude-haiku-4-5-20251001" },
36
+ "claude-1.2": { "date": "2024-11-06", "replacement": "claude-haiku-4-5-20251001" },
37
+ "claude-1.1": { "date": "2024-11-06", "replacement": "claude-haiku-4-5-20251001" },
38
+ "claude-1.0": { "date": "2024-11-06", "replacement": "claude-haiku-4-5-20251001" },
39
+
40
+ "gpt-5-2025-08-07": { "date": "2026-12-11", "replacement": "gpt-5.6-sol" },
41
+ "gpt-5-mini-2025-08-07": { "date": "2026-12-11", "replacement": "gpt-5.6-terra" },
42
+ "gpt-5-nano-2025-08-07": { "date": "2026-12-11", "replacement": "gpt-5.6-luna" },
43
+ "gpt-5-pro-2025-10-06": { "date": "2026-12-11", "replacement": "gpt-5.6-sol (reasoning.mode: pro)" },
44
+ "o3-2025-04-16": { "date": "2026-12-11", "replacement": "gpt-5.6-sol" },
45
+ "o3-pro-2025-06-10": { "date": "2026-12-11", "replacement": "gpt-5.6-sol (reasoning.mode: pro)" },
46
+ "gpt-3.5-turbo": { "date": "2026-10-23", "replacement": "gpt-5.6-terra" },
47
+ "gpt-3.5-turbo-0125": { "date": "2026-10-23", "replacement": "gpt-5.6-terra" },
48
+ "gpt-4": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
49
+ "gpt-4-0613": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
50
+ "gpt-4-1106-preview": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
51
+ "gpt-4-turbo": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
52
+ "gpt-4-turbo-2024-04-09": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
53
+ "gpt-4.1-nano": { "date": "2026-10-23", "replacement": "gpt-5.6-luna" },
54
+ "gpt-4.1-nano-2025-04-14": { "date": "2026-10-23", "replacement": "gpt-5.6-luna" },
55
+ "gpt-4o-2024-05-13": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
56
+ "gpt-image-1": { "date": "2026-10-23", "replacement": "gpt-image-2.5-sunburst o gpt-image-2.5-flare" },
57
+ "o1": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
58
+ "o1-2024-12-17": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
59
+ "o1-pro": { "date": "2026-10-23", "replacement": "gpt-5.6-sol (reasoning.mode: pro)" },
60
+ "o1-pro-2025-03-19": { "date": "2026-10-23", "replacement": "gpt-5.6-sol (reasoning.mode: pro)" },
61
+ "o3-mini": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
62
+ "o3-mini-2025-01-31": { "date": "2026-10-23", "replacement": "gpt-5.6-sol" },
63
+ "o4-mini": { "date": "2026-10-23", "replacement": "gpt-5.6-terra" },
64
+ "o4-mini-2025-04-16": { "date": "2026-10-23", "replacement": "gpt-5.6-terra" },
65
+ "gpt-3.5-turbo-instruct": { "date": "2026-09-28", "replacement": "gpt-5.6-terra" },
66
+ "gpt-3.5-turbo-1106": { "date": "2026-09-28", "replacement": "gpt-5.6-terra" },
67
+ "babbage-002": { "date": "2026-09-28", "replacement": "gpt-5.6-terra" },
68
+ "davinci-002": { "date": "2026-09-28", "replacement": "gpt-5.6-terra" },
69
+ "dall-e-3": { "date": "2026-05-12", "replacement": "gpt-image-2, gpt-image-1 o gpt-image-1-mini" },
70
+ "dall-e-2": { "date": "2026-05-12", "replacement": "gpt-image-2, gpt-image-1 o gpt-image-1-mini" },
71
+ "gpt-4o-realtime-preview": { "date": "2026-05-07", "replacement": "gpt-realtime-1.5" },
72
+ "gpt-4o-realtime-preview-2025-06-03": { "date": "2026-05-07", "replacement": "gpt-realtime-1.5" },
73
+ "gpt-4o-realtime-preview-2024-12-17": { "date": "2026-05-07", "replacement": "gpt-realtime-1.5" },
74
+ "gpt-4-0314": { "date": "2026-03-26", "replacement": "gpt-5 o gpt-4.1" },
75
+ "gpt-4-0125-preview": { "date": "2026-03-26", "replacement": "gpt-5 o gpt-4.1" },
76
+ "gpt-4-turbo-preview": { "date": "2026-03-26", "replacement": "gpt-5 o gpt-4.1" },
77
+ "chatgpt-4o-latest": { "date": "2026-02-17", "replacement": "gpt-5.1-chat-latest" },
78
+ "codex-mini-latest": { "date": "2026-02-12", "replacement": "gpt-5-codex-mini" }
26
79
  },
27
80
  "retirement_floors": {
28
- "claude-sonnet-4-5-20250929": "2026-09-29",
29
81
  "claude-haiku-4-5-20251001": "2026-10-15",
30
82
  "claude-opus-4-5-20251101": "2026-11-24"
31
83
  },
32
84
  "retired_endpoints": [
33
- { "pattern": ".beta.assistants", "retired": "2026-08-26", "replacement_es": "Responses API + Conversations API" },
34
- { "pattern": ".beta.threads", "retired": "2026-08-26", "replacement_es": "Responses API + Conversations API" },
35
- { "pattern": "/v1/assistants", "retired": "2026-08-26", "replacement_es": "Responses API + Conversations API" }
85
+ { "pattern": ".beta.assistants", "retired": "2026-08-26", "replacement_es": "Responses API + Conversations API", "provider": "openai" },
86
+ { "pattern": ".beta.threads", "retired": "2026-08-26", "replacement_es": "Responses API + Conversations API", "provider": "openai" },
87
+ { "pattern": "/v1/assistants", "retired": "2026-08-26", "replacement_es": "Responses API + Conversations API", "provider": "openai" }
36
88
  ]
37
89
  },
38
90
  "codemod": null,
@@ -1,22 +1,24 @@
1
1
  {
2
2
  "id": "arca.wsfe.condicion-iva-receptor",
3
- "version": "2026.10.15",
3
+ "version": "2026.10.16",
4
4
  "provider": "ARCA (ex AFIP)",
5
5
  "title": "Condición frente al IVA del receptor obligatoria en WSFEv1",
6
6
  "kind": "breaking",
7
7
  "severity": "critical",
8
8
  "deadline": "2026-12-01",
9
- "last_verified": "2026-10-15",
9
+ "last_verified": "2026-10-16",
10
10
  "summary_es": "Desde el 1/12/2026 ARCA rechaza automáticamente las solicitudes de CAE que no informen CondicionIVAReceptorId.",
11
11
  "why_es": "La RG 5616/2024 hizo obligatorio informar la condición frente al IVA del receptor en cada comprobante electrónico. El manual del desarrollador de WSFEv1 (v4.8) fija el 1 de diciembre de 2026 como fecha desde la cual el campo es obligatorio: si viaja vacío, nulo o no viaja, el pedido de CAE se rechaza y no se puede facturar.",
12
12
  "sources": [
13
13
  { "title": "VATupdate: Argentina makes recipient VAT status mandatory (WSFEv1 v4.8)", "url": "https://www.vatupdate.com/2026/09/09/argentina-makes-recipient-vat-status-mandatory-in-electronic-invoices/" },
14
14
  { "title": "iProfesional: ARCA rechazará facturas sin la condición del cliente frente al IVA", "url": "https://www.iprofesional.com/impuestos/464104-nuevo-requisito-arca-puede-trabar-emision-facturas" },
15
- { "title": "Tabla de códigos de FEParamGetCondicionIvaReceptor", "url": "https://app.spomsolutions.com/blog/condicion-iva-receptor-rg-5616" }
15
+ { "title": "Tabla de códigos de FEParamGetCondicionIvaReceptor", "url": "https://app.spomsolutions.com/blog/condicion-iva-receptor-rg-5616" },
16
+ { "title": "Afip SDK: factura electrónica (Node y PHP, CreateVoucher / CreateNextVoucher)", "url": "https://docs.afipsdk.com/siguientes-pasos/web-services/factura-electronica" },
17
+ { "title": "afipsdk/afip.php en Packagist", "url": "https://packagist.org/packages/afipsdk/afip.php" }
16
18
  ],
17
19
  "detect": {
18
- "packages": ["@afipsdk/afip.js", "afip.js"],
19
- "calls": ["ElectronicBilling.createVoucher", "ElectronicBilling.createNextVoucher"],
20
+ "packages": ["@afipsdk/afip.js", "@arcasdk/core", "afip.ts", "ts-afip-ws", "facturajs", "afipsdk/afip.php", "fertandil87/afip.php", "ivanalemunioz/afip.php", "eesnaola/afip.php"],
21
+ "calls": ["ElectronicBilling.createVoucher", "ElectronicBilling.createNextVoucher", "ElectronicBilling->CreateVoucher", "ElectronicBilling->CreateNextVoucher"],
20
22
  "raw_markers": ["FECAESolicitar", "FECAEDetRequest"],
21
23
  "field": "CondicionIVAReceptorId"
22
24
  },
package/src/ast.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Utilidades de AST compartidas por todos los codemods.
2
2
  import jscodeshift from 'jscodeshift';
3
3
  import { parse as babelParse } from '@babel/parser';
4
+ import { parsePhp, isPhp } from './php.js';
4
5
 
5
6
  export function jFor(file) {
6
7
  const parser = /\.ts$/.test(file) ? 'ts' : 'tsx';
@@ -22,6 +23,15 @@ export function eolOf(source) {
22
23
  return crlf > lf ? '\r\n' : '\n';
23
24
  }
24
25
 
26
+ // Aplica inserciones de texto { offset, text } de atrás para adelante, así no se corren los offsets.
27
+ export function applyEdits(source, edits) {
28
+ let out = source;
29
+ for (const e of [...edits].sort((a, b) => b.offset - a.offset)) {
30
+ out = out.slice(0, e.offset) + e.text + out.slice(e.offset);
31
+ }
32
+ return out;
33
+ }
34
+
25
35
  // Offset en el texto original de una posición { line, column } de recast.
26
36
  // No usar node.start/node.end para editar texto: recast parsea una copia con las líneas unidas
27
37
  // por os.EOL y las tabs de la indentación expandidas, así que esos offsets no coinciden con el
@@ -146,7 +156,13 @@ export function resolveBinding(path, name) {
146
156
 
147
157
  // Chequeo de sintaxis del resultado, usado por verify y por los tests.
148
158
  export function parseCheck(source, file) {
149
- const plugins = ['jsx'];
159
+ if (isPhp(file)) {
160
+ parsePhp(source, file);
161
+ return true;
162
+ }
163
+ // decorators-legacy: NestJS, Angular y TypeORM usan decoradores (@Injectable()); sin el plugin, el chequeo
164
+ // de sintaxis fallaba y --verify frenaba el PR en esos repos.
165
+ const plugins = ['jsx', 'decorators-legacy'];
150
166
  if (/\.(ts|tsx)$/.test(file)) plugins.push('typescript');
151
167
  if (/\.ts$/.test(file)) plugins.splice(plugins.indexOf('jsx'), 1);
152
168
  babelParse(source, { sourceType: 'unambiguous', plugins, allowAwaitOutsideFunction: true });
@@ -175,6 +191,10 @@ export function tidy(original, output) {
175
191
  let i = 0;
176
192
  let k = 0;
177
193
  while (k < m) {
194
+ // Una línea en blanco que también puede emparejarse más adelante se toma como nueva: así lo
195
+ // insertado con insertAfter queda pegado a la línea anterior (el import nuevo junto a los otros
196
+ // imports) y la línea en blanco original queda después.
197
+ if (i < n && a[i] === b[k] && b[k].trim() === '' && dp[i][k + 1] === dp[i][k]) { k++; continue; }
178
198
  if (i < n && a[i] === b[k]) { keep.push(b[k]); i++; k++; continue; }
179
199
  if (i < n && dp[i + 1][k] >= dp[i][k + 1]) { i++; continue; }
180
200
  if (b[k].trim() !== '') keep.push(b[k]); // línea nueva con contenido: se queda
@@ -0,0 +1,109 @@
1
+ // Reglas del pack arca.wsfe.condicion-iva-receptor, compartidas por los codemods de JS y de PHP:
2
+ // qué se puede inferir sin datos del negocio y con qué mensajes se reporta.
3
+
4
+ import { isTestFile } from '../paths.js';
5
+
6
+ export const PACK = 'arca.wsfe.condicion-iva-receptor';
7
+
8
+ // Prefiltro de los codemods: sin estas palabras no hay nada de ARCA en el archivo y no se parsea
9
+ // (en repos con bundles grandes, parsear todo tardaba minutos y 1,5 GB de RAM).
10
+ export const ARCA_HINT = /electronicbilling|fecaesolicitar|fecaedetrequest/i;
11
+
12
+ const CLASS = {
13
+ A: new Set([1, 2, 3, 201, 202, 203]),
14
+ B: new Set([6, 7, 8, 206, 207, 208]),
15
+ C: new Set([11, 12, 13, 211, 212, 213]),
16
+ };
17
+
18
+ export function classOf(cbteTipo) {
19
+ for (const [k, set] of Object.entries(CLASS)) if (set.has(cbteTipo)) return k;
20
+ return null;
21
+ }
22
+
23
+ // docTipo y cbteTipo: números si el valor es un literal, undefined si no se puede saber.
24
+ export function decideCondition({ docTipo, cbteTipo }, pack) {
25
+ const clase = typeof cbteTipo === 'number' ? classOf(cbteTipo) : null;
26
+ if (docTipo === 99) {
27
+ return { status: 'auto', code: 5, comment: ' parche: 5 = Consumidor Final (DocTipo 99, sin identificar)' };
28
+ }
29
+ if (docTipo === 96 && (clase === 'B' || clase === 'C')) {
30
+ return { status: 'needs-review', code: 5, comment: ' parche: inferido Consumidor Final (DNI + comprobante ' + clase + ') — REVISAR' };
31
+ }
32
+ const valid = clase ? pack.codes_by_class[clase] : null;
33
+ return { status: 'needs-input', clase, valid };
34
+ }
35
+
36
+ export function validCodesText(decision, pack) {
37
+ return (decision.valid || Object.keys(pack.codes).map(Number))
38
+ .map((c) => `${c} (${pack.codes[String(c)]})`).join(', ');
39
+ }
40
+
41
+ export function missingMessage(decision) {
42
+ return `Falta CondicionIVAReceptorId y no se puede inferir sin saber la condición frente al IVA del cliente${decision.clase ? ` (comprobante clase ${decision.clase})` : ''}.`;
43
+ }
44
+
45
+ export function appliedMessage(status) {
46
+ return status === 'auto'
47
+ ? 'Agregado CondicionIVAReceptorId: 5 (Consumidor Final): el comprobante va a un consumidor sin identificar.'
48
+ : 'Agregado CondicionIVAReceptorId: 5 por inferencia (DNI + comprobante B/C). Confirmá que el cliente no sea exento ni no alcanzado.';
49
+ }
50
+
51
+ // "El repo ya usa CondicionIVAReceptorId en otro lado": prefijo para bajar a "revisar" lo que no se pudo leer
52
+ // (en repos ya migrados, el objeto suele salir de un builder que sí pone el campo).
53
+ export function alreadyMigratedPrefix(context) {
54
+ const f = context?.fieldFiles?.[0];
55
+ return f ? `El repo ya usa CondicionIVAReceptorId (en \`${f}\`): verificá que este comprobante también lo lleve. ` : null;
56
+ }
57
+
58
+ // Si el campo puede no llegar a ARCA aunque esté en el código: librería 0.x (WSDL de 2021) o un WSDL viejo
59
+ // en el repo. Verificado en el código de afip.js/afip.php el 16/10/2026 (ver src/context.js).
60
+ export function libraryCaveat(context, ecosystem) {
61
+ const lib = (context?.arcaLibs || []).find((l) => l.ecosystem === ecosystem && l.travels !== 'si');
62
+ if (lib) {
63
+ return `Ojo: con ${lib.name} ${lib.version} el pedido se arma con un WSDL de 2021 que no tiene este campo`
64
+ + `${lib.travels === 'no' ? '' : ' (en JS falta confirmarlo)'}: para que llegue a ARCA hay que actualizar la librería a 1.x.`;
65
+ }
66
+ const wsdl = context?.staleWsdl?.[0];
67
+ if (wsdl) return `Ojo: el repo tiene un WSDL de WSFEv1 sin este campo (\`${wsdl}\`): si el cliente SOAP lo usa, el campo no viaja hasta actualizarlo.`;
68
+ return null;
69
+ }
70
+
71
+ // Integraciones SOAP propias (cualquier lenguaje): solo detección.
72
+ // Con menos ruido que la primera versión (frente B, 16/10): no marca tests, mocks, tipos, copias de la librería
73
+ // ni archivos que solo nombran el método (constantes, reexports); solo los que arman el detalle del comprobante.
74
+ export function soapDetection(source, file, context) {
75
+ if (!/FECAESolicitar|FECAEDetRequest/.test(source) || /CondicionIVAReceptorId/.test(source)) return null;
76
+ if (isTestFile(file) || /\.d\.[cm]?ts$/.test(file)) return null;
77
+ if (/class\s+ElectronicBilling\b/.test(source)) return null; // copia de afip.php/afip.js: la cubren la versión y el WSDL
78
+ const generated = /(^|\/)Reference\.cs$|Stub\.java$/i.test(file) || /class\s+FECAEDetRequest\b/.test(source);
79
+ if (!generated && !/\b(DocTipo|CbteDesde|ImpTotal)\b/.test(source)) return null;
80
+ const line = source.split('\n').findIndex((l) => /FECAESolicitar|FECAEDetRequest/.test(l)) + 1;
81
+ const base = generated
82
+ ? 'Cliente SOAP de WSFEv1 generado sin CondicionIVAReceptorId: hay que regenerarlo con el WSDL vigente (y después completar el campo).'
83
+ : 'Integración SOAP propia con WSFEv1 sin CondicionIVAReceptorId en el FECAEDetRequest. Hay que agregarlo al XML, con la condición frente al IVA del cliente.';
84
+ const prefix = alreadyMigratedPrefix(context);
85
+ return {
86
+ finding: { pack: PACK, file, line, status: prefix ? 'needs-review' : 'needs-input', message: (prefix || '') + base },
87
+ usage: { provider: 'arca', op: generated ? 'WSFEv1 SOAP (cliente generado)' : 'WSFEv1 SOAP', line },
88
+ };
89
+ }
90
+
91
+ // Archivos .wsdl de WSFEv1 sin el campo: si el cliente SOAP arma el pedido con ese esquema
92
+ // (SoapClient en modo WSDL, la librería soap de Node), el campo no viaja aunque esté en el código.
93
+ export function wsdlDetection({ source, file }) {
94
+ const findings = [];
95
+ const usages = [];
96
+ if (/FECAESolicitar|FECAEDetRequest/.test(source) && !source.includes('CondicionIVAReceptorId')) {
97
+ const line = source.split('\n').findIndex((l) => /FECAEDetRequest/.test(l)) + 1 || 1;
98
+ findings.push({ pack: PACK, file, line, status: 'needs-input',
99
+ message: 'WSDL de WSFEv1 sin CondicionIVAReceptorId: si tu cliente SOAP arma el pedido con este archivo, el campo no viaja aunque lo agregues al código. Reemplazalo por el WSDL vigente de WSFEv1.' });
100
+ usages.push({ provider: 'arca', op: 'WSFEv1 WSDL', line });
101
+ }
102
+ return { output: null, findings, usages };
103
+ }
104
+
105
+ // Lenguajes sin codemod (Python, C#, Java, Go, VB.NET): solo la detección de SOAP propio o cliente generado.
106
+ export function soapOnly({ source, file, context }) {
107
+ const soap = soapDetection(source, file, context);
108
+ return { output: null, findings: soap ? [soap.finding] : [], usages: soap ? [soap.usage] : [] };
109
+ }
@@ -0,0 +1,195 @@
1
+ // Pack arca.wsfe.condicion-iva-receptor (PHP: afipsdk/afip.php y sus forks)
2
+ // Mismas reglas que el codemod de JS: agrega CondicionIVAReceptorId a cada
3
+ // $afip->ElectronicBilling->CreateVoucher($data) / CreateNextVoucher($data) cuando se puede inferir
4
+ // sin datos del negocio, y marca el resto para que una persona complete el dato.
5
+ // Edita con inserciones de texto mínimas sobre los offsets exactos de php-parser (ver src/php.js).
6
+ import { eolOf, applyEdits, finding } from '../ast.js';
7
+ import { parsePhp, walkPhp, phpLiteral, phpKey } from '../php.js';
8
+ import {
9
+ PACK, decideCondition, validCodesText, missingMessage, appliedMessage, soapDetection, ARCA_HINT, libraryCaveat, alreadyMigratedPrefix,
10
+ } from './arca-rules.js';
11
+ import { emptyContext } from '../context.js';
12
+
13
+ // En PHP los nombres de método no distinguen mayúsculas: createVoucher también es válido.
14
+ // El inventario usa el nombre oficial, así un mismo método no aparece dos veces.
15
+ const VOUCHER_METHODS = new Map([['createvoucher', 'CreateVoucher'], ['createnextvoucher', 'CreateNextVoucher']]);
16
+ // Claves que cambian la decisión: si se tocan fuera del array literal, no se puede inferir.
17
+ const DECISION_KEYS = new Set(['DocTipo', 'CbteTipo', 'DocNro']);
18
+ const FIELD = 'CondicionIVAReceptorId';
19
+ const HINT_TAIL = 'con la condición frente al IVA del cliente.';
20
+
21
+ const isLookup = (n) => n && (n.kind === 'propertylookup' || n.kind === 'nullsafepropertylookup');
22
+ const identName = (n) => (n && n.kind === 'identifier' ? n.name : null);
23
+
24
+ // '...->ElectronicBilling->CreateVoucher(...)' → nombre oficial del método; null si no es.
25
+ function voucherMethod(call) {
26
+ const what = call.what;
27
+ if (!isLookup(what)) return null;
28
+ const method = identName(what.offset);
29
+ if (!method || !VOUCHER_METHODS.has(method.toLowerCase())) return null;
30
+ if (!isLookup(what.what) || identName(what.what.offset) !== 'ElectronicBilling') return null;
31
+ return VOUCHER_METHODS.get(method.toLowerCase());
32
+ }
33
+
34
+ // Asignaciones por scope: $var = ... y $var['Clave'] = ...
35
+ function collect(ast) {
36
+ const scopes = new Map();
37
+ const calls = [];
38
+ const info = (scope) => {
39
+ if (!scopes.has(scope)) scopes.set(scope, { assigns: new Map(), offsets: new Map() });
40
+ return scopes.get(scope);
41
+ };
42
+ const push = (map, name, value) => map.set(name, [...(map.get(name) || []), value]);
43
+ walkPhp(ast, (node, scope) => {
44
+ if (node.kind === 'assign') {
45
+ const left = node.left;
46
+ if (left.kind === 'variable' && typeof left.name === 'string') push(info(scope).assigns, left.name, node);
47
+ else if (left.kind === 'offsetlookup' && left.what && left.what.kind === 'variable' && typeof left.what.name === 'string' && left.offset) {
48
+ push(info(scope).offsets, left.what.name, { key: phpLiteral(left.offset), node });
49
+ }
50
+ }
51
+ if (node.kind === 'call') {
52
+ const method = voucherMethod(node);
53
+ if (method) calls.push({ node, scope, method });
54
+ }
55
+ });
56
+ return { scopes, calls };
57
+ }
58
+
59
+ // El array con los datos del comprobante: literal en la llamada o $var definida una sola vez,
60
+ // antes de la llamada y en el mismo scope.
61
+ function resolveData(arg, scope, call, scopes) {
62
+ if (!arg) return { reason: 'la llamada no tiene argumentos' };
63
+ if (arg.kind === 'array') return { arr: arg };
64
+ if (arg.kind !== 'variable' || typeof arg.name !== 'string') return { reason: 'no es un array literal ni una variable' };
65
+ const s = scopes.get(scope) || { assigns: new Map(), offsets: new Map() };
66
+ const defs = s.assigns.get(arg.name) || [];
67
+ const v = `$${arg.name}`;
68
+ if (defs.length === 0) {
69
+ const isParam = (scope.arguments || []).some((a) => a.kind === 'parameter' && (a.name?.name ?? a.name) === arg.name);
70
+ if (isParam) return { hint: `Los datos del comprobante llegan en \`${v}\` como parámetro: agregá ${FIELD} donde se arma ese array, ${HINT_TAIL}` };
71
+ return { reason: `${v} no se define en esta función` };
72
+ }
73
+ if (defs.length > 1) return { reason: `${v} se asigna más de una vez` };
74
+ const def = defs[0];
75
+ // $data = armarComprobante(...) / $this->builder->armar(...) / Builder::armar(...): decir qué función lo arma.
76
+ if (def.right.kind === 'call') {
77
+ const what = def.right.what;
78
+ const fn = what && (what.kind === 'name' ? what.name : identName(what.offset));
79
+ if (typeof fn === 'string') return { hint: `Los datos del comprobante los arma \`${fn}()\`: agregá ${FIELD} ahí, ${HINT_TAIL}` };
80
+ }
81
+ if (def.operator !== '=' || def.right.kind !== 'array') return { reason: `${v} no es un array literal` };
82
+ if (def.loc.start.offset > call.loc.start.offset) return { reason: `${v} se define después de la llamada` };
83
+ const offsets = s.offsets.get(arg.name) || [];
84
+ if (offsets.some((o) => o.key === FIELD)) return { migrated: true };
85
+ if (offsets.some((o) => o.key === undefined || DECISION_KEYS.has(o.key))) return { reason: `${v} se completa en varios pasos` };
86
+ return { arr: def.right };
87
+ }
88
+
89
+ export default function transform({ source, file, pack, context = emptyContext() }) {
90
+ const findings = [];
91
+ const usages = [];
92
+ if (!ARCA_HINT.test(source)) return { output: null, findings, usages }; // prefiltro (ver el codemod de JS)
93
+ const caveat = libraryCaveat(context, 'composer');
94
+ const migrated = alreadyMigratedPrefix(context);
95
+ let ast;
96
+ try {
97
+ ast = parsePhp(source, file);
98
+ } catch (err) {
99
+ if (/ElectronicBilling|FECAESolicitar|FECAEDetRequest/.test(source)) {
100
+ findings.push({ pack: PACK, file, line: err.lineNumber || null, status: 'needs-input',
101
+ message: `No pude analizar este archivo PHP (${err.message}). Revisá a mano las llamadas a ElectronicBilling.` });
102
+ }
103
+ return { output: null, findings, usages };
104
+ }
105
+
106
+ const { scopes, calls } = collect(ast);
107
+ const edits = [];
108
+ const edited = new Set(); // el mismo $data en dos llamadas: se inserta una sola vez (ver el codemod de JS)
109
+ for (const { node: call, scope, method } of calls) {
110
+ usages.push({ provider: 'arca', op: `ElectronicBilling->${method}`, line: call.loc.start.line });
111
+ const data = resolveData(call.arguments[0], scope, call, scopes);
112
+ if (data.migrated) continue;
113
+ const entries = data.arr ? data.arr.items.filter((it) => it && it.kind === 'entry') : null;
114
+ if (!entries || entries.some((e) => e.unpack || e.byRef)) {
115
+ const msg = data.hint || `No pude leer los datos del comprobante (${data.reason || 'el array usa ...$otro o referencias'}). Agregá ${FIELD} a mano, ${HINT_TAIL}`;
116
+ findings.push(finding(PACK, file, call, migrated ? 'needs-review' : 'needs-input', (migrated || '') + msg));
117
+ continue;
118
+ }
119
+ const byKey = (k) => entries.find((e) => phpKey(e) === k);
120
+ if (byKey(FIELD) || edited.has(data.arr)) continue; // ya está migrado (o ya lo editamos)
121
+
122
+ const d = decideCondition({ docTipo: phpLiteral(byKey('DocTipo')?.value), cbteTipo: phpLiteral(byKey('CbteTipo')?.value) }, pack);
123
+ if (d.status === 'needs-input') {
124
+ findings.push(finding(PACK, file, call, 'needs-input', missingMessage(d) + (caveat ? ` ${caveat}` : ''),
125
+ { suggestion: `'${FIELD}' => <condición del cliente>, // valores válidos: ${validCodesText(d, pack)}` }));
126
+ continue;
127
+ }
128
+ const anchor = byKey('DocNro') || entries[entries.length - 1];
129
+ edits.push(...insertAfterEntry(source, entries, anchor, { value: d.code, comment: d.comment }));
130
+ edited.add(data.arr);
131
+ // Con afip.php 0.x (WSDL de 2021 sin el campo) el arreglo no alcanza solo: deja de ser "automático".
132
+ const status = caveat && d.status === 'auto' ? 'needs-review' : d.status;
133
+ findings.push(finding(PACK, file, call, status, appliedMessage(d.status) + (caveat ? ` ${caveat}` : '')));
134
+ }
135
+
136
+ const soap = soapDetection(source, file, context);
137
+ if (soap) { findings.push(soap.finding); usages.push(soap.usage); }
138
+
139
+ return { output: edits.length ? applyEdits(source, edits) : null, findings, usages };
140
+ }
141
+
142
+ // Inserta 'CondicionIVAReceptorId' => N después de `anchor`, copiando el estilo del array:
143
+ // comillas de las claves, indentación, alineación de los =>, coma final y fin de línea.
144
+ export function insertAfterEntry(source, entries, anchor, { value, comment }) {
145
+ const eol = eolOf(source);
146
+ const keyed = entries.filter((e) => e.key && e.key.kind === 'string');
147
+ const doubles = keyed.filter((e) => source[e.key.loc.start.offset] === '"').length;
148
+ const q = doubles > keyed.length / 2 ? '"' : "'";
149
+ const key = `${q}${FIELD}${q}`;
150
+
151
+ const end = anchor.loc.end.offset;
152
+ let k = end;
153
+ while (k < source.length && /[ \t]/.test(source[k])) k++;
154
+ const hasComma = source[k] === ',';
155
+ const nl = source.indexOf('\n', end);
156
+ const rest = source.slice(hasComma ? k + 1 : end, nl === -1 ? source.length : nl);
157
+ const endsLine = nl !== -1 && (rest.trim() === '' || /^\s*(\/\/|#)/.test(rest));
158
+
159
+ if (!endsLine) {
160
+ // Array en una sola línea.
161
+ return [{ offset: hasComma ? k + 1 : end, text: `${hasComma ? '' : ','} ${key} => ${value} /*${comment} */${hasComma ? ',' : ''}` }];
162
+ }
163
+
164
+ const lineStart = source.lastIndexOf('\n', anchor.loc.start.offset - 1) + 1;
165
+ const indent = source.slice(lineStart).match(/^[ \t]*/)[0];
166
+ const { before, after } = arrowSpacing(source, keyed, anchor, key.length);
167
+ const isLast = anchor === entries[entries.length - 1];
168
+ const trailing = hasComma || !isLast ? ',' : '';
169
+ const out = [{ offset: nl + 1, text: `${indent}//${comment}${eol}${indent}${key}${before}=>${after}${value}${trailing}${eol}` }];
170
+ if (!hasComma) out.push({ offset: end, text: ',' });
171
+ return out;
172
+ }
173
+
174
+ // Espacios antes y después del => para la línea nueva. Si las claves del array están alineadas
175
+ // con espacios ('DocTipo' => 99), se alinea igual; si no, se copia el espaciado del ancla.
176
+ function arrowSpacing(source, keyed, anchor, newKeyLength) {
177
+ const parts = (e) => {
178
+ const keyEnd = e.key.loc.end.offset;
179
+ const arrow = source.indexOf('=>', keyEnd);
180
+ return {
181
+ width: arrow - e.key.loc.start.offset,
182
+ before: source.slice(keyEnd, arrow),
183
+ after: source.slice(arrow + 2, e.value.loc.start.offset),
184
+ line: e.loc.start.line,
185
+ };
186
+ };
187
+ const all = keyed.map(parts);
188
+ const own = anchor.key && anchor.key.kind === 'string' ? parts(anchor) : { before: ' ', after: ' ' };
189
+ const after = /^[ \t]*$/.test(own.after) && own.after ? own.after : ' ';
190
+ const multiline = new Set(all.map((p) => p.line)).size === all.length;
191
+ const aligned = multiline && all.length > 1 && all.every((p) => p.width === all[0].width && /^ +$/.test(p.before))
192
+ && all.some((p) => p.before.length > 1);
193
+ if (aligned) return { before: ' '.repeat(Math.max(1, all[0].width - newKeyLength)), after };
194
+ return { before: /^[ \t]+$/.test(own.before) ? own.before : ' ', after };
195
+ }