Ir al contenido

Backend — Estado actual

Consolida y reemplaza docs/architecture/architecture-plan.md, docs/billing/facturapi.md, docs/billing/stripe.md y docs/migrations/rds-migration-plan.md (eliminados). Verificado contra el código el 2026-07-13. Pendientes en PROXIMOS_PASOS.md.

Catálogos de referencia viva (no se tocan, siguen vigentes): docs/reference/lambdas-reference.md, docs/reference/auth-reference.md, docs/reference/cloud-costs.md, docs/reference/openapi.json.

Migración documentada originalmente en architecture-plan.md. Fases 0-4 completadas (2026-03-17): repositories vía factory functions (shared/adapters/__init__.py), use cases separados de los handlers, AWS Lambda Powertools en el dominio trips, event bus (TripCreated publicado a SQS incondicionalmente — el feature flag ASYNC_EVENTS_ENABLED mencionado en versiones anteriores del plan ya no existe en el código, el modo async es el único modo). Fase 5 (Powertools en el resto de lambdas) pendiente — ver PROXIMOS_PASOS.md.

El flujo de creación de viaje ya coincide con el diagrama “TARGET” del plan original: el handler responde inmediatamente tras publicar TripCreated, y un consumer (events/invoice-drafter) crea el borrador de forma asíncrona — confirmado y con su bug de packaging corregido en la sesión 2026-07-13 (ver abajo).

Ver docs/ESTADO_ACTUAL.md (root) para el resumen completo verificado. En este repo: lambdas/billing/{get-subscription,create-checkout,portal,webhook,list-payments}, shared/stripe_helpers.py (check_subscription_active_pg, PLAN_TRIPS_LIMIT).

Modelo: Lade actúa como ISV/reseller — una organización Facturapi por equipo. User Secret Key (Lade) para gestionar organizaciones; API Key Test/Live por equipo para operar. Decisión de negocio confirmada 2026-07-12: producción siempre usa Live keys.

Estado — plan de cierre completado (2026-07-12):

  • Unificada la selección de API key (get_team_api_key) en creación de borrador.
  • create_live_api_key() agregada e invocada tras CSD exitoso.
  • FACTURAPI_WEBHOOK_SECRET real generado vía random_password en Terraform.
  • invoices/stamp: guard atómico DRAFT/ERROR → PENDING, valida total > 0, notifica a admin en fallo, reintentable desde ERROR.
  • Nuevo lambda invoices/update (PUT /invoices/{id}) para editar el borrador antes de timbrar.
  • Frontend: DialogEditInvoice.tsx reemplaza la creación manual; Invoice.tsx/InvoiceList.tsx con botones de completar/timbrar/reintentar según estado.
  • Flujo final: toda factura nace DRAFT automáticamente al crear el viaje (async); el timbrado es manual, decidido por el usuario del equipo.

Pendiente de acción manual (no completable desde código): registrar el webhook secret en el dashboard de Facturapi; confirmar con una llamada real que el path de create_live_api_key (PUT /organizations/{id}/apikeys, inferido) es correcto.

Corregido en la sesión 2026-07-13: el borrador automático de factura (create_invoice_draft) vivía en lambdas/trips/create/_invoice_draft.py, pero era importado desde lambdas/events/invoice-drafter/handler.py — un import cruzado entre carpetas de lambdas distintas que Terraform empaqueta por separado (source_dir solo incluye la carpeta propia). Esto rompía el consumer con ModuleNotFoundError en cada invocación, y el borrador nunca se creaba. Se movió la lógica a layers/common/python/shared/facturapi_draft.py (disponible para cualquier lambda vía el layer).

Viajes — bloqueo de recursos y auto-inicio (sesión 2026-07-13)

Sección titulada «Viajes — bloqueo de recursos y auto-inicio (sesión 2026-07-13)»
  • CreateTripUseCase ya no ocupa conductor/vehículo al crear el viaje — solo UpdateTripStatusUseCase lo hace, y solo al transicionar a EN_PROCESO (vía el nuevo servicio compartido shared/domain/services/trip_transitions.py::transition_trip_status).
  • Nueva lambda events/trip-auto-start (EventBridge rate(3 minutes)) transiciona automáticamente a EN_PROCESO los viajes NO_INICIADO cuya StartDate ya llegó, usando GSI2 como índice disperso (GSI2PK="TRIP_PENDING_START", GSI2SK=StartDate) — el GSI2 estaba provisionado desde el diseño original de la tabla pero sin ningún uso en el código hasta ahora.
  • Trip también gana campos denormalizados DriverPhotoUrl, VehicleImageUrl, VehicleBrand, VehicleModel (antes el frontend los mostraba vacíos porque nunca se guardaban en el Trip).

shared/adapters/dynamodb/alert_repository.py (PK=TEAM#{teamId}, SK=ALERT#{createdAt}#{alertId}) + lambdas/alerts/{list,mark-read}. Productores: trip-auto-start (alerta TRIP_AUTO_STARTED), invoice-drafter (alerta INVOICE_DRAFT_FAILED en FacturapiError), CreateTripUseCase (alerta TRIPS_LIMIT_REACHED, con guard anti-spam de 24h).

Pagos (Payments) — cobranza por viaje ✅ COMPLETADO (Fase 1 sesión 2026-07-14, Fase 2 sesión 2026-07-15)

Sección titulada «Pagos (Payments) — cobranza por viaje ✅ COMPLETADO (Fase 1 sesión 2026-07-14, Fase 2 sesión 2026-07-15)»

Motivación: no existía ningún concepto de “el cliente ya pagó este viaje” — Trip solo guarda cost/revenue (montos planeados) e Invoice es únicamente el CFDI fiscal ante el SAT (su PaymentMethod/PaymentForm son catálogos SAT, no cobranza real).

Modelo: Payment es un ledger de abonos, no un status en Trip — soporta pagos parciales múltiples por viaje. shared/models/payment.py (Payment, PaymentMethod, PaymentCreateRequest). El estado de cobro (SIN_COBRAR | PARCIAL | PAGADO | ATRASADO) nunca se guarda, se deriva siempre con shared/domain/services/payment_status.py::compute_trip_payment_status(trip, payments, client)dueDate = trip.endDate + client.paymentTerms días.

Persistencia (shared/adapters/dynamodb/payment_repository.py, vía ports/repositories/payment_repository.py + factory get_payment_repository(), mismo patrón hexagonal que trips):

  • PK=TEAM#{teamId} SK=PAYMENT#{tripId}#{paymentId} — listar pagos de un viaje es query directo, sin GSI.
  • GSI1PK=TEAM#{teamId}#PAYMENTS GSI1SK={paidAt}#{paymentId} — listado cronológico team-wide para Reportes/Cobranza (filtro por clientId en Python, mismo tradeoff aceptado que DynamoDBInvoiceRepository.find_all).

Lambdas (lambdas/payments/):

  • create (Powertools + use_case.py, patrón trips/create) — POST /teams/{teamId}/trips/{tripId}/payments, publica evento PaymentRecorded (sin consumer todavía, SQSEventBus no-opea si no hay cola mapeada).
  • list-by-tripGET /teams/{teamId}/trips/{tripId}/payments, retorna pagos + paymentStatus ya resuelto.
  • listGET /teams/{teamId}/payments?clientId=&from=&to=, ledger crudo team-wide (pagina completo vía GSI1).
  • delete — soft delete, solo OWNER/ADMIN.
  • overdue-check (EventBridge diario, 14:00 UTC) — recorre trips de cada equipo, calcula compute_trip_payment_status, y crea una alerta (AlertRepository, reusada sin cambios) por viaje ATRASADO. El dedup de 24h es por viaje: alert_type = f"PAYMENT_OVERDUE#{tripId}", aprovechando que exists_recent() ya filtra por (team_id, alert_type, since).

Deliberadamente fuera de alcance en Fase 1: no se tocó Trip ni Invoice — el CFDI (fiscal) queda separado de la cobranza real a propósito. Frontend (store, tab de cliente, sección “Finanzas”) va en un commit aparte de lade-app.

Fase 2 — Agregación server-side, exportar CSV/PDF, widget de cobranza ✅ COMPLETADO (sesión 2026-07-15)

Sección titulada «Fase 2 — Agregación server-side, exportar CSV/PDF, widget de cobranza ✅ COMPLETADO (sesión 2026-07-15)»

Problema resuelto: Fase 1 calculaba Reportes/Cobranza en el navegador a partir de GET /teams/{teamId}/trips + GET /teams/{teamId}/payments. DynamoDBTripRepository.find_all() hace un solo Query (sin recorrer LastEvaluatedKey) y trips/list/handler.py además trunca a min(limit, 100) — el API nunca devuelve más de 100 trips, sin importar el limit pedido. Para un equipo con más de 100 viajes en el rango de un reporte, los totales client-side ya estaban mal. payments/list no tenía este problema (pagina completo vía GSI1).

Solución: sin nuevo GSI ni cambios de esquema — se replicó localmente (no se extrajo a un helper compartido) el patrón _iter_team_trips que ya existía en overdue-check (bucle sobre find_all(limit=200, last_key=...) hasta agotar last_key), ahora también en client_repo.find_all para no hacer N+1 get_item por trip (aceptable en overdue-check por ser cron desatendido, no en un endpoint invocado desde el navegador).

Lambdas nuevas (lambdas/reports/, mismo estilo sin Powertools que payments/list — solo lectura, sin use_case.py):

  • revenue-by-clientGET /teams/{teamId}/reports/revenue?from=&to=&clientId={ totalFacturado, totalCobrado, saldoPendiente, cobrosPorMes, topClientesPendientes, payments }. Usado por Reportes.
  • collections-summaryGET /teams/{teamId}/reports/collections?clientId=&status={ rows, count, totalPendiente, totalAtrasado }, reusa compute_trip_payment_status por viaje. Usado por Cobranza.

Ambas con memory_size=256, timeout=30 (mayor que el resto de lambdas de payments porque recorren la partición completa del team, no un solo item).

Frontend (lade-app): reportService.ts, Reports.tsx/Collections.tsx reescritos para usar las lambdas de agregación en vez de calcular en el navegador, exportar CSV/PDF (jspdf+jspdf-autotable) en ambas pantallas, y dos KPIs nuevos (kpi-pending-balance, kpi-overdue-balance) disponibles en el selector de widgets del Home (no agregados al layout por defecto).

Deliberadamente fuera de alcance: notificación por email al cliente cuando un pago se atrasa — único punto de la Fase 2 original que el usuario dejó pendiente. overdue-check ya detecta el atraso y genera una alerta interna, pero no dispara ningún email al Client.email.

Rutas — métricas de Google Routes API (nuevo, sesión 2026-07-13)

Sección titulada «Rutas — métricas de Google Routes API (nuevo, sesión 2026-07-13)»

shared/google_routes.py::compute_route_metrics() calcula distancia, duración, casetas (extraComputations: TOLLS) y polyline codificado una sola vez, al crear la ruta (routes/create). Se guarda en el registro de la Ruta (DistanceMeters, DurationSeconds, TollEstimateMxn, EncodedPolyline) vía una API key de servidor nueva (aws_secretsmanager_secret.google_maps_server_key, distinta de la key de cliente del frontend). routes/update no soporta editar stops todavía, así que no hay recálculo en edición (ver PROXIMOS_PASOS.md).

Workflows .github/workflows/{ci,deploy,destroy}.yml existen y siguen el diseño documentado (lint automático, plan→apply por entorno con aprobación manual, destroy manual con confirmación). Gap real: deploy.yml invoca una lambda de migraciones (lade-{env}-migrations) que fue removida cuando el sistema migró de RDS a DynamoDB — ver PROXIMOS_PASOS.md.