diff --git a/README.md b/README.md index 3fb4814..e6ca032 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,13 @@ de errores HTTP y la descarga de documentos. | Componente | Versión usada | | --- | --- | | Módulo Go | `v2` (`github.com/unkcode-org/gofip-sdk-go/v2`) | -| API GOFIP | `2.18.0` y rutas bajo `/api/v2` | +| API GOFIP | `2.19.0` y rutas bajo `/api/v2` | | Especificación OpenAPI | `3.0.3` | | Go | `1.22.2` o posterior | El contrato HTTP de referencia está en [`docs/openapi.yaml`](docs/openapi.yaml). Los campos `openapi: 3.0.3` e -`info.version: "2.18.0"` indican, respectivamente, la versión del formato OpenAPI +`info.version: "2.19.0"` indican, respectivamente, la versión del formato OpenAPI y la versión del contrato de la API. No deben confundirse con una versión publicada del módulo Go. @@ -27,6 +27,7 @@ publicada del módulo Go. comprobante, opcionales, tipos de documento, monedas y cotizaciones. - Consultas de padrón P4, P5, P10 y P13. - Emisión de facturas y consulta de comprobantes. +- Importación fiscal histórica verificada de GOFIP Legacy, con API key normal del mismo emisor/PV. - Generación y descarga de comprobantes, facturas y recibos en PDF. - Errores tipados para respuestas de la API y respuestas HTTP no estructuradas. - Cliente HTTP y URL base configurables. @@ -335,7 +336,10 @@ no comprueba la existencia del objeto remoto ni genera una URL firmada. **Ambiente:** GOFIP no persiste el ambiente en Documento/Factura. Estas consultas no envían `X-ARCA-Ambiente` y no filtran por homologación/producción. Los registros -exclusivos de Legacy requieren una migración de datos independiente. +exclusivos de Legacy se incorporan mediante `ImportarFactura`, previa verificación +obligatoria en ARCA Producción y API key normal del mismo emisor/PV. Ver +[importación histórica](docs/importacion-facturas.md) para modelos, uso, errores, +idempotencia y límites de PDF. No hay batch ni emisión como fallback. ## Integraciones SEP: un cliente y una credencial distintos @@ -408,6 +412,6 @@ go test -race ./... ``` Ver [arquitectura](docs/arquitectura.md) y [validación manual](docs/validacion.md). -El contrato de referencia es OpenAPI 2.18.0 del backend. No se publica una versión +El contrato de referencia es OpenAPI 2.19.0 del backend. No se publica una versión como parte de estas pruebas. La secuencia prevista es `v2.0.0-rc.1` → `v2.0.0`, con imports `/v2`, validación externa y aprobación de publicación. diff --git a/contrato_importacion_test.go b/contrato_importacion_test.go new file mode 100644 index 0000000..880f433 --- /dev/null +++ b/contrato_importacion_test.go @@ -0,0 +1,40 @@ +package gofip + +import ( + "crypto/sha256" + "fmt" + "os" + "reflect" + "regexp" + "strings" + "testing" + + "github.com/unkcode-org/gofip-sdk-go/v2/modelos/facturacion" +) + +func TestContratoImportacionYProcedenciaExacta(t *testing.T) { + yaml, err := os.ReadFile("docs/openapi.yaml") + if err != nil { + t.Fatal(err) + } + provenance, err := os.ReadFile("docs/CONTRATO.md") + if err != nil { + t.Fatal(err) + } + hash := fmt.Sprintf("%x", sha256.Sum256(yaml)) + if !strings.Contains(string(provenance), hash) || !strings.Contains(string(provenance), "Versión: 2.19.0.") { + t.Fatal("copia OpenAPI sin procedencia o checksum actualizado") + } + for _, token := range []string{`version: "2.19.0"`, "/api/v2/facturas/importaciones:", "mismo emisor y punto de venta", "emisor_id+credencial_id+punto_venta_id", "legacy_import", "SolicitudImportarFactura:", "RespuestaImportarFactura:", "DecimalHistorico:", "VerificacionImportacion:", "CompletitudDocumental:"} { + if !strings.Contains(string(yaml), token) { + t.Fatalf("contrato sin %s", token) + } + } + for _, typ := range []reflect.Type{reflect.TypeOf(OpcionesImportacionFactura{}), reflect.TypeOf(facturacion.SolicitudImportarFactura{}), reflect.TypeOf(facturacion.FacturaHistorica{})} { + for i := 0; i < typ.NumField(); i++ { + if regexp.MustCompile(`(?i)ambiente|emisor`).MatchString(typ.Field(i).Name) { + t.Fatalf("%s amplía autoridad", typ) + } + } + } +} diff --git a/docs/CONTRATO.md b/docs/CONTRATO.md index 99f24c4..2c478b8 100644 --- a/docs/CONTRATO.md +++ b/docs/CONTRATO.md @@ -2,13 +2,14 @@ - Repositorio canónico: unkcode-org/gofip-backend. - Archivo: backend/docs/openapi.yaml. -- Versión: 2.18.0. -- Commit de implementación: ed68c3a3e0e664e8b8ebeda7a8fff20eb1b60aea. -- Base auditada de develop: 12581b385b4fc7321334e4442493ef0159f8a612. -- SHA-256 de los bytes YAML copiados: 90d2171d496a0984e80e835fdfa86ee124bfeee49d472d2d8d74bb02467021e4. +- Versión: 2.19.0. +- Commit de implementación: 7918e9d373a8cbc298258cb9314d5d33e54d38b3. +- Base auditada de develop: ea7beffef41a91e548a1e7b8ed17e388fdd948da. +- SHA-256 de los bytes YAML copiados: 82ed564929b6c46e608d7922dc0a4fb93e1f4e1c2d8ec7bc06d434b4975d92b1. -La copia se verificó byte a byte. El formato del SDK local anterior se respaldó -antes de reemplazarlo; sus rutas y schemas se habían contrastado con el backend. -Para actualizar, editar primero el contrato canónico, su versión y changelog; -verificar el backend y copiar el archivo exacto al SDK. No editar esta copia como -fuente independiente. El commit de implementación todavía no implica un despliegue. +La copia se verificó byte a byte después de estabilizar Backend/OpenAPI. Incluye +importación Legacy verificada en Producción y no agrega ambiente a documentos o +identidad fiscal. Para actualizar, editar primero el contrato canónico, su versión +y changelog; verificar el backend y copiar el archivo exacto al SDK. No editar +esta copia como fuente independiente. El commit de implementación no implica un +despliegue, release ni importación real. diff --git a/docs/arquitectura.md b/docs/arquitectura.md index 6765f9a..79f6a1b 100644 --- a/docs/arquitectura.md +++ b/docs/arquitectura.md @@ -9,6 +9,7 @@ dependencias ni se modifica la semántica del transporte anterior. | Operación | Fuente | Cliente | | --- | --- | --- | +| ImportarFactura | FECompConsultar Producción → persistencia; API key normal del mismo emisor/PV | Bearer | | EmitirFactura | GOFIP → ARCA, persistencia | Bearer | | ObtenerComprobante | Consulta directa a ARCA | Bearer | | ObtenerFacturaPorCAE / ObtenerFacturaPorId | MySQL exclusivamente | Bearer | @@ -24,8 +25,8 @@ Los modelos fiscales persistidos tienen identidad distinta del comprobante ARCA: incluyen documento/factura ID, estado GOFIP, ítems guardados y metadata PDF. Las consultas por CAE e ID comparten `RespuestaFacturaPersistida`. No se recuperan datos desde Legacy, ARCA o entidades mutables como fallback. No existe filtro de ambiente -porque el modelo actual no persiste ese dato. No se agregan migraciones ni índices -únicos de CAE. Un CAE ambiguo produce 409 y admite filtros explícitos. +porque el modelo actual no persiste ese dato. La importación agrega identidad fiscal compartida por emisor/PV/tipo/número, +sin índice único de CAE. Un CAE ambiguo produce 409 y admite filtros explícitos. SEP nunca guarda cookies de navegador ni hace intercambio de bootstrap. Sus respuestas contienen capacidades de corta duración con acceso explícito mediante @@ -37,3 +38,7 @@ El contrato canónico se mantiene en `backend/docs/openapi.yaml` de gofip-backen `docs/openapi.yaml` es una copia exacta. Cada cambio debe comenzar en el backend, incrementar su versión y changelog y luego sincronizar esta copia. El SDK conserva el módulo `/v2` y Go mínimo 1.22.2. + +La importación usa un transporte con redacción de errores y sin retries ni +redirecciones. Su configuración no acepta ambiente y no modifica el transporte +de emisión. Ver [importación histórica](importacion-facturas.md). diff --git a/docs/entrega-importacion-fiscal.md b/docs/entrega-importacion-fiscal.md new file mode 100644 index 0000000..eff24c5 --- /dev/null +++ b/docs/entrega-importacion-fiscal.md @@ -0,0 +1,256 @@ +# Entrega de importación fiscal histórica — actualizada 2026-10-18 + +Revisión de autorización del 2026-10-18: backend +`7918e9d373a8cbc298258cb9314d5d33e54d38b3`. La API key normal del propio emisor/PV +es toda la autorización necesaria. Esta revisión del SDK actualiza sólo textos, +comentarios, contrato copiado y su test; no cambia el método ni las opciones públicas. +La validación actual vuelve a pasar test/vet/race y el consumidor externo. +La evidencia completa de migración y concurrencia está en +`backend/docs/VALIDACION_IMPORTACION_FISCAL.md` del backend. + +## 1. Commits y ubicación + +Rama `codex/importacion-fiscal-historica` en ambos repositorios: + +| Repositorio | Commit | Contenido | +|---|---|---| +| gofip-backend | `77a2292` | Especificación inicial en rojo | +| gofip-backend | `e9577b8790bc853f6ab486d01315ca9771b0fe0b` | Implementación, migración, tests y documentación | +| gofip-sdk-go/v2 | `fb0a31c` | Especificación inicial en rojo | +| gofip-sdk-go/v2 | Commit que incorpora este documento (`git log -1`) | Implementación, contrato sincronizado, ejemplo y validación | + +Backend: `/Users/genarocivilotti/Code/gofip-backend/backend`. +SDK: `/Users/genarocivilotti/.codex/worktrees/importacion-fiscal-historica/sdk-go`. +El checkout original del SDK quedó intacto. No hubo push, PR, merge, tag ni release. + +## 2. Archivos modificados + +El inventario completo está al final. La implementación abarca dominio/puertos, +repositorios MySQL, adapter de consulta SOAP, handlers/ruta, errores, renderer +protegido y contrato/documentación. El SDK agrega método, +modelos tipados, ejemplo individual, tests y consumidor externo. + +## 3. Migración + +`202610160002_importacion_fiscal` registra las identidades de documentos +V2 autorizados en una transacción. Datos incompletos, PV inconsistentes o +identidades incompatibles hacen fallar el backfill con un error que identifica +el documento. Las constraints UNIQUE son la garantía final; no corrige datos. +Crea tres tablas: identidades, operaciones y referencias de importación. +Los campos de snapshot/verificación/completitud y la ampliación decimal se +incorporan al esquema histórico mediante el AutoMigrate existente. El rollback +rechaza datos fiscales/operacionales y conserva las tablas aditivas vacías. +No se agregan columnas ni backfills de ambiente. + +## 4. Constraint fiscal + +```sql +UNIQUE (emisor_id, punto_venta, tipo_comprobante, numero_comprobante) +UNIQUE (documento_id) +``` + +Son índices SQL reales en `identidades_fiscales`, compartidos por emisión e +importación. CAE se valida pero no reemplaza esa identidad. + +## 5. Escritor normal V2 + +`CrearDocumentoFacturaContext` y `RegistrarFacturaAutorizadaContext` registran la +identidad en la misma transacción que la factura autorizada. Un documento pendiente +no reserva identidad. El contrato, hashes, retries y semántica de `EmitirFactura` +permanecen vigentes. Una colisión impide persistir una segunda identidad y hace +rollback, sin sobrescribir la autorización existente. + +## 6. Endpoint + +`POST /api/v2/facturas/importaciones`, Bearer API key, envelope `data`, request +estricto, 1 MiB máximo. Emisor resuelto desde autenticación; no acepta emisor ni +ambiente del body. Idempotency-Key obligatorio y referencia opcional. +201 crea; 200 devuelve existente; 409 para identidad/CAE/datos/referencia/clave +incompatibles; 404 ARCA ausente; 502 respuesta fiscal inválida; 503 indisponible. +Las facturas son documentos autorizados normales y usan los GET existentes por +ID/CAE, con lectura por emisor y aislamiento entre emisores. + +## 7. Autorización + +La API key normal activa del PV es suficiente. El backend recibe la credencial, +su PV y su emisor de la autenticación existente y exige que el PV solicitado sea +exactamente el asociado a esa credencial. Otro PV, aun del mismo emisor, devuelve +404 sin consultar ARCA. Se vuelve a validar la relación antes de persistir. +Claves y referencias se aíslan mediante SHA-256 del array JSON ordenado +`[emisor_id, credencial_id, punto_venta_id, clave_o_referencia]` en tablas separadas. +No hay configuración adicional de autorización para importaciones. + +## 8. Consulta ARCA + +Puerto de un solo método `ConsultarComprobanteProduccion`, independiente del +adapter amplio de emisión. Autenticación WSAA en contexto Producción y SOAP +`FECompConsultar` contra el endpoint oficial productivo. No expone autorización, +CAE/CAEA mutante, numeración ni selector de ambiente. `X-ARCA-Ambiente` no modifica +la política Legacy. No hay modo unverified ni fallback. +El CAE esperado y todas las afirmaciones fiscales enviadas se contrastan. +Los campos y estructuras recuperables conservan presencia, códigos y precisión. + +## 9. API y modelos SDK + +```go +cliente.Facturacion.ImportarFactura(ctx, solicitud, gofip.OpcionesImportacionFactura{ + ClaveIdempotencia: claveEstable, + ReferenciaExterna: referenciaEstable, +}) +``` + +Modelos principales en `modelos/facturacion/importacion.go`: +`SolicitudImportarFactura`, `FacturaHistorica`, `ProcedenciaFactura`, +`DatosFiscalesHistoricos`, `DecimalHistorico`, `DetalleHistoricoFactura`, +`ItemHistorico`, `DatosDocumentalesHistoricos`, `VerificacionImportacion`, +`CompletitudDocumental` y `RespuestaImportarFactura`, además de IVA/tributos/ +asociados/opcionales/compradores/actividades/períodos/observaciones tipados. +`RespuestaFacturaPersistida` agrega verificación/completitud y snapshot histórico. +No hay maps públicos ni batch. Se preservan context, cliente configurable, +cancelación/deadline y errores HTTP tipados con redacción segura. No hay retries +automáticos ni redirects, cookies o replay de body por transporte estándar. + +## 10. Tests ejecutados + +Backend y SDK: `go test ./...`, `go vet ./...`, `go test -race ./...` y +`git diff --check`: PASS. Consumidor externo: test, vet y race: PASS. +SDK y consumidor externo con Go **1.22.2**: +`GOTOOLCHAIN=go1.22.2 go test -ldflags=-linkmode=external ./...`: PASS. +La ejecución directa del enlazador interno antiguo abortó en dyld por LC_UUID en +este macOS; el enlace externo permitió validar ejecución real, sin cambiar el +mínimo Go del módulo. No se oculta ese límite de la herramienta local. +La copia OpenAPI comprueba SHA-256/procedencia y ausencia de selector de autoridad. + +## 11. Concurrencia real + +MariaDB **13.0.2**, dos procesos, cinco goroutines por proceso, diez claves +operacionales distintas, mismo comprobante: **un documento, una factura, una +identidad, una referencia y el mismo documento_id en los diez resultados**. +Se ejecutó además backfill repetible y conservación SQL exacta de +`1234567890123456.123456`. Se verificaron índices reales en information_schema. +No se usó SQLite para probar la garantía multi-proceso. + +## 12. Cero emisión + +La prueba de arquitectura exige un puerto con exactamente un método de consulta. +El test de integración utiliza el adapter SOAP real con transporte controlado: +10 FECompConsultar y **0 FECAESolicitar / otras operaciones mutantes**. +Cualquier otra acción hace fallar la prueba. Autenticación falsa; sin secretos ni +llamadas reales a ARCA. La instancia de MariaDB fue descartable, sin red, y quedó +apagada. Se instaló MariaDB mediante Homebrew para ejecutar esta prueba; no se +activó su servicio del sistema. + +## 13. OpenAPI + +Contrato canónico **2.19.0**, endpoint, modelos, optional/nullable, autorización por credencial/PV, +procedencia, idempotencia, referencias, errores, origen y datos de consulta. +Copia SDK exacta con SHA-256 +`82ed564929b6c46e608d7922dc0a4fb93e1f4e1c2d8ec7bc06d434b4975d92b1`. +Procedencia completa en `docs/CONTRATO.md`; test local detecta drift. + +## 14. Documentación y observabilidad + +Backend: `docs/IMPORTACION_FACTURAS_LEGACY.md` y +`docs/VALIDACION_IMPORTACION_FISCAL.md`. SDK: `docs/importacion-facturas.md`, +README/arquitectura/validación/procedencia y `examples/importar-factura`. +Eventos `invoice.import.started` / `invoice.import`, span `invoice.import` y +contador `gofip.invoices.imports`, con resultados acotados e identificadores +seguros. No incrementa métricas de emisión ni registra bodies, claves, tokens, +certificados o URLs firmadas. + +## 15. Límites y riesgos pendientes + +- No se contactó ARCA real ni se modificaron datos de producción. Certificados, + delegaciones WSFE productivas y disponibilidad requieren validación posterior. +- Si el backfill encuentra datos incompatibles, aborta sin corregirlos. + Drenar todos los escritores anteriores y detener escrituras fiscales durante + el cambio; un binario antiguo no conoce la tabla de identidad compartida. +- PDF histórico queda explícitamente bloqueado en V1 (13106/13107). Se conservan + datos aportados, sin reconstruir desde fichas actuales ni enviar correo. +- La verdad fiscal exacta está en `historico` y columnas DECIMAL ampliadas; + los floats del modelo público anterior siguen siendo proyecciones compatibles. +- La verificación no recupera ítems, nombres ni domicilios históricos inexistentes. + ARCA no certifica esos datos comerciales. Una diferencia fiscal produce conflicto. +- Las reservas fallidas se conservan para reanudar; definir retención operativa + posteriormente. No se borran automáticamente ni se convierten en facturas. +- Una caída de DB tras una emisión normal sigue siendo un problema de conciliación + preexistente: esta tarea no introduce retries ni autorización como recuperación. + +## 16. Primer migrador real de Joyería Elixir, posteriormente + +1. Revisar estos commits y coordinar despliegue/migración de esquema por separado. +2. Preparar configuración/delegación productiva del emisor. +3. Usar la API key normal del PV correspondiente a cada comprobante. +4. Implementar en Elixir el adaptador de `OrderPaymentFiscalData`, sin inventar + ausencias ni recalcular importes; incluir los datos conocidos como afirmaciones. +5. Importar individualmente con clave/referencia estable, persistir documento_id + y luego marcar la operación fiscal `issued`; guardar progreso y errores. +6. Legacy sin mapping significa migración pendiente y jamás habilita emisión. + Mapping existente significa consultar V2. Recién después revisar la eliminación + de `GOFIP_V2_ORDERS_FROM`. + +No se modificaron Elixir, otros consumidores ni APIs Legacy. No hubo importaciones +reales, emisión, despliegue, migración de producción o publicación automática. + +## Inventario Backend (rutas relativas a gofip-backend) + +- `backend/README.md` +- `backend/cmd/main.go` +- `backend/docs/CHANGELOG.md` +- `backend/docs/IMPORTACION_FACTURAS_LEGACY.md` +- `backend/docs/VALIDACION_IMPORTACION_FISCAL.md` +- `backend/docs/importaciones_test.go` +- `backend/docs/openapi.yaml` +- `backend/internal/adapter/db/mysql/importacion_facturas_test.go` +- `backend/internal/adapter/db/mysql/importacion_http_test.go` +- `backend/internal/adapter/db/mysql/importacion_mysql_test.go` +- `backend/internal/adapter/db/mysql/integridad_fiscal.go` +- `backend/internal/adapter/db/mysql/integridad_fiscal_test.go` +- `backend/internal/adapter/db/mysql/migraciones.go` +- `backend/internal/adapter/db/mysql/migraciones_test.go` +- `backend/internal/adapter/db/mysql/repositorio_facturacion.go` +- `backend/internal/adapter/db/mysql/repositorio_importaciones.go` +- `backend/internal/adapter/db/mysql/repositorio_integracion_embedded_test.go` +- `backend/internal/adapter/db/mysql/repositorios.go` +- `backend/internal/adapter/db/mysql/repositorios_test.go` +- `backend/internal/adapter/external/consultahistorica/consultador.go` +- `backend/internal/adapter/external/consultahistorica/consultador_test.go` +- `backend/internal/adapter/rest/dto/consulta_facturas.go` +- `backend/internal/adapter/rest/dto/importacion_facturas.go` +- `backend/internal/adapter/rest/dto/respuestas_facturacion_web.go` +- `backend/internal/adapter/rest/errors/mapeador.go` +- `backend/internal/adapter/rest/handler/consulta_facturas_handler.go` +- `backend/internal/adapter/rest/handler/facturacion_web_handler.go` +- `backend/internal/adapter/rest/handler/handlers.go` +- `backend/internal/adapter/rest/handler/importacion_facturas_handler.go` +- `backend/internal/adapter/rest/router/arca.go` +- `backend/internal/core/domain/facturacion.go` +- `backend/internal/core/domain/importacion_factura.go` +- `backend/internal/core/domain/importacion_validacion.go` +- `backend/internal/core/domain/importacion_validacion_test.go` +- `backend/internal/core/domain/integridad_fiscal.go` +- `backend/internal/core/port/in/casos_uso.go` +- `backend/internal/core/port/in/importacion_facturas.go` +- `backend/internal/core/port/out/importacion_facturas.go` +- `backend/internal/core/port/out/repositorios.go` +- `backend/internal/usecase/comprobante/generar_comprobante.go` +- `backend/internal/usecase/importarfacturas/caso_uso.go` +- `backend/internal/usecase/importarfacturas/caso_uso_test.go` + +## Inventario SDK + +- `README.md` +- `contrato_importacion_test.go` +- `docs/CONTRATO.md` +- `docs/arquitectura.md` +- `docs/entrega-importacion-fiscal.md` +- `docs/folder-structure.md` +- `docs/importacion-facturas.md` +- `docs/openapi.yaml` +- `docs/validacion.md` +- `examples/importar-factura/main.go` +- `importacion_facturas.go` +- `importacion_facturas_test.go` +- `integration/consumer/importacion_test.go` +- `modelos/facturacion/importacion.go` +- `modelos/facturacion/persistidas.go` diff --git a/docs/folder-structure.md b/docs/folder-structure.md index 2ad91e7..5bbc667 100644 --- a/docs/folder-structure.md +++ b/docs/folder-structure.md @@ -28,3 +28,6 @@ evitar un paquete raíz difícil de navegar. `docs/openapi.yaml` es la fuente de verdad del contrato HTTP. El cliente SEP está en `cliente_sep.go`, `sep.go` y `transporte_sep.go`; sus modelos están en `modelos/sep`. Las consultas persistidas están en `facturas_persistidas.go` y `modelos/facturacion/persistidas.go`. Ver [arquitectura](arquitectura.md). + +La primitiva histórica está en `importacion_facturas.go` y sus modelos en +`modelos/facturacion/importacion.go`. Ver [uso y garantías](importacion-facturas.md). diff --git a/docs/importacion-facturas.md b/docs/importacion-facturas.md new file mode 100644 index 0000000..08f068f --- /dev/null +++ b/docs/importacion-facturas.md @@ -0,0 +1,117 @@ +# Importar autorizaciones históricas Legacy + +`ImportarFactura` requiere Backend V2 con OpenAPI 2.19.0 y una API key normal +activa del Punto de Venta. La credencial sólo puede importar comprobantes de su +propio emisor y PV, incluso si ese emisor tiene varios PV. El consumidor no envía +emisor, ambiente ni autoridad adicional. El backend verifica la relación +`CredencialPuntoVenta -> PuntoVenta -> Emisor` y compara el número fiscal del PV +con `solicitud.Factura.PuntoVenta`. Otro PV devuelve 404 sin consultar ARCA. + +```go +solicitud := facturacion.SolicitudImportarFactura{ + Factura: facturacion.FacturaHistorica{ + PuntoVenta: 3, + TipoComprobante: facturacion.TipoComprobante(6), + NumeroComprobante: 12345, + CAE: "00123456789012", + }, + Procedencia: facturacion.ProcedenciaFactura{Sistema: "gofip_legacy"}, +} +resultado, err := cliente.Facturacion.ImportarFactura(ctx, solicitud, + gofip.OpcionesImportacionFactura{ + ClaveIdempotencia: "historico:12345:original", + ReferenciaExterna: "cliente:venta:123:factura:original", + }) +// Persistir resultado.DocumentoID en el cliente sólo si err == nil. +``` + +El backend consulta exclusivamente `FECompConsultar` en ARCA Producción, +compara el CAE esperado y persiste la autorización existente. Legacy siempre +pertenece a producción, incluso si `OpcionesCliente.AmbienteARCA` se configuró +para otras operaciones en homologación. El método no envía `X-ARCA-Ambiente` y +`OpcionesImportacionFactura` no ofrece selector. No crea una autorización, PDF, +correo ni fallback. + +El SDK agrega el envelope `data`; las respuestas no usan envelope. 201 significa +`Importado=true`; 200 significa `YaExistia=true`. Ambos contienen el documento +persistido y evidencia de verificación. El origen de un documento ya existente +se conserva. Luego se usan `ObtenerFacturaPorId` y `ObtenerFacturaPorCAE`, con +lectura por todos los PV del mismo emisor y aislamiento entre emisores. + +## Datos y precisión + +`DatosHistoricos` es un conjunto opcional de afirmaciones fiscales; cada campo +enviado debe coincidir con la consulta. ARCA es la fuente fiscal: un dato ausente +no se completa con reglas actuales ni con valores por defecto. `RegistroID` en +procedencia es opcional. No exigir un ID central de factura que Legacy no posee. + +Los nuevos decimales son `facturacion.DecimalHistorico`, strings exactos con +hasta 16 enteros y seis decimales. Por ejemplo, `"1234.123456"`; JSON numérico no +es válido para esos campos. Los punteros conservan ausencia. Para listas fiscales, +`nil` significa desconocida/ausente y un puntero a slice vacío significa lista +conocida vacía. El campo `Factura.Historico` conserva la respuesta fiscal exacta; +los campos float de lectura anteriores siguen siendo una proyección compatible. + +`Detalle` conserva ítems y datos documentales históricos aportados por el +consumidor. Es información comercial que ARCA no certifica. No recalcula totales. +V1 conserva esa información y comunica completitud, pero devuelve un error +explícito al regenerar PDFs históricos con el renderer actual: 13106 si falta +información, 13107 si requiere el renderer histórico aún no implementado. + +## Idempotencia, referencias y fallas + +La clave operacional es obligatoria y estable (hasta 120 bytes). Su scope es +`emisor_id + credencial_id + punto_venta_id + clave`, separado de la emisión. La misma clave +con contenido distinto produce 409. Tras un timeout se puede repetir exactamente +la misma solicitud y clave; el SDK no reintenta automáticamente. Tampoco sigue +redirecciones, conserva cookies ni permite el replay transparente del body por +el transporte estándar. Un RoundTripper propio sigue siendo responsabilidad del +integrador y debe evitar retries y registros de secretos. + +La identidad definitiva es `emisor + PV + tipo + número`, protegida por UNIQUE +real compartida con emisión normal. Distintas claves para la misma identidad +compatible devuelven el mismo documento. La referencia tiene scope +`emisor_id + credencial_id + punto_venta_id`; repetida hacia otro documento produce 409. No cambia la +unicidad de referencias de emisión. Ambos scopes usan SHA-256 de un array JSON +ordenado con los cuatro strings; los IDs aíslan credenciales, emisores y PV. +La referencia es opcional y no define la identidad fiscal. Una nueva credencial +crea otro scope, pero la identidad fiscal conserva el documento compatible. + +| HTTP / código | Acción del consumidor | +|---|---| +| 400 / 13100 | Corregir solicitud inválida | +| 401 / 12004, 12005 | Revisar validez y expiración de la API key | +| 403 / 12003 | Credencial revocada o inactiva | +| 404 | Verificar que el PV sea el propio de la credencial | +| 404 / 13103 | ARCA no encontró el comprobante; investigar | +| 409 / 13101 | Resolver conflicto de identidad, datos, referencia o clave | +| 413 / 13100 | Reducir body a 1 MiB | +| 502 / 13105 | Revisar respuesta fiscal inválida/incompleta | +| 503 / 13104 | Reanudar con solicitud y clave estables al recuperarse ARCA | + +Todos los errores conservan el tratamiento tipado `ErrorAPI`/`ErrorHTTP` para +respuestas HTTP, saneando diagnóstico remoto y cadenas de transporte. Los errores +locales son seguros. Cancelaciones y deadlines mantienen `errors.Is`. La respuesta +tiene límite de 2 MiB y el cliente HTTP es configurable; se copia su configuración +sin mutar la instancia original. Nunca convertir una falla de importación en +`EmitirFactura`. + +## Ejemplo y validación + +`examples/importar-factura` procesa una sola solicitud tipada sin envelope desde +`GOFIP_IMPORT_REQUEST_FILE`. Exige `GOFIP_IMPORT_IDEMPOTENCY_KEY`, `GOFIP_API_URL` +y `GOFIP_API_KEY`; `GOFIP_IMPORT_EXTERNAL_REFERENCE` es opcional. Ejecutarlo sólo +con datos y entorno autorizados. No se ejecuta como parte de tests. + +```sh +go test ./... +go vet ./... +go test -race ./... +cd integration/consumer +go test ./... +``` + +Los tests usan servidores controlados y no importan datos ni autorizan facturas +reales. El backend aporta la prueba multi-proceso contra MariaDB/MySQL real y +un puerto que sólo expone consulta; este módulo no implementa batch ni contiene +adaptadores de Elixir u otros clientes. diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 093bcf4..39c187d 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: GO ARCA API - version: "2.18.0" + version: "2.19.0" description: >- API REST para gestión de emisores, puntos de venta, credenciales API y facturación electrónica con ARCA. Todos los cuerpos JSON de entrada usan el @@ -56,6 +56,121 @@ tags: - name: WhatsApp description: Gestión de sesiones OpenWA y envío genérico de mensajes para cada emisor. paths: + /api/v2/facturas/importaciones: + post: + tags: + - Facturación persistida + operationId: importarFacturaHistorica + summary: Importa una factura Legacy ya autorizada + description: La credencial API válida puede importar únicamente comprobantes + correspondientes al mismo emisor y punto de venta al que está asociada. + El backend verifica CredencialPuntoVenta -> PuntoVenta -> Emisor y exige + que factura.punto_venta coincida con el número fiscal de ese PuntoVenta. + Un PV fuera de ese scope devuelve 404 sin consultar ARCA. + Legacy siempre consulta FECompConsultar en ARCA Producción; + X-ARCA-Ambiente no selecciona ambiente. No admite emisor ni ambiente en el body. + Nunca autoriza, genera PDF ni envía correo. Idempotency-Key es obligatorio, + separado de emisión y acotado a emisor_id+credencial_id+punto_venta_id; + reutilizarlo con otros datos produce 409. La identidad emisor+PV+tipo+número + es única aun entre credenciales. Las referencias son únicas dentro del + mismo scope de importación; external_reference no tiene unicidad global + ni identifica fiscalmente el comprobante. Replays completados recuperan + el resultado guardado; fallos de consulta pueden reanudarse con la misma + solicitud. Un fallo nunca habilita emisión. + security: + - BearerAuth: [] + parameters: + - name: Idempotency-Key + in: header + required: true + description: Clave estable de importación; no comparte el espacio de claves + de EmitirFactura. + schema: + type: string + minLength: 1 + maxLength: 120 + - name: X-External-Reference + in: header + required: false + description: Referencia exacta dentro del scope emisor_id+credencial_id+punto_venta_id. + schema: + type: string + maxLength: 120 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CuerpoSolicitudImportarFactura' + responses: + '200': + description: Identidad existente compatible; mismo documento y origen original. + content: + application/json: + schema: + $ref: '#/components/schemas/RespuestaImportarFactura' + '201': + description: Factura histórica verificada y persistida. + content: + application/json: + schema: + $ref: '#/components/schemas/RespuestaImportarFactura' + '400': + description: Solicitud inválida, campo desconocido o emisor/ambiente no permitido + en el body. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/DetallesError' + '401': + description: Credencial inválida o expirada. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/DetallesError' + '403': + description: Credencial revocada o acceso denegado por las reglas normales de autenticación. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/DetallesError' + '404': + description: Comprobante inexistente en ARCA o recurso ajeno no accesible. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/DetallesError' + '409': + description: Conflicto fiscal, CAE/mismatch, idempotencia o referencia vinculada + a otro documento. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/DetallesError' + '413': + description: Body mayor a 1 MiB. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/DetallesError' + '500': + description: Fallo de persistencia; repetir la misma importación permite recuperar un commit previo sin emitir. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/DetallesError' + '502': + description: Respuesta fiscal inválida o incompleta. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/DetallesError' + '503': + description: Consulta ARCA indisponible o timeout; no se crea una factura. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/DetallesError' /api/v2/facturas/cae/{cae}: get: tags: @@ -3350,6 +3465,12 @@ paths: schema: $ref: "#/components/schemas/CuerpoSolicitudGenerarPDFFactura" responses: + '409': + description: Factura legacy_import sin regeneración documental histórica disponible (13106 o 13107). Nunca utiliza fichas actuales, envía correos ni emite. + content: + application/problem+json: + schema: + $ref: '#/components/schemas/DetallesError' "200": description: URL firmada de descarga del PDF content: @@ -4299,6 +4420,647 @@ components: schema: type: string schemas: + DecimalHistorico: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + IVAHistorico: + type: object + additionalProperties: false + properties: + id: + type: integer + format: int64 + description: id. + base_imponible: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + importe: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + required: + - id + - base_imponible + - importe + description: IVAHistorico. + TributoHistorico: + type: object + additionalProperties: false + properties: + id: + type: integer + format: int64 + description: id. + descripcion: + type: string + description: descripcion. + nullable: true + base_imponible: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + alicuota: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + importe: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + required: + - id + - base_imponible + - alicuota + - importe + description: TributoHistorico. + OpcionalHistorico: + type: object + additionalProperties: false + properties: + id: + type: string + description: id. + valor: + type: string + description: valor. + nullable: true + required: + - id + description: OpcionalHistorico. + CompradorHistorico: + type: object + additionalProperties: false + properties: + tipo_documento: + type: integer + format: int64 + description: tipo documento. + numero_documento: + type: string + description: numero documento. + porcentaje: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + required: + - tipo_documento + - numero_documento + - porcentaje + description: CompradorHistorico. + AsociadoHistorico: + type: object + additionalProperties: false + properties: + tipo: + type: integer + format: int64 + description: tipo. + punto_venta: + type: integer + format: int64 + description: punto venta. + numero: + type: integer + format: int64 + description: numero. + cuit: + type: string + description: cuit. + nullable: true + fecha: + type: string + description: fecha. + nullable: true + required: + - tipo + - punto_venta + - numero + description: AsociadoHistorico. + ActividadHistorica: + type: object + additionalProperties: false + properties: + id: + type: integer + format: int64 + description: id. + required: + - id + description: ActividadHistorica. + PeriodoHistorico: + type: object + additionalProperties: false + properties: + fecha_desde: + type: string + description: fecha desde. + fecha_hasta: + type: string + description: fecha hasta. + required: + - fecha_desde + - fecha_hasta + description: PeriodoHistorico. + ObservacionHistorica: + type: object + additionalProperties: false + properties: + codigo: + type: integer + format: int64 + description: codigo. + mensaje: + type: string + description: mensaje. + nullable: true + required: + - codigo + description: ObservacionHistorica. + DatosFiscalesHistoricos: + type: object + additionalProperties: false + properties: + resultado: + type: string + nullable: true + description: resultado. + tipo_emision: + type: string + nullable: true + description: tipo emision. + cae: + type: string + nullable: true + description: cae. + fecha_vencimiento_cae: + type: string + nullable: true + description: fecha vencimiento cae. + fecha_comprobante: + type: string + nullable: true + description: fecha comprobante. + numero_documento: + type: string + nullable: true + description: numero documento. + moneda_id: + type: string + nullable: true + description: moneda id. + fecha_servicio_desde: + type: string + nullable: true + description: fecha servicio desde. + fecha_servicio_hasta: + type: string + nullable: true + description: fecha servicio hasta. + fecha_vencimiento_pago: + type: string + nullable: true + description: fecha vencimiento pago. + fecha_proceso: + type: string + nullable: true + description: fecha proceso. + cancela_misma_moneda: + type: string + nullable: true + description: cancela misma moneda. + punto_venta: + type: integer + format: int64 + nullable: true + description: punto venta. + tipo_comprobante: + type: integer + format: int64 + nullable: true + description: tipo comprobante. + comprobante_desde: + type: integer + format: int64 + nullable: true + description: comprobante desde. + comprobante_hasta: + type: integer + format: int64 + nullable: true + description: comprobante hasta. + concepto: + type: integer + format: int64 + nullable: true + description: concepto. + tipo_documento: + type: integer + format: int64 + nullable: true + description: tipo documento. + condicion_iva_receptor: + type: integer + format: int64 + nullable: true + description: Identificador ARCA histórico; no equivale a CondicionFiscal del + catálogo interno GOFIP. Permanece ausente cuando ARCA no lo devuelve. + cotizacion: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + nullable: true + importe_total: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + nullable: true + importe_neto: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + nullable: true + importe_iva: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + nullable: true + importe_exento: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + nullable: true + importe_no_gravado: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + nullable: true + importe_tributos: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + nullable: true + alicuotas_iva: + type: array + items: + $ref: '#/components/schemas/IVAHistorico' + nullable: true + description: alicuotas iva. + maxItems: 1000 + tributos: + type: array + items: + $ref: '#/components/schemas/TributoHistorico' + nullable: true + description: tributos. + maxItems: 1000 + comprobantes_asociados: + type: array + items: + $ref: '#/components/schemas/AsociadoHistorico' + nullable: true + description: comprobantes asociados. + maxItems: 1000 + opcionales: + type: array + items: + $ref: '#/components/schemas/OpcionalHistorico' + nullable: true + description: opcionales. + maxItems: 1000 + compradores: + type: array + items: + $ref: '#/components/schemas/CompradorHistorico' + nullable: true + description: compradores. + maxItems: 1000 + actividades: + type: array + items: + $ref: '#/components/schemas/ActividadHistorica' + nullable: true + description: actividades. + maxItems: 1000 + observaciones: + type: array + items: + $ref: '#/components/schemas/ObservacionHistorica' + nullable: true + description: observaciones. + maxItems: 1000 + periodo_asociado: + allOf: + - $ref: '#/components/schemas/PeriodoHistorico' + nullable: true + description: Dato opcional, ausente cuando no consta. + description: Campos presentes de la fuente fiscal. En el request son valores esperados + que se comparan con ARCA; un campo ausente no equivale a cero/lista vacía. No + se inventan datos. La respuesta conserva decimales y estructuras históricas. + FacturaHistorica: + type: object + additionalProperties: false + properties: + punto_venta: + type: integer + minimum: 1 + maximum: 99999 + description: punto venta. + tipo_comprobante: + type: integer + minimum: 1 + description: tipo comprobante. + numero_comprobante: + type: integer + format: int64 + minimum: 1 + maximum: 99999999 + description: numero comprobante. + cae: + type: string + pattern: ^[0-9]{1,20}$ + description: cae. + datos_historicos: + allOf: + - $ref: '#/components/schemas/DatosFiscalesHistoricos' + nullable: true + description: Afirmaciones fiscales opcionales. Todos los valores aportados deben + coincidir con la consulta ARCA. No completan silenciosamente campos ausentes + en la fuente fiscal. + required: + - punto_venta + - tipo_comprobante + - numero_comprobante + - cae + description: FacturaHistorica. + ProcedenciaFactura: + type: object + additionalProperties: false + properties: + sistema: + type: string + enum: + - gofip_legacy + description: sistema. + registro_id: + type: string + maxLength: 120 + description: Identificador estable del registro en el sistema cliente. Es opcional; + no se exige un invoice_id central Legacy inexistente. + required: + - sistema + description: ProcedenciaFactura. + ItemHistorico: + type: object + additionalProperties: false + properties: + referencia_original: + type: string + maxLength: 120 + description: referencia original. + descripcion: + type: string + maxLength: 255 + description: descripcion. + cantidad: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + precio_unitario: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + subtotal: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + monto_iva: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + total: + type: string + pattern: ^-?[0-9]{1,16}(\.[0-9]{1,6})?$ + maxLength: 24 + description: Decimal exacto; no usar notación exponencial ni float para recalcular + históricos. + codigo_iva: + type: string + description: codigo iva. + iva_exento: + type: boolean + nullable: true + description: iva exento. + iva_no_gravado: + type: boolean + nullable: true + description: iva no gravado. + required: + - descripcion + - cantidad + - precio_unitario + - subtotal + - monto_iva + - total + description: ItemHistorico. + DatosDocumentalesHistoricos: + type: object + additionalProperties: false + properties: + razon_social_emisor: + type: string + description: razon social emisor. + nullable: true + domicilio_emisor: + type: string + description: domicilio emisor. + nullable: true + condicion_fiscal_emisor: + type: string + description: condicion fiscal emisor. + nullable: true + fecha_inicio_actividad_emisor: + type: string + description: fecha inicio actividad emisor. + nullable: true + nombre_receptor: + type: string + description: nombre receptor. + nullable: true + domicilio_receptor: + type: string + description: domicilio receptor. + nullable: true + condicion_fiscal_receptor: + type: string + description: condicion fiscal receptor. + nullable: true + metodo_pago: + type: string + description: metodo pago. + nullable: true + email_receptor: + type: string + description: email receptor. + nullable: true + telefono_receptor: + type: string + description: telefono receptor. + nullable: true + description: DatosDocumentalesHistoricos. + DetalleHistoricoFactura: + type: object + additionalProperties: false + properties: + items: + type: array + maxItems: 1000 + items: + $ref: '#/components/schemas/ItemHistorico' + description: items. + nullable: true + datos_documentales: + allOf: + - $ref: '#/components/schemas/DatosDocumentalesHistoricos' + nullable: true + description: Dato opcional, ausente cuando no consta. + description: DetalleHistoricoFactura. + SolicitudImportarFactura: + type: object + additionalProperties: false + properties: + factura: + $ref: '#/components/schemas/FacturaHistorica' + procedencia: + $ref: '#/components/schemas/ProcedenciaFactura' + detalle: + allOf: + - $ref: '#/components/schemas/DetalleHistoricoFactura' + nullable: true + description: Datos comerciales/documentales del cliente, conservados con procedencia; + ARCA no verifica ítems, nombres ni domicilios históricos. + required: + - factura + - procedencia + description: SolicitudImportarFactura. + CuerpoSolicitudImportarFactura: + type: object + additionalProperties: false + properties: + data: + $ref: '#/components/schemas/SolicitudImportarFactura' + required: + - data + description: CuerpoSolicitudImportarFactura. + VerificacionImportacion: + type: object + additionalProperties: false + properties: + metodo: + type: string + enum: + - wsfe.FECompConsultar + description: metodo. + verificado_at: + type: string + format: date-time + description: verificado at. + required: + - metodo + - verificado_at + description: VerificacionImportacion. + CompletitudDocumental: + type: object + additionalProperties: false + properties: + tiene_items: + type: boolean + description: tiene items. + tiene_datos_documentales: + type: boolean + description: tiene datos documentales. + pdf_regenerable: + type: boolean + enum: + - false + description: Siempre false en V1. La conservación del detalle histórico no habilita + el renderizador que utiliza fichas actuales. + motivos: + type: array + items: + type: string + description: motivos item. + description: motivos. + required: + - tiene_items + - tiene_datos_documentales + - pdf_regenerable + - motivos + description: V1 conserva los datos aportados pero no regenera documentos históricos + con el renderizador actual. Esa operación falla explícitamente y nunca envía correo + ni emite. + RespuestaImportarFactura: + type: object + additionalProperties: false + properties: + documento_id: + type: string + format: uuid + description: documento id. + importado: + type: boolean + description: importado. + ya_existia: + type: boolean + description: ya existia. + verificacion: + $ref: '#/components/schemas/VerificacionImportacion' + documento: + $ref: '#/components/schemas/RespuestaFacturaPersistida' + required: + - documento_id + - importado + - ya_existia + - verificacion + - documento + description: RespuestaImportarFactura. RespuestaFacturaPersistida: type: object description: Datos realmente persistidos. No incorpora fichas mutables de cliente, snapshots históricos inexistentes, @@ -4338,6 +5100,11 @@ components: - api - web - recurrente + - legacy_import + verificacion: + $ref: "#/components/schemas/VerificacionImportacion" + completitud_documental: + $ref: "#/components/schemas/CompletitudDocumental" external_reference: type: string factura: @@ -6521,7 +7288,7 @@ components: enum: [pendiente, autorizado, error] origen: type: string - enum: [api, web, recurrente] + enum: [api, web, recurrente, legacy_import] usuario_id: type: string format: uuid @@ -6587,7 +7354,7 @@ components: enum: [pendiente, autorizado, error] origen: type: string - enum: [api, web, recurrente] + enum: [api, web, recurrente, legacy_import] usuario_id: type: string format: uuid @@ -6658,6 +7425,8 @@ components: type: object required: [factura_id, items, comprobantes_asociados, alicuotas_iva, tributos] properties: + historico: + $ref: "#/components/schemas/DatosFiscalesHistoricos" factura_id: type: string format: uuid diff --git a/docs/validacion.md b/docs/validacion.md index 83f47f3..1f84959 100644 --- a/docs/validacion.md +++ b/docs/validacion.md @@ -4,7 +4,7 @@ Definir `GOFIP_API_URL`, `GOFIP_API_KEY` y, sólo para SEP, `GOFIP_SEP_KEY` mediante variables del entorno. No pegarlas en logs, comandos compartidos ni archivos -versionados. La URL corresponde al backend V2 actualizado con OpenAPI 2.18.0. +versionados. La URL corresponde al backend V2 actualizado con OpenAPI 2.19.0. Los ejemplos de consulta no generan efectos fiscales. El ejemplo de emisión sí emite: usar homologación y una venta de prueba deliberada. @@ -58,3 +58,13 @@ crear/eliminar una base temporal exclusiva. No usa tablas de la base del DSN. La prueba real de GOFIP y MySQL queda a cargo del integrador antes de publicar. El módulo externo de prueba está incluido en `integration/consumer`: ejecutar `go test ./...` desde esa carpeta. El replace local permite validar los imports `/v2` sin publicar tags. + +## Importaciones históricas + +Ver [guía de importación](importacion-facturas.md). La suite comprueba contrato, +precisión, ausencia de selector de ambiente, redacción, cancelación, límites y +cero retries/fallbacks con servidores falsos. El backend tiene la prueba real +opt-in `TestImportacionMySQLMultiproceso` (dos procesos/diez solicitudes) usando +`GOFIP_IMPORT_TEST_MYSQL_DSN` de un servidor descartable, además de backfill y +precisión SQL. La guía del backend describe la autorización por API key del PV +y el backfill transaccional que aborta ante datos fiscales incompatibles. diff --git a/examples/importar-factura/main.go b/examples/importar-factura/main.go new file mode 100644 index 0000000..5a8ba48 --- /dev/null +++ b/examples/importar-factura/main.go @@ -0,0 +1,50 @@ +// Importa una única autorización Legacy existente. Ejecutar sólo con una +// solicitud revisada y la API key normal del mismo emisor/PV en el entorno autorizado. +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "os" + "time" + + gofip "github.com/unkcode-org/gofip-sdk-go/v2" + "github.com/unkcode-org/gofip-sdk-go/v2/modelos/facturacion" +) + +func main() { + path, key := os.Getenv("GOFIP_IMPORT_REQUEST_FILE"), os.Getenv("GOFIP_IMPORT_IDEMPOTENCY_KEY") + if path == "" || key == "" { + log.Fatal("faltan GOFIP_IMPORT_REQUEST_FILE/GOFIP_IMPORT_IDEMPOTENCY_KEY") + } + f, err := os.Open(path) + if err != nil { + log.Fatal("no se pudo abrir la solicitud histórica") + } + defer f.Close() + var solicitud facturacion.SolicitudImportarFactura + dec := json.NewDecoder(io.LimitReader(f, (1<<20)+1)) + dec.DisallowUnknownFields() + if dec.Decode(&solicitud) != nil || dec.Decode(&struct{}{}) != io.EOF { + log.Fatal("solicitud histórica inválida; usar el DTO sin envelope data") + } + c, err := gofip.NuevoCliente(gofip.OpcionesCliente{URLBase: os.Getenv("GOFIP_API_URL"), ClaveAPI: os.Getenv("GOFIP_API_KEY")}) + if err != nil { + log.Fatal("configuración GOFIP inválida") + } + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + r, err := c.Facturacion.ImportarFactura(ctx, solicitud, gofip.OpcionesImportacionFactura{ClaveIdempotencia: key, ReferenciaExterna: os.Getenv("GOFIP_IMPORT_EXTERNAL_REFERENCE")}) + if err != nil { + var api *gofip.ErrorAPI + if errors.As(err, &api) { + log.Fatalf("importación fiscal: HTTP %d código %s", api.Estado, api.Codigo) + } + log.Fatal("no se pudo importar; conservar la clave para reanudar y revisar el resultado") + } + fmt.Printf("Documento: %s; importado: %t; ya existía: %t\n", r.DocumentoID, r.Importado, r.YaExistia) +} diff --git a/importacion_facturas.go b/importacion_facturas.go new file mode 100644 index 0000000..cea01c4 --- /dev/null +++ b/importacion_facturas.go @@ -0,0 +1,109 @@ +package gofip + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "regexp" + "strings" + + "github.com/unkcode-org/gofip-sdk-go/v2/modelos/facturacion" +) + +// OpcionesImportacionFactura has no environment selector. Legacy imports are +// verified in Production by the server, independently of OpcionesCliente. +type OpcionesImportacionFactura struct { + ClaveIdempotencia string + ReferenciaExterna string +} + +const limiteImportacionRespuesta = 2 << 20 + +var patronCodigoImportacion = regexp.MustCompile(`^[0-9]{5}$`) +var patronCAEImportacion = regexp.MustCompile(`^[0-9]{1,20}$`) +var patronDocumentoImportacion = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) + +// ImportarFactura imports an existing authorization; it never calls EmitirFactura. +// The API key may import only invoices from its own issuer and point of sale. No +// application retries, redirects, PDF generation or Legacy fallbacks. +func (s *ServicioFacturacion) ImportarFactura(ctx context.Context, solicitud facturacion.SolicitudImportarFactura, o OpcionesImportacionFactura) (*facturacion.RespuestaImportarFactura, error) { + f := solicitud.Factura + if f.PuntoVenta < 1 || f.PuntoVenta > 99999 || f.TipoComprobante < 1 || f.NumeroComprobante < 1 || f.NumeroComprobante > 99999999 || !patronCAEImportacion.MatchString(f.CAE) || solicitud.Procedencia.Sistema != "gofip_legacy" || len(solicitud.Procedencia.RegistroID) > 120 || len(o.ClaveIdempotencia) == 0 || len(o.ClaveIdempotencia) > 120 || len(o.ReferenciaExterna) > 120 { + return nil, errors.New("solicitud de importación inválida") + } + for _, v := range []string{o.ClaveIdempotencia, o.ReferenciaExterna} { + if strings.IndexFunc(v, func(r rune) bool { return r < 32 || r == 127 }) >= 0 { + return nil, errors.New("cabecera de importación inválida") + } + } + b, err := json.Marshal(cuerpoDatos[facturacion.SolicitudImportarFactura]{Datos: solicitud}) + if err != nil { + return nil, errors.New("serializar solicitud de importación inválida") + } + if len(b) > 1<<20 { + return nil, errors.New("solicitud de importación excede 1 MiB") + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.transporte.urlBase+"/api/v2/facturas/importaciones", bytes.NewReader(b)) + if err != nil { + return nil, errors.New("crear solicitud de importación") + } + // Disable transparent body replay by the standard transport. A caller may + // explicitly repeat the operation using its durable fiscal/idempotency keys. + req.GetBody = nil + req.Header.Set("Authorization", "Bearer "+s.transporte.claveAPI) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("Idempotency-Key", o.ClaveIdempotencia) + if o.ReferenciaExterna != "" { + req.Header.Set("X-External-Reference", o.ReferenciaExterna) + } + client := *s.transporte.cliente + client.Jar = nil + client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } + resp, err := client.Do(req) + if err != nil { + return nil, errorImportacionSeguro(ctx, err, "fallo de transporte de importación") + } + defer resp.Body.Close() + raw, err := io.ReadAll(io.LimitReader(resp.Body, limiteImportacionRespuesta+1)) + if err != nil { + return nil, errorImportacionSeguro(ctx, err, "fallo al leer respuesta de importación") + } + if len(raw) > limiteImportacionRespuesta { + return nil, errors.New("respuesta de importación excede 2 MiB") + } + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + var problem ErrorAPI + if json.Unmarshal(raw, &problem) == nil && patronCodigoImportacion.MatchString(problem.Codigo) { + return nil, &ErrorAPI{Estado: resp.StatusCode, Codigo: problem.Codigo, Titulo: "Error de importación fiscal", Detalle: http.StatusText(resp.StatusCode)} + } + return nil, &ErrorHTTP{Estado: resp.StatusCode, Cuerpo: http.StatusText(resp.StatusCode)} + } + var result facturacion.RespuestaImportarFactura + dec := json.NewDecoder(bytes.NewReader(raw)) + if dec.Decode(&result) != nil { + return nil, errors.New("respuesta de importación inválida") + } + if err := dec.Decode(&struct{}{}); err != io.EOF { + return nil, errors.New("respuesta de importación contiene datos adicionales") + } + if !patronDocumentoImportacion.MatchString(result.DocumentoID) || result.Importado == result.YaExistia || result.Importado != (resp.StatusCode == 201) || result.Verificacion.Metodo != "wsfe.FECompConsultar" || result.Verificacion.VerificadoAt.IsZero() || result.Documento == nil || result.Documento.DocumentoID != result.DocumentoID || result.Documento.EmisorID == "" || result.Documento.Estado != "autorizado" || result.Documento.Factura == nil || result.Documento.Factura.CAE != f.CAE || result.Documento.Factura.PuntoVenta != f.PuntoVenta || result.Documento.Factura.TipoComprobante != int(f.TipoComprobante) || result.Documento.Factura.NumeroComprobante != f.NumeroComprobante { + return nil, errors.New("respuesta de importación incompleta o incompatible") + } + return &result, nil +} +func errorImportacionSeguro(ctx context.Context, err error, message string) error { + if errors.Is(err, context.Canceled) || errors.Is(ctx.Err(), context.Canceled) { + return fmt.Errorf("importación fiscal: %w", context.Canceled) + } + if errors.Is(err, context.DeadlineExceeded) || errors.Is(ctx.Err(), context.DeadlineExceeded) { + return fmt.Errorf("importación fiscal: %w", context.DeadlineExceeded) + } + // Arbitrary transports may include headers, bodies or URLs in both Error and + // Unwrap. Preserve only known context causes, never those diagnostic chains. + return errors.New(message) +} diff --git a/importacion_facturas_test.go b/importacion_facturas_test.go new file mode 100644 index 0000000..567d536 --- /dev/null +++ b/importacion_facturas_test.go @@ -0,0 +1,113 @@ +package gofip + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/unkcode-org/gofip-sdk-go/v2/modelos/facturacion" +) + +func solicitudHistoricaSDK() facturacion.SolicitudImportarFactura { + return facturacion.SolicitudImportarFactura{Factura: facturacion.FacturaHistorica{PuntoVenta: 3, TipoComprobante: 6, NumeroComprobante: 12345, CAE: "00123456789012"}, Procedencia: facturacion.ProcedenciaFactura{Sistema: "gofip_legacy"}} +} + +const respuestaHistoricaSDK = `{"documento_id":"01900000-0000-7000-8000-000000000001","importado":true,"ya_existia":false,"verificacion":{"metodo":"wsfe.FECompConsultar","verificado_at":"2026-10-16T20:00:00Z"},"documento":{"documento_id":"01900000-0000-7000-8000-000000000001","emisor_id":"emisor-a","punto_venta_id":"pv-3","estado":"autorizado","origen":"legacy_import","factura":{"cae":"00123456789012","punto_venta":3,"tipo_comprobante":6,"numero_comprobante":12345,"historico":{"cotizacion":"1234.123456","importe_total":"121.00"}},"pdf":{"disponible":false}}}` + +func TestImportacionSDKContratoTipadoSinSeleccionAmbiente(t *testing.T) { + calls := 0 + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + if r.Method != "POST" || r.URL.Path != "/api/v2/facturas/importaciones" || r.Header.Get("Authorization") != "Bearer clave-prueba" || r.Header.Get("Idempotency-Key") != "historico:123" || r.Header.Get("X-External-Reference") != "cliente:123" || r.Header.Get("X-ARCA-Ambiente") != "" { + t.Error("contrato/autenticación/ambiente incorrecto") + } + b, _ := io.ReadAll(r.Body) + if !strings.Contains(string(b), `"data":{"factura":`) || strings.Contains(string(b), "ambiente") || strings.Contains(string(b), "emisor_id") { + t.Error("body no tipado o amplía autoridad") + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(201) + _, _ = io.WriteString(w, respuestaHistoricaSDK) + })) + defer s.Close() + c, err := NuevoCliente(OpcionesCliente{URLBase: s.URL, ClaveAPI: "clave-prueba", AmbienteARCA: AmbienteHomologacion, ClienteHTTP: s.Client()}) + if err != nil { + t.Fatal(err) + } + r, err := c.Facturacion.ImportarFactura(context.Background(), solicitudHistoricaSDK(), OpcionesImportacionFactura{ClaveIdempotencia: "historico:123", ReferenciaExterna: "cliente:123"}) + if err != nil { + t.Fatal(err) + } + if calls != 1 || r.DocumentoID != documentoFiscalPrueba || r.Documento.Factura.Historico.Cotizacion == nil || *r.Documento.Factura.Historico.Cotizacion != "1234.123456" { + t.Fatal("datos históricos alterados") + } +} + +func TestImportacionSDKRechazaIdentidadDevueltaIncompatible(t *testing.T) { + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(201) + _, _ = io.WriteString(w, strings.Replace(respuestaHistoricaSDK, `"punto_venta":3`, `"punto_venta":4`, 1)) + })) + defer s.Close() + c := nuevoClientePrueba(t, s.URL, s.Client()) + if _, err := c.Facturacion.ImportarFactura(context.Background(), solicitudHistoricaSDK(), OpcionesImportacionFactura{ClaveIdempotencia: "uno"}); err == nil { + t.Fatal("aceptó identidad fiscal distinta") + } +} +func TestImportacionSDKErroresSegurosLimitesYNoRetries(t *testing.T) { + secret := "clave-api-no-exponer" + for _, tc := range []struct { + name string + status int + body string + }{{"conflicto", 409, `{"code":"13101","detail":"` + secret + `"}`}, {"no-json", 502, secret}, {"body-grande", 503, strings.Repeat("x", (2<<20)+1)}, {"respuesta-invalida", 201, `{"documento_id":"` + secret + `"}`}, {"json-adicional", 201, respuestaHistoricaSDK + ` {}`}} { + t.Run(tc.name, func(t *testing.T) { + calls := 0 + transport := roundTripperPrueba(func(r *http.Request) (*http.Response, error) { + calls++ + return &http.Response{StatusCode: tc.status, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(tc.body)), Request: r}, nil + }) + c, _ := NuevoCliente(OpcionesCliente{URLBase: "https://example.test", ClaveAPI: secret, ClienteHTTP: &http.Client{Transport: transport}}) + _, err := c.Facturacion.ImportarFactura(context.Background(), solicitudHistoricaSDK(), OpcionesImportacionFactura{ClaveIdempotencia: "uno"}) + if err == nil || strings.Contains(fmt.Sprintf("%+v", err), secret) || calls != 1 { + t.Fatalf("error/retry inseguro: %v calls=%d", err, calls) + } + if tc.status == 409 { + var api *ErrorAPI + if !errors.As(err, &api) || api.Estado != 409 || api.Codigo != "13101" { + t.Fatal("perdió error tipado", err) + } + } + }) + } + transport := roundTripperPrueba(func(*http.Request) (*http.Response, error) { return nil, errors.New(secret) }) + c, _ := NuevoCliente(OpcionesCliente{URLBase: "https://example.test", ClaveAPI: secret, ClienteHTTP: &http.Client{Transport: transport}}) + _, err := c.Facturacion.ImportarFactura(context.Background(), solicitudHistoricaSDK(), OpcionesImportacionFactura{ClaveIdempotencia: "uno"}) + if err == nil || strings.Contains(fmt.Sprintf("%+v", err), secret) { + t.Fatal("filtró error de transporte") + } +} +func TestImportacionSDKCancelacionYRedireccion(t *testing.T) { + calls := 0 + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + http.Redirect(w, r, "/otra-ruta", http.StatusTemporaryRedirect) + })) + defer s.Close() + c := nuevoClientePrueba(t, s.URL, s.Client()) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + _, err := c.Facturacion.ImportarFactura(ctx, solicitudHistoricaSDK(), OpcionesImportacionFactura{ClaveIdempotencia: "uno"}) + if !errors.Is(err, context.Canceled) || calls != 0 { + t.Fatal("cancelación perdida", err) + } + _, err = c.Facturacion.ImportarFactura(context.Background(), solicitudHistoricaSDK(), OpcionesImportacionFactura{ClaveIdempotencia: "uno"}) + if err == nil || calls != 1 { + t.Fatal("siguió redirect de importación", err) + } +} diff --git a/integration/consumer/importacion_test.go b/integration/consumer/importacion_test.go new file mode 100644 index 0000000..a3f2bfe --- /dev/null +++ b/integration/consumer/importacion_test.go @@ -0,0 +1,34 @@ +package consumer_test + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + gofip "github.com/unkcode-org/gofip-sdk-go/v2" + "github.com/unkcode-org/gofip-sdk-go/v2/modelos/facturacion" +) + +func TestConsumidorImportacionHistorica(t *testing.T) { + const id = "01900000-0000-7000-8000-000000000001" + calls := 0 + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + if r.URL.Path != "/api/v2/facturas/importaciones" || r.Method != "POST" || r.Header.Get("X-ARCA-Ambiente") != "" || r.Header.Get("Idempotency-Key") != "original" { + t.Error("ruta mutante o contrato incorrecto") + } + w.WriteHeader(200) + _, _ = fmt.Fprintf(w, `{"documento_id":%q,"importado":false,"ya_existia":true,"verificacion":{"metodo":"wsfe.FECompConsultar","verificado_at":"2026-10-16T20:00:00Z"},"documento":{"documento_id":%q,"emisor_id":"emisor-a","estado":"autorizado","origen":"api","factura":{"cae":"00123456789012","punto_venta":3,"tipo_comprobante":6,"numero_comprobante":12345}}}`, id, id) + })) + defer s.Close() + c, err := gofip.NuevoCliente(gofip.OpcionesCliente{URLBase: s.URL, ClaveAPI: "test", AmbienteARCA: gofip.AmbienteHomologacion, ClienteHTTP: s.Client()}) + if err != nil { + t.Fatal(err) + } + r, err := c.Facturacion.ImportarFactura(context.Background(), facturacion.SolicitudImportarFactura{Factura: facturacion.FacturaHistorica{PuntoVenta: 3, TipoComprobante: facturacion.TipoComprobante(6), NumeroComprobante: 12345, CAE: "00123456789012"}, Procedencia: facturacion.ProcedenciaFactura{Sistema: "gofip_legacy"}}, gofip.OpcionesImportacionFactura{ClaveIdempotencia: "original", ReferenciaExterna: "cliente:original"}) + if err != nil || r.Importado || !r.YaExistia || r.DocumentoID != id || r.Documento.Origen != "api" || calls != 1 { + t.Fatalf("importación incompatible: %+v %v", r, err) + } +} diff --git a/modelos/facturacion/importacion.go b/modelos/facturacion/importacion.go new file mode 100644 index 0000000..299a1af --- /dev/null +++ b/modelos/facturacion/importacion.go @@ -0,0 +1,183 @@ +// Modelos de importación del contrato OpenAPI 2.19.0. Legacy siempre consulta Producción. +package facturacion + +import ( + "encoding/json" + "errors" + "regexp" + "time" +) + +// DecimalHistorico conserva un decimal exacto como string JSON. +type DecimalHistorico string + +var patronDecimalHistorico = regexp.MustCompile(`^-?[0-9]{1,16}(\.[0-9]{1,6})?$`) + +func (d DecimalHistorico) MarshalJSON() ([]byte, error) { + if !patronDecimalHistorico.MatchString(string(d)) { + return nil, errors.New("decimal histórico inválido") + } + return json.Marshal(string(d)) +} +func (d *DecimalHistorico) UnmarshalJSON(b []byte) error { + var value string + if json.Unmarshal(b, &value) != nil || !patronDecimalHistorico.MatchString(value) { + return errors.New("decimal histórico inválido") + } + *d = DecimalHistorico(value) + return nil +} + +type FacturaHistorica struct { + PuntoVenta int `json:"punto_venta"` + TipoComprobante TipoComprobante `json:"tipo_comprobante"` + NumeroComprobante int64 `json:"numero_comprobante"` + CAE string `json:"cae"` + DatosHistoricos *DatosFiscalesHistoricos `json:"datos_historicos,omitempty"` +} + +type SolicitudImportarFactura struct { + Factura FacturaHistorica `json:"factura"` + Procedencia ProcedenciaFactura `json:"procedencia"` + Detalle *DetalleHistoricoFactura `json:"detalle,omitempty"` +} + +type ProcedenciaFactura struct { + Sistema string `json:"sistema"` + RegistroID string `json:"registro_id,omitempty"` +} + +type DatosFiscalesHistoricos struct { + Resultado *string `json:"resultado,omitempty"` + TipoEmision *string `json:"tipo_emision,omitempty"` + CAE *string `json:"cae,omitempty"` + PuntoVenta *int `json:"punto_venta,omitempty"` + TipoComprobante *int `json:"tipo_comprobante,omitempty"` + ComprobanteDesde *int64 `json:"comprobante_desde,omitempty"` + ComprobanteHasta *int64 `json:"comprobante_hasta,omitempty"` + FechaVencimientoCAE *string `json:"fecha_vencimiento_cae,omitempty"` + FechaComprobante *string `json:"fecha_comprobante,omitempty"` + Concepto *int `json:"concepto,omitempty"` + TipoDocumento *int `json:"tipo_documento,omitempty"` + NumeroDocumento *string `json:"numero_documento,omitempty"` + MonedaID *string `json:"moneda_id,omitempty"` + Cotizacion *DecimalHistorico `json:"cotizacion,omitempty"` + ImporteTotal *DecimalHistorico `json:"importe_total,omitempty"` + ImporteNeto *DecimalHistorico `json:"importe_neto,omitempty"` + ImporteIVA *DecimalHistorico `json:"importe_iva,omitempty"` + ImporteExento *DecimalHistorico `json:"importe_exento,omitempty"` + ImporteNoGravado *DecimalHistorico `json:"importe_no_gravado,omitempty"` + ImporteTributos *DecimalHistorico `json:"importe_tributos,omitempty"` + FechaServicioDesde *string `json:"fecha_servicio_desde,omitempty"` + FechaServicioHasta *string `json:"fecha_servicio_hasta,omitempty"` + FechaVencimientoPago *string `json:"fecha_vencimiento_pago,omitempty"` + FechaProceso *string `json:"fecha_proceso,omitempty"` + CancelaMismaMoneda *string `json:"cancela_misma_moneda,omitempty"` + CondicionIVAReceptor *int `json:"condicion_iva_receptor,omitempty"` + AlicuotasIVA *[]IVAHistorico `json:"alicuotas_iva,omitempty"` + Tributos *[]TributoHistorico `json:"tributos,omitempty"` + ComprobantesAsociados *[]AsociadoHistorico `json:"comprobantes_asociados,omitempty"` + Opcionales *[]OpcionalHistorico `json:"opcionales,omitempty"` + Compradores *[]CompradorHistorico `json:"compradores,omitempty"` + Actividades *[]ActividadHistorica `json:"actividades,omitempty"` + PeriodoAsociado *PeriodoHistorico `json:"periodo_asociado,omitempty"` + Observaciones *[]ObservacionHistorica `json:"observaciones,omitempty"` +} + +type IVAHistorico struct { + ID int `json:"id" xml:"Id"` + BaseImponible DecimalHistorico `json:"base_imponible" xml:"BaseImp"` + Importe DecimalHistorico `json:"importe" xml:"Importe"` +} + +type TributoHistorico struct { + ID int `json:"id" xml:"Id"` + Descripcion *string `json:"descripcion,omitempty" xml:"Desc"` + BaseImponible DecimalHistorico `json:"base_imponible" xml:"BaseImp"` + Alicuota DecimalHistorico `json:"alicuota" xml:"Alic"` + Importe DecimalHistorico `json:"importe" xml:"Importe"` +} + +type OpcionalHistorico struct { + ID string `json:"id" xml:"Id"` + Valor *string `json:"valor,omitempty" xml:"Valor"` +} + +type CompradorHistorico struct { + TipoDocumento int `json:"tipo_documento" xml:"DocTipo"` + NumeroDocumento string `json:"numero_documento" xml:"DocNro"` + Porcentaje DecimalHistorico `json:"porcentaje" xml:"Porcentaje"` +} + +type AsociadoHistorico struct { + Tipo int `json:"tipo" xml:"Tipo"` + PuntoVenta int `json:"punto_venta" xml:"PtoVta"` + Numero int64 `json:"numero" xml:"Nro"` + CUIT *string `json:"cuit,omitempty" xml:"Cuit"` + Fecha *string `json:"fecha,omitempty" xml:"CbteFch"` +} + +type ActividadHistorica struct { + ID int64 `json:"id" xml:"Id"` +} + +type PeriodoHistorico struct { + FechaDesde string `json:"fecha_desde" xml:"FchDesde"` + FechaHasta string `json:"fecha_hasta" xml:"FchHasta"` +} + +type ObservacionHistorica struct { + Codigo int `json:"codigo" xml:"Code"` + Mensaje *string `json:"mensaje,omitempty" xml:"Msg"` +} + +type DetalleHistoricoFactura struct { + Items []ItemHistorico `json:"items,omitempty"` + DatosDocumentales *DatosDocumentalesHistoricos `json:"datos_documentales,omitempty"` +} + +type ItemHistorico struct { + ReferenciaOriginal string `json:"referencia_original,omitempty"` + Descripcion string `json:"descripcion"` + Cantidad DecimalHistorico `json:"cantidad"` + PrecioUnitario DecimalHistorico `json:"precio_unitario"` + Subtotal DecimalHistorico `json:"subtotal"` + MontoIVA DecimalHistorico `json:"monto_iva"` + Total DecimalHistorico `json:"total"` + CodigoIVA string `json:"codigo_iva,omitempty"` + IVAExento *bool `json:"iva_exento,omitempty"` + IVANoGravado *bool `json:"iva_no_gravado,omitempty"` +} + +type DatosDocumentalesHistoricos struct { + RazonSocialEmisor *string `json:"razon_social_emisor,omitempty"` + DomicilioEmisor *string `json:"domicilio_emisor,omitempty"` + CondicionFiscalEmisor *string `json:"condicion_fiscal_emisor,omitempty"` + FechaInicioActividadEmisor *string `json:"fecha_inicio_actividad_emisor,omitempty"` + NombreReceptor *string `json:"nombre_receptor,omitempty"` + DomicilioReceptor *string `json:"domicilio_receptor,omitempty"` + CondicionFiscalReceptor *string `json:"condicion_fiscal_receptor,omitempty"` + MetodoPago *string `json:"metodo_pago,omitempty"` + EmailReceptor *string `json:"email_receptor,omitempty"` + TelefonoReceptor *string `json:"telefono_receptor,omitempty"` +} + +type VerificacionImportacion struct { + Metodo string `json:"metodo"` + VerificadoAt time.Time `json:"verificado_at"` +} + +type CompletitudDocumental struct { + TieneItems bool `json:"tiene_items"` + TieneDatosDocumentales bool `json:"tiene_datos_documentales"` + PDFRegenerable bool `json:"pdf_regenerable"` + Motivos []string `json:"motivos"` +} + +type RespuestaImportarFactura struct { + DocumentoID string `json:"documento_id"` + Importado bool `json:"importado"` + YaExistia bool `json:"ya_existia"` + Verificacion VerificacionImportacion `json:"verificacion"` + Documento *RespuestaFacturaPersistida `json:"documento"` +} diff --git a/modelos/facturacion/persistidas.go b/modelos/facturacion/persistidas.go index a4c503a..5ebe853 100644 --- a/modelos/facturacion/persistidas.go +++ b/modelos/facturacion/persistidas.go @@ -1,19 +1,21 @@ -// Modelos del contrato OpenAPI 2.18.0, schemas RespuestaFacturaPersistida y DatosFacturaCompleta. +// Modelos del contrato OpenAPI 2.19.0, schemas RespuestaFacturaPersistida y DatosFacturaCompleta. package facturacion import "time" type RespuestaFacturaPersistida struct { - DocumentoID string `json:"documento_id"` - EmisorID string `json:"emisor_id"` - PuntoVentaID string `json:"punto_venta_id"` - Estado string `json:"estado"` - Origen string `json:"origen"` - ExternalReference string `json:"external_reference,omitempty"` - Factura *DetalleDatosFactura `json:"factura"` - PDF EstadoPDFFactura `json:"pdf"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Verificacion *VerificacionImportacion `json:"verificacion,omitempty"` + CompletitudDocumental *CompletitudDocumental `json:"completitud_documental,omitempty"` + DocumentoID string `json:"documento_id"` + EmisorID string `json:"emisor_id"` + PuntoVentaID string `json:"punto_venta_id"` + Estado string `json:"estado"` + Origen string `json:"origen"` + ExternalReference string `json:"external_reference,omitempty"` + Factura *DetalleDatosFactura `json:"factura"` + PDF EstadoPDFFactura `json:"pdf"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type EstadoPDFFactura struct { @@ -24,6 +26,7 @@ type EstadoPDFFactura struct { } type DetalleDatosFactura struct { + Historico *DatosFiscalesHistoricos `json:"historico,omitempty"` FacturaID string `json:"factura_id"` CAE string `json:"cae"` FechaVencimientoCAE string `json:"fecha_vencimiento_cae"`