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

674 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
```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. 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
```txt
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:
```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
```
SQL histórico/superado relacionado con la antigua propuesta de snapshots
VeriFactu en `issued_invoices`:
```txt
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
```txt
sin fila
→ not_configured
```
Orden recomendado:
```txt
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:
```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<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:
```txt
operationQualificationCode
exemptionReasonCode
```
Se requiere una fase específica para definir:
```txt
fuente normativa
códigos
labels
compatibilidades
versionado
```
## 28. Integración en `IssuedInvoice`
Estado documental vigente:
```txt
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:
```txt
verifactuPolicy
```
Sí forma parte del SSOT actual:
```txt
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
```txt
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
```