FactuGES_ERP/docs/recurring-proformas/DOCUMENTO DE DISEÑO — recurring-proformas.md

47 KiB
Raw Blame History

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

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:

  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:

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

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;
  • nextOccurrenceDate avanzado 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.