verifactu-rails 0.1.0.pre

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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/COMPLIANCE.md +149 -0
  3. data/LICENSE +21 -0
  4. data/README.md +404 -0
  5. data/doc/FUENTES.md +505 -0
  6. data/lib/generators/verifactu/install/install_generator.rb +61 -0
  7. data/lib/generators/verifactu/install/templates/instalar_verifactu.rb.tt +19 -0
  8. data/lib/generators/verifactu/install/templates/verifactu.rb.tt +64 -0
  9. data/lib/verifactu-rails.rb +31 -0
  10. data/lib/verifactu_rails/certificado.rb +122 -0
  11. data/lib/verifactu_rails/consulta.rb +279 -0
  12. data/lib/verifactu_rails/desglose.rb +396 -0
  13. data/lib/verifactu_rails/envio.rb +153 -0
  14. data/lib/verifactu_rails/error.rb +31 -0
  15. data/lib/verifactu_rails/formato.rb +199 -0
  16. data/lib/verifactu_rails/huella.rb +130 -0
  17. data/lib/verifactu_rails/importe.rb +80 -0
  18. data/lib/verifactu_rails/libro/autochequeo.rb +67 -0
  19. data/lib/verifactu_rails/libro/cadena.rb +110 -0
  20. data/lib/verifactu_rails/libro/migracion.rb +92 -0
  21. data/lib/verifactu_rails/libro/reconciliacion.rb +224 -0
  22. data/lib/verifactu_rails/libro/registro.rb +74 -0
  23. data/lib/verifactu_rails/libro/remesa.rb +120 -0
  24. data/lib/verifactu_rails/libro.rb +89 -0
  25. data/lib/verifactu_rails/qr.rb +56 -0
  26. data/lib/verifactu_rails/railtie.rb +30 -0
  27. data/lib/verifactu_rails/registro.rb +833 -0
  28. data/lib/verifactu_rails/respuesta.rb +154 -0
  29. data/lib/verifactu_rails/schemas/ConsultaLR.xsd +54 -0
  30. data/lib/verifactu_rails/schemas/EventosSIF.xsd +823 -0
  31. data/lib/verifactu_rails/schemas/PROCEDENCIA.md +64 -0
  32. data/lib/verifactu_rails/schemas/RespuestaConsultaLR.xsd +201 -0
  33. data/lib/verifactu_rails/schemas/RespuestaSuministro.xsd +139 -0
  34. data/lib/verifactu_rails/schemas/RespuestaValRegistNoVeriFactu.xsd +103 -0
  35. data/lib/verifactu_rails/schemas/SuministroInformacion.xsd +1390 -0
  36. data/lib/verifactu_rails/schemas/SuministroLR.xsd +25 -0
  37. data/lib/verifactu_rails/schemas/catalog.xml +5 -0
  38. data/lib/verifactu_rails/schemas/xmldsig-core-schema.xsd +318 -0
  39. data/lib/verifactu_rails/sistema_informatico.rb +88 -0
  40. data/lib/verifactu_rails/transporte.rb +127 -0
  41. data/lib/verifactu_rails/version.rb +5 -0
  42. metadata +124 -0
data/doc/FUENTES.md ADDED
@@ -0,0 +1,505 @@
1
+ # Fuentes y hallazgos
2
+
3
+ Qué dice la documentación oficial de VERI\*FACTU, qué se ha comprobado contra el
4
+ servicio real de la AEAT y qué sigue siendo suposición. Es la justificación de
5
+ las decisiones de diseño de la gema y el respaldo de [COMPLIANCE.md](../COMPLIANCE.md).
6
+
7
+ Cada bloque lleva marcado de dónde sale lo que afirma:
8
+
9
+ - **[doc]** — de la documentación oficial de la AEAT.
10
+ - **[real]** — comprobado contra el entorno de pruebas (preproducción).
11
+ - **[?]** — deducido o supuesto, sin comprobar.
12
+
13
+ ## Documentos
14
+
15
+ Los PDF no se versionan aquí: pesan casi 2 MB y no producen diffs legibles. Se
16
+ registran URL, versión y SHA-256 para detectar que han cambiado.
17
+
18
+ | Documento | Versión | SHA-256 |
19
+ |---|---|---|
20
+ | [Especificaciones huella/hash](https://www.agenciatributaria.es/static_files/AEAT_Desarrolladores/EEDD/IVA/VERI-FACTU/Veri-Factu_especificaciones_huella_hash_registros.pdf) | 0.1.2 | `f4334c254bb875b417247b54315199f8…` |
21
+ | [Validaciones y errores](https://www.agenciatributaria.es/static_files/AEAT_Desarrolladores/EEDD/IVA/VERI-FACTU/Validaciones_Errores_Veri-Factu.pdf) | 1.2.2 | `426eb926fc098a36a163f66ca5f40d9e…` |
22
+ | [Especificaciones del código QR](https://www.agenciatributaria.es/static_files/AEAT_Desarrolladores/EEDD/IVA/VERI-FACTU/DetalleEspecificacTecnCodigoQRfactura.pdf) | 0.5.0 | `f86b3c260d8a4963dbc18c5007732b53…` |
23
+ | [FAQs de empresas de desarrollo](https://www.agenciatributaria.es/static_files/AEAT_Desarrolladores/EEDD/IVA/VERI-FACTU/FAQs-Desarrolladores.pdf) | 1.3 | `73906dc8afbbb9da35f6cb489980352b…` |
24
+
25
+ Índice por si cambian de nombre:
26
+ [Documentación VERI\*FACTU para desarrolladores](https://www.agenciatributaria.es/AEAT.desarrolladores/Desarrolladores/_menu_/Documentacion/Sistemas_Informaticos_de_Facturacion_y_Sistemas_VERI_FACTU/Sistemas_Informaticos_de_Facturacion_y_Sistemas_VERI_FACTU.html).
27
+ Para comprobar si han cambiado: `curl -sL -A "Mozilla/5.0" "<url>" | shasum -a 256`.
28
+
29
+ Además: `DsRegistroVeriFactu.xlsx` (Diseños de registro v1.0), Descripción SWeb
30
+ v1.0.3 y el
31
+ [listado de errores](https://prewww2.aeat.es/static_files/common/internet/dep/aplicaciones/es/aeat/tikeV1.0/cont/ws/errores.properties)
32
+ (ISO-8859-1, 247 códigos: 44 rechazan el envío completo, 193 la factura y 10
33
+ producen aceptación con obligación de subsanar).
34
+
35
+ ## La huella
36
+
37
+ **[doc]** Los ceros a la derecha son irrelevantes: *"se tratarán indistintamente
38
+ los valores con una o dos posiciones en los decimales"*. La exigencia real no es
39
+ un formato concreto sino **coherencia entre la huella y el XML**. "Siempre 2
40
+ decimales" es válido, y el `241.4` del ejemplo oficial también.
41
+
42
+ **[doc]** Los espacios al borde hay que recortarlos. La gema los **rechaza**, que
43
+ es más estricto: casi siempre son un defecto de los datos de origen y recortar en
44
+ silencio lo taparía.
45
+
46
+ **[doc]** Un campo vacío va como `Campo=` (nombre, igual, nada). Es el caso del
47
+ primer registro de la cadena.
48
+
49
+ **[real]** La huella que construye la gema coincide con la que recalcula la AEAT,
50
+ tanto en altas como en anulaciones. La serialización de la anulación es distinta
51
+ —no lleva importes ni tipo de factura, solo IDFactura, fecha de generación y
52
+ huella anterior— y también cuadra.
53
+
54
+ **[doc]** Una huella que no coincide **no provoca rechazo**: es *error admisible*
55
+ (ap. 4.3.1), el registro se acepta y queda anotado, pero obliga a subsanarlo.
56
+
57
+ Los tres vectores oficiales del ap. 6 están en `test/diferencial_test.rb` y se
58
+ reproducen exactamente, encadenamiento incluido.
59
+
60
+ ## Validaciones implementadas
61
+
62
+ **[doc]** De Validaciones v1.2.2:
63
+
64
+ | Ap. | Regla | Dónde |
65
+ |---|---|---|
66
+ | 3.1.3.1 | `IDEmisorFactura` = NIF del `ObligadoEmision` de la cabecera | `Envio#validar_emisores!` |
67
+ | 3.1.3.1 | `FechaExpedicionFactura` no futura ni anterior a 28-10-2024 | `RegistroAlta#validar_fecha_expedicion!` |
68
+ | 3.1.3.1 | `NumSerieFactura`: ASCII 32-126, prohibidos `"` `'` `<` `>` `=` | `Formato.num_serie` |
69
+ | 3.1.3.3 | `TipoRectificativa` obligatorio y exclusivo de R1-R5 | `RegistroAlta#validar_rectificativa!` |
70
+ | 3.1.3.4 | `FacturasRectificadas` **no obligatoria**, exclusiva de R1-R5 | ídem |
71
+ | 3.1.3.5 | `FacturasSustituidas` **no obligatoria**, exclusiva de F3 | `#validar_sustitutiva!` |
72
+ | 3.1.3.6 | `ImporteRectificacion` obligatorio y exclusivo de `TipoRectificativa=S` | `#validar_importe_rectificacion!` |
73
+ | 3.1.3.13 | Destinatarios obligatorios en F1/F3/R1-R4, prohibidos en F2/R5 | `#validar_destinatarios!` |
74
+ | 2 | `RechazoPrevio` distinto de "N" solo dentro de una subsanación | `#validar_subsanacion!` |
75
+ | 8 y 9 | `FacturaSimplificadaArt7273` solo en F1/F3/R1-R4; `FacturaSinIdentifDestinatarioArt61d` solo en F2/R5 | `#validar_marcas!` |
76
+ | 10 | `Macrodato` obligatorio si `ImporteTotal >= |100.000.000,00|` | `#validar_macrodato!` |
77
+ | 11 | `EmitidaPorTerceroODestinatario` "T" exige `Tercero`; "D" exige `Destinatarios` | `#validar_emisor_tercero!` |
78
+ | 12 | `Tercero` solo con "T", NIF distinto del emisor, sin `IDType` 07, y desde ES solo 03 | ídem y `Tercero` |
79
+ | 13 | Reglas de `IDOtro` del destinatario (07 exige ES; desde ES solo 03 o 07) | `IdOtro.normalizar` |
80
+ | 14 | `Cupon` solo "S" y solo con R1 o R5 | `#validar_cupon!` |
81
+ | 15.1 | Ventanas temporales de `TipoImpositivo` | `Detalle#validar_en_fecha!` |
82
+ | 15.3 | El recargo de equivalencia tiene que cuadrar con el tipo impositivo | ídem |
83
+ | 15.4 | `CalificacionOperacion` S2 solo en F1/F3/R1-R4 | `#validar_inversion_sujeto_pasivo!` |
84
+ | 15.4-15.7 | Coherencia de `Calificacion`, `OperacionExenta` y recargo | `Detalle#validar_coherencia!` |
85
+ | 15.6 | `ClaveRegimen`: obligatoria con IVA/IPSI/IGIC, prohibida con Otros, y contenida en la lista que corresponda al impuesto | `Detalle#validar_clave_regimen!` |
86
+ | 15.6.1-3, 15.6.5-6, 15.6.8, 15.6.10 | Cada clave de régimen ata la calificación o la exención de su línea | `Detalle#validar_regimen_de_la_linea!` |
87
+ | 15.6.4, 15.6.7, 15.6.9 | Claves 06, 10 y 14: tipo de factura, destinatarios y fecha de operación | `RegistroAlta#validar_desglose_cruzado!` |
88
+
89
+ Tres cosas que conviene entender:
90
+
91
+ - **El 5 % ya no es declarable a secas.** Fue una rebaja temporal cuya ventana
92
+ cerró el 30-09-2024, y como `FechaExpedicionFactura` no puede ser anterior al
93
+ 28-10-2024, hoy solo cabe informando una `FechaOperacion` dentro de la ventana.
94
+ Igual para el 2 % y el 7,5 %. Se mide contra `FechaOperacion`, o la de
95
+ expedición si falta.
96
+ - **Las reglas cruzadas no caben en el objeto del que hablan.** Un `Detalle` no
97
+ conoce el `TipoFactura` ni la fecha de operación, así que 15.1, 15.3 y 15.4 las
98
+ dispara `RegistroAlta`, igual que `Envio` comprueba que el emisor de cada
99
+ registro sea el obligado de la cabecera.
100
+ - **Fuera de las ventanas que la norma menciona no se impone nada.** Inventar
101
+ restricciones donde el texto calla repetiría el error que ya hubo al hacer
102
+ obligatorias 3.1.3.4 y 3.1.3.5, que no lo son.
103
+
104
+ **Una regla del ap. 13 se decidió NO implementar**: "si un destinatario se
105
+ identifica con `IDType=02`, `TipoFactura` debe ser F1/F3/R1-R4" es redundante,
106
+ porque los tipos se reparten entre los que exigen destinatario y los que lo
107
+ prohíben. Escribirla habría dejado una comprobación incapaz de fallar, que
108
+ aparenta cobertura sin cubrir nada.
109
+
110
+ **Sin implementar**: ap. 15.2 `BaseImponibleACoste`, porque el campo no está
111
+ soportado por la gema. Arrastra una consecuencia: la clave de régimen **`06`
112
+ (grupo de entidades, nivel avanzado) exige ese campo**, así que un registro que
113
+ la use no se puede construir bien. La gema lo rechaza en local diciendo por qué,
114
+ en vez de armar algo que la AEAT va a rechazar de todas formas.
115
+
116
+ **Sobre el alcance de la 15.6, para no confundirlo:** sus reglas se acotan a
117
+ "Impuesto = 01 (IVA), 03 (IGIC) o no se cumplimenta". En IPSI **no aplican**, y
118
+ eso importa porque ahí las claves 18, 19 y 20 significan otra cosa —art. 73.4 y
119
+ 5 de la Ordenanza fiscal de Ceuta, operaciones interiores exentas y régimen de
120
+ estimación objetiva—. Aplicarles las reglas de IVA sería inventar restricciones
121
+ sobre una lista distinta.
122
+
123
+ ### Qué hacen las otras implementaciones con la 15.6
124
+
125
+ Comprobado leyendo su código fuente, no su documentación:
126
+
127
+ - **`josemmo/Verifactu-PHP`**: no implementa **ninguna**. `ClaveRegimen` es un
128
+ enum puro; la única regla cruzada que tiene es la de la clave 18 con el recargo
129
+ de equivalencia.
130
+ - **`mybooking-es/verifactu-rb`**: implementa la mayoría, y es la referencia más
131
+ completa que hemos encontrado. Le faltan las tres del bloque de la clave 14
132
+ (destinatarios con NIF que empiece por P/Q/S/V y tipo de factura) y la de la
133
+ clave 20 con IGIC. Su regla de exenciones del criterio de caja queda además en
134
+ una rama inalcanzable, porque su constructor exige `calificacion_operacion` y
135
+ la comprobación de `OperacionExenta` vive en el `elsif`.
136
+
137
+ **[doc] Otros errores admisibles** (se aceptan, obligan a subsanar): `ImporteTotal`
138
+ o `CuotaTotal` que no cuadran con el desglose, con margen de ±10,00 € (no aplica
139
+ si `ClaveRegimen` es 03, 05, 06, 08 o 09); `PrimerRegistro="S"` cuando ya existen
140
+ registros para ese SIF y NIF; y `FechaHoraHusoGenRegistro` posterior a la hora de
141
+ la AEAT (este exceptuado de subsanación).
142
+
143
+ ## Listas de códigos
144
+
145
+ **[doc]** De la hoja "6)Listas" del Excel de diseños, que es la fuente autorizada
146
+ de los enumerados que las Validaciones citan sin desarrollar:
147
+
148
+ | Lista | Campo | Valores |
149
+ |---|---|---|
150
+ | L1 | `Impuesto` | 01 IVA, 02 IPSI, 03 IGIC, 05 Otros |
151
+ | L2 | `TipoFactura` | F1, F2, F3, R1–R5 |
152
+ | L3 | `TipoRectificativa` | S sustitución, I diferencias |
153
+ | L6 | `EmitidaPorTerceroODestinatario` | D, T |
154
+ | L7 | `IDType` | 02 NIF-IVA, 03 pasaporte, 04 doc. oficial, 05 cert. residencia, 06 otro, 07 no censado |
155
+ | L8A | `ClaveRegimen` con IVA | 01–11, 14, 15, 17, 18, 19, 20 |
156
+ | L8B | `ClaveRegimen` con IGIC | 01–11, 14, 15, 17, 18, 19 (+ 20 según Validaciones) |
157
+ | L9 | `CalificacionOperacion` | S1, S2, N1, N2 |
158
+ | **L10** | `OperacionExenta` | **E1–E6 únicamente** |
159
+ | L12 | `TipoHuella` | 01 SHA-256 |
160
+ | L15 | `IDVersion` | 1.0 |
161
+ | L16 | `GeneradoPor` | E expedidor, D destinatario, T tercero |
162
+ | L17 | `RechazoPrevio` | N, S, X |
163
+
164
+ L10 confirma que **E7 y E8 solo se admiten con IGIC**, y así lo hace `Detalle`:
165
+ `EXENCIONES` es E1–E6 y `EXENCIONES_IGIC` añade las dos, según el impuesto de la
166
+ línea.
167
+
168
+ `ClaveRegimen = 21` no aparece en L8A ni L8B pero sí en el XSD y en el ap. 15.6.11
169
+ de Validaciones. El Excel es v1.0 y Validaciones v1.2.2: se sigue a la más
170
+ reciente.
171
+
172
+ ## Las operativas de subsanación y rechazo
173
+
174
+ **[doc]** Los cuadros de operativa del Excel definen las variantes de alta y de
175
+ anulación, cada una distinguida por una combinación de campos:
176
+
177
+ | Operativa de alta | `Subsanacion` | `RechazoPrevio` |
178
+ |---|---|---|
179
+ | Alta inicial ("normal") | ausente o N | ausente o N |
180
+ | Alta de subsanación | S | ausente o N |
181
+ | Alta por rechazo de subsanación | S | S |
182
+ | Alta por rechazo / sin registro previo | S | X |
183
+
184
+ Las de anulación combinan `SinRegistroPrevio` y `RechazoPrevio` del mismo modo.
185
+
186
+ **Las construye todas**: `RegistroAlta` admite `subsanacion:` y
187
+ `rechazo_previo:`, y `RegistroAnulacion` admite `sin_registro_previo:` y
188
+ `rechazo_previo:`. Importa porque una huella que no cuadra es error admisible y
189
+ **obliga a subsanar**: sin estos campos, quien recibiera un `AceptadoConErrores`
190
+ no tendría con qué corregirlo. La subsanación está además contrastada contra el
191
+ servicio real (ver más abajo).
192
+
193
+ `RechazoPrevio = X` es el camino de migración desde NO VERI\*FACTU: registros que
194
+ existen en el SIF pero nunca se remitieron.
195
+
196
+ ## Endpoints, TLS y certificados
197
+
198
+ **[doc]** Los dominios de preproducción se corresponden uno a uno con producción:
199
+
200
+ | Preproducción | Producción | Uso |
201
+ |---|---|---|
202
+ | `prewww1.aeat.es` | `www1.agenciatributaria.gob.es` | Web services, certificado normal |
203
+ | `prewww2.aeat.es` | `www2.agenciatributaria.gob.es` | Estáticos (de aquí salen los XSD) y cotejo del QR |
204
+ | `prewww10.aeat.es` | `www10.agenciatributaria.gob.es` | Web services con **certificado de sello** |
205
+
206
+ **Aviso operativo del propio portal:** preproducción es para pruebas *puntuales*.
207
+ Nada de pruebas masivas ni de validaciones integradas en procesos de producción;
208
+ un uso que consideren abusivo puede acabar en bloqueo de acceso.
209
+
210
+ **[real] `ca_file` no hace falta.** Tras la renovación de noviembre de 2025, los
211
+ cinco endpoints validan con el almacén de confianza del sistema (`Verify return
212
+ code: 0`). Sirven cadenas de CA públicas: Entrust OV TLS y Sectigo, ambas bajo
213
+ USERTrust RSA. `ca_file` sigue existiendo por si hay que anclar la cadena en algún
214
+ entorno, pero ante un fallo de verificación lo probable es un almacén anticuado o
215
+ un proxy interceptando el TLS. Nunca `VERIFY_NONE`.
216
+
217
+ **[real]** El mTLS funciona con certificado de representante de la FNMT, y
218
+ `Certificado#sello?` acierta el caso negativo (va a `prewww1`, que es lo
219
+ correcto).
220
+
221
+ **[?] El caso del sello queda sin contrastar, y seguirá así.** La AEAT no emite
222
+ certificados de prueba —preproducción exige un certificado real de una CA
223
+ reconocida— y el de sello de entidad de la FNMT es de pago y solo para personas
224
+ jurídicas. Con él hay que declarar `sello: true` explícitamente en vez de fiarse
225
+ de la heurística sobre el sujeto del certificado, que nunca ha visto un sello
226
+ real.
227
+
228
+ ## Cadenas, instalaciones y varias fuentes de facturación
229
+
230
+ **[doc]** La cadena es **una por SIF + NIF obligado**, no una por serie. Pero
231
+ "SIF" no es el producto: lo identifica el bloque `SistemaInformatico` completo, y
232
+ dentro de él el `NumeroInstalacion` distingue instalaciones. Las FAQs:
233
+
234
+ > cada una de esas facturaciones distintas (sean de distintos OEF o del mismo OEF
235
+ > pero de distintos centros de facturación independientes, como tiendas) debe
236
+ > tener un nº de instalación propio y distinto al resto (pasado, presente o
237
+ > futuro) **porque se consideran SIF independientes, como si fueran "SIF
238
+ > virtuales"**
239
+
240
+ Con varias fuentes (tiendas, TPV, web, un job) hay dos arquitecturas válidas: un
241
+ solo SIF, que obliga a serializar todas las fuentes contra una cadena con un lock
242
+ de base de datos; o un SIF virtual por fuente, con cadenas independientes y sin
243
+ lock entre ellas. La segunda es la que la AEAT contempla expresamente y la que
244
+ evita el problema de raíz.
245
+
246
+ El `NumeroInstalacion` **no puede repetirse nunca**, ni al reinstalar sobre la
247
+ misma máquina. Las FAQs recomiendan un timestamp de instalación o un secuencial
248
+ propio del obligado. `IndicadorMultiplesOT` va a "S" cuando un SIF en la nube
249
+ atiende a varios obligados a la vez.
250
+
251
+ **Regla dura de diseño: la gema no autogenera nunca un `NumeroInstalacion`.** Si
252
+ lo hiciera, un contenedor que se recrea en cada despliegue produciría una
253
+ instalación por despliegue —y en el límite una por factura—, cada registro saldría
254
+ `PrimerRegistro="S"`, la AEAT lo aceptaría y la cadena dejaría de demostrar nada.
255
+ Lo que lo impide no es técnico: las FAQs prohíben que la identidad del SIF cambie
256
+ "con cada factura ni con cada sesión o arranque del producto", la trazabilidad es
257
+ obligación legal del productor (art. 29.2.j LGT) y certificar por declaración
258
+ responsable un SIF que no cumple el RD 1007/2023 es sancionable.
259
+
260
+ ## La AEAT no impide bifurcar la cadena
261
+
262
+ **[real]** Se enviaron dos altas distintas apuntando ambas al **mismo**
263
+ predecesor. La AEAT aceptó las dos con `Correcto`, sin aviso ni error admisible.
264
+
265
+ ```
266
+ Registro 1 (PrimerRegistro)
267
+ ├── Registro 2 -> anterior: Registro 1
268
+ └── Registro 3 -> anterior: Registro 1 <-- cadena bifurcada, aceptada
269
+ ```
270
+
271
+ Es el hallazgo con más consecuencias de diseño de toda la integración:
272
+
273
+ - **No hay red de seguridad.** La AEAT no valida al recibir que el
274
+ `RegistroAnterior` sea de verdad el último anotado. Una condición de carrera
275
+ produce una cadena rota que se acepta en silencio y no se descubre al enviar.
276
+ - **El lock no es una optimización, es lo único que sostiene la integridad**, y
277
+ dentro de la gema lo respalda un índice único sobre `(cadena_id,
278
+ huella_anterior)`.
279
+ - **Que no lo rechacen no significa que no lo vean.** La AEAT conserva todos los
280
+ registros, y la lista L1E de tipos de anomalía incluye "el campo huella del
281
+ registro anterior no se corresponde con la huella del registro anterior". Lo que
282
+ no hay es detección síncrona.
283
+
284
+ **La huella anterior no la da la AEAT**: hay que guardarla. Cada registro almacena
285
+ la suya y el siguiente lee la última de su cadena bajo lock.
286
+
287
+ ## Qué queda anotado: la consulta devuelve una foto, no un libro
288
+
289
+ **[real]** Consultada una cadena con **6 registros de facturación sobre 4
290
+ facturas**, la consulta devolvió **4 filas**: una por factura, con su estado
291
+ actual, no el histórico. De ahí tres conclusiones:
292
+
293
+ - **La subsanación sustituye al original, no convive con él.** La factura
294
+ subsanada aparece una sola vez, con los importes corregidos y
295
+ `TimestampUltimaModificacion` a la hora de la subsanación. `Correcto` al
296
+ subsanar significa de verdad "he reemplazado el registro anterior".
297
+ - **La anulación surte efecto**: la factura queda en `Anulado`, un estado que
298
+ `RespuestaSuministro` ni siquiera puede expresar (solo conoce Correcto,
299
+ AceptadoConErrores e Incorrecto). Sin este servicio no hay forma de observarlo.
300
+ - **La cadena NO se puede reconstruir entera desde la consulta.** Es la cara B: los
301
+ eslabones sustituidos desaparecen, así que los registros que encadenaban tras
302
+ ellos *parecen* huérfanos aunque la cadena esté intacta. Por lo mismo, se
303
+ informaron **cero** registros marcados `PrimerRegistro`, porque el primero de la
304
+ cadena había sido sustituido.
305
+
306
+ **Corolario para la capa Rails: el histórico de la cadena hay que guardarlo uno
307
+ mismo.** La consulta sirve para reconciliar el estado de cada factura, no para
308
+ auditar el encadenamiento. Detectar una bifurcación sigue dependiendo del SIF.
309
+
310
+ Detalles confirmados de paso: las huellas devueltas **coinciden** con las
311
+ almacenadas; los importes vuelven **sin ceros a la derecha** (`181.5`, `121`,
312
+ `133.1`); y el orden de las filas **no es cronológico**, así que no conviene
313
+ apoyarse en él.
314
+
315
+ **[real] `Subsanacion="S"` evita el rechazo por duplicado.** Reenviar una factura
316
+ con el mismo `IDFactura` e importes distintos se anota como `Correcto`, sin
317
+ `RegistroDuplicado`. El mismo cuerpo sin la marca habría chocado con "Registro de
318
+ facturación duplicado".
319
+
320
+ ## Reconciliación
321
+
322
+ **[real]** Dos altas remitidas y reconciliadas a continuación: 2 facturas
323
+ locales, 2 filas de la AEAT, 0 divergencias.
324
+
325
+ **[real] La AEAT imputa el periodo por FECHA DE EXPEDICIÓN.** Es lo que asume
326
+ `Reconciliacion#vigentes` al elegir qué facturas locales revisar.
327
+
328
+ **[?] El cotejo del `SistemaInformatico` lo aplica el servidor: probable, no
329
+ comprobado.** El razonamiento es indirecto: había 4 facturas anotadas bajo el
330
+ mismo NIF y el mismo periodo en otra instalación, y la consulta filtrada devolvió
331
+ solo las 2 de la instalación consultada. **Falta la premisa**: que esas 4
332
+ siguieran almacenadas ese día. Preproducción no tiene trascendencia tributaria y
333
+ nada garantiza que no purguen datos, así que "las purgaron" explica lo observado
334
+ igual de bien. Se cierra pidiendo el mismo periodo con el SIF de la otra
335
+ instalación.
336
+
337
+ Consecuencia práctica que no depende de eso: **el SIF que se manda en el filtro
338
+ tiene que ser el mismo con el que se anotaron los registros**. Con uno distinto la
339
+ respuesta viene `SinDatos`, y eso se disfraza de "no consta ninguna factura". Por
340
+ eso `Reconciliacion` filtra además en cliente por el `NumeroInstalacion` de cada
341
+ fila.
342
+
343
+ ## El código QR: la AEAT NO devuelve la URL
344
+
345
+ **[doc]** Conviene decirlo explícito porque es la suposición natural y es falsa:
346
+ la URL de cotejo no llega en ninguna respuesta. No está en
347
+ `RespuestaSuministro.xsd`, ni en `RespuestaConsultaLR.xsd`, ni se menciona en
348
+ Validaciones. La construye el propio SIF con datos que ya tiene.
349
+
350
+ ```
351
+ Pruebas: https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR?
352
+ Producción: https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR?
353
+ ```
354
+
355
+ Cuatro parámetros: `nif`, `numserie`, `fecha` (DD-MM-AAAA) e `importe`.
356
+
357
+ - **El host no es el del SOAP.** El cotejo va por `prewww2`/`www2`, los envíos por
358
+ `prewww1`/`prewww10`.
359
+ - **El URL encoding no es opcional y el caso es alcanzable.** `Formato.num_serie`
360
+ admite `&`, `%`, `+` y espacios, así que un número de serie válido para el envío
361
+ puede partir la URL si se concatena sin codificar. UTF-8.
362
+ - Hay una URL distinta para NO VERI\*FACTU (`ValidarQRNoVerifactu`), fuera de
363
+ alcance.
364
+
365
+ **Consecuencia de diseño.** Como el QR no depende de la respuesta de la AEAT, se
366
+ genera al crear el registro y bajo el mismo lock, antes de enviar nada: la
367
+ impresión de la factura queda desacoplada del envío asíncrono. Ojo al matiz: que
368
+ el QR sea *válido* no significa que la factura *conste*. Si el envío nunca se
369
+ completa, quien escanee obtendrá un "no consta".
370
+
371
+ ## Ritmo de remisión: los lotes NO son el modo normal
372
+
373
+ **[doc]** Las FAQs son tajantes:
374
+
375
+ > debe asegurarse que la generación del RF se produzca de forma "simultánea"
376
+ > (entiéndase inmediata o sin demora apreciable) a la expedición de la factura
377
+ > para su instantáneo almacenamiento o remisión a la AEAT
378
+
379
+ Un comercio que factura cada diez minutos mandará siempre un registro por
380
+ petición. El tope de 1000 es un techo para quien factura rápido, no un objetivo:
381
+ el tamaño del lote lo dicta el ritmo de facturación, no una decisión de diseño.
382
+
383
+ **[real] Un envío admite una cadena entera, no solo un eslabón.** Varios registros
384
+ encadenados entre sí dentro del mismo `RegFactuSistemaFacturacion` se anotan
385
+ todos. Esto separa dos cosas fáciles de confundir: el **cálculo** de la cadena
386
+ sigue necesitando el lock por SIF+NIF, pero el **transporte** puede agrupar.
387
+
388
+ **[real] `TiempoEsperaEnvio` no escala con el tamaño del lote**: 60 s tanto tras
389
+ un lote de tres como tras un envío de uno. Es el valor inicial que fija el art.
390
+ 16.2 de la Orden. Agrupar sale más barato que encadenar peticiones: mismo coste de
391
+ espera, más registros dentro. Medido solo en preproducción, y la AEAT lo devuelve
392
+ en cada respuesta precisamente porque puede variarlo.
393
+
394
+ **[doc]** `TiempoEsperaEnvio` significa esperar esos segundos **o** acumular hasta
395
+ el límite de lote, lo que ocurra primero.
396
+
397
+ ## Obligaciones del SIF que no se leen en el esquema (OM art. 7.i)
398
+
399
+ **[doc]** Dos comprobaciones **antes de generar cada registro**, que no se deducen
400
+ de ningún XSD:
401
+
402
+ > 1.º El último registro de facturación generado está correctamente encadenado.
403
+ > 2.º La fecha y hora de generación del último registro de facturación generado
404
+ > no es superior en más de un minuto a la fecha y hora actuales que se utilizarán
405
+ > para fechar el registro de facturación a generar.
406
+
407
+ En la primera se mira **un eslabón hacia atrás**, no la cadena entera: que la
408
+ `Huella` del `RegistroAnterior` del RF n-1 se corresponda con la huella del RF
409
+ n-2.
410
+
411
+ La segunda confunde y las FAQs lo aclaran: que pasen horas entre registros **no es
412
+ problema**. Lo que no se admite es que el registro a generar tenga fecha anterior
413
+ en más de un minuto al ya generado. Es un control de que el reloj no va hacia
414
+ atrás, no de que factures rápido.
415
+
416
+ **Y lo más importante: detectar una anomalía NO puede parar la caja.**
417
+
418
+ > será preciso generar el siguiente RF, ya que la facturación por este motivo
419
+ > **NUNCA debe interrumpirse**
420
+
421
+ Conviene no confundir dos clases de fallo:
422
+
423
+ - **Datos inválidos del registro** (un tipo impositivo inexistente, un desglose
424
+ incoherente): no se puede generar el registro, y ahí sí hay que fallar. Con esos
425
+ datos tampoco se puede emitir la factura.
426
+ - **Anomalía de trazabilidad** (el eslabón anterior no cuadra): se anota, se avisa
427
+ y se sigue. Nunca bloquea.
428
+
429
+ Las FAQs añaden que el orden de generación debe seguir el orden cronológico de
430
+ expedición, lo que encaja con serializar bajo lock.
431
+
432
+ ## Recuperación ante desastre
433
+
434
+ **[doc]** Si se pierde la base de datos local a mitad de año hay dos salidas:
435
+
436
+ 1. **Recuperar el último eslabón desde la AEAT.** La consulta devuelve, de cada
437
+ registro, su `Huella` y su `FechaHoraHusoGenRegistro`. El de marca temporal
438
+ mayor es el último de la cadena, y con su `IDFactura` + `Huella` se reanuda.
439
+ Solo vale en modalidad VERI\*FACTU, que es donde la AEAT los conserva.
440
+ 2. **No recuperarlo: abrir instalación nueva.** Reinstalar exige un nº de
441
+ instalación nuevo, y una instalación nueva arranca su propia cadena con
442
+ `PrimerRegistro="S"`.
443
+
444
+ De aquí sale una idea que ordena bastante: **la cadena no es un hilo eterno del
445
+ contribuyente, es por instalación**. Romperla no es un pecado irreparable; es
446
+ motivo para abrir una instalación nueva. Lo que sí es irreparable es reutilizar un
447
+ número de factura, y eso sí lo detecta la AEAT.
448
+
449
+ ## Respuestas y errores del servicio
450
+
451
+ **[doc]** Estados: `EstadoEnvio` (Correcto / ParcialmenteCorrecto / Incorrecto),
452
+ `EstadoRegistro` (Correcto / AceptadoConErrores / Incorrecto) y
453
+ `EstadoRegistroDuplicado` (Correcta / AceptadaConErrores / Anulada). La consulta
454
+ usa otros: Correcto, AceptadoConErrores y **Anulado**, que el canal de envío no
455
+ sabe expresar.
456
+
457
+ **[doc]** Solo hay que escapar `&` como `&amp;` y `<` como `&lt;`, lo que respalda
458
+ la asimetría huella-cruda / XML-escapado. Ceros a la izquierda prohibidos en
459
+ numéricos, irrelevantes tras el separador decimal.
460
+
461
+ **[real] Los fallos de servicio llegan como SOAP Fault**, no como
462
+ `RespuestaSuministro`, con el código dentro de `faultstring` en el formato
463
+ `Codigo[NNNN].descripción`.
464
+
465
+ **[real] La identificación del obligado es por el PAR NIF + NombreRazon.** Con el
466
+ NIF correcto y un nombre que no cuadre con el censo, la AEAT responde `4104` "el
467
+ NIF no está identificado", que apunta al campo equivocado; el detalle del error sí
468
+ devuelve los dos campos. Lo mismo para el destinatario, con el código `1239`. Un
469
+ NIF con dígito de control incorrecto da `4116`.
470
+
471
+ **[real]** Un sobre SOAP con dos declaraciones XML devuelve `Codigo[102].Error
472
+ interno en el servidor`: es un fallo de parseo, no de negocio, y el mensaje no
473
+ orienta en absoluto.
474
+
475
+ **[real]** Estos códigos existen como error propio de la AEAT, lo que respalda las
476
+ validaciones implementadas leyendo el ap. 15:
477
+
478
+ | Código | Regla |
479
+ |---|---|
480
+ | 1237 | N1/N2 con IVA no admiten tipo, cuota ni recargo |
481
+ | 1238 | Una exenta no admite esos cuatro campos |
482
+ | 1245 | `ClaveRegimen` obligatoria con IVA/IPSI/IGIC |
483
+ | 1232-1234 | Reglas cruzadas de `IDType` y `CodigoPais` |
484
+ | 1235-1236 | Ventanas temporales de `TipoImpositivo` |
485
+
486
+ **[doc]** WSDL de pruebas:
487
+ `https://prewww2.aeat.es/static_files/common/internet/dep/aplicaciones/es/aeat/tikeV1.0/cont/ws/SistemaFacturacion.wsdl`
488
+
489
+ ## Lo que sigue sin comprobarse
490
+
491
+ - **El certificado de sello** y su endpoint (`prewww10`/`www10`). No hay forma
492
+ barata de probarlo: la AEAT no emite certificados de prueba.
493
+ - **El cotejo del SIF en servidor**, por la premisa que falta (ver arriba).
494
+ - **Las operativas de subsanación por rechazo y de anulación sin registro
495
+ previo**, que la gema no construye.
496
+ - **Los caminos de rechazo**: una factura válida no los recorre, así que las
497
+ validaciones de error solo están ejercitadas contra los tests propios.
498
+
499
+ ## Nota operativa
500
+
501
+ Los guiones de `examples/` contra preproducción y la suite de tests **no deben
502
+ compartir base de datos**. La suite tira las tablas, las recrea y vacía cadenas y
503
+ registros en cada test; correrla contra una base con datos de pruebas reales los
504
+ destruye. La gema lo impide abortando si el nombre de la base no parece de tests,
505
+ pero la separación conviene hacerla explícita con `VF_DATABASE_URL`.
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+
6
+ module Verifactu
7
+ module Generators
8
+ # `rails g verifactu:install`
9
+ #
10
+ # Deja dos ficheros y nada más: la migración del libro registro y un
11
+ # initializer con la identificación del SIF.
12
+ #
13
+ # Lo que este generador NO hace, y es deliberado:
14
+ #
15
+ # - No abre ninguna cadena. El NumeroInstalacion no se autogenera en
16
+ # ninguna parte de esta gema (ver Cadena.abrir!): un contenedor que se
17
+ # recrea en cada despliegue acabaría con una instalación por despliegue,
18
+ # y eso vacía de sentido el encadenamiento.
19
+ # - No toca el certificado ni escribe dónde vive. Certificado recibe los
20
+ # bytes ya cargados y no sabe leer ficheros ni ENV a propósito: la
21
+ # política de almacenamiento de la credencial es de quien despliega.
22
+ class InstallGenerator < Rails::Generators::Base
23
+ include ActiveRecord::Generators::Migration
24
+
25
+ source_root File.expand_path('templates', __dir__)
26
+
27
+ desc 'Instala el libro registro VERI*FACTU: migración e initializer.'
28
+
29
+ def crear_initializer
30
+ template 'verifactu.rb.tt', 'config/initializers/verifactu.rb'
31
+ end
32
+
33
+ def crear_migracion
34
+ migration_template 'instalar_verifactu.rb.tt',
35
+ 'db/migrate/instalar_verifactu.rb'
36
+ end
37
+
38
+ def siguientes_pasos
39
+ say <<~TXT
40
+
41
+ Instalado. Quedan dos cosas, y ninguna se puede hacer por ti:
42
+
43
+ 1. Edita config/initializers/verifactu.rb. Los valores que trae son
44
+ inválidos a propósito, para que falle pronto y no mande a la AEAT
45
+ una identificación de SIF inventada.
46
+ 2. rails db:migrate
47
+
48
+ Después, abre una cadena por cada fuente de facturación (una por
49
+ tienda, TPV o sede: son SIF virtuales distintos). En un seed, en una
50
+ tarea rake o en la consola, NO en el initializer: una cadena es un
51
+ dato, se abre una vez y a conciencia.
52
+
53
+ VerifactuRails::Libro::Cadena.abrir!(
54
+ numero_instalacion: 'TIENDA-VALENCIA-20260810120000',
55
+ nif_obligado: '...', nombre_obligado: '...')
56
+
57
+ TXT
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'verifactu_rails/libro'
4
+
5
+ # Libro registro VERI*FACTU.
6
+ #
7
+ # El esquema no se copia aquí: se HEREDA de la gema. Así lo que ejecuta tu
8
+ # db:migrate es exactamente el mismo esquema que ejercita la suite de tests de
9
+ # verifactu-rails, incluidos los índices únicos que impiden bifurcar la cadena
10
+ # (que son la única red que hay: está comprobado que la AEAT acepta una cadena
11
+ # bifurcada sin avisar).
12
+ #
13
+ # El contrato que hace esto seguro: VerifactuRails::Libro::Migracion es el
14
+ # esquema v1 y está CONGELADO. Ningún cambio futuro de la gema lo tocará; los
15
+ # cambios de esquema llegarán como migraciones nuevas y aparte. Si no fuera así,
16
+ # una app ya migrada y una instalación nueva acabarían con esquemas distintos sin
17
+ # que nadie se enterara.
18
+ class <%= migration_class_name %> < VerifactuRails::Libro::Migracion
19
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Identificación del SISTEMA INFORMÁTICO DE FACTURACIÓN, que viaja en CADA
4
+ # registro remitido a la AEAT.
5
+ #
6
+ # Describe al SIF que despliegas TÚ, no a esta gema: verifactu-rails es un
7
+ # componente de tu sistema de facturación, no el sistema.
8
+ #
9
+ # LOS VALORES DE ABAJO SON INVÁLIDOS A PROPÓSITO. Con ellos, el primer
10
+ # `anotar_alta!` levanta una ValidacionError antes de construir nada. Es
11
+ # preferible a un valor de relleno plausible, que se remitiría a la AEAT como
12
+ # identificación real de tu sistema.
13
+ #
14
+ # Va dentro de `on_load(:active_record)` porque la capa Libro se carga con
15
+ # ActiveRecord, no antes: el núcleo de la gema no depende de Rails y se sigue
16
+ # pudiendo usar suelto.
17
+ #
18
+ # AQUÍ NO SE ABRE NINGUNA CADENA. Esto es configuración; una cadena es un dato, y
19
+ # `Cadena.abrir!` va en un seed, en una tarea rake de alta de tienda o en tu
20
+ # panel de administración, una sola vez y a conciencia. Ponerlo en este fichero
21
+ # rompe de dos maneras: el segundo arranque choca contra el índice único de
22
+ # numero_instalacion, y un initializer que consulta la base de datos revienta
23
+ # donde todavía no la hay (assets:precompile, db:create, un contenedor de CI).
24
+ ActiveSupport.on_load(:active_record) do
25
+ VerifactuRails::Libro.configure do |c|
26
+ # Quién PRODUCE el software. Si lo has desarrollado tú para tu propio uso,
27
+ # eres tú; si lo vendes, eres tú igualmente (no tu cliente).
28
+ c.productor_nombre = 'CAMBIAME: razón social del productor'
29
+ c.productor_nif = 'CAMBIAME'
30
+
31
+ # Nombre comercial del SIF (máx. 30 caracteres).
32
+ c.nombre_sistema = 'CAMBIAME'
33
+
34
+ # IdSistemaInformatico: EXACTAMENTE dos posiciones, mayúscula (sin Ñ) o
35
+ # dígito. Distingue entre sí los sistemas de un mismo productor.
36
+ c.id_sistema = 'CAMBIAME'
37
+
38
+ # Versión del SIF (máx. 50). Tiene que cambiar cuando cambie el sistema:
39
+ # es lo que permite a la AEAT saber qué versión emitió cada registro.
40
+ c.version = 'CAMBIAME'
41
+
42
+ # Preproducción hasta que esto vaya en serio. Aviso de la propia AEAT: el
43
+ # entorno de pruebas es para pruebas PUNTUALES, no para carga.
44
+ c.entorno = Rails.env.production? ? :produccion : :pruebas
45
+
46
+ # ¿Puede este SIF usarse para VARIOS obligados tributarios? Y, si puede,
47
+ # ¿se está usando así ahora mismo? Declarar `multiples_ot` sin `multi_ot`
48
+ # es incoherente y la gema lo rechaza.
49
+ c.multi_ot = false
50
+ c.multiples_ot = false
51
+
52
+ # Anomalías de trazabilidad del art. 7.i) de la Orden HAC/1177/2024.
53
+ #
54
+ # NO interrumpen la facturación: la norma dice que ante una anomalía la
55
+ # facturación "nunca debe interrumpirse". Se anotan en el registro y se
56
+ # avisan por aquí. Por defecto no se hace nada, que en la práctica es
57
+ # perderlas: mándalas a donde de verdad las vayas a mirar.
58
+ c.al_detectar_anomalia = lambda do |anomalias, registro|
59
+ Rails.logger.warn(
60
+ "[verifactu] anomalías #{anomalias.inspect} en el registro #{registro.id}"
61
+ )
62
+ end
63
+ end
64
+ end