parche-cli 0.6.9 → 0.7.2

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/CHANGELOG.md CHANGED
@@ -1,6 +1,42 @@
1
1
  # Cambios
2
2
 
3
- Los cambios de cada versión de `parche-cli` y de la Action `Pepo08/parche@v0`. En npm están la 0.1.1, la 0.5.1 y de la 0.6.0 en adelante. La 0.1.0 y de la 0.2.0 a la 0.5.0 no se publicaron en npm; lo que traían está incluido en la 0.5.1. La fecha de cada versión es la del cambio; la de publicación figura en npm y en los [Releases](https://github.com/Pepo08/parche/releases).
3
+ Los cambios de cada versión de `parche-cli` y de la Action `Pepo08/parche@v0`. En npm están la 0.1.1, la 0.5.1, de la 0.6.0 a la 0.7.0 y de la 0.7.2 en adelante. La 0.1.0 y de la 0.2.0 a la 0.5.0 no se publicaron en npm; lo que traían está incluido en la 0.5.1. La 0.7.1 tampoco: salió junto con la 0.7.2. La fecha de cada versión es la del cambio; la de publicación figura en npm y en los [Releases](https://github.com/Pepo08/parche/releases).
4
+
5
+ ## 0.7.2 — 2026-10-24
6
+
7
+ ARCA: lo que pidió el re-escaneo de repos públicos con la 0.7.0. Un PR preparado quedaba parcial sin decirlo, y dos casos podían sacar un PR que no anda.
8
+
9
+ ### Cambios
10
+ - **Clave casi bien escrita en una asignación aparte:** `det.CondicionIVAReceptor = x`, `det['CondicionIVAReceptor'] = x` (JS y TS) y `$det['CondicionIVAReceptor'] = $x` (PHP) se renombran a `CondicionIVAReceptorId` cuando `det` es, en la misma función, un objeto o array con claves de WSFEv1. En un repo real, el informe de comprobantes con CAEA lo hacía así, y la 0.7.0 renombraba las otras tres apariciones y esta ni la nombraba: el PR quedaba parcial en silencio. Si no se puede confirmar que `det` sea el comprobante (por ejemplo, si llega como parámetro), Parche lo avisa para revisar y no lo toca.
11
+ - **El renombre ya no rompe el tipado:** si un `interface` o un `type` del repo declara la clave mal escrita (`CondicionIvaReceptorId?: number`), renombrarla solo en el objeto hacía que TypeScript no compilara, y el chequeo de sintaxis no lo veía. Ahora no se renombra, y el aviso dice en qué archivo y línea está el tipo para cambiar los dos.
12
+ - **Archivos con saltos de línea `\r` sueltos (`\r\r\n`):** en JS y TS, Parche no los edita. El parser cuenta cada `\r` como una línea y la edición caía en otra parte del archivo; en un repo real, dentro de un comentario y con "Sintaxis OK". Ahora el hallazgo pasa a "revisar", con lo que hay que cambiar a mano y la línea que muestra git.
13
+
14
+ ## 0.7.1 — 2026-10-24
15
+
16
+ La Action, lista para el GitHub Marketplace como "Parche API Changes". No cambia nada de lo que hace Parche.
17
+
18
+ ### Cambios
19
+ - **Ícono de la Action:** `git-pull-request`. El que tenía, `tool`, está en la lista de íconos que el Marketplace no acepta y el panel de publicación lo marcaba como error.
20
+ - **Descripción de la Action en inglés y en menos de 125 caracteres:** es la tarjeta del Marketplace, que ve gente de cualquier país. "Dependabot para APIs" queda como frase de presentación.
21
+ - **README:** un "GitHub Action quick start" en inglés al principio (la página de la Action en el Marketplace es el README): qué hace la plantilla, los permisos y los inputs.
22
+ - **Input `verify`:** la descripción nombra todos los tests que puede correr (`npm test`, `composer test` o `vendor/bin/phpunit`), no solo `npm test`.
23
+ - Un test controla lo que revisa el Marketplace: el nombre, el largo de la descripción, el ícono y el color.
24
+
25
+ ## 0.7.0 — 2026-10-24
26
+
27
+ ARCA: Parche encuentra a los repos que creen estar migrados y no lo están, reconoce `@arcasdk/core` (la segunda librería de ARCA más descargada en npm) y avisa de la librería vieja en más casos. Salió de los pendientes del escaneo de 174 repos públicos (frente B, puntos 12 a 14).
28
+
29
+ ### Nuevo
30
+ - **Clave casi bien escrita:** si el detalle del comprobante lleva `CondicionIVAReceptor` (sin `Id`), `CondicionIvaReceptorId` o `condicionIVAReceptorId`, Parche la renombra a `CondicionIVAReceptorId` sin tocar el valor, en JS y en PHP. Ese elemento no existe en el WSDL de WSFEv1: el dato nunca llegaba y desde el 1/12/2026 ARCA rechaza el CAE. Pasaba en un repo real con integración SOAP propia, en cuatro archivos. El PR sale borrador, para revisar que el valor sea un código válido. Solo se toca dentro de objetos o arrays con al menos dos claves de WSFEv1 (`DocTipo`, `CbteDesde`, `ImpTotal`…), nunca en tests ni en modelos propios con un nombre parecido. Si el comprobante tiene las dos claves, no edita y pide revisar.
31
+ - **`@arcasdk/core`:** `arca.electronicBillingService.createVoucher(...)` y `createNextVoucher(...)` reciben los mismos arreglos que `afip.js` (consumidor final, DNI con B o C, PR parcial). Su comprobante usa las mismas claves de WSFEv1 (tipos de la 2.0.2, leídos en el paquete de npm). Probado sobre cinco repos públicos que la usan.
32
+ - **Guía "Cómo agregar `CondicionIVAReceptorId` antes del 1/12/2026"** (`docs/guia-condicion-iva-receptor.md`): qué cambia, el campo en afip.js, afip.php, @arcasdk/core y SOAP propio, los códigos por clase de comprobante, los tres errores que aparecieron en repos reales y cómo probarlo en homologación. Para quien prefiere hacerlo a mano.
33
+
34
+ ### Cambios
35
+ - **`ts-afip-ws`:** se avisa como librería vieja. Es un fork de afip.js 0.x y su última versión (1.3.2, de 2023) trae `wsfe.wsdl` y `wsfe-production.wsdl` sin el campo. El aviso dice qué hacer: pasar a `@afipsdk/afip.js` 1.x o actualizar el WSDL que trae la librería (lo mismo para `eesnaola/afip.php`, que no tiene una 1.x).
36
+ - **El aviso de librería vieja también sale cuando Parche no puede leer los datos del comprobante** (por ejemplo, si llegan de otra función). Antes salía solo en los arreglos automáticos y en lo que pedía un dato con el objeto a la vista.
37
+ - **Cuerpo del issue:** el pie dice "¿Te sirvió este aviso?" en vez de "¿Este PR te sirvió?".
38
+ - **Checklist:** "confirmar que cada cliente con CUIT tiene su condición" sale solo si queda algo pendiente. En un PR que arregla todo, como uno de puros consumidores finales, no aplicaba (nuevo campo de pack: `review_checklist_if_pending_es`).
39
+ - **Pack `arca.wsfe.condicion-iva-receptor` 2026.10.24:** fuentes releídas el 24/10/2026 (sin prórroga del 1/12); la explicación ya no cita una versión del manual de WSFEv1, que la página de ARCA rotula distinto; suma `facturas` (LaPyme) y las llamadas de `@arcasdk/core` a la detección.
4
40
 
5
41
  ## 0.6.9 — 2026-10-23
6
42
 
package/README.md CHANGED
@@ -6,7 +6,27 @@
6
6
 
7
7
  > **In English.** Parche reads your code and inventories every third-party API it uses: SDKs, direct HTTP calls and AI model IDs. When a provider ships a dated change that affects you, it tells you the file and the line, or opens a small PR with a deterministic fix. Every changed file is parsed before the PR, and with `--verify` your own tests run too. What can't be inferred is never made up: the PR asks for it. Today's packs cover Argentina's ARCA e-invoicing change (due December 1, 2026), Mercado Pago Payments → Orders, and OpenAI and Anthropic model and parameter deprecations.
8
8
  >
9
- > Try it with `npx parche-cli scan .` (Node 22+): it runs locally, with no account and no telemetry ([security and data](SECURITY.md)). GitHub Action: `uses: Pepo08/parche@v0`. The rest of this README is in Spanish for now · [getparche.com](https://getparche.com) · hola@getparche.com
9
+ > Try it with `npx parche-cli scan .` (Node 22+): it runs locally, with no account and no telemetry ([security and data](SECURITY.md)). GitHub Action: [quick start below](#github-action-quick-start). The rest of this README is in Spanish for now · [getparche.com](https://getparche.com) · hola@getparche.com
10
+
11
+ ### GitHub Action quick start
12
+
13
+ Copy [`templates/github-workflow.yml`](templates/github-workflow.yml) to `.github/workflows/parche.yml`. Every Monday it installs your dependencies (so your tests can run) and runs Parche, which opens one PR per API change that affects the repo, as a draft if something needs a human, and lists everything else in the job summary. The job needs `contents: write` and `pull-requests: write`. The key step:
14
+
15
+ ```yaml
16
+ - uses: Pepo08/parche@v0
17
+ with:
18
+ verify: 'true' # run your tests before opening each PR
19
+ ```
20
+
21
+ | Input | Default | What it does |
22
+ |---|---|---|
23
+ | `path` | `.` | Folder to scan. |
24
+ | `pack` | all packs | Run a single pack (list them with `npx parche-cli packs`). |
25
+ | `verify` | `'true'` | Run the repo's tests (`npm test`, `composer test` or `vendor/bin/phpunit`) before opening each PR. |
26
+ | `dry-run` | `'false'` | Build the branches and PR bodies without opening any PR. |
27
+ | `token` | `github.token` | Pushes the branches and opens the PRs. With the default `GITHUB_TOKEN`, GitHub holds the PR's workflows until someone approves them; with a GitHub App token or a personal token they run on their own. |
28
+
29
+ `@v0` follows every 0.x release; to pin one, use its release commit: `Pepo08/parche@<sha> # v0.7.2`. PR bodies and the job output are in Spanish for now.
10
30
 
11
31
  Parche lee tu código y arma el inventario de **todas** las APIs de terceros que usás: SDKs (Stripe, Twilio, OpenAI, AWS, Mercado Pago, ARCA…), llamadas HTTP directas y modelos de IA. Lo cruza contra una base de **cambios de API verificados** y, cuando un proveedor cambia algo que te afecta, te avisa o abre un PR con el arreglo, explica qué cambió y por qué, y marca lo que necesita una persona. Ningún PR sale sin pasar la verificación. Las APIs de pagos y facturación de Latinoamérica son el fuerte, pero el inventario cubre cualquier API.
12
32
 
@@ -130,7 +150,7 @@ Cómo contribuir, las reglas para el código y cómo se saca una versión: [`CON
130
150
 
131
151
  | Pack | Proveedor | Tipo | Fecha límite | Arreglo |
132
152
  |---|---|---|---|---|
133
- | `arca.wsfe.condicion-iva-receptor` | ARCA | rompe | 2026-12-01 | sí, en JS (`afip.js`) y PHP (`afipsdk/afip.php` y forks): consumidor final y DNI B/C; con `DocTipo` en una variable, consumidor final al ejecutarse (PR parcial) |
153
+ | `arca.wsfe.condicion-iva-receptor` | ARCA | rompe | 2026-12-01 | sí, en JS (`afip.js` y `@arcasdk/core`) y PHP (`afipsdk/afip.php` y forks): consumidor final y DNI B/C; con `DocTipo` en una variable, consumidor final al ejecutarse (PR parcial) |
134
154
  | `mercadopago.payments-to-orders` | Mercado Pago | migración | — | sí (creación de pagos en SDK Node v2) |
135
155
  | `ai.model-retirements` | Anthropic, OpenAI | retiro | varias | solo detección (también modelos fine-tuned de OpenAI, `ft:…`). De OpenAI, los modelos que su página de deprecaciones apaga desde 2026: texto, código, imagen, video, voz, transcripción y realtime; y los productos que cierran: Assistants API, prompts reutilizables (`pmpt_…`, `v1/prompts`), la plataforma de Evals, ChatKit con un workflow de Agent Builder y la Videos API (cerrada el 24/9/2026). De Anthropic, los modelos retirados o con fecha, también por su alias (`claude-sonnet-4-5`, que apunta a `claude-sonnet-4-5-20250929` y se retira el 30/11/2026), `temperature`, `top_p` y `top_k` con un valor que no es el de por defecto en Claude 4.7 y posteriores, que dan error 400 (SDK de JS/TS y de Python, y HTTP directo), y los mismos tres parámetros con cualquier modelo en la API version `2026-10-21`, que usan el SDK de TypeScript 1.0.0 y el de Python 2.0.0 (del 21/10/2026): dice si el pedido ya falla, si falla en una instalación nueva o si va a fallar cuando suban el SDK |
136
156
 
@@ -159,6 +179,8 @@ Para reportar una vulnerabilidad, y el detalle de todo esto: [`SECURITY.md`](SEC
159
179
 
160
180
  ## ARCA en PHP
161
181
 
182
+ **¿Tenés que agregar el campo a mano?** La guía paso a paso, para afip.js, afip.php, @arcasdk/core y SOAP propio: [Cómo agregar `CondicionIVAReceptorId` antes del 1/12/2026](docs/guia-condicion-iva-receptor.md).
183
+
162
184
  Para `afipsdk/afip.php` y sus forks (`fertandil87/afip.php`, `ivanalemunioz/afip.php`, `eesnaola/afip.php`), Parche busca `$afip->ElectronicBilling->CreateVoucher($data)` y `CreateNextVoucher($data)` (también en minúsculas: PHP no distingue) y agrega una línea por factura:
163
185
 
164
186
  ```php
@@ -171,7 +193,9 @@ Para `afipsdk/afip.php` y sus forks (`fertandil87/afip.php`, `ivanalemunioz/afip
171
193
  - Copia el estilo del array: comillas de las claves, indentación (espacios o tabs), alineación de los `=>`, coma final y fin de línea (LF o CRLF).
172
194
  - Saltea `vendor/`.
173
195
 
174
- **La versión de la librería importa.** Con `afip.js`/`afip.php` 0.x (y `eesnaola/afip.php`) el pedido se arma con un WSDL de 2021 que no tiene `CondicionIVAReceptorId`: aunque el campo esté en el código, no llega a ARCA. Parche lee la versión instalada (`composer.lock`, `package-lock.json`, `node_modules`) y, en esos casos, el arreglo sale como **"revisar"** con el aviso de actualizar a 1.x. También avisa si el repo tiene un `.wsdl` de WSFEv1 sin el campo.
196
+ **La versión de la librería importa.** Con `afip.js`/`afip.php` 0.x, `eesnaola/afip.php` y `ts-afip-ws` (un fork de afip.js 0.x; su última versión, la 1.3.2, trae el WSDL sin el campo) el pedido se arma con un WSDL de 2021 que no tiene `CondicionIVAReceptorId`: aunque el campo esté en el código, no llega a ARCA. Parche lee la versión instalada (`composer.lock`, `package-lock.json`, `node_modules`) y, en esos casos, el arreglo sale como **"revisar"** y lo que pide un dato lleva el mismo aviso: qué actualizar para que el campo viaje. También avisa si el repo tiene un `.wsdl` de WSFEv1 sin el campo.
197
+
198
+ **Clave casi bien escrita.** Si el detalle del comprobante lleva `CondicionIVAReceptor` (sin `Id`), `CondicionIvaReceptorId` o `condicionIVAReceptorId`, ese elemento no existe en el WSDL: el dato nunca llega y el repo parece migrado. Parche renombra la clave a `CondicionIVAReceptorId` sin tocar el valor, en JS y en PHP, y el PR sale **borrador** para revisar que el valor sea un código válido. Solo lo hace dentro de objetos o arrays con al menos dos claves de WSFEv1 (`DocTipo`, `CbteDesde`, `ImpTotal`…), para no tocar modelos propios con un nombre parecido, y nunca en tests. También renombra una asignación aparte (`det.CondicionIVAReceptor = x`, `det['CondicionIVAReceptor'] = x` o `$det['CondicionIVAReceptor'] = $x`) cuando `det` es, en la misma función, un objeto o array con esas claves; si no puede confirmarlo, lo avisa sin tocar. Si un `interface` o un `type` del repo declara la clave mal escrita, no renombra, porque TypeScript dejaría de compilar, y el aviso dice dónde está ese tipo.
175
199
 
176
200
  **Otros lenguajes:** en Python, C#, Java, Go y VB.NET, Parche detecta integraciones SOAP propias y clientes SOAP generados sin el campo (no los reescribe).
177
201
 
@@ -203,7 +227,7 @@ Cuando `DocTipo` no es un número fijo (`DocTipo: docTipo`), Parche no puede sab
203
227
  ## Límites del MVP
204
228
 
205
229
  - JS/TS (ESM y CommonJS) y, para ARCA, PHP. Siguen Python y Java.
206
- - ARCA: cubre `@afipsdk/afip.js` y `afipsdk/afip.php` (con sus forks). Las integraciones SOAP propias (en cualquier lenguaje) se detectan pero no se reescriben.
230
+ - ARCA: cubre `@afipsdk/afip.js`, `@arcasdk/core` (`arca.electronicBillingService.createVoucher` y `createNextVoucher`) y `afipsdk/afip.php` (con sus forks). Las integraciones SOAP propias (en cualquier lenguaje) se detectan pero no se reescriben, salvo el renombre de una clave casi bien escrita. Un archivo JS o TS con saltos de línea `\r` sueltos (por ejemplo `\r\r\n`) no se edita: el aviso dice qué cambiar a mano.
207
231
  - IA, parámetros de Claude 4.7 y posteriores: avisa solo si el modelo del pedido se lee en el mismo archivo (un literal, una constante asignada una vez o el valor por defecto de una variable de entorno) y el parámetro es un número escrito en el código. Si el modelo llega de otro archivo o de la configuración, no avisa: no se puede saber si el pedido va a fallar.
208
232
  - IA, API version `2026-10-21` de Anthropic: la versión del SDK sale de `package-lock.json`, `node_modules` o el rango de `package.json`, y en Python solo de `requirements.txt` (todavía no de `pyproject.toml`, `poetry.lock` ni `uv.lock`). Un rango sin número (`latest`, `*`) cuenta como versión desconocida. Por HTTP directo avisa solo si el archivo manda `anthropic-version: 2026-10-21`. Los SDKs de Go, Java, .NET, Ruby y PHP no se leen.
209
233
  - IA, alias de Anthropic: avisa los de Claude Sonnet 4.5, Opus 4.5 y Haiku 4.5 (`claude-sonnet-4-5`, `claude-opus-4-5`, `claude-haiku-4-5`), escritos enteros entre comillas. Los alias de modelos ya retirados (`claude-3-5-sonnet-latest`, `claude-3-7-sonnet-latest` y otros) todavía no: la documentación actual de Anthropic ya no los nombra.
@@ -15,6 +15,7 @@
15
15
  "afip.ts": { "provider": "ARCA (ex AFIP)", "category": "facturación" },
16
16
  "ts-afip-ws": { "provider": "ARCA (ex AFIP)", "category": "facturación" },
17
17
  "facturajs": { "provider": "ARCA (ex AFIP)", "category": "facturación" },
18
+ "facturas": { "provider": "ARCA (ex AFIP)", "category": "facturación" },
18
19
  "openai": { "provider": "OpenAI", "category": "ia" },
19
20
  "@anthropic-ai/sdk": { "provider": "Anthropic", "category": "ia" },
20
21
  "@google/generative-ai": { "provider": "Google (Gemini)", "category": "ia" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parche-cli",
3
- "version": "0.6.9",
3
+ "version": "0.7.2",
4
4
  "description": "Dependabot para APIs: detecta cambios de cualquier API de terceros en tu código (ARCA, Mercado Pago, OpenAI, Anthropic) y abre el PR con el arreglo.",
5
5
  "keywords": [
6
6
  "api",
@@ -1,24 +1,24 @@
1
1
  {
2
2
  "id": "arca.wsfe.condicion-iva-receptor",
3
- "version": "2026.10.16",
3
+ "version": "2026.10.24",
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-16",
9
+ "last_verified": "2026-10-24",
10
10
  "summary_es": "Desde el 1/12/2026 ARCA rechaza automáticamente las solicitudes de CAE que no informen CondicionIVAReceptorId.",
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.",
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 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
- { "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/" },
13
+ { "title": "VATupdate: Argentina makes recipient VAT status mandatory in electronic invoices", "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
15
  { "title": "Tabla de códigos de FEParamGetCondicionIvaReceptor", "url": "https://app.spomsolutions.com/blog/condicion-iva-receptor-rg-5616" },
16
16
  { "title": "Afip SDK: factura electrónica (Node y PHP, CreateVoucher / CreateNextVoucher)", "url": "https://docs.afipsdk.com/siguientes-pasos/web-services/factura-electronica" },
17
17
  { "title": "afipsdk/afip.php en Packagist", "url": "https://packagist.org/packages/afipsdk/afip.php" }
18
18
  ],
19
19
  "detect": {
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
+ "packages": ["@afipsdk/afip.js", "@arcasdk/core", "afip.ts", "ts-afip-ws", "facturajs", "facturas", "afipsdk/afip.php", "fertandil87/afip.php", "ivanalemunioz/afip.php", "eesnaola/afip.php"],
21
+ "calls": ["ElectronicBilling.createVoucher", "ElectronicBilling.createNextVoucher", "ElectronicBilling->CreateVoucher", "ElectronicBilling->CreateNextVoucher", "electronicBillingService.createVoucher", "electronicBillingService.createNextVoucher"],
22
22
  "raw_markers": ["FECAESolicitar", "FECAEDetRequest"],
23
23
  "field": "CondicionIVAReceptorId"
24
24
  },
@@ -44,8 +44,10 @@
44
44
  "codemod": "arca-wsfe-condicion-iva",
45
45
  "review_checklist_es": [
46
46
  "Probar la emisión en homologación (wsfehomo) con cada tipo de comprobante que emitís.",
47
- "Confirmar que cada cliente con CUIT tiene guardada su condición frente al IVA y que se envía en cada CAE.",
48
47
  "Pedir la condición frente al IVA en el alta de clientes nuevos.",
49
48
  "Validar los códigos con FEParamGetCondicionIvaReceptor (la tabla puede cambiar)."
49
+ ],
50
+ "review_checklist_if_pending_es": [
51
+ "Confirmar que cada cliente con CUIT tiene guardada su condición frente al IVA y que se envía en cada CAE."
50
52
  ]
51
53
  }
package/src/ast.js CHANGED
@@ -23,11 +23,12 @@ export function eolOf(source) {
23
23
  return crlf > lf ? '\r\n' : '\n';
24
24
  }
25
25
 
26
- // Aplica inserciones de texto { offset, text } de atrás para adelante, así no se corren los offsets.
26
+ // Aplica ediciones de texto { offset, text, remove } de atrás para adelante, así no se corren los offsets.
27
+ // Sin `remove` es una inserción; con `remove`, `text` reemplaza esa cantidad de caracteres desde `offset`.
27
28
  export function applyEdits(source, edits) {
28
29
  let out = source;
29
30
  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
+ out = out.slice(0, e.offset) + e.text + out.slice(e.offset + (e.remove || 0));
31
32
  }
32
33
  return out;
33
34
  }
@@ -90,6 +91,15 @@ export function lineOf(node) {
90
91
  return node && node.loc ? node.loc.start.line : null;
91
92
  }
92
93
 
94
+ // La línea del archivo (contando \n, como git y GitHub) de una línea del parser, que también cuenta cada \r suelto.
95
+ // Solo difieren en archivos con \r\r\n o \r solos (frente B, punto 22).
96
+ export function fileLine(source, line) {
97
+ const breaks = /\r\n|[\n\r]/g;
98
+ let offset = 0;
99
+ for (let i = 1; i < line && breaks.exec(source); i++) offset = breaks.lastIndex;
100
+ return source.slice(0, offset).split('\n').length;
101
+ }
102
+
93
103
  // Copia profunda de un nodo sin metadatos de posición, para poder usarlo dos veces.
94
104
  export function cloneNode(node) {
95
105
  if (Array.isArray(node)) return node.map(cloneNode);
@@ -79,6 +79,46 @@ export function partialLeftMessage(pending) {
79
79
  return `Tiene el arreglo parcial de Parche (solo DocTipo 99 → 5). ${pending.message}`;
80
80
  }
81
81
 
82
+ // Clave casi igual al campo (frente B, punto 13): `CondicionIVAReceptor` sin Id, `CondicionIvaReceptorId`,
83
+ // `condicionIVAReceptorId`. Ese elemento no existe en el WSDL de WSFEv1, así que el dato nunca viaja y el repo
84
+ // parece migrado (pablocla/erpia lo tenía en cuatro archivos). Solo se toca dentro del detalle de un comprobante
85
+ // (dos o más claves de WSFEv1), para no renombrar modelos o DTOs propios que se llamen parecido.
86
+ const FIELD = 'CondicionIVAReceptorId';
87
+ export const NEAR_HINT = /condicion_?iva_?receptor/i;
88
+ const DETAIL_KEYS = new Set(['Concepto', 'DocTipo', 'DocNro', 'CbteDesde', 'CbteHasta', 'CbteFch', 'ImpTotal', 'ImpNeto', 'ImpIVA', 'MonId', 'MonCotiz']);
89
+ export const isNearMissKey = (key) => typeof key === 'string' && key !== FIELD && /^condicion_?iva_?receptor(?:_?id)?$/i.test(key);
90
+ export const isVoucherDetail = (keys) => keys.filter((k) => DETAIL_KEYS.has(k)).length >= 2;
91
+
92
+ export function nearMissMessage(key, pack) {
93
+ return `\`${key}\` no es un campo de WSFEv1: ARCA espera \`${FIELD}\`, así que hoy el dato no llega y desde el ${dmy(pack.deadline)} ARCA rechaza el CAE. `
94
+ + 'Lo renombré sin tocar el valor: revisá que sea un código de FEParamGetCondicionIvaReceptor.';
95
+ }
96
+
97
+ export function nearMissBothMessage(key) {
98
+ return `El comprobante tiene \`${key}\` y también \`${FIELD}\`. \`${key}\` no es un campo de WSFEv1: se puede borrar. Revisá que \`${FIELD}\` lleve el valor correcto.`;
99
+ }
100
+
101
+ // Frente B, punto 25: una asignación aparte (`det.CondicionIVAReceptor = x`) a un objeto que no se puede confirmar
102
+ // como el detalle del comprobante. No se toca, pero se dice: un arreglo parcial nunca queda en silencio.
103
+ export function nearMissUnsureMessage(target, key) {
104
+ return `\`${target}\`: si es el detalle del comprobante, \`${key}\` no es un campo de WSFEv1 y ARCA espera \`${FIELD}\`. `
105
+ + 'No lo cambié porque no pude confirmar que sea el comprobante: revisalo a mano.';
106
+ }
107
+
108
+ // Frente B, punto 23: el tipo del repo declara la clave mal escrita. Renombrar solo el objeto rompe el tipado.
109
+ export function nearMissTypedMessage(key, decl, pack) {
110
+ return `\`${key}\` no es un campo de WSFEv1: ARCA espera \`${FIELD}\`, así que hoy el dato no llega y desde el ${dmy(pack.deadline)} ARCA rechaza el CAE. `
111
+ + `No lo renombré porque el tipo de \`${decl.file}:${decl.line}\` declara \`${key}\` y TypeScript dejaría de compilar: renombralo en los dos lugares (y en los tests que lo usen).`;
112
+ }
113
+
114
+ // Frente B, punto 22: con un \r suelto (\r\r\n) el parser cuenta una línea más que el archivo y la edición caía en
115
+ // otra línea. En esos archivos no se edita: se dice qué cambio hacer a mano.
116
+ export const LOOSE_EOL = /\r(?!\n)|[\u2028\u2029]/;
117
+ export function looseEolMessage(manual) {
118
+ return 'No edité este archivo: tiene saltos de línea `\\r` sueltos (por ejemplo `\\r\\r\\n`) y Parche no puede ubicar el cambio con certeza. '
119
+ + `Hacelo a mano: ${manual}.`;
120
+ }
121
+
82
122
  // "El repo ya usa CondicionIVAReceptorId en otro lado": prefijo para bajar a "revisar" lo que no se pudo leer
83
123
  // (en repos ya migrados, el objeto suele salir de un builder que sí pone el campo).
84
124
  export function alreadyMigratedPrefix(context) {
@@ -92,7 +132,7 @@ export function libraryCaveat(context, ecosystem) {
92
132
  const lib = (context?.arcaLibs || []).find((l) => l.ecosystem === ecosystem && l.travels !== 'si');
93
133
  if (lib) {
94
134
  return `Ojo: con ${lib.name} ${lib.version} el pedido se arma con un WSDL de 2021 que no tiene este campo`
95
- + `${lib.travels === 'no' ? '' : ' (en JS falta confirmarlo)'}: para que llegue a ARCA hay que actualizar la librería a 1.x.`;
135
+ + `${lib.travels === 'no' ? '' : ' (en JS falta confirmarlo)'}: para que llegue a ARCA hay que ${lib.fix || 'actualizar la librería a 1.x'}.`;
96
136
  }
97
137
  const wsdl = context?.staleWsdl?.[0];
98
138
  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.`;
@@ -8,8 +8,10 @@ import { parsePhp, walkPhp, phpLiteral, phpKey } from '../php.js';
8
8
  import {
9
9
  PACK, decideCondition, validCodesText, missingMessage, appliedMessage, soapDetection, ARCA_HINT, libraryCaveat, alreadyMigratedPrefix,
10
10
  canApplyAtRuntime, PARTIAL_COMMENT, partialAppliedMessage, partialPending, partialLeftMessage,
11
+ NEAR_HINT, isNearMissKey, isVoucherDetail, nearMissMessage, nearMissBothMessage, nearMissUnsureMessage,
11
12
  } from './arca-rules.js';
12
13
  import { emptyContext } from '../context.js';
14
+ import { isTestFile } from '../paths.js';
13
15
 
14
16
  // En PHP los nombres de método no distinguen mayúsculas: createVoucher también es válido.
15
17
  // El inventario usa el nombre oficial, así un mismo método no aparece dos veces.
@@ -112,7 +114,7 @@ function resolveData(arg, scope, call, scopes) {
112
114
  export default function transform({ source, file, pack, context = emptyContext() }) {
113
115
  const findings = [];
114
116
  const usages = [];
115
- if (!ARCA_HINT.test(source)) return { output: null, findings, usages }; // prefiltro (ver el codemod de JS)
117
+ if (!ARCA_HINT.test(source) && !NEAR_HINT.test(source)) return { output: null, findings, usages }; // prefiltro (ver el codemod de JS)
116
118
  const caveat = libraryCaveat(context, 'composer');
117
119
  const migrated = alreadyMigratedPrefix(context);
118
120
  let ast;
@@ -129,6 +131,58 @@ export default function transform({ source, file, pack, context = emptyContext()
129
131
  const { scopes, calls } = collect(ast);
130
132
  const edits = [];
131
133
  const edited = new Set(); // el mismo $data en dos llamadas: se inserta una sola vez (ver el codemod de JS)
134
+
135
+ // Punto 13 del frente B: 'CondicionIVAReceptor' => $x (sin Id) se renombra, como en JS. Solo cambia el texto
136
+ // entre las comillas de la clave; el array queda marcado para que la llamada no reciba un campo nuevo.
137
+ let nearMiss = false; // el archivo ya tiene un aviso de la clave: el de SOAP sin el campo sobra
138
+ if (!isTestFile(file)) {
139
+ walkPhp(ast, (node) => {
140
+ if (node.kind !== 'array') return;
141
+ const entries = node.items.filter((it) => it && it.kind === 'entry');
142
+ const near = entries.find((e) => isNearMissKey(phpKey(e)));
143
+ if (!near || !isVoucherDetail(entries.map(phpKey))) return;
144
+ edited.add(node);
145
+ nearMiss = true;
146
+ if (entries.some((e) => phpKey(e) === FIELD)) {
147
+ findings.push(finding(PACK, file, near, 'needs-review', nearMissBothMessage(phpKey(near))));
148
+ return;
149
+ }
150
+ const { start, end } = near.key.loc;
151
+ edits.push({ offset: start.offset + 1, remove: end.offset - start.offset - 2, text: FIELD });
152
+ findings.push(finding(PACK, file, near, 'needs-review', nearMissMessage(phpKey(near), pack) + (caveat ? ` ${caveat}` : '')));
153
+ });
154
+
155
+ // Punto 25: $det['CondicionIVAReceptor'] = $x; en una asignación aparte. Se renombra si $det es, en la misma
156
+ // función, un array literal con claves de WSFEv1 definido una sola vez; si no se puede confirmar, se avisa.
157
+ for (const s of scopes.values()) {
158
+ for (const [name, offsets] of s.offsets) {
159
+ for (const o of offsets) {
160
+ if (!isNearMissKey(o.key) || o.node.left.offset.kind !== 'string') continue;
161
+ const defs = s.assigns.get(name) || [];
162
+ const arr = defs.length === 1 && defs[0].operator === '=' && defs[0].right.kind === 'array' ? defs[0].right : null;
163
+ const entries = arr ? arr.items.filter((it) => it && it.kind === 'entry') : [];
164
+ if (!arr || !isVoucherDetail(entries.map(phpKey))) {
165
+ // Solo con la grafía de WSFEv1 (con mayúscula): $cliente['condicionIvaReceptor'] es un campo propio.
166
+ if (ARCA_HINT.test(source) && /^Condicion/.test(o.key)) {
167
+ findings.push(finding(PACK, file, o.node, 'needs-review', nearMissUnsureMessage(`$${name}['${o.key}']`, o.key)));
168
+ nearMiss = true;
169
+ }
170
+ continue;
171
+ }
172
+ edited.add(arr);
173
+ nearMiss = true;
174
+ if (entries.some((e) => phpKey(e) === FIELD) || offsets.some((x) => x.key === FIELD)) {
175
+ findings.push(finding(PACK, file, o.node, 'needs-review', nearMissBothMessage(o.key)));
176
+ continue;
177
+ }
178
+ const { start, end } = o.node.left.offset.loc;
179
+ edits.push({ offset: start.offset + 1, remove: end.offset - start.offset - 2, text: FIELD });
180
+ findings.push(finding(PACK, file, o.node, 'needs-review', nearMissMessage(o.key, pack) + (caveat ? ` ${caveat}` : '')));
181
+ }
182
+ }
183
+ }
184
+ }
185
+
132
186
  for (const { node: call, scope, method } of calls) {
133
187
  usages.push({ provider: 'arca', op: `ElectronicBilling->${method}`, line: call.loc.start.line });
134
188
  const data = resolveData(call.arguments[0], scope, call, scopes);
@@ -136,7 +190,7 @@ export default function transform({ source, file, pack, context = emptyContext()
136
190
  const entries = data.arr ? data.arr.items.filter((it) => it && it.kind === 'entry') : null;
137
191
  if (!entries || entries.some((e) => e.unpack || e.byRef)) {
138
192
  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}`;
139
- findings.push(finding(PACK, file, call, migrated ? 'needs-review' : 'needs-input', (migrated || '') + msg));
193
+ findings.push(finding(PACK, file, call, migrated ? 'needs-review' : 'needs-input', (migrated || '') + msg + (caveat ? ` ${caveat}` : '')));
140
194
  continue;
141
195
  }
142
196
  const byKey = (k) => entries.find((e) => phpKey(e) === k);
@@ -175,7 +229,7 @@ export default function transform({ source, file, pack, context = emptyContext()
175
229
  findings.push(finding(PACK, file, call, status, appliedMessage(d.status) + (caveat ? ` ${caveat}` : '')));
176
230
  }
177
231
 
178
- const soap = soapDetection(source, file, context);
232
+ const soap = nearMiss ? null : soapDetection(source, file, context); // ya tiene su aviso (ver el codemod de JS)
179
233
  if (soap) { findings.push(soap.finding); usages.push(soap.usage); }
180
234
 
181
235
  return { output: edits.length ? applyEdits(source, edits) : null, findings, usages };
@@ -2,13 +2,16 @@
2
2
  // Agrega CondicionIVAReceptorId a cada pedido de CAE hecho con afip.js cuando se puede inferir
3
3
  // sin datos del negocio, y marca el resto para que una persona complete el dato.
4
4
  import {
5
- jFor, detectQuote, eolOf, offsetAt, applyEdits, getProp, isQuotedKey, isPlainProp, literalValue, resolveBinding, finding,
5
+ jFor, detectQuote, eolOf, offsetAt, applyEdits, getProp, isQuotedKey, isPlainProp, keyName, literalValue, resolveBinding, finding, fileLine,
6
6
  } from '../ast.js';
7
7
  import {
8
8
  PACK, decideCondition, validCodesText, missingMessage, appliedMessage, soapDetection, ARCA_HINT, libraryCaveat, alreadyMigratedPrefix,
9
9
  canApplyAtRuntime, PARTIAL_COMMENT, partialAppliedMessage, partialPending, partialLeftMessage,
10
+ NEAR_HINT, isNearMissKey, isVoucherDetail, nearMissMessage, nearMissBothMessage, nearMissUnsureMessage, nearMissTypedMessage,
11
+ LOOSE_EOL, looseEolMessage,
10
12
  } from './arca-rules.js';
11
13
  import { emptyContext } from '../context.js';
14
+ import { isTestFile } from '../paths.js';
12
15
 
13
16
  const VOUCHER_METHODS = new Set(['createVoucher', 'createNextVoucher']);
14
17
  const WRAPPERS = new Set(['TSAsExpression', 'TSNonNullExpression', 'TSTypeAssertion', 'TSSatisfiesExpression', 'ParenthesizedExpression']);
@@ -18,7 +21,10 @@ const unwrap = (n) => {
18
21
  while (n && WRAPPERS.has(n.type)) n = n.expression;
19
22
  return n;
20
23
  };
21
- const isEBMember = (n) => !!(n && n.type === 'MemberExpression' && !n.computed && n.property.type === 'Identifier' && n.property.name === 'ElectronicBilling');
24
+ // afip.ElectronicBilling (afip.js) o arca.electronicBillingService (@arcasdk/core): las dos reciben el mismo
25
+ // comprobante, con las claves de WSFEv1 (tipos de @arcasdk/core 2.0.2, leídos en su paquete de npm el 24/10/2026).
26
+ const BILLING_MEMBERS = new Set(['ElectronicBilling', 'electronicBillingService']);
27
+ const isEBMember = (n) => !!(n && n.type === 'MemberExpression' && !n.computed && n.property.type === 'Identifier' && BILLING_MEMBERS.has(n.property.name));
22
28
 
23
29
  // ¿`node` es `<algo>.ElectronicBilling`? También con casts de TS y con un alias en el mismo scope
24
30
  // (`const eb = afip.ElectronicBilling; eb.createVoucher(...)`): los dos aparecieron en repos reales.
@@ -32,6 +38,13 @@ function isElectronicBilling(node, p) {
32
38
  return false;
33
39
  }
34
40
 
41
+ // Para el inventario: `ElectronicBilling` o `electronicBillingService`, también si la llamada pasa por un alias.
42
+ function billingName(node, p) {
43
+ let n = unwrap(node);
44
+ if (n && n.type === 'Identifier') n = unwrap(resolveBinding(p, n.name)?.node.init);
45
+ return isEBMember(n) ? n.property.name : 'ElectronicBilling';
46
+ }
47
+
35
48
  function voucherCallee(p) {
36
49
  const callee = unwrap(p.node.callee);
37
50
  if (!callee || callee.type !== 'MemberExpression' || callee.computed || callee.property.type !== 'Identifier') return null;
@@ -119,7 +132,8 @@ const suggestionFor = (d, pack) => `'CondicionIVAReceptorId': <condición del cl
119
132
  export default function transform({ source, file, pack, context = emptyContext() }) {
120
133
  const findings = [];
121
134
  const usages = [];
122
- if (!ARCA_HINT.test(source)) return { output: null, findings, usages };
135
+ const arcaFile = ARCA_HINT.test(source);
136
+ if (!arcaFile && !NEAR_HINT.test(source)) return { output: null, findings, usages };
123
137
  const caveat = libraryCaveat(context, 'npm');
124
138
  const migrated = alreadyMigratedPrefix(context);
125
139
  const j = jFor(file);
@@ -128,8 +142,10 @@ export default function transform({ source, file, pack, context = emptyContext()
128
142
  root = j(source);
129
143
  } catch (err) {
130
144
  // Antes se descartaba en silencio: un archivo de ARCA que no parsea es justo el que hay que mirar.
131
- findings.push({ pack: PACK, file, line: err.loc?.line || null, status: 'needs-input',
132
- message: `No pude analizar este archivo (${err.message.split('\n')[0]}). Revisá a mano las llamadas a ElectronicBilling.` });
145
+ if (arcaFile) {
146
+ findings.push({ pack: PACK, file, line: err.loc?.line || null, status: 'needs-input',
147
+ message: `No pude analizar este archivo (${err.message.split('\n')[0]}). Revisá a mano las llamadas a ElectronicBilling.` });
148
+ }
133
149
  return { output: null, findings, usages };
134
150
  }
135
151
  let changed = false;
@@ -137,10 +153,80 @@ export default function transform({ source, file, pack, context = emptyContext()
137
153
  // Objetos ya editados: el mismo `payload` puede ir a dos llamadas (lo vimos en un repo real). Como se edita
138
154
  // texto y el árbol no se entera, sin esto se insertaba dos veces → clave duplicada y TypeScript no compila.
139
155
  const edited = new Set();
156
+ // Cada cambio, con cómo hacerlo a mano: si el archivo tiene \r sueltos no se edita y se dice eso (punto 22).
157
+ const applied = [];
158
+ const apply = (f, manual) => { findings.push(f); applied.push({ f, manual }); };
159
+ // Punto 23: si un tipo del repo declara la clave mal escrita, renombrarla solo acá rompe el tipado.
160
+ const typedConflict = (key) => (/\.[cm]?tsx?$/.test(file) ? (context.nearMissTypes || []).find((t) => t.key === key) : null);
161
+
162
+ // Punto 13 del frente B: la clave casi bien escrita se renombra; el nombre correcto lo fija el WSDL.
163
+ // Va antes que las llamadas, así el comprobante renombrado no recibe además un CondicionIVAReceptorId nuevo.
164
+ let nearMiss = false; // el archivo ya tiene un aviso de la clave: el de SOAP sin el campo sobra
165
+ if (!isTestFile(file)) {
166
+ root.find(j.ObjectExpression).forEach((p) => {
167
+ const obj = p.node;
168
+ const props = obj.properties.filter(isPlainProp);
169
+ const near = props.find((pr) => isNearMissKey(keyName(pr)));
170
+ if (!near || !isVoucherDetail(props.map(keyName))) return;
171
+ edited.add(obj);
172
+ nearMiss = true;
173
+ if (getProp(obj, 'CondicionIVAReceptorId')) {
174
+ findings.push(finding(PACK, file, near, 'needs-review', nearMissBothMessage(keyName(near))));
175
+ return;
176
+ }
177
+ const typed = typedConflict(keyName(near));
178
+ if (typed) {
179
+ findings.push(finding(PACK, file, near, 'needs-review', nearMissTypedMessage(keyName(near), typed, pack)));
180
+ return;
181
+ }
182
+ edits.push(renameKey(source, near));
183
+ changed = true;
184
+ apply(finding(PACK, file, near, 'needs-review', nearMissMessage(keyName(near), pack) + (caveat ? ` ${caveat}` : '')),
185
+ `renombrá \`${keyName(near)}\` a \`CondicionIVAReceptorId\``);
186
+ });
187
+
188
+ // Punto 25: la misma clave en una asignación aparte (det.CondicionIVAReceptor = x, det['CondicionIVAReceptor'] = x),
189
+ // como en el informe de CAEA de un repo real. Se renombra si `det` es, en el mismo scope, un objeto literal con
190
+ // claves de WSFEv1. Si no se puede confirmar que sea el comprobante, se avisa sin tocar nada.
191
+ root.find(j.AssignmentExpression).forEach((p) => {
192
+ const left = unwrap(p.node.left);
193
+ if (!left || left.type !== 'MemberExpression') return;
194
+ const key = memberName(left);
195
+ if (!isNearMissKey(key)) return;
196
+ const target = unwrap(left.object);
197
+ const decl = target && target.type === 'Identifier' ? resolveBinding(p, target.name) : null;
198
+ const obj = unwrap(decl && decl.node.init);
199
+ if (!obj || obj.type !== 'ObjectExpression' || !isVoucherDetail(obj.properties.filter(isPlainProp).map(keyName))) {
200
+ // Solo con la grafía de WSFEv1 (con mayúscula): `cliente.condicionIvaReceptor = …` es un campo propio.
201
+ if (arcaFile && /^Condicion/.test(key)) {
202
+ const shown = target && target.type === 'Identifier' ? `${target.name}.${key}` : textOf(source, left);
203
+ findings.push(finding(PACK, file, p.node, 'needs-review', nearMissUnsureMessage(shown, key)));
204
+ nearMiss = true;
205
+ }
206
+ return;
207
+ }
208
+ edited.add(obj);
209
+ nearMiss = true;
210
+ if (getProp(obj, 'CondicionIVAReceptorId') || fieldAssignedLater(j, root, decl, target.name)) {
211
+ findings.push(finding(PACK, file, p.node, 'needs-review', nearMissBothMessage(key)));
212
+ return;
213
+ }
214
+ const typed = typedConflict(key);
215
+ if (typed) {
216
+ findings.push(finding(PACK, file, p.node, 'needs-review', nearMissTypedMessage(key, typed, pack)));
217
+ return;
218
+ }
219
+ edits.push(renameMember(source, left));
220
+ changed = true;
221
+ apply(finding(PACK, file, p.node, 'needs-review', nearMissMessage(key, pack) + (caveat ? ` ${caveat}` : '')),
222
+ `renombrá \`${target.name}.${key}\` a \`${target.name}.CondicionIVAReceptorId\``);
223
+ });
224
+ }
140
225
 
141
226
  root.find(j.CallExpression).filter((p) => voucherCallee(p) !== null).forEach((p) => {
142
227
  const call = p.node;
143
- usages.push({ provider: 'arca', op: `ElectronicBilling.${voucherCallee(p).property.name}`, line: call.loc?.start.line });
228
+ const callee = voucherCallee(p);
229
+ usages.push({ provider: 'arca', op: `${billingName(callee.object, p)}.${callee.property.name}`, line: call.loc?.start.line });
144
230
  const arg = call.arguments[0];
145
231
  let obj = null;
146
232
  let binding = null;
@@ -151,7 +237,9 @@ export default function transform({ source, file, pack, context = emptyContext()
151
237
  }
152
238
  if (!obj || otherSpread(obj)) {
153
239
  // Si el repo ya usa el campo en otro lado, lo más probable es que el builder ya lo ponga: "revisar".
154
- findings.push(finding(PACK, file, call, migrated ? 'needs-review' : 'needs-input', (migrated || '') + unreadableMessage(j, root, p, arg, obj)));
240
+ // El aviso de librería vieja también va acá (frente B, punto 14: en los repos de jretondo los datos llegan de afuera).
241
+ findings.push(finding(PACK, file, call, migrated ? 'needs-review' : 'needs-input',
242
+ (migrated || '') + unreadableMessage(j, root, p, arg, obj) + (caveat ? ` ${caveat}` : '')));
155
243
  return;
156
244
  }
157
245
  if (getProp(obj, 'CondicionIVAReceptorId') || edited.has(obj)) return; // ya está migrado (o ya lo editamos)
@@ -179,8 +267,9 @@ export default function transform({ source, file, pack, context = emptyContext()
179
267
  edits.push(...insertAfterProp(source, anchor, `...(Number(${expr}) === 99 ? { ${keyText}: 5 } : {})`, PARTIAL_COMMENT, { isLast }));
180
268
  edited.add(obj);
181
269
  changed = true;
182
- findings.push(finding(PACK, file, call, 'needs-review', partialAppliedMessage(expr) + (caveat ? ` ${caveat}` : ''),
183
- { partial: partialPending(d, pack, suggestionFor(d, pack)) }));
270
+ apply(finding(PACK, file, call, 'needs-review', partialAppliedMessage(expr) + (caveat ? ` ${caveat}` : ''),
271
+ { partial: partialPending(d, pack, suggestionFor(d, pack)) }),
272
+ 'agregá `CondicionIVAReceptorId: 5` al comprobante cuando DocTipo es 99 (consumidor final); con otro DocTipo falta la condición frente al IVA del cliente');
184
273
  return;
185
274
  }
186
275
  findings.push(finding(PACK, file, call, 'needs-input', missingMessage(d) + (caveat ? ` ${caveat}` : ''),
@@ -193,15 +282,50 @@ export default function transform({ source, file, pack, context = emptyContext()
193
282
  changed = true;
194
283
  // Con librería vieja o WSDL viejo el arreglo no alcanza solo: deja de ser "automático".
195
284
  const status = caveat && d.status === 'auto' ? 'needs-review' : d.status;
196
- findings.push(finding(PACK, file, call, status, appliedMessage(d.status) + (caveat ? ` ${caveat}` : '')));
285
+ apply(finding(PACK, file, call, status, appliedMessage(d.status) + (caveat ? ` ${caveat}` : '')),
286
+ `agregá \`CondicionIVAReceptorId: ${d.code}\` al comprobante`);
197
287
  });
198
288
 
199
- const soap = soapDetection(source, file, context);
289
+ // Punto 22: con \r sueltos (\r\r\n) las líneas del parser no son las del archivo: se informan las de git.
290
+ const looseEol = LOOSE_EOL.test(source);
291
+ if (looseEol) for (const x of [...findings, ...usages]) if (x.line) x.line = fileLine(source, x.line);
292
+
293
+ // Con la clave renombrada (o avisada) el archivo ya tiene su aviso: el de SOAP sin CondicionIVAReceptorId sobra.
294
+ const soap = nearMiss ? null : soapDetection(source, file, context);
200
295
  if (soap) { findings.push(soap.finding); usages.push(soap.usage); }
201
296
 
297
+ // Y la edición caía en otro lado (una vez, dentro de un comentario y con "Sintaxis OK"). No se edita: cada cambio
298
+ // pasa a "revisar" con lo que hay que hacer a mano.
299
+ if (changed && looseEol) {
300
+ for (const { f, manual } of applied) {
301
+ f.status = 'needs-review';
302
+ f.message = looseEolMessage(manual);
303
+ delete f.partial;
304
+ }
305
+ return { output: null, findings, usages };
306
+ }
307
+
202
308
  return { output: changed ? applyEdits(source, edits) : null, findings, usages };
203
309
  }
204
310
 
311
+ // det.CondicionIVAReceptor → det.CondicionIVAReceptorId; det['CondicionIVAReceptor'] → det['CondicionIVAReceptorId'] (mismas comillas).
312
+ function renameMember(source, member) {
313
+ const start = offsetAt(source, member.property.loc.start);
314
+ const end = offsetAt(source, member.property.loc.end);
315
+ return member.computed ? { offset: start + 1, remove: end - start - 2, text: 'CondicionIVAReceptorId' }
316
+ : { offset: start, remove: end - start, text: 'CondicionIVAReceptorId' };
317
+ }
318
+
319
+ // `CondicionIVAReceptor: x` → `CondicionIVAReceptorId: x`, con las mismas comillas y sin tocar el valor.
320
+ // Si era abreviada (`{ CondicionIVAReceptor }`), queda `CondicionIVAReceptorId: CondicionIVAReceptor`.
321
+ function renameKey(source, prop) {
322
+ const start = offsetAt(source, prop.key.loc.start);
323
+ const end = offsetAt(source, prop.key.loc.end);
324
+ if (prop.shorthand) return { offset: start, remove: end - start, text: `CondicionIVAReceptorId: ${keyName(prop)}` };
325
+ const q = isQuotedKey(prop) ? source[start] : '';
326
+ return { offset: start, remove: end - start, text: `${q}CondicionIVAReceptorId${q}` };
327
+ }
328
+
205
329
  // Cuando no se puede leer el comprobante, decir DÓNDE hay que agregar el dato: la función que lo arma
206
330
  // (y de qué módulo viene) o que llega como parámetro. Lo encontrado en repos reales: payload = buildVoucherPayload(...).
207
331
  function unreadableMessage(j, root, p, arg, obj) {
package/src/context.js CHANGED
@@ -6,9 +6,12 @@
6
6
  // fieldFiles: archivos que ya usan CondicionIVAReceptorId (el repo ya empezó a migrar)
7
7
  // staleWsdl: WSDL de WSFEv1 del repo que no tienen CondicionIVAReceptorId
8
8
  // arcaLibs: librerías de ARCA del repo con lo que se sabe de si el campo llega a viajar
9
+ // nearMissTypes: interfaces y types de TypeScript que declaran la clave casi bien escrita (CondicionIvaReceptorId?: number)
9
10
  import fs from 'node:fs';
10
11
  import path from 'node:path';
12
+ import { parse as babelParse } from '@babel/parser';
11
13
  import { findFiles } from './paths.js';
14
+ import { NEAR_HINT, isNearMissKey } from './codemods/arca-rules.js';
12
15
 
13
16
  const readJson = (p) => {
14
17
  try {
@@ -59,8 +62,13 @@ export const openRange = (range) => /^\s*(?:>=?|\*|x|latest)/i.test(range || '')
59
62
  // afip.js / afip.php ≥ 1.0: mandan JSON a la API de Afip SDK → sí.
60
63
  // 0.x: arman el SOAP con un WSDL de 2021 incluido en la librería, sin el campo → no (PHP) o sin confirmar (JS).
61
64
  // fertandil87/afip.php: actualizó su WSDL (lo incluye) → sí. eesnaola/afip.php: WSDL de 2021 → igual que 0.x.
65
+ // ts-afip-ws: fork de afip.js 0.x (frente B, punto 14: los repos de jretondo). Su última versión, la 1.3.2 de 2023,
66
+ // trae wsfe.wsdl y wsfe-production.wsdl sin el campo (leído en el paquete de npm el 24/10/2026) → sin confirmar.
62
67
  const ARCA_LIBS = {
63
- npm: { '@afipsdk/afip.js': (v) => (major(v) >= 1 ? 'si' : 'sin-confirmar') },
68
+ npm: {
69
+ '@afipsdk/afip.js': (v) => (major(v) >= 1 ? 'si' : 'sin-confirmar'),
70
+ 'ts-afip-ws': () => 'sin-confirmar',
71
+ },
64
72
  composer: {
65
73
  'afipsdk/afip.php': (v) => (major(v) >= 1 ? 'si' : 'no'),
66
74
  'ivanalemunioz/afip.php': (v) => (major(v) >= 1 ? 'si' : 'no'),
@@ -69,13 +77,18 @@ const ARCA_LIBS = {
69
77
  },
70
78
  };
71
79
  const major = (v) => Number(String(v).split('.')[0]);
80
+ // Qué hacer para que el campo viaje, cuando no es "actualizar la librería a 1.x" (los forks no tienen 1.x).
81
+ const FIXES = {
82
+ 'ts-afip-ws': 'pasar a @afipsdk/afip.js 1.x o actualizar el WSDL que trae la librería',
83
+ 'eesnaola/afip.php': 'pasar a afipsdk/afip.php 1.x o actualizar el WSDL que trae la librería',
84
+ };
72
85
 
73
86
  export function arcaLibraries(versions) {
74
87
  const out = [];
75
88
  for (const [eco, libs] of Object.entries(ARCA_LIBS)) {
76
89
  for (const [name, rule] of Object.entries(libs)) {
77
90
  const v = versions[eco]?.[name];
78
- if (v) out.push({ ecosystem: eco, name, version: v.version, source: v.source, travels: rule(v.version) });
91
+ if (v) out.push({ ecosystem: eco, name, version: v.version, source: v.source, travels: rule(v.version), ...(FIXES[name] ? { fix: FIXES[name] } : {}) });
79
92
  }
80
93
  }
81
94
  return out;
@@ -94,7 +107,33 @@ export function buildContext(root, files, manifests) {
94
107
  const text = fs.readFileSync(path.join(root, rel), 'utf8');
95
108
  return /FECAESolicitar|FECAEDetRequest/.test(text) && !text.includes('CondicionIVAReceptorId');
96
109
  });
97
- return { versions, fieldFiles, staleWsdl, arcaLibs: arcaLibraries(versions) };
110
+ return { versions, fieldFiles, staleWsdl, arcaLibs: arcaLibraries(versions), nearMissTypes: nearMissTypes(root, files) };
111
+ }
112
+
113
+ // Frente B, punto 23: si un tipo declara la clave mal escrita, renombrarla solo en el objeto rompe el tipado
114
+ // (TS2353) y el chequeo de sintaxis no lo ve. El codemod de JS no renombra cuando la encuentra acá.
115
+ export function nearMissTypes(root, files) {
116
+ const out = [];
117
+ for (const rel of files.filter((f) => /\.[cm]?tsx?$/.test(f))) {
118
+ let text;
119
+ try { text = fs.readFileSync(path.join(root, rel), 'utf8'); } catch { continue; }
120
+ if (!NEAR_HINT.test(text)) continue;
121
+ let ast;
122
+ try {
123
+ ast = babelParse(text, { sourceType: 'unambiguous', plugins: ['typescript', 'decorators-legacy', ...(/x$/.test(rel) ? ['jsx'] : [])], errorRecovery: true });
124
+ } catch { continue; }
125
+ const walk = (n) => {
126
+ if (!n || typeof n !== 'object') return;
127
+ if (Array.isArray(n)) { n.forEach(walk); return; }
128
+ if (n.type === 'TSPropertySignature') {
129
+ const key = n.key && (n.key.type === 'Identifier' ? n.key.name : n.key.type === 'StringLiteral' ? n.key.value : null);
130
+ if (isNearMissKey(key)) out.push({ file: rel, line: n.loc.start.line, key });
131
+ }
132
+ for (const k of Object.keys(n)) if (k !== 'loc' && k !== 'start' && k !== 'end') walk(n[k]);
133
+ };
134
+ walk(ast.program);
135
+ }
136
+ return out;
98
137
  }
99
138
 
100
- export const emptyContext = () => ({ versions: { npm: {}, composer: {}, pypi: {} }, fieldFiles: [], staleWsdl: [], arcaLibs: [] });
139
+ export const emptyContext = () => ({ versions: { npm: {}, composer: {}, pypi: {} }, fieldFiles: [], staleWsdl: [], arcaLibs: [], nearMissTypes: [] });
package/src/report.js CHANGED
@@ -158,9 +158,15 @@ export function prBody({ pack, findings, changed, verification, ciApproval = fal
158
158
  out.push('- CI de este PR: lo abrió GitHub Actions con el `GITHUB_TOKEN`, así que GitHub deja sus workflows esperando aprobación. Alguien con permiso de escritura tiene que tocar **Approve workflows to run**. Para que corran solos, pasale a la Action un token de una GitHub App o un token personal (input `token`).');
159
159
  }
160
160
  }
161
- out.push('', asIssue ? '### Checklist' : '### Checklist antes de mergear', '', ...pack.review_checklist_es.map((x) => `- [ ] ${x}`), '');
161
+ // Los ítems que dependen de un dato del negocio (por ejemplo, los clientes con CUIT) solo van si queda algo
162
+ // pendiente: en un PR que arregla todo, como uno de puros consumidores finales, no aplican (frente B, punto 12).
163
+ const checklist = [...pack.review_checklist_es, ...(pending.length ? pack.review_checklist_if_pending_es || [] : [])];
164
+ out.push('', asIssue ? '### Checklist' : '### Checklist antes de mergear', '', ...checklist.map((x) => `- [ ] ${x}`), '');
162
165
  out.push('### Fuentes', '', ...pack.sources.map((s) => `- [${s.title}](${s.url})`), '');
163
- out.push(`<sub>Pack \`${pack.id}\` v${pack.version}, verificado el ${pack.last_verified}. Resumen: ${Object.entries(c).map(([k, v]) => `${LABEL[k]} ${v}`).join(' · ')}. ¿Este PR te sirvió? Mergealo, editalo o cerralo: Parche aprende de cada resultado. [¿Qué es Parche y por qué recibiste esto?](https://github.com/Pepo08/parche/blob/main/docs/por-que-recibi-este-pr.md)</sub>`, '');
166
+ const feedback = asIssue
167
+ ? '¿Te sirvió este aviso? Cerralo cuando lo resuelvas, o contestá si algo no aplica: Parche aprende de cada resultado.'
168
+ : '¿Este PR te sirvió? Mergealo, editalo o cerralo: Parche aprende de cada resultado.';
169
+ out.push(`<sub>Pack \`${pack.id}\` v${pack.version}, verificado el ${pack.last_verified}. Resumen: ${Object.entries(c).map(([k, v]) => `${LABEL[k]} ${v}`).join(' · ')}. ${feedback} [¿Qué es Parche y por qué recibiste esto?](https://github.com/Pepo08/parche/blob/main/docs/por-que-recibi-este-pr.md)</sub>`, '');
164
170
  return out.join('\n');
165
171
  }
166
172