# Backend ERP – Perfil VeriFactu empresarial ## 1. Estado del documento Estado: **IMPLEMENTADO PARCIALMENTE** Este documento define el diseño backend del perfil VeriFactu de empresa dentro del módulo `customer-invoices`. Decisiones consolidadas: - El perfil VeriFactu pertenece al bounded context `customer-invoices`. - Es una configuración separada de `Company`. - Tiene scope de empresa mediante `companyId`. - El estado de configuración es explícito. - Se permite `obligated_without_verifactu` como estado real. - Los catálogos de clasificación y exención quedan modelados por código pero no se cierran todavía como enums. - `IssuedInvoice` deberá persistir dos snapshots distintos en una fase posterior: - clasificación fiscal efectiva; - política VeriFactu efectiva. - `VerifactuRecord` mantiene responsabilidad operativa y no absorbe el perfil empresarial. - El perfil no expone secretos ni passwords de certificados. ## 2. Propósito Proporcionar una fuente de verdad empresarial para: - aplicabilidad de la normativa; - exención y su motivo; - uso o no de modalidad VeriFactu; - fecha efectiva de activación; - renovación automática si aplica; - defaults de clasificación fiscal; - snapshot histórico al emitir una factura. El diseño debe integrarse con el flujo actual: ```txt Proforma → IssueProformaUseCase → ProformaIssueReadModelAssembler → proforma-to-issued-invoice-props-converter → IssuedInvoice → VerifactuRecord ``` sin trasladar esta resolución al frontend. ## 3. Ownership El perfil debe vivir como agregado/configuración separado dentro de: ```txt modules/customer-invoices ``` No debe vivir dentro de: ```txt identity companies Company aggregate ``` Motivo: - gobierna facturación; - afecta directamente a `IssuedInvoice`; - sus defaults deben materializarse al emitir; - identidad solo aporta `companyId`, company context y autorización. ## 4. Modelo de estados ```ts export const CompanyVerifactuProfileStatus = { NotConfigured: "not_configured", Exempt: "exempt", ObligatedWithoutVerifactu: "obligated_without_verifactu", ObligatedWithVerifactu: "obligated_with_verifactu", } as const; export type CompanyVerifactuProfileStatus = (typeof CompanyVerifactuProfileStatus)[keyof typeof CompanyVerifactuProfileStatus]; ``` Semántica: ```txt not_configured → la empresa aún no ha definido su perfil. exempt → la empresa queda configurada como exenta/no aplicable. obligated_without_verifactu → la empresa está obligada, pero no opera bajo modalidad VeriFactu. obligated_with_verifactu → la empresa está obligada y utiliza VeriFactu. ``` Se acepta `obligated_without_verifactu` como estado real del modelo. ## 5. Invariantes del estado ### `not_configured` ```txt exemptionReasonCode = null exemptionReasonDetail = null verifactuEffectiveAt = null autoRenewMode = null ``` No debe producir defaults efectivos de clasificación VeriFactu. ### `exempt` Puede requerir: ```txt exemptionReasonCode exemptionReasonDetail ``` No permite activación VeriFactu. ### `obligated_without_verifactu` ```txt verifactuEffectiveAt = null autoRenewMode = null ``` Puede tener defaults fiscales si son aplicables. ### `obligated_with_verifactu` Requiere: ```txt verifactuEffectiveAt != null ``` `autoRenewMode` solo tiene sentido aquí. ## 6. Obligación y exención V1: ```ts exemptionReasonCode: string | null; exemptionReasonDetail: string | null; ``` No se cierra todavía un enum legal. Los códigos deberán validarse contra un catálogo backend cuando exista. ## 7. Activación VeriFactu ```ts export const AutoRenewMode = { Disabled: "disabled", Enabled: "enabled", } as const; export type AutoRenewMode = (typeof AutoRenewMode)[keyof typeof AutoRenewMode]; ``` Campos: ```ts verifactuEffectiveAt: Date | null; autoRenewMode: AutoRenewMode | null; ``` No se modelan periodos históricos en V1. ## 8. Defaults empresariales Solo defaults de clasificación fiscal: ```ts export interface CompanyVerifactuDefaults { taxRegimeCode: string | null; operationQualificationCode: string | null; exemptionReasonCode: string | null; } ``` No pertenecen: ```txt taxPercentage recPercentage retentionPercentage taxMode tax breakdown ``` ## 9. Precedencia de defaults ```txt document explicit → customer → company VeriFactu defaults → backend fallback ``` Backend resolverá la precedencia cuando la integración con emisión entre en alcance. ## 10. Clasificación fiscal vs cálculo fiscal ### Cálculo fiscal ```txt taxPercentage recPercentage retentionPercentage taxable base totals tax breakdown ``` ### Clasificación VeriFactu ```txt regimeCode operationQualificationCode exemptionReasonCode ``` No reutilizar `ProformaFiscalData` como sustituto de clasificación VeriFactu. ## 11. Snapshot de clasificación fiscal ```ts export interface IssuedInvoiceFiscalClassificationSnapshot { regimeCode: string | null; regimeLabel: string | null; operationQualificationCode: string | null; operationQualificationLabel: string | null; exemptionReasonCode: string | null; exemptionReasonLabel: string | null; } ``` Queda diseñado, pero no implementado todavía en esta fase. ## 12. Snapshot de política VeriFactu ```ts export interface IssuedInvoiceVerifactuPolicySnapshot { profileStatus: CompanyVerifactuProfileStatus; usesVerifactu: boolean; effectiveAt: string | null; } ``` Queda diseñado, pero no implementado todavía en esta fase. ## 13. Resolución durante la emisión ```txt CompanyVerifactuProfile │ ▼ ProformaIssueReadModelAssembler │ ├── carga proforma ├── carga cliente ├── carga perfil VeriFactu └── prepara contexto │ ▼ proforma-to-issued-invoice-props-converter │ ├── resuelve precedencia ├── construye FiscalClassificationSnapshot └── construye VerifactuPolicySnapshot │ ▼ IssuedInvoice.create(...) ``` ## 14. Responsabilidad de `VerifactuRecord` `IssuedInvoice`: - snapshot documental e histórico; - clasificación fiscal aplicada; - política VeriFactu aplicada. `VerifactuRecord`: - estado operativo; - URL; - QR; - UUID; - tipo de operación. No debe contener defaults empresariales ni obligación/exención. ## 15. Certificados El perfil no contiene: ```txt password private key secret name certificate binary ``` Solo podría admitir una referencia lógica no sensible si realmente fuera necesaria. ## 16. Boundary AEAT Fuera de alcance: ```txt enqueue worker/jobs retries queries hash chaining cancelación rectificación payload AEAT outbox ``` ## 17. API ```txt GET /api/customer-invoices/settings/verifactu PUT /api/customer-invoices/settings/verifactu ``` Ambos usan `requireIdentityTenant(params)` y los controladores resuelven `companyId` mediante `this.getTenantId()`. ## 18. GET Si no existe fila: ```txt profile_status = "not_configured" configured = false ``` Response conceptual: ```ts export interface CompanyVerifactuProfileResponseDTO { profile_status: CompanyVerifactuProfileStatus; configured: boolean; uses_verifactu: boolean; exemption_reason_code: string | null; exemption_reason_detail: string | null; verifactu_effective_at: string | null; auto_renew_mode: AutoRenewMode | null; defaults: { tax_regime_code: string | null; operation_qualification_code: string | null; exemption_reason_code: string | null; }; updated_at: string | null; } ``` ## 19. PUT ```ts export interface UpdateCompanyVerifactuProfileRequestDTO { profile_status: CompanyVerifactuProfileStatus; exemption_reason_code?: string | null; exemption_reason_detail?: string | null; verifactu_effective_at?: string | null; auto_renew_mode?: AutoRenewMode | null; defaults: { tax_regime_code?: string | null; operation_qualification_code?: string | null; exemption_reason_code?: string | null; }; } ``` Debe ser idempotente para el mismo payload. ## 20. Errores ```txt 400 → combinación estructural inválida 403 → falta de autorización 409 → conflicto de concurrencia si se activa 422 → validación de negocio si encaja con la convención del módulo ``` No usar 404 por ausencia inicial de perfil. ## 21. Persistencia Tabla conceptual: ```txt company_verifactu_profiles ``` Campos: ```txt id company_id profile_status exemption_reason_code exemption_reason_detail verifactu_effective_at auto_renew_mode default_tax_regime_code default_operation_qualification_code default_exemption_reason_code created_at updated_at ``` Restricciones: ```txt company_id NOT NULL profile_status NOT NULL UNIQUE(company_id) ``` Una fila activa por empresa. DDL operativo vigente: ```txt docs/customer-invoices/sql/add-company-verifactu-profiles.sql docs/customer-invoices/sql/add-issued-invoice-verifactu-snapshots.sql ``` ## 22. Concurrencia Se difiere en V1. La implementación actual no introduce `expected_updated_at` ni control optimista específico, porque el backend real no aporta hoy un patrón reutilizable de concurrency/versionado para este tipo de settings. ## 23. Rollout ```txt sin fila → not_configured ``` Orden recomendado: ```txt 1. ejecutar DDL de `company_verifactu_profiles` 2. ejecutar DDL de snapshots VeriFactu en `issued_invoices` 3. persistencia y GET seguro 4. PUT/configuración 5. UI Settings 6. uso de defaults en emisión 7. enforcement posterior ``` La ausencia de perfil no debe romper automáticamente la emisión actual durante transición. ## 24. Seguridad Nunca aceptar `companyId` del frontend. Implementado: - autenticación; - company context; - aislamiento por `companyId`. Permisos finos propuestos para una fase posterior: ```txt customer-invoices:verifactu-profile:read customer-invoices:verifactu-profile:update ``` Aún no se han activado guards de permisos específicos, porque el módulo `customer-invoices` no tenía previamente una infraestructura de permiso fino conectada en estas rutas. ## 25. Auditoría Deben ser auditables cambios en: ```txt profileStatus exemptionReasonCode verifactuEffectiveAt defaults ``` No se ha implementado auditoría adicional en esta fase. ## 26. Modelo conceptual del agregado ```ts export interface CompanyVerifactuProfileProps { companyId: UniqueID; profileStatus: CompanyVerifactuProfileStatus; exemptionReasonCode: string | null; exemptionReasonDetail: string | null; verifactuEffectiveAt: Date | null; autoRenewMode: AutoRenewMode | null; defaults: CompanyVerifactuDefaults; createdAt: Date; updatedAt: Date; } export class CompanyVerifactuProfile extends AggregateRoot { public static create( props: CompanyVerifactuProfileProps, ): Result; public reconfigure( props: CompanyVerifactuProfileReconfiguration, ): Result; } ``` Usar `import type` explícito en cabecera. ## 27. Catálogos pendientes No se consolidan todavía enums para: ```txt operationQualificationCode exemptionReasonCode ``` Se requiere una fase específica para definir: ```txt fuente normativa códigos labels compatibilidades versionado ``` ## 28. Integración en `IssuedInvoice` Implementado en Fase B: ```txt CompanyVerifactuProfile → ProformaIssueReadModelAssembler → resolución backend de clasificación/política → ProformaToIssuedInvoiceConverter → IssuedInvoice → persistencia física en issued_invoices ``` `IssuedInvoice` mantiene `taxRegime` como snapshot legacy existente y añade además: ```txt fiscalClassification verifactuPolicy ``` Decisiones reales: - `taxRegimeCode` se resuelve con precedencia `document -> customer -> company -> sin valor`. - `operationQualificationCode` se resuelve hoy desde defaults empresariales o `null`. - `exemptionReasonCode` se resuelve hoy desde defaults empresariales o `null`. - `verifactuPolicy.profileStatus` se persiste como señal de snapshot V2. - facturas antiguas sin esa señal cargan con `fiscalClassification = null` y `verifactuPolicy = null`. - no se inventan labels de `operationQualification` ni `exemptionReason`; se exponen como `null` hasta disponer de catálogo backend real. ## 29. Tests implementados Aggregate: - invariantes por cada estado; - combinaciones inválidas; - reconfigure. Repository: - una fila por company; - aislamiento entre companies. Use cases / controllers: - GET sin fila; - PUT válido/inválido; - autorización; Fase B: - precedencia `document -> customer -> company` para `taxRegimeCode`; - fallback legacy sin perfil; - validación de `IssuedInvoiceVerifactuPolicy`. ## 30. Criterios de aceptación 1. Existe una configuración VeriFactu por empresa. 2. Pertenece a `customer-invoices`. 3. Los cuatro estados están explícitamente modelados. 4. `obligated_without_verifactu` está soportado. 5. Los defaults se resuelven en backend. 6. Cálculo fiscal y clasificación VeriFactu están separados. 7. `GET` devuelve `not_configured` si no existe fila. 8. `PUT` crea o reconfigura el perfil de forma idempotente para el mismo payload. 9. `VerifactuRecord` sigue siendo operativo. 10. No se exponen secretos. 11. Empresas sin perfil se leen como `not_configured`. 12. `IssuedInvoice` persiste snapshots de clasificación/política sin reescribir facturas legacy. ## 31. Regla mental final ```txt Company → identidad/contexto CompanyVerifactuProfile → política/defaults empresariales IssuedInvoice → snapshot histórico efectivo VerifactuRecord → estado operativo AEAT integration → lifecycle futuro ```