3542 lines
47 KiB
Markdown
3542 lines
47 KiB
Markdown
|
|
# DOCUMENTO DE DISEÑO — `recurring-proformas`
|
|||
|
|
|
|||
|
|
## 1. Objetivo
|
|||
|
|
|
|||
|
|
Diseñar la característica **Proformas recurrentes** del ERP.
|
|||
|
|
|
|||
|
|
Una `RecurringProforma` representa una plantilla comercial programada que genera periódicamente nuevas `Proforma`.
|
|||
|
|
|
|||
|
|
La característica NO genera directamente `IssuedInvoice`.
|
|||
|
|
|
|||
|
|
Flujo obligatorio:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecurringProforma
|
|||
|
|
↓
|
|||
|
|
RecurringProformaOccurrence
|
|||
|
|
↓
|
|||
|
|
Proforma
|
|||
|
|
↓ flujo normal existente
|
|||
|
|
IssuedInvoice
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
La generación recurrente termina en la creación de una `Proforma`.
|
|||
|
|
|
|||
|
|
A partir de ese momento, la Proforma sigue exactamente el mismo ciclo de vida que una Proforma creada manualmente.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. Alcance V1
|
|||
|
|
|
|||
|
|
V1 incluye:
|
|||
|
|
|
|||
|
|
- creación de plantillas recurrentes;
|
|||
|
|
- edición;
|
|||
|
|
- activación explícita;
|
|||
|
|
- pausa y reanudación;
|
|||
|
|
- finalización;
|
|||
|
|
- archivado;
|
|||
|
|
- calendario recurrente;
|
|||
|
|
- generación automática;
|
|||
|
|
- generación manual anticipada de la próxima ocurrencia;
|
|||
|
|
- historial de ocurrencias;
|
|||
|
|
- retry de ocurrencias fallidas;
|
|||
|
|
- recuperación de ejecuciones abandonadas;
|
|||
|
|
- creación desde una Proforma existente;
|
|||
|
|
- duplicación de una recurrencia;
|
|||
|
|
- herencia dinámica de determinadas configuraciones;
|
|||
|
|
- idempotencia;
|
|||
|
|
- locking transaccional;
|
|||
|
|
- auditoría;
|
|||
|
|
- frontend completo de gestión.
|
|||
|
|
|
|||
|
|
V1 NO incluye:
|
|||
|
|
|
|||
|
|
- emisión automática de `IssuedInvoice`;
|
|||
|
|
- envío automático a VeriFactu;
|
|||
|
|
- `Subscription`;
|
|||
|
|
- contratos;
|
|||
|
|
- trials;
|
|||
|
|
- renovaciones;
|
|||
|
|
- facturación por consumo;
|
|||
|
|
- prorrateos;
|
|||
|
|
- cobro automático;
|
|||
|
|
- generación extraordinaria fuera de calendario;
|
|||
|
|
- `skip` de ocurrencias;
|
|||
|
|
- expresiones cron configurables;
|
|||
|
|
- calendarios laborales;
|
|||
|
|
- festivos;
|
|||
|
|
- reglas avanzadas de períodos;
|
|||
|
|
- variables dinámicas en textos;
|
|||
|
|
- productos o artículos;
|
|||
|
|
- permisos específicos de recurrencias;
|
|||
|
|
- timezone por recurrencia;
|
|||
|
|
- historial completo de intentos de ejecución.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Ubicación funcional
|
|||
|
|
|
|||
|
|
`RecurringProforma` pertenece al módulo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
customer-invoices
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
como agregado independiente.
|
|||
|
|
|
|||
|
|
Estructura conceptual:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
customer-invoices
|
|||
|
|
├─ proformas
|
|||
|
|
├─ issued-invoices
|
|||
|
|
└─ recurring-proformas
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No se crea un módulo genérico:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
recurring-billing
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
porque actualmente solo existe un caso funcional real: generar Proformas recurrentes.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Principio fundamental
|
|||
|
|
|
|||
|
|
`RecurringProforma` no es una Proforma futura parcialmente creada.
|
|||
|
|
|
|||
|
|
Representa:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
configuración comercial futura
|
|||
|
|
+
|
|||
|
|
calendario
|
|||
|
|
+
|
|||
|
|
estado de automatización
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Cada ocurrencia genera una `Proforma` completamente independiente.
|
|||
|
|
|
|||
|
|
Las Proformas previamente generadas nunca se modifican porque cambie posteriormente la `RecurringProforma`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Agregados y objetos principales
|
|||
|
|
|
|||
|
|
El subdominio contiene:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecurringProforma
|
|||
|
|
RecurringProformaItem
|
|||
|
|
RecurringProformaItems
|
|||
|
|
RecurrenceSchedule
|
|||
|
|
RecurringProformaOccurrence
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Aggregate Roots
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecurringProforma
|
|||
|
|
RecurringProformaOccurrence
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`RecurringProformaOccurrence` es Aggregate Root independiente.
|
|||
|
|
|
|||
|
|
No se carga como colección dentro de `RecurringProforma`.
|
|||
|
|
|
|||
|
|
Motivos:
|
|||
|
|
|
|||
|
|
- tiene identidad propia;
|
|||
|
|
- tiene estados propios;
|
|||
|
|
- tiene lifecycle propio;
|
|||
|
|
- tiene retry;
|
|||
|
|
- puede crecer indefinidamente;
|
|||
|
|
- necesita consultas y paginación independientes;
|
|||
|
|
- participa directamente en idempotencia y recuperación.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 6. `RecurringProforma`
|
|||
|
|
|
|||
|
|
## 6.1 Identidad
|
|||
|
|
|
|||
|
|
Toda `RecurringProforma` tiene:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
id: UniqueID
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`UniqueID` contiene un UUID, siguiendo la convención general de entidades del ERP.
|
|||
|
|
|
|||
|
|
El UUID es la identidad única técnica.
|
|||
|
|
|
|||
|
|
No existe numeración documental tipo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RP-0001
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
en V1.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6.2 Campos internos de identificación
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
name
|
|||
|
|
description
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### `name`
|
|||
|
|
|
|||
|
|
Obligatorio.
|
|||
|
|
|
|||
|
|
Reglas:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
trimmed
|
|||
|
|
no vacío
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No es único.
|
|||
|
|
|
|||
|
|
Se permiten nombres duplicados dentro de una misma compañía.
|
|||
|
|
|
|||
|
|
### `description`
|
|||
|
|
|
|||
|
|
Opcional.
|
|||
|
|
|
|||
|
|
Uso exclusivamente interno.
|
|||
|
|
|
|||
|
|
No se copia a las Proformas generadas.
|
|||
|
|
|
|||
|
|
Debe diferenciarse de cualquier descripción comercial existente en `Proforma`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6.3 Tenant
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
companyId: UniqueID
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Toda `RecurringProforma` pertenece a una compañía.
|
|||
|
|
|
|||
|
|
Es company-scoped.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6.4 Customer
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
customerId?: UniqueID
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
En `draft` puede no existir.
|
|||
|
|
|
|||
|
|
En:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
active
|
|||
|
|
paused
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
es obligatorio.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 7. Estados de `RecurringProforma`
|
|||
|
|
|
|||
|
|
Estados V1:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
draft
|
|||
|
|
active
|
|||
|
|
paused
|
|||
|
|
completed
|
|||
|
|
archived
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## `draft`
|
|||
|
|
|
|||
|
|
Representa una plantilla en preparación.
|
|||
|
|
|
|||
|
|
Puede estar incompleta.
|
|||
|
|
|
|||
|
|
No participa en el scheduler.
|
|||
|
|
|
|||
|
|
No genera occurrences.
|
|||
|
|
|
|||
|
|
Puede editarse libremente dentro de las invariantes mínimas del draft.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## `active`
|
|||
|
|
|
|||
|
|
Participa en el scheduler.
|
|||
|
|
|
|||
|
|
Debe encontrarse permanentemente en un estado válido para generar Proformas.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## `paused`
|
|||
|
|
|
|||
|
|
Suspende completamente la generación.
|
|||
|
|
|
|||
|
|
No permite:
|
|||
|
|
|
|||
|
|
- generación automática;
|
|||
|
|
- `Generate next occurrence`.
|
|||
|
|
|
|||
|
|
El calendario no se modifica durante la pausa.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## `completed`
|
|||
|
|
|
|||
|
|
Recurrencia finalizada.
|
|||
|
|
|
|||
|
|
No genera más occurrences.
|
|||
|
|
|
|||
|
|
Es inmutable.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## `archived`
|
|||
|
|
|
|||
|
|
Estado histórico final.
|
|||
|
|
|
|||
|
|
Solo accesible desde `completed`.
|
|||
|
|
|
|||
|
|
Es inmutable.
|
|||
|
|
|
|||
|
|
No puede restaurarse en V1.
|
|||
|
|
|
|||
|
|
Para reutilizarla debe duplicarse.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 8. Transiciones de estado
|
|||
|
|
|
|||
|
|
Permitidas:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
draft → active
|
|||
|
|
draft → completed
|
|||
|
|
|
|||
|
|
active → paused
|
|||
|
|
active → completed
|
|||
|
|
|
|||
|
|
paused → active
|
|||
|
|
paused → completed
|
|||
|
|
|
|||
|
|
completed → archived
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Prohibidas:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
completed → active
|
|||
|
|
completed → paused
|
|||
|
|
completed → draft
|
|||
|
|
|
|||
|
|
archived → *
|
|||
|
|
|
|||
|
|
draft → archived
|
|||
|
|
active → archived
|
|||
|
|
paused → archived
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`completed` y `archived` son inmutables por `PATCH`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 9. Draft incompleto
|
|||
|
|
|
|||
|
|
`RecurringProforma.create()` debe aplicar solamente invariantes mínimas.
|
|||
|
|
|
|||
|
|
Un draft puede crearse únicamente con:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
name
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Pueden faltar:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
customer
|
|||
|
|
items
|
|||
|
|
schedule
|
|||
|
|
campos comerciales
|
|||
|
|
paymentTerm
|
|||
|
|
paymentMethod
|
|||
|
|
language
|
|||
|
|
currency
|
|||
|
|
series
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
La barrera fuerte está en:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
draft → active
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
mediante activation readiness.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 10. Contenido comercial
|
|||
|
|
|
|||
|
|
`RecurringProforma` debe replicar los campos comerciales no fiscales actualmente necesarios para crear una `Proforma`.
|
|||
|
|
|
|||
|
|
Antes de implementación debe auditarse el agregado y persistencia real de `Proforma`.
|
|||
|
|
|
|||
|
|
Solo se replicarán campos existentes y necesarios.
|
|||
|
|
|
|||
|
|
No se inventará un modelo comercial paralelo.
|
|||
|
|
|
|||
|
|
Se incluyen conceptualmente:
|
|||
|
|
|
|||
|
|
- descripción comercial;
|
|||
|
|
- notas;
|
|||
|
|
- observaciones;
|
|||
|
|
- términos;
|
|||
|
|
- textos comerciales;
|
|||
|
|
- descuentos globales;
|
|||
|
|
- líneas;
|
|||
|
|
- condiciones de pago;
|
|||
|
|
- forma de pago;
|
|||
|
|
- idioma;
|
|||
|
|
- moneda;
|
|||
|
|
- series documentales;
|
|||
|
|
- cualquier otro campo comercial no fiscal realmente usado por `Proforma`.
|
|||
|
|
|
|||
|
|
No se replican:
|
|||
|
|
|
|||
|
|
- impuestos materializados;
|
|||
|
|
- tax breakdown;
|
|||
|
|
- totales derivados;
|
|||
|
|
- estado documental;
|
|||
|
|
- referencia documental;
|
|||
|
|
- fechas de Proforma;
|
|||
|
|
- datos de emisión;
|
|||
|
|
- VeriFactu.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 11. `RecurringProformaItem`
|
|||
|
|
|
|||
|
|
Entidad independiente de `ProformaItem`.
|
|||
|
|
|
|||
|
|
No se reutiliza directamente `ProformaItem`.
|
|||
|
|
|
|||
|
|
Campos V1:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
id: UniqueID
|
|||
|
|
description
|
|||
|
|
quantity
|
|||
|
|
unitPrice
|
|||
|
|
discount
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`discount` utiliza exactamente el mismo modelo funcional de descuento que el `ProformaItem` actual.
|
|||
|
|
|
|||
|
|
No contiene:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
taxes
|
|||
|
|
taxDefinitionId
|
|||
|
|
productId
|
|||
|
|
articleId
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Actualmente el ERP no dispone de productos ni artículos.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 12. `RecurringProformaItems`
|
|||
|
|
|
|||
|
|
Colección basada en:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Collection<RecurringProformaItem>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Actualización:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
items omitido
|
|||
|
|
→ no modificar
|
|||
|
|
|
|||
|
|
items enviado
|
|||
|
|
→ reemplazo completo
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No se introduce actualización granular:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
create/update/delete
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
por línea en V1.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 13. Items vacíos
|
|||
|
|
|
|||
|
|
En `draft`:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
items = []
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
es válido.
|
|||
|
|
|
|||
|
|
En:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
active
|
|||
|
|
paused
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
no se puede guardar una colección vacía.
|
|||
|
|
|
|||
|
|
Activation readiness bloquea un draft vacío mediante:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
missing_items
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 14. Fiscalidad
|
|||
|
|
|
|||
|
|
`RecurringProforma` no almacena fiscalidad materializada.
|
|||
|
|
|
|||
|
|
Cada ocurrencia debe resolver nuevamente la fiscalidad vigente.
|
|||
|
|
|
|||
|
|
Flujo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecurringProforma
|
|||
|
|
↓
|
|||
|
|
Customer actual
|
|||
|
|
↓
|
|||
|
|
contexto fiscal actual
|
|||
|
|
↓
|
|||
|
|
creación normal de Proforma
|
|||
|
|
↓
|
|||
|
|
impuestos
|
|||
|
|
↓
|
|||
|
|
totales
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
La recurrencia no implementa:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecurringProformaTaxCalculator
|
|||
|
|
RecurringProformaTotalsCalculator
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
La lógica fiscal debe reutilizar la misma lógica utilizada por una Proforma creada manualmente.
|
|||
|
|
|
|||
|
|
Si la situación fiscal vigente impide construir una Proforma válida:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Occurrence → failed
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No se crea una Proforma parcial.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 15. Recipient
|
|||
|
|
|
|||
|
|
En cada ocurrencia se obtiene nuevamente el Customer actual.
|
|||
|
|
|
|||
|
|
El recipient snapshot de la nueva Proforma debe construirse usando los datos actuales del Customer.
|
|||
|
|
|
|||
|
|
Cambios posteriores del Customer nunca modifican Proformas previamente generadas.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 16. Cambio de Customer
|
|||
|
|
|
|||
|
|
Permitido en:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
draft
|
|||
|
|
active
|
|||
|
|
paused
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No permitido en:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
completed
|
|||
|
|
archived
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
En `active` y `paused`:
|
|||
|
|
|
|||
|
|
- el cambio afecta solo a futuras occurrences;
|
|||
|
|
- las Proformas existentes permanecen intactas;
|
|||
|
|
- se aplican las mismas reglas de compatibilidad que en cambio de Customer de Proforma;
|
|||
|
|
- idioma o moneda explícitos se respetan;
|
|||
|
|
- incompatibilidades reales bloquean el cambio;
|
|||
|
|
- la fiscalidad futura se recalcula;
|
|||
|
|
- el recipient futuro se obtiene del nuevo Customer.
|
|||
|
|
|
|||
|
|
`active` y `paused` nunca pueden quedar con:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
customerId = null
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 17. Customer inactivo
|
|||
|
|
|
|||
|
|
Si el Customer pasa a `inactive`:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Occurrence → failed
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
La recurrencia no pasa automáticamente a `paused`.
|
|||
|
|
|
|||
|
|
La occurrence fallida bloquea las siguientes.
|
|||
|
|
|
|||
|
|
Opciones de resolución:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
reactivar Customer
|
|||
|
|
→ retry
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
o:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
cambiar Customer
|
|||
|
|
→ retry
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 18. Campos heredables
|
|||
|
|
|
|||
|
|
Los siguientes campos son opcionales:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
paymentTermId
|
|||
|
|
paymentMethodId
|
|||
|
|
language
|
|||
|
|
currency
|
|||
|
|
proformaSeriesId
|
|||
|
|
issuedInvoiceSeriesId
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Semántica:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
null
|
|||
|
|
→ herencia dinámica
|
|||
|
|
|
|||
|
|
valor explícito
|
|||
|
|
→ override autoritativo
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Los valores heredados se resuelven en cada occurrence.
|
|||
|
|
|
|||
|
|
Nunca se escriben de vuelta en `RecurringProforma`.
|
|||
|
|
|
|||
|
|
Ejemplo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecurringProforma.currency = null
|
|||
|
|
Customer.currency = EUR
|
|||
|
|
|
|||
|
|
Occurrence
|
|||
|
|
→ Proforma.currency = EUR
|
|||
|
|
|
|||
|
|
RecurringProforma.currency continúa = null
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 19. Overrides explícitos
|
|||
|
|
|
|||
|
|
Si el usuario selecciona manualmente un valor, aunque coincida con el valor heredado actual:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
→ pasa a ser override
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Si selecciona nuevamente:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Heredar
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
se persiste:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
null
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 20. PaymentMethod y PaymentTerm
|
|||
|
|
|
|||
|
|
Si existe override explícito y el recurso pasa a estar:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
inactive
|
|||
|
|
unavailable
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
la occurrence falla.
|
|||
|
|
|
|||
|
|
No existe fallback automático.
|
|||
|
|
|
|||
|
|
El usuario debe:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
reactivar
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
o:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
cambiar el override
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
y posteriormente:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
retry
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Si el valor es heredado, se utiliza la resolución normal vigente de creación de Proforma.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 21. Idioma y moneda
|
|||
|
|
|
|||
|
|
También pueden heredarse dinámicamente.
|
|||
|
|
|
|||
|
|
Override explícito:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
→ autoritativo
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Valor heredado:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
→ se resuelve nuevamente en cada occurrence
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Cualquier incompatibilidad que impida generar una Proforma válida provoca:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Occurrence → failed
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 22. Series documentales
|
|||
|
|
|
|||
|
|
`RecurringProforma` puede guardar opcionalmente:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
proformaSeriesId?: UniqueID
|
|||
|
|
issuedInvoiceSeriesId?: UniqueID
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
La semántica debe replicar la de `Proforma`.
|
|||
|
|
|
|||
|
|
### Serie explícita
|
|||
|
|
|
|||
|
|
Si se configura explícitamente:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
→ debe utilizarse
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Si deja de estar:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
activa
|
|||
|
|
vigente
|
|||
|
|
disponible
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
la occurrence falla.
|
|||
|
|
|
|||
|
|
No existe fallback silencioso.
|
|||
|
|
|
|||
|
|
### Serie heredada
|
|||
|
|
|
|||
|
|
Si es `null`:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
→ usar resolución/default vigente
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Issued invoice series
|
|||
|
|
|
|||
|
|
`issuedInvoiceSeriesId` solamente se copia como preferencia/configuración a la Proforma creada.
|
|||
|
|
|
|||
|
|
No implica emisión automática ni reserva de numeración.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 23. Regla general explícito/heredado
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
valor explícito
|
|||
|
|
→ autoritativo
|
|||
|
|
→ si deja de ser válido, failed
|
|||
|
|
|
|||
|
|
null
|
|||
|
|
→ resolver dinámicamente
|
|||
|
|
→ no persistir resultado en RecurringProforma
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 24. `RecurrenceSchedule`
|
|||
|
|
|
|||
|
|
Value Object inmutable.
|
|||
|
|
|
|||
|
|
Campos:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
startDate
|
|||
|
|
endDate?
|
|||
|
|
unit
|
|||
|
|
interval
|
|||
|
|
monthlyRule?
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`unit`:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
week
|
|||
|
|
month
|
|||
|
|
year
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`interval`:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
integer > 0
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`monthlyRule`:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
same_day
|
|||
|
|
last_day_of_month
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 25. Invariantes de `RecurrenceSchedule`
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
startDate obligatorio
|
|||
|
|
|
|||
|
|
interval > 0
|
|||
|
|
|
|||
|
|
endDate == null
|
|||
|
|
OR
|
|||
|
|
endDate >= startDate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Para:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
unit = month
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`monthlyRule` es obligatorio.
|
|||
|
|
|
|||
|
|
Para:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
unit = week
|
|||
|
|
unit = year
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`monthlyRule` debe ser `null`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 26. `startDate`
|
|||
|
|
|
|||
|
|
`startDate` representa siempre:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
la primera occurrence
|
|||
|
|
+
|
|||
|
|
el ancla del calendario
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No existe:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
firstOccurrenceDate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
separada.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 27. Recurrencia semanal
|
|||
|
|
|
|||
|
|
Ejemplo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
startDate = 07/09/2026
|
|||
|
|
unit = week
|
|||
|
|
interval = 2
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Resultado:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
07/09
|
|||
|
|
21/09
|
|||
|
|
05/10
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
El día de la semana deriva de `startDate`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 28. Recurrencia mensual `same_day`
|
|||
|
|
|
|||
|
|
Ejemplo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
startDate = 31/01/2027
|
|||
|
|
unit = month
|
|||
|
|
interval = 1
|
|||
|
|
monthlyRule = same_day
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Resultado:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
31/01
|
|||
|
|
28/02
|
|||
|
|
31/03
|
|||
|
|
30/04
|
|||
|
|
31/05
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Si el día ancla no existe se usa el último día válido del mes.
|
|||
|
|
|
|||
|
|
El ancla original nunca se pierde.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 29. Recurrencia mensual `last_day_of_month`
|
|||
|
|
|
|||
|
|
`startDate` sigue siendo siempre la primera occurrence exacta.
|
|||
|
|
|
|||
|
|
Ejemplo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
startDate = 15/01/2027
|
|||
|
|
interval = 1
|
|||
|
|
monthlyRule = last_day_of_month
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Resultado:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
15/01
|
|||
|
|
28/02
|
|||
|
|
31/03
|
|||
|
|
30/04
|
|||
|
|
31/05
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Con:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
interval = 3
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
resultado:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
15/01
|
|||
|
|
30/04
|
|||
|
|
31/07
|
|||
|
|
31/10
|
|||
|
|
31/01
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 30. Recurrencia anual
|
|||
|
|
|
|||
|
|
El ancla se conserva desde `startDate`.
|
|||
|
|
|
|||
|
|
Caso:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
startDate = 29/02/2028
|
|||
|
|
unit = year
|
|||
|
|
interval = 1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Resultado:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
29/02/2028
|
|||
|
|
28/02/2029
|
|||
|
|
28/02/2030
|
|||
|
|
28/02/2031
|
|||
|
|
29/02/2032
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Con `interval > 1` se aplica la misma regla sobre los años objetivo.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 31. Cálculo del calendario
|
|||
|
|
|
|||
|
|
No calcular:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
next = previous occurrence + interval
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
si eso puede perder el ancla.
|
|||
|
|
|
|||
|
|
El cálculo debe derivarse siempre del calendario original basado en:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
startDate
|
|||
|
|
+
|
|||
|
|
índice lógico de occurrence
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
o una implementación matemáticamente equivalente que conserve el ancla.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 32. `endDate`
|
|||
|
|
|
|||
|
|
Opcional.
|
|||
|
|
|
|||
|
|
Si es `null`:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
recurrencia indefinida
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Es inclusiva:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
scheduledDate <= endDate
|
|||
|
|
→ válida
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`endDate` no genera por sí misma una occurrence.
|
|||
|
|
|
|||
|
|
Solo limita el calendario.
|
|||
|
|
|
|||
|
|
Ejemplo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
startDate = 01/01
|
|||
|
|
interval = 1 month
|
|||
|
|
endDate = 15/03
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Genera:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
01/01
|
|||
|
|
01/02
|
|||
|
|
01/03
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No genera:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
15/03
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 33. Modificación de `endDate`
|
|||
|
|
|
|||
|
|
Puede:
|
|||
|
|
|
|||
|
|
- añadirse;
|
|||
|
|
- adelantarse;
|
|||
|
|
- retrasarse.
|
|||
|
|
|
|||
|
|
No puede quedar antes de una `scheduledDate` ya materializada como `generated`.
|
|||
|
|
|
|||
|
|
Si al adelantarla ya no quedan occurrences futuras y no existen incidencias:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
→ completed
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 34. Finalización automática
|
|||
|
|
|
|||
|
|
Al generar la última occurrence válida:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
next calculated date > endDate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
entonces:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
nextOccurrenceDate = null
|
|||
|
|
status = completed
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
si no existen incidencias pendientes.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 35. EndDate pasada durante activación
|
|||
|
|
|
|||
|
|
Es válido activar una recurrencia cuyo rango completo esté en el pasado.
|
|||
|
|
|
|||
|
|
Si existen occurrences válidas dentro del rango:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
→ se procesan todas
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Tras resolverlas correctamente:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
→ completed
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Si una falla:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
→ permanece active
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
hasta resolverla.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 36. `nextOccurrenceDate`
|
|||
|
|
|
|||
|
|
Estado interno derivado y persistido en `RecurringProforma`.
|
|||
|
|
|
|||
|
|
No pertenece a `RecurrenceSchedule`.
|
|||
|
|
|
|||
|
|
No es editable desde API ni frontend.
|
|||
|
|
|
|||
|
|
Semántica:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
draft
|
|||
|
|
→ null
|
|||
|
|
|
|||
|
|
active
|
|||
|
|
→ próxima occurrence lógica
|
|||
|
|
|
|||
|
|
paused
|
|||
|
|
→ conserva el mismo valor
|
|||
|
|
|
|||
|
|
failed occurrence
|
|||
|
|
→ continúa apuntando a esa scheduledDate
|
|||
|
|
|
|||
|
|
completed
|
|||
|
|
→ null
|
|||
|
|
|
|||
|
|
archived
|
|||
|
|
→ null
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Al activar:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
nextOccurrenceDate = startDate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 37. Hora y timezone
|
|||
|
|
|
|||
|
|
Las recurrencias trabajan con:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
DATE
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
no con hora configurable.
|
|||
|
|
|
|||
|
|
No existe timezone por `RecurringProforma`.
|
|||
|
|
|
|||
|
|
El scheduler determina `today` usando:
|
|||
|
|
|
|||
|
|
1. timezone de la compañía, si existe en el ERP;
|
|||
|
|
2. en su defecto, política global del sistema.
|
|||
|
|
|
|||
|
|
Si cambia la timezone:
|
|||
|
|
|
|||
|
|
- no se modifican fechas del calendario;
|
|||
|
|
- no se recalcula `nextOccurrenceDate`;
|
|||
|
|
- solo cambia la evaluación de cuál es el día actual operativo.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 38. Scheduler
|
|||
|
|
|
|||
|
|
La frecuencia exacta del scheduler es configuración de infraestructura.
|
|||
|
|
|
|||
|
|
No pertenece al dominio.
|
|||
|
|
|
|||
|
|
Debe ejecutarse periódicamente y procesar:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
status = active
|
|||
|
|
AND nextOccurrenceDate <= today
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Debe recuperar occurrences atrasadas.
|
|||
|
|
|
|||
|
|
No debe depender exclusivamente de:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
nextOccurrenceDate == today
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 39. Ocurrencias atrasadas
|
|||
|
|
|
|||
|
|
Si existen varias fechas vencidas:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
→ procesar secuencialmente
|
|||
|
|
→ en orden cronológico
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Ejemplo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
enero
|
|||
|
|
febrero
|
|||
|
|
marzo
|
|||
|
|
abril
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Si marzo falla:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
enero → generated
|
|||
|
|
febrero → generated
|
|||
|
|
marzo → failed
|
|||
|
|
abril → no procesada
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 40. Pausa
|
|||
|
|
|
|||
|
|
`pause` suspende la ejecución.
|
|||
|
|
|
|||
|
|
No modifica el calendario.
|
|||
|
|
|
|||
|
|
Ejemplo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
nextOccurrenceDate = 01/10
|
|||
|
|
|
|||
|
|
pause
|
|||
|
|
resume en diciembre
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Después del resume:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
01/10
|
|||
|
|
01/11
|
|||
|
|
01/12
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
se procesan en orden.
|
|||
|
|
|
|||
|
|
Pausar no equivale a hacer `skip`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 41. `RecurringProformaOccurrence`
|
|||
|
|
|
|||
|
|
Aggregate Root independiente.
|
|||
|
|
|
|||
|
|
Campos V1:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
id: UniqueID
|
|||
|
|
companyId: UniqueID
|
|||
|
|
recurringProformaId: UniqueID
|
|||
|
|
|
|||
|
|
scheduledDate
|
|||
|
|
|
|||
|
|
status
|
|||
|
|
|
|||
|
|
attemptCount
|
|||
|
|
|
|||
|
|
lastAttemptAt?
|
|||
|
|
processingStartedAt?
|
|||
|
|
|
|||
|
|
lastErrorCode?
|
|||
|
|
lastErrorMessage?
|
|||
|
|
|
|||
|
|
proformaId?
|
|||
|
|
generatedAt?
|
|||
|
|
|
|||
|
|
createdAt
|
|||
|
|
updatedAt
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No contiene snapshots comerciales ni fiscales.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 42. Estados de occurrence
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
pending
|
|||
|
|
processing
|
|||
|
|
failed
|
|||
|
|
generated
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### `pending`
|
|||
|
|
|
|||
|
|
Occurrence identificada pero no procesada.
|
|||
|
|
|
|||
|
|
### `processing`
|
|||
|
|
|
|||
|
|
Ejecución en curso.
|
|||
|
|
|
|||
|
|
### `failed`
|
|||
|
|
|
|||
|
|
Último intento falló.
|
|||
|
|
|
|||
|
|
Bloquea occurrences posteriores.
|
|||
|
|
|
|||
|
|
### `generated`
|
|||
|
|
|
|||
|
|
Proforma creada correctamente.
|
|||
|
|
|
|||
|
|
Estado terminal.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 43. Transiciones de occurrence
|
|||
|
|
|
|||
|
|
Permitidas:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
pending → processing
|
|||
|
|
|
|||
|
|
processing → generated
|
|||
|
|
processing → failed
|
|||
|
|
|
|||
|
|
failed → processing
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`generated` no permite ninguna transición posterior.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 44. Identidad lógica de occurrence
|
|||
|
|
|
|||
|
|
Debe existir:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
UNIQUE(recurring_proforma_id, scheduled_date)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Esto garantiza que una fecha lógica de una recurrencia solo tenga una occurrence.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 45. Relación con Proforma
|
|||
|
|
|
|||
|
|
`RecurringProformaOccurrence` guarda:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
proformaId?: UniqueID
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Debe existir:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
UNIQUE(proforma_id)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Una Proforma no puede pertenecer a dos occurrences.
|
|||
|
|
|
|||
|
|
La Proforma NO contiene:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
recurringProformaId
|
|||
|
|
recurringOccurrenceId
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
La dirección de la relación es exclusivamente:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Occurrence → Proforma
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 46. Proforma vinculada
|
|||
|
|
|
|||
|
|
En V1 una Proforma vinculada a una occurrence no puede eliminarse físicamente.
|
|||
|
|
|
|||
|
|
La FK debe proteger la trazabilidad.
|
|||
|
|
|
|||
|
|
No usar:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
ON DELETE SET NULL
|
|||
|
|
ON DELETE CASCADE
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
sobre `proforma_id`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 47. Ciclo posterior de la Proforma
|
|||
|
|
|
|||
|
|
Una vez:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Occurrence.status = generated
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
la occurrence no sigue el lifecycle de la Proforma.
|
|||
|
|
|
|||
|
|
La Proforma puede posteriormente:
|
|||
|
|
|
|||
|
|
- editarse;
|
|||
|
|
- cambiar de estado;
|
|||
|
|
- archivarse;
|
|||
|
|
- emitirse.
|
|||
|
|
|
|||
|
|
La occurrence continúa:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
generated
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 48. IssuedInvoice en historial
|
|||
|
|
|
|||
|
|
`RecurringProformaOccurrence` NO guarda:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
issuedInvoiceId
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
El historial puede resolver como dato de lectura si la Proforma fue posteriormente emitida y qué `IssuedInvoice` resultó.
|
|||
|
|
|
|||
|
|
Es información derivada, no persistida en occurrence.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 49. Creación lazy de occurrences
|
|||
|
|
|
|||
|
|
No se pre-generan occurrences futuras.
|
|||
|
|
|
|||
|
|
Flujo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecurringProforma.nextOccurrenceDate
|
|||
|
|
↓
|
|||
|
|
scheduler / generate next
|
|||
|
|
↓
|
|||
|
|
crear o recuperar occurrence
|
|||
|
|
↓
|
|||
|
|
pending
|
|||
|
|
↓
|
|||
|
|
processing
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 50. `attemptCount`
|
|||
|
|
|
|||
|
|
Cada intento real incrementa:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
attemptCount += 1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
y actualiza:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
lastAttemptAt
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Incluye:
|
|||
|
|
|
|||
|
|
- primer intento;
|
|||
|
|
- retry manual;
|
|||
|
|
- recuperación de `processing` abandonado.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 51. Fallos
|
|||
|
|
|
|||
|
|
Al fallar:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
status = failed
|
|||
|
|
lastErrorCode = ...
|
|||
|
|
lastErrorMessage = ...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No se guarda historial completo de intentos.
|
|||
|
|
|
|||
|
|
No existe:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecurringProformaOccurrenceAttempt
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
en V1.
|
|||
|
|
|
|||
|
|
Al generar finalmente correctamente:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
status = generated
|
|||
|
|
lastErrorCode = null
|
|||
|
|
lastErrorMessage = null
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
La auditoría conserva los eventos funcionales anteriores.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 52. Fallo secuencial
|
|||
|
|
|
|||
|
|
Una occurrence `failed`:
|
|||
|
|
|
|||
|
|
- no pausa formalmente la `RecurringProforma`;
|
|||
|
|
- bloquea occurrences posteriores;
|
|||
|
|
- bloquea `complete`;
|
|||
|
|
- requiere `retry`.
|
|||
|
|
|
|||
|
|
No existe `skip` en V1.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 53. Retry
|
|||
|
|
|
|||
|
|
Solo aplicable a:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
status = failed
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Utiliza la configuración actual de `RecurringProforma`.
|
|||
|
|
|
|||
|
|
No utiliza un snapshot de la configuración que existía durante el intento anterior.
|
|||
|
|
|
|||
|
|
Ejemplo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
PaymentMethod A inactive
|
|||
|
|
→ failed
|
|||
|
|
|
|||
|
|
usuario cambia a PaymentMethod B
|
|||
|
|
|
|||
|
|
retry
|
|||
|
|
→ usa B
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Se reutiliza la misma occurrence.
|
|||
|
|
|
|||
|
|
Nunca se crea otra para la misma `scheduledDate`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 54. `processing` abandonado
|
|||
|
|
|
|||
|
|
Una occurrence puede quedar en `processing` si el proceso cae.
|
|||
|
|
|
|||
|
|
Se utiliza:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
processingStartedAt
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Si supera un threshold configurado en infraestructura:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
→ puede recuperarse
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Debe reutilizarse la misma occurrence.
|
|||
|
|
|
|||
|
|
No introducir en dominio:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
workerId
|
|||
|
|
heartbeat
|
|||
|
|
leaseOwner
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
salvo necesidad técnica futura.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 55. Generación manual
|
|||
|
|
|
|||
|
|
Existe en V1:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Generate next occurrence
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Solo disponible en:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
active
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No disponible en:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
draft
|
|||
|
|
paused
|
|||
|
|
completed
|
|||
|
|
archived
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Procesa anticipadamente:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
nextOccurrenceDate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No crea una occurrence extraordinaria.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 56. Generación anticipada
|
|||
|
|
|
|||
|
|
Ejemplo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
nextOccurrenceDate = 01/10
|
|||
|
|
usuario genera el 25/09
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Resultado:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Occurrence.scheduledDate = 01/10
|
|||
|
|
Proforma.proformaDate = 25/09
|
|||
|
|
Proforma.operationDate = 25/09
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
La siguiente fecha del calendario se calcula desde:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
scheduledDate / calendario original
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
no desde la fecha efectiva de generación.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 57. Fechas de Proforma
|
|||
|
|
|
|||
|
|
En V1:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
proformaDate = generationDate
|
|||
|
|
operationDate = generationDate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Después de generarse, el usuario puede modificar `operationDate` mediante el flujo normal de edición de Proforma.
|
|||
|
|
|
|||
|
|
No se implementan reglas como:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
último día del mes anterior
|
|||
|
|
inicio del período
|
|||
|
|
offset de N días
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 58. Generación anticipada fallida
|
|||
|
|
|
|||
|
|
Se comporta exactamente igual que una occurrence automática fallida:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
status = failed
|
|||
|
|
nextOccurrenceDate no avanza
|
|||
|
|
bloquea posteriores
|
|||
|
|
requiere retry
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 59. Activación readiness
|
|||
|
|
|
|||
|
|
La transición:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
draft → active
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
requiere validación previa.
|
|||
|
|
|
|||
|
|
Endpoint:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
GET /recurring-proformas/:recurring_proforma_id/activation-readiness
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Contrato conceptual:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"can_activate": false,
|
|||
|
|
"blockers": [
|
|||
|
|
{
|
|||
|
|
"code": "missing_items"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 60. Activation blockers
|
|||
|
|
|
|||
|
|
Candidatos iniciales:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
missing_customer
|
|||
|
|
customer_inactive
|
|||
|
|
missing_items
|
|||
|
|
missing_description
|
|||
|
|
missing_schedule
|
|||
|
|
invalid_schedule
|
|||
|
|
|
|||
|
|
missing_payment_method
|
|||
|
|
payment_method_unavailable
|
|||
|
|
payment_method_inactive
|
|||
|
|
|
|||
|
|
missing_payment_term
|
|||
|
|
payment_term_unavailable
|
|||
|
|
payment_term_inactive
|
|||
|
|
|
|||
|
|
invalid_currency
|
|||
|
|
invalid_language
|
|||
|
|
|
|||
|
|
proforma_series_unavailable
|
|||
|
|
issued_invoice_series_unavailable
|
|||
|
|
|
|||
|
|
invalid_fiscal_configuration
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Los nombres definitivos deberán reutilizar los errores y códigos existentes cuando sea posible.
|
|||
|
|
|
|||
|
|
No duplicar semánticas ya existentes.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 61. Readiness vs ejecución
|
|||
|
|
|
|||
|
|
Readiness solamente valida:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
¿puede activarse ahora?
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Cada occurrence vuelve a resolver y validar los datos vivos.
|
|||
|
|
|
|||
|
|
Ejemplo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
activation readiness = OK
|
|||
|
|
↓
|
|||
|
|
PaymentMethod se desactiva
|
|||
|
|
↓
|
|||
|
|
siguiente occurrence = failed
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 62. Edición por estado
|
|||
|
|
|
|||
|
|
## `draft`
|
|||
|
|
|
|||
|
|
Editable:
|
|||
|
|
|
|||
|
|
- name;
|
|||
|
|
- description;
|
|||
|
|
- Customer;
|
|||
|
|
- items;
|
|||
|
|
- campos comerciales;
|
|||
|
|
- overrides;
|
|||
|
|
- calendario;
|
|||
|
|
- endDate.
|
|||
|
|
|
|||
|
|
Puede quedar incompleto.
|
|||
|
|
|
|||
|
|
## `active` / `paused`
|
|||
|
|
|
|||
|
|
Editable:
|
|||
|
|
|
|||
|
|
- name;
|
|||
|
|
- description;
|
|||
|
|
- Customer;
|
|||
|
|
- items;
|
|||
|
|
- campos comerciales;
|
|||
|
|
- overrides;
|
|||
|
|
- endDate.
|
|||
|
|
|
|||
|
|
Todo PATCH debe dejar la recurrencia en un estado generable.
|
|||
|
|
|
|||
|
|
No puede eliminar:
|
|||
|
|
|
|||
|
|
- Customer;
|
|||
|
|
- todas las líneas;
|
|||
|
|
- schedule.
|
|||
|
|
|
|||
|
|
## `completed` / `archived`
|
|||
|
|
|
|||
|
|
Completamente inmutables.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 63. Edición del calendario
|
|||
|
|
|
|||
|
|
Mientras no exista ninguna occurrence:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
startDate
|
|||
|
|
unit
|
|||
|
|
interval
|
|||
|
|
monthlyRule
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
pueden modificarse.
|
|||
|
|
|
|||
|
|
Una vez exista historial:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
startDate
|
|||
|
|
unit
|
|||
|
|
interval
|
|||
|
|
monthlyRule
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
quedan bloqueados.
|
|||
|
|
|
|||
|
|
`endDate` sigue siendo modificable según sus reglas propias.
|
|||
|
|
|
|||
|
|
Para cambiar la cadencia se debe:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
complete
|
|||
|
|
→ duplicate
|
|||
|
|
→ configurar nuevo calendario
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 64. Estado resultante válido en active/paused
|
|||
|
|
|
|||
|
|
Cualquier `PATCH` sobre:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
active
|
|||
|
|
paused
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
debe validar el estado completo resultante.
|
|||
|
|
|
|||
|
|
Ejemplo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
paymentMethodId: null
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
solo se permite si la herencia resultante resuelve un método válido.
|
|||
|
|
|
|||
|
|
Misma regla para:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
paymentTermId
|
|||
|
|
language
|
|||
|
|
currency
|
|||
|
|
proformaSeriesId
|
|||
|
|
issuedInvoiceSeriesId
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 65. Finalización manual
|
|||
|
|
|
|||
|
|
Permitida desde:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
draft
|
|||
|
|
active
|
|||
|
|
paused
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Una `draft` puede pasar directamente a `completed`.
|
|||
|
|
|
|||
|
|
Esto permite conservar una plantilla descartada sin borrarla.
|
|||
|
|
|
|||
|
|
No puede completarse mientras exista una occurrence:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
failed
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 66. Archivado
|
|||
|
|
|
|||
|
|
Solo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
completed → archived
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No restaurable en V1.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 67. Borrado físico
|
|||
|
|
|
|||
|
|
Permitido únicamente cuando:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
status = draft
|
|||
|
|
AND no existen occurrences
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
En cualquier otro caso:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
DELETE rechazado
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 68. Duplicación
|
|||
|
|
|
|||
|
|
Endpoint:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
POST /recurring-proformas/:recurring_proforma_id/duplicate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Resultado:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
nuevo UniqueID
|
|||
|
|
status = draft
|
|||
|
|
nextOccurrenceDate = null
|
|||
|
|
sin occurrences
|
|||
|
|
sin historial
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Se copia configuración comercial y schedule.
|
|||
|
|
|
|||
|
|
No se copia estado operativo.
|
|||
|
|
|
|||
|
|
El nombre puede mantenerse igual porque no existe unicidad.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 69. Crear desde Proforma
|
|||
|
|
|
|||
|
|
Endpoint:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
POST /recurring-proformas/from-proforma/:proforma_id
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Crea:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecurringProforma draft
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Copia:
|
|||
|
|
|
|||
|
|
- Customer;
|
|||
|
|
- campos comerciales;
|
|||
|
|
- items;
|
|||
|
|
- descuentos;
|
|||
|
|
- condiciones actuales;
|
|||
|
|
- payment term;
|
|||
|
|
- payment method;
|
|||
|
|
- idioma;
|
|||
|
|
- moneda;
|
|||
|
|
- series.
|
|||
|
|
|
|||
|
|
No copia:
|
|||
|
|
|
|||
|
|
- ID;
|
|||
|
|
- referencia;
|
|||
|
|
- status;
|
|||
|
|
- proformaDate;
|
|||
|
|
- operationDate;
|
|||
|
|
- impuestos;
|
|||
|
|
- totales;
|
|||
|
|
- issued invoice;
|
|||
|
|
- VeriFactu.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 70. Procedencia al crear desde Proforma
|
|||
|
|
|
|||
|
|
Si `Proforma` permite conocer si un campo era:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
inherited
|
|||
|
|
explicit
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
debe conservarse esa procedencia.
|
|||
|
|
|
|||
|
|
Si no puede demostrarse:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
→ copiar el valor actual como override explícito
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Nunca inferir una herencia que el modelo actual no pueda demostrar.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 71. Persistencia
|
|||
|
|
|
|||
|
|
Tablas V1:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
recurring_proformas
|
|||
|
|
recurring_proforma_items
|
|||
|
|
recurring_proforma_occurrences
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No se crean:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
recurring_proforma_attempts
|
|||
|
|
recurring_proforma_history
|
|||
|
|
recurrence_schedules
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 72. Tabla `recurring_proformas`
|
|||
|
|
|
|||
|
|
Campos conceptuales:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
id
|
|||
|
|
company_id
|
|||
|
|
|
|||
|
|
name
|
|||
|
|
description
|
|||
|
|
status
|
|||
|
|
|
|||
|
|
customer_id
|
|||
|
|
|
|||
|
|
campos comerciales no fiscales
|
|||
|
|
|
|||
|
|
payment_term_id
|
|||
|
|
payment_method_id
|
|||
|
|
language_code
|
|||
|
|
currency_code
|
|||
|
|
|
|||
|
|
proforma_series_id
|
|||
|
|
issued_invoice_series_id
|
|||
|
|
|
|||
|
|
start_date
|
|||
|
|
end_date
|
|||
|
|
recurrence_unit
|
|||
|
|
recurrence_interval
|
|||
|
|
monthly_rule
|
|||
|
|
|
|||
|
|
next_occurrence_date
|
|||
|
|
|
|||
|
|
created_at
|
|||
|
|
updated_at
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Los campos comerciales definitivos deben salir de auditoría del modelo actual de `Proforma`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 73. Índices `recurring_proformas`
|
|||
|
|
|
|||
|
|
Inicialmente:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
INDEX (company_id, status, next_occurrence_date)
|
|||
|
|
INDEX (company_id, customer_id)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No indexar `name` en V1 salvo evidencia real de necesidad.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 74. Tabla `recurring_proforma_items`
|
|||
|
|
|
|||
|
|
Campos conceptuales:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
id
|
|||
|
|
recurring_proforma_id
|
|||
|
|
|
|||
|
|
position
|
|||
|
|
|
|||
|
|
description
|
|||
|
|
|
|||
|
|
quantity_*
|
|||
|
|
unit_price_*
|
|||
|
|
discount_*
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Sin:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
company_id
|
|||
|
|
created_at
|
|||
|
|
updated_at
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`companyId` se deriva desde `recurring_proforma_id`, igual que en `proforma_items`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 75. Items FK
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
recurring_proforma_id
|
|||
|
|
→ recurring_proformas.id
|
|||
|
|
ON DELETE CASCADE
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Las líneas forman parte interna del agregado.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 76. Orden de items
|
|||
|
|
|
|||
|
|
Debe conservarse orden comercial.
|
|||
|
|
|
|||
|
|
Usar:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
position
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
si ese es el patrón necesario tras auditar `ProformaItem`.
|
|||
|
|
|
|||
|
|
Recomendado:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
UNIQUE(recurring_proforma_id, position)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 77. Tabla `recurring_proforma_occurrences`
|
|||
|
|
|
|||
|
|
Campos:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
id
|
|||
|
|
company_id
|
|||
|
|
recurring_proforma_id
|
|||
|
|
|
|||
|
|
scheduled_date
|
|||
|
|
status
|
|||
|
|
|
|||
|
|
attempt_count
|
|||
|
|
|
|||
|
|
last_attempt_at
|
|||
|
|
processing_started_at
|
|||
|
|
|
|||
|
|
last_error_code
|
|||
|
|
last_error_message
|
|||
|
|
|
|||
|
|
proforma_id
|
|||
|
|
generated_at
|
|||
|
|
|
|||
|
|
created_at
|
|||
|
|
updated_at
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 78. `company_id` en occurrences
|
|||
|
|
|
|||
|
|
`RecurringProformaOccurrence` sí almacena explícitamente:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
company_id
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Motivo:
|
|||
|
|
|
|||
|
|
- consultas tenant-scoped frecuentes;
|
|||
|
|
- scheduler;
|
|||
|
|
- incidencias;
|
|||
|
|
- recuperación de processing;
|
|||
|
|
- evitar JOINs o consultas previas.
|
|||
|
|
|
|||
|
|
Debe coincidir siempre con:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecurringProforma.companyId
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No se utiliza FK compuesta.
|
|||
|
|
|
|||
|
|
La coherencia se garantiza explícitamente en dominio/repositorio.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 79. Constraints occurrences
|
|||
|
|
|
|||
|
|
Obligatorios:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
UNIQUE(recurring_proforma_id, scheduled_date)
|
|||
|
|
UNIQUE(proforma_id)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
FKs:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
recurring_proforma_id
|
|||
|
|
→ recurring_proformas.id
|
|||
|
|
ON DELETE RESTRICT
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
proforma_id
|
|||
|
|
→ proformas.id
|
|||
|
|
ON DELETE RESTRICT
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 80. Índices occurrences
|
|||
|
|
|
|||
|
|
Inicialmente:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
INDEX (company_id, status)
|
|||
|
|
INDEX (company_id, status, processing_started_at)
|
|||
|
|
|
|||
|
|
UNIQUE (recurring_proforma_id, scheduled_date)
|
|||
|
|
UNIQUE (proforma_id)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No añadir índices adicionales sin necesidad demostrada.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 81. `hasIssue`
|
|||
|
|
|
|||
|
|
No se persiste.
|
|||
|
|
|
|||
|
|
Se deriva:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
exists occurrence
|
|||
|
|
where status = failed
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Puede exponerse en list/detail:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
has_issue: boolean
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 82. Datos derivados no persistidos inicialmente
|
|||
|
|
|
|||
|
|
No guardar:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
last_occurrence_date
|
|||
|
|
last_generated_at
|
|||
|
|
occurrence_count
|
|||
|
|
has_issue
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Se materializarán únicamente si en el futuro existe evidencia de problemas de rendimiento.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 83. Repositorios
|
|||
|
|
|
|||
|
|
Contratos separados:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
IRecurringProformaRepository
|
|||
|
|
IRecurringProformaOccurrenceRepository
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No usar repositorio genérico.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 84. Finders
|
|||
|
|
|
|||
|
|
Separar consultas especializadas:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecurringProformaFinder
|
|||
|
|
RecurringProformaOccurrenceFinder
|
|||
|
|
RecurringProformaExecutionFinder
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Sin CQRS formal.
|
|||
|
|
|
|||
|
|
`ExecutionFinder` puede resolver:
|
|||
|
|
|
|||
|
|
- recurrencias vencidas;
|
|||
|
|
- incidencias;
|
|||
|
|
- stale processing;
|
|||
|
|
- consultas operativas del scheduler.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 85. Application services
|
|||
|
|
|
|||
|
|
Servicios candidatos V1:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecurringProformaCreator
|
|||
|
|
RecurringProformaUpdater
|
|||
|
|
RecurringProformaDeleter
|
|||
|
|
RecurringProformaStatusChanger
|
|||
|
|
RecurringProformaReadinessChecker
|
|||
|
|
RecurringProformaDuplicator
|
|||
|
|
RecurringProformaFromProformaCreator
|
|||
|
|
|
|||
|
|
RecurringProformaMaterializer
|
|||
|
|
|
|||
|
|
RecurringProformaOccurrenceProcessor
|
|||
|
|
RecurringProformaOccurrenceRetryService
|
|||
|
|
RecurringProformaSchedulerProcessor
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Evitar:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecurringProformaManager
|
|||
|
|
RecurringBillingEngine
|
|||
|
|
GenericRecurringService
|
|||
|
|
GenericSchedulerService
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 86. `RecurringProformaMaterializer`
|
|||
|
|
|
|||
|
|
Responsabilidad:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecurringProforma actual
|
|||
|
|
+
|
|||
|
|
Customer actual
|
|||
|
|
+
|
|||
|
|
defaults actuales
|
|||
|
|
+
|
|||
|
|
catálogos actuales
|
|||
|
|
+
|
|||
|
|
contexto fiscal actual
|
|||
|
|
↓
|
|||
|
|
entrada válida para crear Proforma
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No persiste.
|
|||
|
|
|
|||
|
|
No implementa lógica fiscal propia.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 87. Reutilización de `ProformaCreator`
|
|||
|
|
|
|||
|
|
La generación recurrente debe acabar utilizando el mismo servicio responsable de crear Proformas normales.
|
|||
|
|
|
|||
|
|
No debe existir una segunda implementación de creación de Proforma específica para recurrencias.
|
|||
|
|
|
|||
|
|
La recurrencia materializa la entrada.
|
|||
|
|
|
|||
|
|
`ProformaCreator` aplica:
|
|||
|
|
|
|||
|
|
- invariantes;
|
|||
|
|
- creación;
|
|||
|
|
- fiscalidad;
|
|||
|
|
- cálculos;
|
|||
|
|
- persistencia mediante el flujo habitual.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 88. Transacción de generación
|
|||
|
|
|
|||
|
|
La generación exitosa debe ser atómica.
|
|||
|
|
|
|||
|
|
Conceptualmente:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
BEGIN
|
|||
|
|
|
|||
|
|
lock RecurringProforma
|
|||
|
|
|
|||
|
|
crear/recuperar occurrence
|
|||
|
|
mark processing
|
|||
|
|
|
|||
|
|
resolver configuración vigente
|
|||
|
|
|
|||
|
|
crear Proforma
|
|||
|
|
persistir Proforma
|
|||
|
|
|
|||
|
|
occurrence.markGenerated(proformaId)
|
|||
|
|
|
|||
|
|
avanzar nextOccurrenceDate
|
|||
|
|
o completar RecurringProforma
|
|||
|
|
|
|||
|
|
persistir RecurringProforma
|
|||
|
|
persistir occurrence
|
|||
|
|
|
|||
|
|
auditoría
|
|||
|
|
|
|||
|
|
COMMIT
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Si cualquier paso falla:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
ROLLBACK
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No debe quedar:
|
|||
|
|
|
|||
|
|
- Proforma huérfana;
|
|||
|
|
- occurrence generated parcial;
|
|||
|
|
- `nextOccurrenceDate` avanzado incorrectamente.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 89. Persistencia del fallo
|
|||
|
|
|
|||
|
|
Tras rollback de la generación, el estado:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
failed
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
se registra en una transacción separada.
|
|||
|
|
|
|||
|
|
Debe conservar:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
attemptCount
|
|||
|
|
lastAttemptAt
|
|||
|
|
lastErrorCode
|
|||
|
|
lastErrorMessage
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 90. Locking
|
|||
|
|
|
|||
|
|
V1 usa locking pesimista/transaccional por `RecurringProforma`.
|
|||
|
|
|
|||
|
|
Objetivo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
solo una ejecución puede avanzar simultáneamente
|
|||
|
|
nextOccurrenceDate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Segunda barrera defensiva:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
UNIQUE(recurring_proforma_id, scheduled_date)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No introducir distributed lock externo salvo necesidad real del despliegue.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 91. Scheduler
|
|||
|
|
|
|||
|
|
Infrastructure contiene un worker periódico.
|
|||
|
|
|
|||
|
|
Debe limitarse a invocar Application.
|
|||
|
|
|
|||
|
|
Conceptualmente:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Infrastructure scheduler
|
|||
|
|
↓
|
|||
|
|
ProcessDueRecurringProformasUseCase
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No contiene:
|
|||
|
|
|
|||
|
|
- cálculo de calendario;
|
|||
|
|
- creación de Proforma;
|
|||
|
|
- lógica fiscal;
|
|||
|
|
- transiciones de dominio.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 92. Procesamiento por recurrencia
|
|||
|
|
|
|||
|
|
Para cada recurrencia:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
procesar occurrences vencidas
|
|||
|
|
en orden cronológico
|
|||
|
|
de forma secuencial
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Recurrencias distintas pueden procesarse independientemente.
|
|||
|
|
|
|||
|
|
Un fallo en una recurrencia no impide procesar las demás.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 93. Recovery
|
|||
|
|
|
|||
|
|
Puede existir:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecoverStaleRecurringProformaOccurrencesUseCase
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
o integrarse de forma limpia en el worker principal.
|
|||
|
|
|
|||
|
|
La lógica debe permanecer en Application.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 94. Auditoría
|
|||
|
|
|
|||
|
|
Eventos funcionales V1:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
recurring_proforma.created
|
|||
|
|
recurring_proforma.updated
|
|||
|
|
recurring_proforma.activated
|
|||
|
|
recurring_proforma.paused
|
|||
|
|
recurring_proforma.resumed
|
|||
|
|
recurring_proforma.completed
|
|||
|
|
recurring_proforma.archived
|
|||
|
|
recurring_proforma.deleted
|
|||
|
|
|
|||
|
|
recurring_proforma_occurrence.generated
|
|||
|
|
recurring_proforma_occurrence.failed
|
|||
|
|
recurring_proforma_occurrence.retried
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No auditar eventos puramente técnicos:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
pending → processing
|
|||
|
|
lock acquired
|
|||
|
|
lease recovered
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 95. Estado auditable de occurrence
|
|||
|
|
|
|||
|
|
Debe incluir al menos:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
scheduledDate
|
|||
|
|
status
|
|||
|
|
attemptCount
|
|||
|
|
proformaId
|
|||
|
|
lastErrorCode
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`generated` debe permitir identificar la Proforma creada.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 96. Permisos
|
|||
|
|
|
|||
|
|
V1 no introduce permisos específicos de recurrencias.
|
|||
|
|
|
|||
|
|
Se reutiliza la autorización existente del módulo `customer-invoices`.
|
|||
|
|
|
|||
|
|
El worker ejecuta como sistema.
|
|||
|
|
|
|||
|
|
La auditoría debe distinguir:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
usuario
|
|||
|
|
sistema
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 97. API HTTP
|
|||
|
|
|
|||
|
|
Base:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
/recurring-proformas
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Todas las operaciones de creación relacionadas con recurrencias quedan bajo este recurso.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 98. CRUD
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
GET /recurring-proformas
|
|||
|
|
POST /recurring-proformas
|
|||
|
|
GET /recurring-proformas/:recurring_proforma_id
|
|||
|
|
PATCH /recurring-proformas/:recurring_proforma_id
|
|||
|
|
DELETE /recurring-proformas/:recurring_proforma_id
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 99. Readiness y lifecycle
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
GET /recurring-proformas/:recurring_proforma_id/activation-readiness
|
|||
|
|
|
|||
|
|
PATCH /recurring-proformas/:recurring_proforma_id/activate
|
|||
|
|
PATCH /recurring-proformas/:recurring_proforma_id/pause
|
|||
|
|
PATCH /recurring-proformas/:recurring_proforma_id/resume
|
|||
|
|
PATCH /recurring-proformas/:recurring_proforma_id/complete
|
|||
|
|
PATCH /recurring-proformas/:recurring_proforma_id/archive
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`status` no es editable mediante PATCH general.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 100. Ocurrencias
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
GET /recurring-proformas/:recurring_proforma_id/occurrences
|
|||
|
|
|
|||
|
|
GET /recurring-proformas/:recurring_proforma_id/occurrences/:occurrence_id
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 101. Acciones manuales
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
POST /recurring-proformas/:recurring_proforma_id/generate-next-occurrence
|
|||
|
|
|
|||
|
|
POST /recurring-proformas/:recurring_proforma_id/occurrences/:occurrence_id/retry
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 102. Creación desde Proforma
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
POST /recurring-proformas/from-proforma/:proforma_id
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 103. Duplicación
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
POST /recurring-proformas/:recurring_proforma_id/duplicate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 104. DTO CREATE
|
|||
|
|
|
|||
|
|
`POST /recurring-proformas` puede contener solamente:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"name": "Mantenimiento mensual"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
El backend crea:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
status = draft
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No acepta:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
id
|
|||
|
|
company_id
|
|||
|
|
status
|
|||
|
|
next_occurrence_date
|
|||
|
|
created_at
|
|||
|
|
updated_at
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 105. Semántica PATCH
|
|||
|
|
|
|||
|
|
Regla general:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
omitido
|
|||
|
|
→ no modificar
|
|||
|
|
|
|||
|
|
null
|
|||
|
|
→ ausencia explícita / quitar override cuando aplique
|
|||
|
|
|
|||
|
|
valor
|
|||
|
|
→ configurar explícitamente
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Ejemplo:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"payment_method_id": null
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
significa:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
eliminar override
|
|||
|
|
→ volver a herencia
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 106. PATCH collections
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
items omitido
|
|||
|
|
→ no tocar
|
|||
|
|
|
|||
|
|
items enviado
|
|||
|
|
→ reemplazo completo
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No introducir actualización granular de items.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 107. DTO Schedule
|
|||
|
|
|
|||
|
|
Shape conceptual:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"start_date": "2026-09-01",
|
|||
|
|
"end_date": null,
|
|||
|
|
"unit": "month",
|
|||
|
|
"interval": 1,
|
|||
|
|
"monthly_rule": "same_day"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
En PATCH el objeto puede modificarse parcialmente.
|
|||
|
|
|
|||
|
|
Ejemplo:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"schedule": {
|
|||
|
|
"end_date": "2027-12-31"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Application reconstruye y valida el `RecurrenceSchedule` completo.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 108. DTOs exactos
|
|||
|
|
|
|||
|
|
Todo valor exacto debe seguir las convenciones existentes del ERP:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Money
|
|||
|
|
Amount
|
|||
|
|
Percentage
|
|||
|
|
Quantity
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No deben viajar como `number` cuando requieran exactitud decimal.
|
|||
|
|
|
|||
|
|
Los schemas Zod son SSOT.
|
|||
|
|
|
|||
|
|
Los snapshot builders deben tiparse contra los DTO inferidos.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 109. Responses heredables
|
|||
|
|
|
|||
|
|
El frontend debe conocer:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
valor configurado
|
|||
|
|
valor actualmente resuelto
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Ejemplo conceptual:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"payment_method": {
|
|||
|
|
"configured_id": null,
|
|||
|
|
"resolved": {
|
|||
|
|
"id": "...",
|
|||
|
|
"name": "Transferencia"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No es necesario un campo redundante:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
source = inherited
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
porque puede derivarse de `configured_id`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 110. Listado HTTP
|
|||
|
|
|
|||
|
|
Filtros V1:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
search
|
|||
|
|
status
|
|||
|
|
customer_id
|
|||
|
|
has_issue
|
|||
|
|
next_occurrence_date_from
|
|||
|
|
next_occurrence_date_to
|
|||
|
|
page
|
|||
|
|
per_page
|
|||
|
|
sort
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Búsqueda rápida al menos por:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
name
|
|||
|
|
description
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
y por Customer si el Finder actual permite hacerlo de forma consistente con otros listados.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 111. Ordenación
|
|||
|
|
|
|||
|
|
Campos útiles:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
name
|
|||
|
|
status
|
|||
|
|
next_occurrence_date
|
|||
|
|
created_at
|
|||
|
|
updated_at
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
La lista debe seguir las mismas convenciones de paginación y sorting ya utilizadas en el ERP.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 112. Frontend
|
|||
|
|
|
|||
|
|
Nombre visible:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Proformas recurrentes
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No usar:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Facturación recurrente
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
porque V1 no emite facturas automáticamente.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 113. Navegación
|
|||
|
|
|
|||
|
|
Dentro de facturación de clientes:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Facturación de clientes
|
|||
|
|
├─ Proformas
|
|||
|
|
├─ Proformas archivadas
|
|||
|
|
├─ Proformas recurrentes
|
|||
|
|
├─ Proformas recurrentes archivadas
|
|||
|
|
└─ Facturas emitidas
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 114. Rutas frontend
|
|||
|
|
|
|||
|
|
Conceptualmente:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
/recurring-proformas
|
|||
|
|
/recurring-proformas/create
|
|||
|
|
/recurring-proformas/:recurring_proforma_id
|
|||
|
|
/recurring-proformas/:recurring_proforma_id/update
|
|||
|
|
|
|||
|
|
/recurring-proformas/archived
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 115. Listado frontend
|
|||
|
|
|
|||
|
|
Debe reutilizar el mismo patrón visual que el listado actual de Proformas:
|
|||
|
|
|
|||
|
|
- filtros;
|
|||
|
|
- tabla;
|
|||
|
|
- sorting;
|
|||
|
|
- paginación;
|
|||
|
|
- búsqueda;
|
|||
|
|
- acciones;
|
|||
|
|
- loading;
|
|||
|
|
- errores;
|
|||
|
|
- componentes Rdx existentes.
|
|||
|
|
|
|||
|
|
Columnas recomendadas:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Nombre
|
|||
|
|
Cliente
|
|||
|
|
Estado
|
|||
|
|
Frecuencia
|
|||
|
|
Próxima generación
|
|||
|
|
Incidencias
|
|||
|
|
Última actualización
|
|||
|
|
Acciones
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 116. Filtro segmentado
|
|||
|
|
|
|||
|
|
Utilizar un control equivalente a:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
ProformaStatusSegmentedFilter
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Estados operativos:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Todas
|
|||
|
|
Borrador
|
|||
|
|
Activas
|
|||
|
|
Pausadas
|
|||
|
|
Completadas
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`archived` no aparece ahí.
|
|||
|
|
|
|||
|
|
Las archivadas tienen entrada propia en menú lateral.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 117. Archivadas
|
|||
|
|
|
|||
|
|
Ruta:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
/recurring-proformas/archived
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Debe reutilizar exactamente el mismo listado y tabla con filtro fijo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
status = archived
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 118. Incidencias
|
|||
|
|
|
|||
|
|
Una incidencia NO es estado de `RecurringProforma`.
|
|||
|
|
|
|||
|
|
Visualmente:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Activa + Incidencia
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`hasIssue` deriva de una occurrence `failed`.
|
|||
|
|
|
|||
|
|
Debe existir filtro:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Todas
|
|||
|
|
Con incidencias
|
|||
|
|
Sin incidencias
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 119. Editor
|
|||
|
|
|
|||
|
|
Secciones recomendadas:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Identificación interna
|
|||
|
|
Cliente
|
|||
|
|
Contenido comercial
|
|||
|
|
Condiciones heredables
|
|||
|
|
Calendario
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 120. Identificación interna UI
|
|||
|
|
|
|||
|
|
Campos:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Nombre
|
|||
|
|
Descripción interna
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Debe indicarse que:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
description
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
es exclusivamente interna y no se copia a Proformas.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 121. UI de herencia
|
|||
|
|
|
|||
|
|
Debe distinguir claramente:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Heredar
|
|||
|
|
vs
|
|||
|
|
override explícito
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Ejemplo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Forma de pago
|
|||
|
|
[ Heredar del cliente ] Transferencia actual
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No mostrar simplemente el valor resuelto como si estuviera fijado.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 122. Preview de calendario
|
|||
|
|
|
|||
|
|
El editor debe mostrar aproximadamente las próximas 3–5 fechas calculadas.
|
|||
|
|
|
|||
|
|
Ejemplo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Próximas fechas
|
|||
|
|
|
|||
|
|
01/09/2026
|
|||
|
|
01/10/2026
|
|||
|
|
01/11/2026
|
|||
|
|
01/12/2026
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
La lógica debe coincidir exactamente con `RecurrenceSchedule`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 123. Activación UI
|
|||
|
|
|
|||
|
|
Antes de activar:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
GET activation-readiness
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Si existen blockers:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
→ mostrar lista funcional traducida
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
El backend vuelve a validar igualmente durante la transición.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 124. Historial UI
|
|||
|
|
|
|||
|
|
Tabla de occurrences:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Fecha programada
|
|||
|
|
Estado
|
|||
|
|
Proforma
|
|||
|
|
Factura emitida
|
|||
|
|
Intentos
|
|||
|
|
Último intento
|
|||
|
|
Acciones
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 125. Estados occurrence en UI
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
pending → Pendiente
|
|||
|
|
processing → Procesando
|
|||
|
|
failed → Fallida
|
|||
|
|
generated → Generada
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`processing` es solo informativo.
|
|||
|
|
|
|||
|
|
No tiene acciones manuales.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 126. Errores UI
|
|||
|
|
|
|||
|
|
`lastErrorCode` es la fuente para mensajes funcionales traducidos.
|
|||
|
|
|
|||
|
|
`lastErrorMessage`:
|
|||
|
|
|
|||
|
|
- no se muestra crudo en tabla;
|
|||
|
|
- puede utilizarse en detalle técnico/soporte cuando aporte valor.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 127. Generate next UI
|
|||
|
|
|
|||
|
|
Solo disponible en `active`.
|
|||
|
|
|
|||
|
|
Si la fecha futura se genera anticipadamente, el modal debe explicar:
|
|||
|
|
|
|||
|
|
- cuál era la fecha programada;
|
|||
|
|
- que esa occurrence será consumida;
|
|||
|
|
- que la Proforma usará la fecha actual de generación.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 128. Retry UI
|
|||
|
|
|
|||
|
|
Solo visible para:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Occurrence.status = failed
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Debe indicar que:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
el retry utiliza la configuración actual de la recurrencia
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
No existe acción:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Skip
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 129. Pause UI
|
|||
|
|
|
|||
|
|
Debe advertir:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
la pausa no desplaza ni elimina fechas
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Las occurrences vencidas durante la pausa se recuperarán al reanudar.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 130. Resume UI
|
|||
|
|
|
|||
|
|
Si hay occurrences vencidas, puede informar de que serán procesadas al reanudar.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 131. Completed / Archived UI
|
|||
|
|
|
|||
|
|
Ambos read-only.
|
|||
|
|
|
|||
|
|
Acciones:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
completed
|
|||
|
|
→ Duplicate
|
|||
|
|
→ Archive
|
|||
|
|
|
|||
|
|
archived
|
|||
|
|
→ Duplicate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 132. Matriz de acciones UI
|
|||
|
|
|
|||
|
|
| Estado | Editar | Activar | Pausar | Reanudar | Generar próxima | Completar | Archivar | Duplicar |
|
|||
|
|
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
|||
|
|
| draft | Sí | Sí | No | No | No | Sí | No | Sí |
|
|||
|
|
| active | Sí | No | Sí | No | Sí | Sí* | No | Sí |
|
|||
|
|
| paused | Sí | No | No | Sí | No | Sí* | No | Sí |
|
|||
|
|
| completed | No | No | No | No | No | No | Sí | Sí |
|
|||
|
|
| archived | No | No | No | No | No | No | No | Sí |
|
|||
|
|
|
|||
|
|
`*` No disponible si existe occurrence `failed`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 133. Testing obligatorio
|
|||
|
|
|
|||
|
|
Debe cubrirse específicamente:
|
|||
|
|
|
|||
|
|
## Calendario
|
|||
|
|
|
|||
|
|
- week;
|
|||
|
|
- month;
|
|||
|
|
- year;
|
|||
|
|
- interval > 1;
|
|||
|
|
- día 31;
|
|||
|
|
- 29 febrero;
|
|||
|
|
- last day;
|
|||
|
|
- endDate inclusiva;
|
|||
|
|
- endDate entre occurrences;
|
|||
|
|
- conservación del ancla.
|
|||
|
|
|
|||
|
|
## Estados
|
|||
|
|
|
|||
|
|
Todas las transiciones válidas e inválidas.
|
|||
|
|
|
|||
|
|
## Draft
|
|||
|
|
|
|||
|
|
Creación únicamente con `name`.
|
|||
|
|
|
|||
|
|
## Herencia
|
|||
|
|
|
|||
|
|
Para todos los campos heredables:
|
|||
|
|
|
|||
|
|
- inherited;
|
|||
|
|
- explicit;
|
|||
|
|
- cambio posterior de defaults;
|
|||
|
|
- recurso explícito inactivo;
|
|||
|
|
- eliminación de override;
|
|||
|
|
- no materialización del valor heredado en la recurrencia.
|
|||
|
|
|
|||
|
|
## Customer
|
|||
|
|
|
|||
|
|
- inactive;
|
|||
|
|
- cambio de Customer;
|
|||
|
|
- compatibilidad;
|
|||
|
|
- efecto solo futuro.
|
|||
|
|
|
|||
|
|
## Generación
|
|||
|
|
|
|||
|
|
- success;
|
|||
|
|
- fiscalidad actual;
|
|||
|
|
- recipient actual;
|
|||
|
|
- descuentos;
|
|||
|
|
- series;
|
|||
|
|
- fechas;
|
|||
|
|
- nextOccurrenceDate.
|
|||
|
|
|
|||
|
|
## Idempotencia
|
|||
|
|
|
|||
|
|
Ejecutar dos veces:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
(recurringProformaId, scheduledDate)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
debe producir:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
1 occurrence
|
|||
|
|
1 Proforma
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Concurrencia
|
|||
|
|
|
|||
|
|
Dos workers sobre la misma recurrencia:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
→ exactamente una generación efectiva
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Atomicidad
|
|||
|
|
|
|||
|
|
Si falla la generación:
|
|||
|
|
|
|||
|
|
- no queda Proforma;
|
|||
|
|
- no avanza calendario;
|
|||
|
|
- occurrence termina `failed`.
|
|||
|
|
|
|||
|
|
## Retry
|
|||
|
|
|
|||
|
|
- usa configuración actual;
|
|||
|
|
- misma occurrence;
|
|||
|
|
- incrementa intentos;
|
|||
|
|
- no duplica Proforma.
|
|||
|
|
|
|||
|
|
## Recovery
|
|||
|
|
|
|||
|
|
`processing` abandonado debe recuperarse sin duplicados.
|
|||
|
|
|
|||
|
|
## Pause/resume
|
|||
|
|
|
|||
|
|
Debe recuperar fechas vencidas sin desplazar calendario.
|
|||
|
|
|
|||
|
|
## Generate next
|
|||
|
|
|
|||
|
|
Debe diferenciar:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
scheduledDate
|
|||
|
|
generationDate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## EndDate
|
|||
|
|
|
|||
|
|
Debe completar automáticamente al agotarse el calendario.
|
|||
|
|
|
|||
|
|
## Delete
|
|||
|
|
|
|||
|
|
Solo:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
draft + sin occurrences
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## From Proforma
|
|||
|
|
|
|||
|
|
Debe copiar únicamente configuración comercial válida y respetar procedencia cuando sea demostrable.
|
|||
|
|
|
|||
|
|
## Duplicate
|
|||
|
|
|
|||
|
|
Debe crear:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
nuevo UUID
|
|||
|
|
draft
|
|||
|
|
sin occurrences
|
|||
|
|
sin nextOccurrenceDate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 134. Criterio E2E principal de aceptación
|
|||
|
|
|
|||
|
|
V1 debe demostrar correctamente este escenario:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
1. Crear RecurringProforma draft.
|
|||
|
|
2. Guardarla inicialmente solo con name.
|
|||
|
|
3. Configurar Customer, líneas y calendario mensual.
|
|||
|
|
4. Configurar campos heredados y overrides.
|
|||
|
|
5. Ejecutar activation-readiness.
|
|||
|
|
6. Activar.
|
|||
|
|
7. Generar primera Proforma automáticamente.
|
|||
|
|
8. Comprobar recipient actual.
|
|||
|
|
9. Comprobar campos comerciales.
|
|||
|
|
10. Comprobar fiscalidad e impuestos actuales.
|
|||
|
|
11. Comprobar totales.
|
|||
|
|
12. Desactivar un PaymentMethod explícito.
|
|||
|
|
13. Ejecutar siguiente occurrence.
|
|||
|
|
14. La occurrence falla.
|
|||
|
|
15. No se crea Proforma.
|
|||
|
|
16. No se procesa la occurrence posterior.
|
|||
|
|
17. Cambiar PaymentMethod.
|
|||
|
|
18. Retry.
|
|||
|
|
19. Se genera exactamente una Proforma para esa scheduledDate.
|
|||
|
|
20. El calendario continúa.
|
|||
|
|
21. Pausar.
|
|||
|
|
22. Dejar vencer varias fechas.
|
|||
|
|
23. Reanudar.
|
|||
|
|
24. Procesar las fechas atrasadas en orden.
|
|||
|
|
25. Alcanzar endDate.
|
|||
|
|
26. Completar automáticamente.
|
|||
|
|
27. Archivar.
|
|||
|
|
28. Consultar historial.
|
|||
|
|
29. Navegar a las Proformas generadas.
|
|||
|
|
30. Resolver de forma derivada las IssuedInvoices resultantes.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 135. Decisiones anti-overengineering
|
|||
|
|
|
|||
|
|
No introducir en V1:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Subscription
|
|||
|
|
Contract
|
|||
|
|
RecurringBillingEngine
|
|||
|
|
GenericScheduler
|
|||
|
|
GenericRecurrence
|
|||
|
|
Domain Event Bus
|
|||
|
|
CQRS formal
|
|||
|
|
OccurrenceAttempt
|
|||
|
|
snapshot comercial por occurrence
|
|||
|
|
snapshot fiscal por occurrence
|
|||
|
|
cron configurable
|
|||
|
|
timezone por recurrencia
|
|||
|
|
skip
|
|||
|
|
cancel occurrence
|
|||
|
|
generación extraordinaria
|
|||
|
|
auto issue
|
|||
|
|
auto VeriFactu
|
|||
|
|
products
|
|||
|
|
articles
|
|||
|
|
price lists
|
|||
|
|
period expressions
|
|||
|
|
template variables
|
|||
|
|
business day calendars
|
|||
|
|
holiday calendars
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Preferencia permanente:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
claridad semántica
|
|||
|
|
>
|
|||
|
|
abstracción anticipada
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 136. Regla arquitectónica final
|
|||
|
|
|
|||
|
|
La separación funcional debe mantenerse:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecurringProforma
|
|||
|
|
→ define intención comercial futura y calendario
|
|||
|
|
|
|||
|
|
RecurringProformaOccurrence
|
|||
|
|
→ registra ejecución de una fecha lógica
|
|||
|
|
|
|||
|
|
Proforma
|
|||
|
|
→ representa el documento comercial generado
|
|||
|
|
|
|||
|
|
IssuedInvoice
|
|||
|
|
→ representa el snapshot fiscal definitivo tras emisión
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Ningún agregado debe asumir responsabilidades pertenecientes a otro.
|
|||
|
|
|
|||
|
|
En particular:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecurringProforma
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
no calcula fiscalidad ni emite facturas.
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
RecurringProformaOccurrence
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
no sigue el lifecycle posterior de la Proforma.
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Proforma
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
no conoce que fue creada por una recurrencia.
|
|||
|
|
|
|||
|
|
La colaboración entre estos conceptos se orquesta desde Application.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 137. Regla final de V1
|
|||
|
|
|
|||
|
|
`recurring-proformas` debe proporcionar una automatización:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
explícita
|
|||
|
|
auditable
|
|||
|
|
idempotente
|
|||
|
|
recuperable
|
|||
|
|
tenant-scoped
|
|||
|
|
fiscalmente segura
|
|||
|
|
sin emisión automática
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
La característica debe priorizar:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
generar correctamente una única Proforma
|
|||
|
|
para cada occurrence lógica
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
por encima de cualquier automatización adicional.
|
|||
|
|
|
|||
|
|
La evolución posterior hacia emisión automática, períodos avanzados, variables dinámicas o conceptos de suscripción deberá diseñarse como extensión explícita y no condicionar innecesariamente V1.
|