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. |