NutriTrack SaaS / subscriptions
Minado: 2026-06-18 commit a3f91bc spec-miner v1.0
Spec minado automáticamente desde el código existente. Esta es la línea base (baseline) del módulo subscriptions. Los futuros cambios usarán bloques ADDED / MODIFIED / REMOVED sobre estos requisitos. Anclas de delta:
Requisito Crear suscripción en modo trial SubscriptionService.create_subscription
📦 Subscription 📦 Plan 📦 Clinic ⚓ service.create_subscription() ▶ triggers: Enviar email de bienvenida
Una clínica sin suscripción activa SHALL poder crear una nueva suscripción. Si el plan tiene trial_days > 0, la suscripción MUST comenzar en estado TRIALING con trial_ends_at = now + trial_days. Si trial_days == 0, inicia directamente en ACTIVE. El sistema MUST enviar el email de bienvenida tras guardar la suscripción.
Escenarios
Plan con periodo de prueba (14 días por defecto) test_create_subscription_starts_trialing
WHEN La clínica no tiene suscripción activa y se crea con un plan cuyo trial_days = 14
THEN La suscripción se persiste con status = TRIALING, trial_ends_at = now + 14 days, seats_dietitians = 1, seats_patients = 0
Plan sin periodo de prueba
WHEN La clínica no tiene suscripción activa y se crea con un plan cuyo trial_days = 0
THEN La suscripción se persiste con status = ACTIVE y trial_ends_at = None
Clínica ya tiene suscripción activa test_create_subscription_duplicate_raises
WHEN La clínica ya posee una suscripción en estado ACTIVE o TRIALING
THEN Se lanza SubscriptionError("Clinic already has an active subscription")
Requisito Cobrar factura SubscriptionService.charge_invoice
📦 Invoice 📦 Subscription ⚓ service.charge_invoice() ⬅ depends_on: Generar factura del periodo
El sistema SHALL intentar cobrar una factura impagada a través del payment_gateway. La operación es idempotente: si invoice.paid = True MUST retornar sin cargo adicional. Cada intento incrementa attempt_count. Si el cobro tiene éxito, el periodo avanza 30 días y la suscripción vuelve a ACTIVE si estaba en PAST_DUE. Si el cobro falla y attempt_count >= 3, la suscripción pasa a PAST_DUE y se envía notificación.
Escenarios
Cobro exitoso — primer intento test_charge_invoice_success
WHEN invoice.paid = False y el payment_gateway retorna result.success = True
THEN invoice.paid = True, invoice.paid_at = now; current_period_start y current_period_end avanzan 30 días
Cobro fallido — máximo de intentos alcanzado test_charge_invoice_max_attempts_past_due
WHEN El payment_gateway falla y invoice.attempt_count alcanza 3
THEN sub.status = PAST_DUE y se envía email de pago fallido a la clínica
Factura ya cobrada (idempotencia) test_charge_invoice_idempotent
WHEN invoice.paid = True
THEN Se retorna la factura sin llamar al payment_gateway ni modificar nada
Requisito Upgrade de plan SubscriptionService.upgrade_plan
📦 Subscription 📦 Plan ⚓ service.upgrade_plan()
Una suscripción activa SHALL poder cambiar a un plan de tier superior (STARTER < PRO < CLINIC). El nuevo plan MUST tener un índice de tier estrictamente mayor que el actual. Tras el cambio se envía email de confirmación.
Escenarios
Upgrade válido (STARTER → PRO) test_upgrade_plan_ok
WHEN La suscripción está en STARTER y se solicita cambio a PRO
THEN sub.plan_id se actualiza, se persiste y se envía send_plan_upgraded
Upgrade inválido — mismo tier o inferior test_upgrade_plan_same_tier_raises
WHEN El nuevo plan tiene tier igual o inferior al actual
THEN Se lanza SubscriptionError("upgrade_plan requires a higher-tier plan")
Invariante Suscripción activa única por clínica SubscriptionService.create_subscription
📦 Subscription 📦 Clinic ⚓ service.create_subscription() ✓ test_create_subscription_duplicate_raises
Una clínica MUST tener como máximo una suscripción en estado ACTIVE o TRIALING en cualquier momento. El sistema impone esta restricción verificando db.get_active_subscription(clinic_id) antes de crear.
✓ Última verificación: 2026-06-18 · a3f91bc
Invariante Factura no cobra dos veces SubscriptionService.charge_invoice
📦 Invoice ⚓ service.charge_invoice() ✓ test_charge_invoice_idempotent
charge_invoice es idempotente: si invoice.paid = True, el payment_gateway MUST NOT ser llamado y invoice.attempt_count MUST NOT incrementarse.
✓ Última verificación: 2026-06-18 · a3f91bc
Invariante Periodo de facturación siempre avanza SubscriptionService.charge_invoice
📦 Subscription 📦 Invoice ⚓ service.charge_invoice()
Tras un cobro exitoso, sub.current_period_end MUST ser siempre mayor que el current_period_end anterior. El sistema incrementa en exactamente 30 días; nunca retrocede ni se resetea a una fecha pasada.
✓ Última verificación: 2026-06-18 · a3f91bc