47 KiB
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:
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;
skipde 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.
Columna Acciones
La columna Acciones de recurring-proformas reutiliza RdxRowActions, la
misma primitive visual del listado de Proformas. Se presenta como última
columna, alineada a la derecha, con trigger compacto, menú accesible y
separación visual para acciones destructivas.
La celda solo representa callbacks preparados por la página; no invoca APIs
directamente, no consume DTOs y no reconstruye reglas de dominio. El contenido
del menú depende del lifecycle de RecurringProforma, mientras que su
presentación e interacción siguen el estándar global del ERP.
3. Ubicación funcional
RecurringProforma pertenece al módulo:
customer-invoices
como agregado independiente.
Estructura conceptual:
customer-invoices
├─ proformas
├─ issued-invoices
└─ recurring-proformas
No se crea un módulo genérico:
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:
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:
RecurringProforma
RecurringProformaItem
RecurringProformaItems
RecurrenceSchedule
RecurringProformaOccurrence
Aggregate Roots
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:
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:
RP-0001
en V1.
6.2 Campos internos de identificación
name
description
name
Obligatorio.
Reglas:
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
companyId: UniqueID
Toda RecurringProforma pertenece a una compañía.
Es company-scoped.
6.4 Customer
customerId?: UniqueID
En draft puede no existir.
En:
active
paused
es obligatorio.
7. Estados de RecurringProforma
Estados V1:
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:
draft → active
draft → completed
active → paused
active → completed
paused → active
paused → completed
completed → archived
Prohibidas:
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:
name
Pueden faltar:
customer
items
schedule
campos comerciales
paymentTerm
paymentMethod
language
currency
series
La barrera fuerte está en:
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:
id: UniqueID
description
quantity
unitPrice
discount
discount utiliza exactamente el mismo modelo funcional de descuento que el ProformaItem actual.
No contiene:
taxes
taxDefinitionId
productId
articleId
Actualmente el ERP no dispone de productos ni artículos.
12. RecurringProformaItems
Colección basada en:
Collection<RecurringProformaItem>
Actualización:
items omitido
→ no modificar
items enviado
→ reemplazo completo
No se introduce actualización granular:
create/update/delete
por línea en V1.
13. Items vacíos
En draft:
items = []
es válido.
En:
active
paused
no se puede guardar una colección vacía.
Activation readiness bloquea un draft vacío mediante:
missing_items
14. Fiscalidad
RecurringProforma no almacena fiscalidad materializada.
Cada ocurrencia debe resolver nuevamente la fiscalidad vigente.
Flujo:
RecurringProforma
↓
Customer actual
↓
contexto fiscal actual
↓
creación normal de Proforma
↓
impuestos
↓
totales
La recurrencia no implementa:
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:
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:
draft
active
paused
No permitido en:
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:
customerId = null
17. Customer inactivo
Si el Customer pasa a inactive:
Occurrence → failed
La recurrencia no pasa automáticamente a paused.
La occurrence fallida bloquea las siguientes.
Opciones de resolución:
reactivar Customer
→ retry
o:
cambiar Customer
→ retry
18. Campos heredables
Los siguientes campos son opcionales:
paymentTermId
paymentMethodId
language
currency
proformaSeriesId
issuedInvoiceSeriesId
Semántica:
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:
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:
→ pasa a ser override
Si selecciona nuevamente:
Heredar
se persiste:
null
20. PaymentMethod y PaymentTerm
Si existe override explícito y el recurso pasa a estar:
inactive
unavailable
la occurrence falla.
No existe fallback automático.
El usuario debe:
reactivar
o:
cambiar el override
y posteriormente:
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:
→ autoritativo
Valor heredado:
→ se resuelve nuevamente en cada occurrence
Cualquier incompatibilidad que impida generar una Proforma válida provoca:
Occurrence → failed
22. Series documentales
RecurringProforma puede guardar opcionalmente:
proformaSeriesId?: UniqueID
issuedInvoiceSeriesId?: UniqueID
La semántica debe replicar la de Proforma.
Serie explícita
Si se configura explícitamente:
→ debe utilizarse
Si deja de estar:
activa
vigente
disponible
la occurrence falla.
No existe fallback silencioso.
Serie heredada
Si es null:
→ 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
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:
startDate
endDate?
unit
interval
monthlyRule?
unit:
week
month
year
interval:
integer > 0
monthlyRule:
same_day
last_day_of_month
25. Invariantes de RecurrenceSchedule
startDate obligatorio
interval > 0
endDate == null
OR
endDate >= startDate
Para:
unit = month
monthlyRule es obligatorio.
Para:
unit = week
unit = year
monthlyRule debe ser null.
26. startDate
startDate representa siempre:
la primera occurrence
+
el ancla del calendario
No existe:
firstOccurrenceDate
separada.
27. Recurrencia semanal
Ejemplo:
startDate = 07/09/2026
unit = week
interval = 2
Resultado:
07/09
21/09
05/10
...
El día de la semana deriva de startDate.
28. Recurrencia mensual same_day
Ejemplo:
startDate = 31/01/2027
unit = month
interval = 1
monthlyRule = same_day
Resultado:
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:
startDate = 15/01/2027
interval = 1
monthlyRule = last_day_of_month
Resultado:
15/01
28/02
31/03
30/04
31/05
Con:
interval = 3
resultado:
15/01
30/04
31/07
31/10
31/01
...
30. Recurrencia anual
El ancla se conserva desde startDate.
Caso:
startDate = 29/02/2028
unit = year
interval = 1
Resultado:
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:
next = previous occurrence + interval
si eso puede perder el ancla.
El cálculo debe derivarse siempre del calendario original basado en:
startDate
+
índice lógico de occurrence
o una implementación matemáticamente equivalente que conserve el ancla.
32. endDate
Opcional.
Si es null:
recurrencia indefinida
Es inclusiva:
scheduledDate <= endDate
→ válida
endDate no genera por sí misma una occurrence.
Solo limita el calendario.
Ejemplo:
startDate = 01/01
interval = 1 month
endDate = 15/03
Genera:
01/01
01/02
01/03
No genera:
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:
→ completed
34. Finalización automática
Al generar la última occurrence válida:
next calculated date > endDate
entonces:
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:
→ se procesan todas
Tras resolverlas correctamente:
→ completed
Si una falla:
→ 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:
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:
nextOccurrenceDate = startDate
37. Hora y timezone
Las recurrencias trabajan con:
DATE
no con hora configurable.
No existe timezone por RecurringProforma.
El scheduler determina today usando:
- timezone de la compañía, si existe en el ERP;
- 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:
status = active
AND nextOccurrenceDate <= today
Debe recuperar occurrences atrasadas.
No debe depender exclusivamente de:
nextOccurrenceDate == today
39. Ocurrencias atrasadas
Si existen varias fechas vencidas:
→ procesar secuencialmente
→ en orden cronológico
Ejemplo:
enero
febrero
marzo
abril
Si marzo falla:
enero → generated
febrero → generated
marzo → failed
abril → no procesada
40. Pausa
pause suspende la ejecución.
No modifica el calendario.
Ejemplo:
nextOccurrenceDate = 01/10
pause
resume en diciembre
Después del resume:
01/10
01/11
01/12
se procesan en orden.
Pausar no equivale a hacer skip.
41. RecurringProformaOccurrence
Aggregate Root independiente.
Campos V1:
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
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:
pending → processing
processing → generated
processing → failed
failed → processing
generated no permite ninguna transición posterior.
44. Identidad lógica de occurrence
Debe existir:
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:
proformaId?: UniqueID
Debe existir:
UNIQUE(proforma_id)
Una Proforma no puede pertenecer a dos occurrences.
La Proforma NO contiene:
recurringProformaId
recurringOccurrenceId
La dirección de la relación es exclusivamente:
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:
ON DELETE SET NULL
ON DELETE CASCADE
sobre proforma_id.
47. Ciclo posterior de la Proforma
Una vez:
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:
generated
48. IssuedInvoice en historial
RecurringProformaOccurrence NO guarda:
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:
RecurringProforma.nextOccurrenceDate
↓
scheduler / generate next
↓
crear o recuperar occurrence
↓
pending
↓
processing
50. attemptCount
Cada intento real incrementa:
attemptCount += 1
y actualiza:
lastAttemptAt
Incluye:
- primer intento;
- retry manual;
- recuperación de
processingabandonado.
51. Fallos
Al fallar:
status = failed
lastErrorCode = ...
lastErrorMessage = ...
No se guarda historial completo de intentos.
No existe:
RecurringProformaOccurrenceAttempt
en V1.
Al generar finalmente correctamente:
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:
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:
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:
processingStartedAt
Si supera un threshold configurado en infraestructura:
→ puede recuperarse
Debe reutilizarse la misma occurrence.
No introducir en dominio:
workerId
heartbeat
leaseOwner
salvo necesidad técnica futura.
55. Generación manual
Existe en V1:
Generate next occurrence
Solo disponible en:
active
No disponible en:
draft
paused
completed
archived
Procesa anticipadamente:
nextOccurrenceDate
No crea una occurrence extraordinaria.
56. Generación anticipada
Ejemplo:
nextOccurrenceDate = 01/10
usuario genera el 25/09
Resultado:
Occurrence.scheduledDate = 01/10
Proforma.proformaDate = 25/09
Proforma.operationDate = 25/09
La siguiente fecha del calendario se calcula desde:
scheduledDate / calendario original
no desde la fecha efectiva de generación.
57. Fechas de Proforma
En V1:
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:
ú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:
status = failed
nextOccurrenceDate no avanza
bloquea posteriores
requiere retry
59. Activación readiness
La transición:
draft → active
requiere validación previa.
Endpoint:
GET /recurring-proformas/:recurring_proforma_id/activation-readiness
Contrato conceptual:
{
"can_activate": false,
"blockers": [
{
"code": "missing_items"
}
]
}
60. Activation blockers
Candidatos iniciales:
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:
¿puede activarse ahora?
Cada occurrence vuelve a resolver y validar los datos vivos.
Ejemplo:
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:
startDate
unit
interval
monthlyRule
pueden modificarse.
Una vez exista historial:
startDate
unit
interval
monthlyRule
quedan bloqueados.
endDate sigue siendo modificable según sus reglas propias.
Para cambiar la cadencia se debe:
complete
→ duplicate
→ configurar nuevo calendario
64. Estado resultante válido en active/paused
Cualquier PATCH sobre:
active
paused
debe validar el estado completo resultante.
Ejemplo:
paymentMethodId: null
solo se permite si la herencia resultante resuelve un método válido.
Misma regla para:
paymentTermId
language
currency
proformaSeriesId
issuedInvoiceSeriesId
65. Finalización manual
Permitida desde:
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:
failed
66. Archivado
Solo:
completed → archived
No restaurable en V1.
67. Borrado físico
Permitido únicamente cuando:
status = draft
AND no existen occurrences
En cualquier otro caso:
DELETE rechazado
68. Duplicación
Endpoint:
POST /recurring-proformas/:recurring_proforma_id/duplicate
Resultado:
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:
POST /recurring-proformas/from-proforma/:proforma_id
Crea:
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:
inherited
explicit
debe conservarse esa procedencia.
Si no puede demostrarse:
→ copiar el valor actual como override explícito
Nunca inferir una herencia que el modelo actual no pueda demostrar.
71. Persistencia
Tablas V1:
recurring_proformas
recurring_proforma_items
recurring_proforma_occurrences
No se crean:
recurring_proforma_attempts
recurring_proforma_history
recurrence_schedules
72. Tabla recurring_proformas
Campos conceptuales:
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:
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:
id
recurring_proforma_id
position
description
quantity_*
unit_price_*
discount_*
Sin:
company_id
created_at
updated_at
companyId se deriva desde recurring_proforma_id, igual que en proforma_items.
75. Items FK
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:
position
si ese es el patrón necesario tras auditar ProformaItem.
Recomendado:
UNIQUE(recurring_proforma_id, position)
77. Tabla recurring_proforma_occurrences
Campos:
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:
company_id
Motivo:
- consultas tenant-scoped frecuentes;
- scheduler;
- incidencias;
- recuperación de processing;
- evitar JOINs o consultas previas.
Debe coincidir siempre con:
RecurringProforma.companyId
No se utiliza FK compuesta.
La coherencia se garantiza explícitamente en dominio/repositorio.
79. Constraints occurrences
Obligatorios:
UNIQUE(recurring_proforma_id, scheduled_date)
UNIQUE(proforma_id)
FKs:
recurring_proforma_id
→ recurring_proformas.id
ON DELETE RESTRICT
proforma_id
→ proformas.id
ON DELETE RESTRICT
80. Índices occurrences
Inicialmente:
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:
exists occurrence
where status = failed
Puede exponerse en list/detail:
has_issue: boolean
82. Datos derivados no persistidos inicialmente
No guardar:
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:
IRecurringProformaRepository
IRecurringProformaOccurrenceRepository
No usar repositorio genérico.
84. Finders
Separar consultas especializadas:
RecurringProformaFinder
RecurringProformaOccurrenceFinder
RecurringProformaExecutionFinder
Sin CQRS formal.
ExecutionFinder puede resolver:
- recurrencias vencidas;
- incidencias;
- stale processing;
- consultas operativas del scheduler.
85. Application services
Servicios candidatos V1:
RecurringProformaCreator
RecurringProformaUpdater
RecurringProformaDeleter
RecurringProformaStatusChanger
RecurringProformaReadinessChecker
RecurringProformaDuplicator
RecurringProformaFromProformaCreator
RecurringProformaMaterializer
RecurringProformaOccurrenceProcessor
RecurringProformaOccurrenceRetryService
RecurringProformaSchedulerProcessor
Evitar:
RecurringProformaManager
RecurringBillingEngine
GenericRecurringService
GenericSchedulerService
86. RecurringProformaMaterializer
Responsabilidad:
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:
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:
ROLLBACK
No debe quedar:
- Proforma huérfana;
- occurrence generated parcial;
nextOccurrenceDateavanzado incorrectamente.
89. Persistencia del fallo
Tras rollback de la generación, el estado:
failed
se registra en una transacción separada.
Debe conservar:
attemptCount
lastAttemptAt
lastErrorCode
lastErrorMessage
90. Locking
V1 usa locking pesimista/transaccional por RecurringProforma.
Objetivo:
solo una ejecución puede avanzar simultáneamente
nextOccurrenceDate
Segunda barrera defensiva:
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:
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:
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:
RecoverStaleRecurringProformaOccurrencesUseCase
o integrarse de forma limpia en el worker principal.
La lógica debe permanecer en Application.
94. Auditoría
Eventos funcionales V1:
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:
pending → processing
lock acquired
lease recovered
95. Estado auditable de occurrence
Debe incluir al menos:
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:
usuario
sistema
97. API HTTP
Base:
/recurring-proformas
Todas las operaciones de creación relacionadas con recurrencias quedan bajo este recurso.
98. CRUD
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
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
GET /recurring-proformas/:recurring_proforma_id/occurrences
GET /recurring-proformas/:recurring_proforma_id/occurrences/:occurrence_id
101. Acciones manuales
POST /recurring-proformas/:recurring_proforma_id/generate-next-occurrence
POST /recurring-proformas/:recurring_proforma_id/occurrences/:occurrence_id/retry
102. Creación desde Proforma
POST /recurring-proformas/from-proforma/:proforma_id
103. Duplicación
POST /recurring-proformas/:recurring_proforma_id/duplicate
104. DTO CREATE
POST /recurring-proformas puede contener solamente:
{
"name": "Mantenimiento mensual"
}
El backend crea:
status = draft
No acepta:
id
company_id
status
next_occurrence_date
created_at
updated_at
105. Semántica PATCH
Regla general:
omitido
→ no modificar
null
→ ausencia explícita / quitar override cuando aplique
valor
→ configurar explícitamente
Ejemplo:
{
"payment_method_id": null
}
significa:
eliminar override
→ volver a herencia
106. PATCH collections
items omitido
→ no tocar
items enviado
→ reemplazo completo
No introducir actualización granular de items.
107. DTO Schedule
Shape conceptual:
{
"start_date": "2026-09-01",
"end_date": null,
"unit": "month",
"interval": 1,
"monthly_rule": "same_day"
}
En PATCH el objeto puede modificarse parcialmente.
Ejemplo:
{
"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:
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:
valor configurado
valor actualmente resuelto
Ejemplo conceptual:
{
"payment_method": {
"configured_id": null,
"resolved": {
"id": "...",
"name": "Transferencia"
}
}
}
No es necesario un campo redundante:
source = inherited
porque puede derivarse de configured_id.
110. Listado HTTP
Filtros V1:
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:
name
description
y por Customer si el Finder actual permite hacerlo de forma consistente con otros listados.
111. Ordenación
Campos útiles:
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:
Proformas recurrentes
No usar:
Facturación recurrente
porque V1 no emite facturas automáticamente.
113. Navegación
Dentro de facturación de clientes:
Facturación de clientes
├─ Proformas
├─ Proformas archivadas
├─ Proformas recurrentes
├─ Proformas recurrentes archivadas
└─ Facturas emitidas
114. Rutas frontend
Conceptualmente:
/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:
Nombre
Cliente
Estado
Frecuencia
Próxima generación
Incidencias
Última actualización
Acciones
116. Filtro segmentado
Utilizar un control equivalente a:
ProformaStatusSegmentedFilter
Estados operativos:
Todas
Borrador
Activas
Pausadas
Completadas
archived no aparece ahí.
Las archivadas tienen entrada propia en menú lateral.
117. Archivadas
Ruta:
/recurring-proformas/archived
Debe reutilizar exactamente el mismo listado y tabla con filtro fijo:
status = archived
118. Incidencias
Una incidencia NO es estado de RecurringProforma.
Visualmente:
Activa + Incidencia
hasIssue deriva de una occurrence failed.
Debe existir filtro:
Todas
Con incidencias
Sin incidencias
119. Editor
Secciones recomendadas:
Identificación interna
Cliente
Contenido comercial
Condiciones heredables
Calendario
120. Identificación interna UI
Campos:
Nombre
Descripción interna
Debe indicarse que:
description
es exclusivamente interna y no se copia a Proformas.
121. UI de herencia
Debe distinguir claramente:
Heredar
vs
override explícito
Ejemplo:
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:
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:
GET activation-readiness
Si existen blockers:
→ mostrar lista funcional traducida
El backend vuelve a validar igualmente durante la transición.
124. Historial UI
Tabla de occurrences:
Fecha programada
Estado
Proforma
Factura emitida
Intentos
Último intento
Acciones
125. Estados occurrence en UI
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:
Occurrence.status = failed
Debe indicar que:
el retry utiliza la configuración actual de la recurrencia
No existe acción:
Skip
129. Pause UI
Debe advertir:
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:
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:
(recurringProformaId, scheduledDate)
debe producir:
1 occurrence
1 Proforma
Concurrencia
Dos workers sobre la misma recurrencia:
→ 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:
scheduledDate
generationDate
EndDate
Debe completar automáticamente al agotarse el calendario.
Delete
Solo:
draft + sin occurrences
From Proforma
Debe copiar únicamente configuración comercial válida y respetar procedencia cuando sea demostrable.
Duplicate
Debe crear:
nuevo UUID
draft
sin occurrences
sin nextOccurrenceDate
134. Criterio E2E principal de aceptación
V1 debe demostrar correctamente este escenario:
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:
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:
claridad semántica
>
abstracción anticipada
136. Regla arquitectónica final
La separación funcional debe mantenerse:
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:
RecurringProforma
no calcula fiscalidad ni emite facturas.
RecurringProformaOccurrence
no sigue el lifecycle posterior de la Proforma.
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:
explícita
auditable
idempotente
recuperable
tenant-scoped
fiscalmente segura
sin emisión automática
La característica debe priorizar:
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.