Backend — Estado actual
Consolida y reemplaza
docs/architecture/architecture-plan.md,docs/billing/facturapi.md,docs/billing/stripe.mdydocs/migrations/rds-migration-plan.md(eliminados). Verificado contra el código el 2026-07-13. Pendientes enPROXIMOS_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.
Arquitectura (Hexagonal + Event-Driven)
Sección titulada «Arquitectura (Hexagonal + Event-Driven)»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).
Billing (Stripe)
Sección titulada «Billing (Stripe)»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).
Facturación CFDI (Facturapi)
Sección titulada «Facturación CFDI (Facturapi)»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_SECRETreal generado víarandom_passworden Terraform.invoices/stamp: guard atómico DRAFT/ERROR → PENDING, validatotal > 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.tsxreemplaza la creación manual;Invoice.tsx/InvoiceList.tsxcon botones de completar/timbrar/reintentar según estado. - Flujo final: toda factura nace
DRAFTautomá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)»CreateTripUseCaseya no ocupa conductor/vehículo al crear el viaje — soloUpdateTripStatusUseCaselo hace, y solo al transicionar aEN_PROCESO(vía el nuevo servicio compartidoshared/domain/services/trip_transitions.py::transition_trip_status).- Nueva lambda
events/trip-auto-start(EventBridgerate(3 minutes)) transiciona automáticamente aEN_PROCESOlos viajesNO_INICIADOcuyaStartDateya 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).
Alertas (nuevo, sesión 2026-07-13)
Sección titulada «Alertas (nuevo, sesión 2026-07-13)»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}#PAYMENTSGSI1SK={paidAt}#{paymentId}— listado cronológico team-wide para Reportes/Cobranza (filtro porclientIden Python, mismo tradeoff aceptado queDynamoDBInvoiceRepository.find_all).
Lambdas (lambdas/payments/):
create(Powertools +use_case.py, patróntrips/create) —POST /teams/{teamId}/trips/{tripId}/payments, publica eventoPaymentRecorded(sin consumer todavía,SQSEventBusno-opea si no hay cola mapeada).list-by-trip—GET /teams/{teamId}/trips/{tripId}/payments, retorna pagos +paymentStatusya resuelto.list—GET /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, calculacompute_trip_payment_status, y crea una alerta (AlertRepository, reusada sin cambios) por viajeATRASADO. El dedup de 24h es por viaje:alert_type = f"PAYMENT_OVERDUE#{tripId}", aprovechando queexists_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-client—GET /teams/{teamId}/reports/revenue?from=&to=&clientId=→{ totalFacturado, totalCobrado, saldoPendiente, cobrosPorMes, topClientesPendientes, payments }. Usado por Reportes.collections-summary—GET /teams/{teamId}/reports/collections?clientId=&status=→{ rows, count, totalPendiente, totalAtrasado }, reusacompute_trip_payment_statuspor 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.