FactuGES_ERP/docs/customer-invoices/settings/verifactu-company-profile-backend-design.md
david e0ba5ef229 feat: add proforma duplicator service and related tests
- Implemented ProformaDuplicator service for duplicating proforma documents.
- Added tests for duplicating proformas, including scenarios for successful duplication, audit failure propagation, and payment term adaptations.
- Created ProformaStatusChanger service to handle status changes of proformas with corresponding tests.
- Developed ProformaArchiver service for unarchiving proformas and added tests to verify functionality.
- Introduced a test fixture for creating proforma instances for testing purposes.
2026-08-26 13:03:39 +02:00

15 KiB
Raw Permalink Blame History

Backend ERP – Perfil VeriFactu empresarial

1. Estado del documento

Estado: PARTIALLY SUPERSEDED

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 no incorpora actualmente verifactuPolicy como requisito vigente.
  • La integración vigente de IssuedInvoice con VeriFactu es:
    • snapshot fiscal efectivo cuando aplique;
    • relación operativa actual IssuedInvoice 1 -> 0..1 VerifactuRecord.
  • La dirección objetivo futura sigue siendo:
    • IssuedInvoice 1 -> 0..N VerifactuRecord para histórico operativo.
  • VerifactuRecord mantiene responsabilidad operativa y no absorbe el perfil empresarial.
  • El perfil no expone secretos ni passwords de certificados.

Alcance canónico actual de este documento:

  • diseño del agregado CompanyVerifactuProfile;
  • reglas de configuración empresarial VeriFactu;
  • API/settings de perfil empresarial;
  • interacción conceptual con emisión solo en lo relativo a clasificación fiscal.

Secciones superadas dentro de este mismo documento:

  • cualquier requisito que pida persistir IssuedInvoice.verifactuPolicy;
  • cualquier DDL o criterio de aceptación que reintroduzca verifactu_profile_status, uses_verifactu o verifactu_effective_at en issued_invoices;
  • cualquier lectura que presente IssuedInvoice 1 -> 0..N VerifactuRecord como comportamiento ya implantado.

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 fiscal al emitir una factura.

El diseño debe integrarse con el flujo actual:

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:

modules/customer-invoices

No debe vivir dentro de:

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

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:

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

exemptionReasonCode = null
exemptionReasonDetail = null
verifactuEffectiveAt = null
autoRenewMode = null

No debe producir defaults efectivos de clasificación VeriFactu.

exempt

Puede requerir:

exemptionReasonCode
exemptionReasonDetail

No permite activación VeriFactu.

obligated_without_verifactu

verifactuEffectiveAt = null
autoRenewMode = null

Puede tener defaults fiscales si son aplicables.

obligated_with_verifactu

Requiere:

verifactuEffectiveAt != null

autoRenewMode solo tiene sentido aquí.

6. Obligación y exención

V1:

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

export const AutoRenewMode = {
  Disabled: "disabled",
  Enabled: "enabled",
} as const;

export type AutoRenewMode =
  (typeof AutoRenewMode)[keyof typeof AutoRenewMode];

Campos:

verifactuEffectiveAt: Date | null;
autoRenewMode: AutoRenewMode | null;

No se modelan periodos históricos en V1.

8. Defaults empresariales

Solo defaults de clasificación fiscal:

export interface CompanyVerifactuDefaults {
  taxRegimeCode: string | null;
  operationQualificationCode: string | null;
  exemptionReasonCode: string | null;
}

No pertenecen:

taxPercentage
recPercentage
retentionPercentage
taxMode
tax breakdown

9. Precedencia de defaults

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

taxPercentage
recPercentage
retentionPercentage
taxable base
totals
tax breakdown

Clasificación VeriFactu

regimeCode
operationQualificationCode
exemptionReasonCode

No reutilizar ProformaFiscalData como sustituto de clasificación VeriFactu.

11. Snapshot de clasificación fiscal

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. Nota superada sobre política VeriFactu en IssuedInvoice

La propuesta anterior de añadir un snapshot verifactuPolicy a IssuedInvoice queda superada y no forma parte del SSOT vigente.

Estado actual:

  • IssuedInvoice no debe añadir ni persistir verifactuPolicy;
  • no debe reintroducir columnas legacy como verifactu_profile_status, uses_verifactu o verifactu_effective_at;
  • VerifactuRecord sigue siendo la pieza operativa para el lifecycle VeriFactu;
  • la cardinalidad vigente implementada es IssuedInvoice 1 -> 0..1 VerifactuRecord.

Dirección futura:

  • evolucionar a IssuedInvoice 1 -> 0..N VerifactuRecord cuando el histórico operativo entre en alcance real.

13. Resolución durante la emisión

CompanyVerifactuProfile
        │
        ▼
ProformaIssueReadModelAssembler
        │
        ├── carga proforma
        ├── carga cliente
        ├── carga perfil VeriFactu
        └── prepara contexto
                │
                ▼
proforma-to-issued-invoice-props-converter
        │
        ├── resuelve precedencia
        └── construye FiscalClassificationSnapshot
                │
                ▼
IssuedInvoice.create(...)

14. Responsabilidad de VerifactuRecord

IssuedInvoice:

  • snapshot documental e histórico;
  • clasificación fiscal aplicada;
  • relación documental con el contexto de emisión.

VerifactuRecord:

  • estado operativo;
  • URL;
  • QR;
  • UUID;
  • tipo de operación.
  • histórico operativo futuro.

No debe contener defaults empresariales ni obligación/exención.

Cardinalidad:

  • vigente: IssuedInvoice 1 -> 0..1 VerifactuRecord;
  • objetivo futuro: IssuedInvoice 1 -> 0..N VerifactuRecord.

15. Certificados

El perfil no contiene:

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:

enqueue
worker/jobs
retries
queries
hash chaining
cancelación
rectificación
payload AEAT
outbox

17. API

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:

profile_status = "not_configured"
configured = false

Response conceptual:

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

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

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:

company_verifactu_profiles

Campos:

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:

company_id NOT NULL
profile_status NOT NULL
UNIQUE(company_id)

Una fila activa por empresa.

DDL operativo vigente:

docs/customer-invoices/sql/add-company-verifactu-profiles.sql

SQL histórico/superado relacionado con la antigua propuesta de snapshots VeriFactu en issued_invoices:

docs/customer-invoices/sql/add-issued-invoice-verifactu-snapshots.sql
docs/customer-invoices/sql/drop-issued-invoice-verifactu-snapshot-columns.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

sin fila
→ not_configured

Orden recomendado:

1. ejecutar DDL de `company_verifactu_profiles`
2. persistencia y GET seguro
3. PUT/configuración
4. UI Settings
5. uso de defaults en emisión
6. 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:

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:

profileStatus
exemptionReasonCode
verifactuEffectiveAt
defaults

No se ha implementado auditoría adicional en esta fase.

26. Modelo conceptual del agregado

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<CompanyVerifactuProfileProps> {
  public static create(
    props: CompanyVerifactuProfileProps,
  ): Result<CompanyVerifactuProfile, Error>;

  public reconfigure(
    props: CompanyVerifactuProfileReconfiguration,
  ): Result<void, Error>;
}

Usar import type explícito en cabecera.

27. Catálogos pendientes

No se consolidan todavía enums para:

operationQualificationCode
exemptionReasonCode

Se requiere una fase específica para definir:

fuente normativa
códigos
labels
compatibilidades
versionado

28. Integración en IssuedInvoice

Estado documental vigente:

CompanyVerifactuProfile
→ configuración empresarial
→ resolución backend de clasificación fiscal cuando aplique
→ IssuedInvoice / emisión
→ VerifactuRecord operativo

IssuedInvoice mantiene taxRegime como snapshot legacy existente y, como evolución vigente de diseño, solo puede incorporar snapshots fiscales compatibles con el documento emitido.

No forma parte del SSOT actual:

verifactuPolicy

Sí forma parte del SSOT actual:

fiscalClassification

Decisiones vigentes:

  • 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.
  • no se inventan labels de operationQualification ni exemptionReason; se exponen como null hasta disponer de catálogo backend real.
  • la cardinalidad implementada entre IssuedInvoice y VerifactuRecord sigue siendo 1 -> 0..1.
  • la evolución futura deseada es 1 -> 0..N, pero no debe documentarse como runtime actual.

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;
  • separación explícita entre perfil empresarial y estado operativo VeriFactu.

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 no reintroduce verifactuPolicy como requisito vigente.
  13. La cardinalidad vigente es IssuedInvoice 1 -> 0..1 VerifactuRecord.
  14. La dirección futura deseada es IssuedInvoice 1 -> 0..N VerifactuRecord.

31. Regla mental final

Company
→ identidad/contexto

CompanyVerifactuProfile
→ política/defaults empresariales

IssuedInvoice
→ snapshot fiscal histórico efectivo cuando aplique
→ relación actual 1 -> 0..1 con VerifactuRecord

VerifactuRecord
→ estado operativo
→ futuro histórico 0..N

AEAT integration
→ lifecycle futuro