- 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.
674 lines
15 KiB
Markdown
674 lines
15 KiB
Markdown
# 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
|
||
```
|