






Royal Parking Services
Sitio de control de estacionamiento y portal de permisos
- Rol
- Full-stack Engineer
- Tipo de trabajo
- Colaboración con equipo
- Industria
- Gestión de estacionamientos • Servicios inmobiliarios
Resumen
El desafío
Enlaces
Privado — proyecto de cliente
Arquitectura del sistema
La aplicación es un único despliegue de Next.js App Router que atiende a tres públicos desde un mismo código base, separados por route groups y no por aplicaciones distintas: un sitio de marketing público con checkout de pago anónimo, los flujos de credenciales en su propio layout a pantalla completa, y el portal de clientes autenticado con su propio shell con barra lateral. Los tres comparten un único cliente HTTP, un único store de Redux y un único design system en Tailwind, con excepciones para rutas públicas allí donde el checkout anónimo no debe heredar el comportamiento autenticado.
Arquitectura de la aplicación
- Next.js 15 App Router con tres route groups: público, credenciales y dashboard privado.
- Server Components por defecto para las páginas de marketing y de artículos, con los componentes de cliente aislados en las superficies interactivas.
- Una única instancia compartida de axios con interceptores de request y response, más una allowlist de rutas públicas para que el checkout anónimo se saltee tanto la inyección del token como las redirecciones por 401.
- Una máquina de estados de checkout de cuatro pasos del lado del cliente, contenida en un único objeto de estado tipado, con una ruta de retorno de 3-D Secure aparte que reconstruye el estado a partir de los parámetros de la URL.
- Contenido híbrido: CMS con ISR para el blog y módulos TypeScript versionados en el repo para los recursos y los textos de servicios.
Funcionalidades de la plataforma
- Búsqueda pública de actas por patente o por número de acta, mediante un único término de búsqueda unificado.
- Selección de varias actas con total en vivo y filtrado de las que se pueden pagar según su estado.
- Visualización de las reglas de cada playa con fotos de la cartelería, deduplicadas por playa.
- Emisión de permisos con programas horarios por día de la semana y un listado dinámico de vehículos.
- Verificación de disponibilidad del número de permiso del lado del servidor, con debounce.
- Tarjetas guardadas con selección de tarjeta predeterminada y comprobantes de factura alojados por Stripe.
- Formulario de contacto con normalización de números de teléfono y un mapa importado dinámicamente.
Modelo de datos
- Acta: un ciclo de vida de doce estados, cuatro campos de monto distintos, vencimiento de la reducción, marca de reincidente, y relaciones con el inspector, la infracción, la playa, las imágenes y las reglas de la playa.
- Permiso: tipo, período de vigencia, titular, valor, rango de fechas, relación con la playa, array de programas horarios y array de vehículos.
- Company como raíz del tenant, con límites contractuales, campos de precios, período de facturación y perfil de administrador.
- Invoice y PaymentMethod, respaldados por Stripe, con URLs de comprobante y metadata de la tarjeta.
- Un envelope paginado consistente en todos los endpoints de listado.
Autenticación
- JWT Bearer
- Interceptores de Axios
- Logout automático ante 401
- Route guard
- Desambiguación de superficies
- Sanitización de open redirects
- Recuperar / restablecer contraseña
- Excepciones para rutas públicas
Infraestructura y deploy
Self-hosted en un VPS Linux detrás de PM2, desplegado por Bitbucket Pipelines vía SSH, con URLs de backend, credenciales del CMS y claves de Stripe separadas por entorno.
Hosting y despliegue
- Proceso de Node 20 gestionado por PM2 que sirve el servidor de producción de Next.js en un puerto dedicado.
- Bitbucket Pipelines disparado por rama: main despliega producción y develop despliega staging, y cada uno envía por SSH un script de despliegue a una cuenta de deployer.
- El script de despliegue regenera el archivo de entorno desde cero en cada despliegue, instala dependencias, compila con un límite de heap ampliado y después reinicia o arranca el proceso en PM2.
Secretos y configuración
- Todos los valores salen de las deployment variables de Bitbucket, y cada uno se valida como no vacío antes de escribirse.
- El script se refactorizó para pasar las variables por nombre a través del entorno de SSH en lugar de por posición, después de que el aplanado de argumentos de SSH hiciera que un solo valor vacío corriera todos los argumentos siguientes y corrompiera el entorno en silencio.
- El flag de entorno definido al desplegar distingue en runtime el comportamiento de producción del de staging.
Entrega
- Optimización de imágenes de Next.js con AVIF y WebP, y una allowlist de remote patterns restringida al host de uploads del CMS.
- Una instancia de CMS compartida entre varios proyectos, en la que este sitio acota sus consultas por el slug del proyecto.
- Logs de procesos de PM2 más un directorio de logs de despliegue por aplicación.
Construido con
Conviven dos integraciones de Stripe, cada una adecuada a su flujo. El checkout anónimo del conductor usa PaymentIntents con el Payment Element; el portal autenticado guarda tarjetas a través de la API de tokenización. En ambos casos, la autoridad sobre el monto está íntegramente del lado del servidor.
Checkout público
- Pago único sobre una o más actas de infracción pendientes: sin suscripciones, sin marketplace, sin pagos divididos.
- El endpoint que crea el intent recibe los identificadores de las actas y la patente, pero ningún monto; el client secret devuelto monta el Payment Element.
- El intent se restringe a tarjeta del lado del servidor, lo que suprime los prompts de billeteras y las ofertas de guardar la tarjeta; es una decisión intencional para un pago único anónimo.
- La moneda es CAD, formateada para el locale en-CA, con el locale de Stripe Elements fijado para evitar que el idioma del navegador se filtre en un sitio que está sólo en inglés.
Autenticación reforzada del cliente (SCA)
- La confirmación corre con redirect-if-required: los pagos sin redirección se resuelven en el lugar, y los que requieren redirección aterrizan en una ruta de retorno dedicada.
- La ruta de retorno recupera el PaymentIntent para obtener el estado canónico en lugar de confiar en el parámetro de estado de la redirección.
- El endpoint de confirmación es idempotente por contrato del backend, así que tanto el camino sincrónico como el de retorno de 3-D Secure pueden llamarlo sin riesgo.
- La confirmación vuelve a validar los identificadores de las actas y la patente del lado del servidor, de modo que los parámetros que viajan por la URL de retorno explícitamente no son una frontera de confianza.
Estados de la orden y tarjetas guardadas
- El estado de la orden lo determina el ciclo de vida del acta: nueva, pago parcial y pagada en su totalidad, más estados de recordatorio y de apelación.
- Tres estados no se pueden pagar online y se filtran antes de la selección.
- Resultado: número de confirmación más una URL opcional de comprobante alojado, tanto en el paso de éxito en el lugar como en la página de retorno.
- Las tarjetas guardadas del portal usan tokenización sobre un card element y sólo envían el token: los números de tarjeta nunca llegan al backend de la aplicación.
- Un tema de appearance de Stripe personalizado y alineado con la paleta de la marca en ambos flujos.
Sitio público y checkout
- Sitio de marketing: Home con contadores de estadísticas animados y carruseles de funcionalidades, Services con secciones enlazadas por anclas, una página dedicada al pago móvil, About, Resources, Blog, detalle de artículo, Contact, Privacy Policy y Terms of Use.
- Checkout de pago público: un wizard de cuatro pasos más una ruta de retorno dedicada para 3-D Secure, con visualización de evidencia, selección de varias actas y el Payment Element de Stripe.
Portal de clientes
- Flujos de inicio de sesión, recuperación de contraseña y restablecimiento de contraseña.
- Formulario de emisión de permisos con programas horarios y registro de múltiples vehículos.
- Registro de permisos con búsqueda por patente, diálogos de detalle, edición y cancelación, más estadísticas de cantidad de vehículos.
- Gestión de tarjetas guardadas, historial de facturas con comprobantes alojados y configuración de la empresa.
Contenido y design system
- Cinco artículos de recursos extensos y cuatro posts de blog escritos como datos estructurados en TypeScript con un modelo de bloques tipado, renderizados por un único componente de artículo compartido que también renderiza el HTML que viene del CMS.
- Una capa de tokens de Tailwind propia sobre daisyUI, con primitivas reutilizables de banner, FAQ, paginación y select, un loader para las transiciones de ruta y un botón para volver arriba.
- Integraciones entregadas: Stripe en dos flujos, el CMS, un widget de chat que se oculta según la ruta y un mapa interactivo.