# 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. ## 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: ```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 ``` 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.