FactuGES_ERP/docs/customer-invoices/settings/verifactu-company-profile-backend-design.md

674 lines
15 KiB
Markdown
Raw Normal View 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:
```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
```