Base de datos

pakAG usa MySQL (nombre de base de datos erronka segun schema.sql; .env.example lo documenta como pakag, ver riesgos conocidos).

Fuente de verdad del esquema: schema.sql en la raiz del monorepo.

Tablas

addresses

Direcciones de entrega geocodificadas. Se crean en el flujo de creacion de paquetes (Google Geocoding API).

ColumnaTipoNotas
idINT UNSIGNED AUTO_INCREMENTPK
streetVARCHAR(255)
cityVARCHAR(100)
postal_codeVARCHAR(20)
countryVARCHAR(100)Default 'Espana'
latitudeDECIMAL(10,7)Geocodificado por Google API
longitudeDECIMAL(10,7)Geocodificado por Google API
created_atTIMESTAMP

[!NOTE] Las consultas SQL aliasan latitude/longitude como lat/lng en los tipos TypeScript. Los nombres de columnas en BD difieren de los nombres de campos en los tipos del frontend.


users

Usuarios del sistema: admins y repartidores.

ColumnaTipoNotas
idINT UNSIGNED AUTO_INCREMENTPK
nameVARCHAR(150)
emailVARCHAR(255) UNIQUE
password_hashVARCHAR(255)bcrypt
roleENUM('admin','distributor')Default 'distributor'
is_activeBOOLEANDefault TRUE. Los usuarios nuevos se crean inactivos mediante el flujo de activacion
created_atTIMESTAMP
updated_atTIMESTAMPSe actualiza automaticamente al cambiar la fila

packages

Entidad principal del dominio. Representa un paquete a entregar.

ColumnaTipoNotas
idINT UNSIGNED AUTO_INCREMENTPK
tracking_codeVARCHAR(50) UNIQUECodigo corto legible, por ejemplo PAK-20260001
recipient_nameVARCHAR(150)
recipient_emailVARCHAR(255)Recibe emails de notificacion de estado
weight_kgDECIMAL(6,3)
descriptionTEXT NULLOpcional
statusENUM(...)Ver maquina de estados abajo
estimated_deliveryDATE NULLSe define al crear ruta; se actualiza al optimizar la ruta
address_idINT UNSIGNEDFK -> addresses.id
assigned_toINT UNSIGNED NULLFK -> users.id. ON DELETE SET NULL
created_byINT UNSIGNEDFK -> users.id
created_atTIMESTAMP
updated_atTIMESTAMP

Maquina de estados de paquete

pending -> assigned -> in_transit -> delivered
                           -> undelivered
                           -> failed

Cada transicion genera una entrada en package_status_logs y un email al destinatario. Consulta Modelo de dominio para la tabla completa de efectos secundarios.


tokens

Tabla unica para todos los tipos de token del sistema.

ColumnaTipoNotas
idINT UNSIGNED AUTO_INCREMENTPK
tokenVARCHAR(512) UNIQUEValor opaco del token (hex de 32 bytes o cadena JWT)
typeENUM(...)refresh_token, tracking_token, reset_pwd_token, activate_account_token
user_idINT UNSIGNED NULLFK -> users.id. Definido para refresh_token; ON DELETE CASCADE
package_idINT UNSIGNED NULLFK -> packages.id. Definido para tracking_token; ON DELETE CASCADE
expires_atTIMESTAMP
revokedBOOLEANDefault FALSE. Se revoca al usarlo o cerrar sesion
created_atTIMESTAMP

Vida de los tokens (configurada por env):

  • refresh_token: JWT_REFRESH_EXPIRES_DAYS (default 7 dias)
  • tracking_token: TRACKING_EXPIRES_DAYS (default 30 dias)
  • reset_pwd_token / activate_account_token: 24 horas (hardcodeado)

package_status_logs

Historial completo de cada cambio de estado de paquete.

ColumnaTipoNotas
idINT UNSIGNED AUTO_INCREMENTPK
package_idINT UNSIGNEDFK -> packages.id. ON DELETE CASCADE
old_statusENUM NULLNULL para la asignacion inicial de estado
new_statusENUM
changed_byINT UNSIGNEDFK -> users.id
notesTEXT NULL
changed_atTIMESTAMP

routes

Una ruta de entrega por repartidor y dia natural (forzado por constraint unico).

ColumnaTipoNotas
idINT UNSIGNED AUTO_INCREMENTPK
user_idINT UNSIGNEDFK -> users.id (debe ser distributor)
route_dateDATE
statusENUM('planned','in_progress','completed')Default 'planned'
created_atTIMESTAMP
updated_atTIMESTAMP

Constraint unico: (user_id, route_date): una ruta por repartidor y dia.


route_stops

Paradas individuales dentro de una ruta. Una parada = una entrega de paquete.

ColumnaTipoNotas
idINT UNSIGNED AUTO_INCREMENTPK
route_idINT UNSIGNEDFK -> routes.id. ON DELETE CASCADE
package_idINT UNSIGNEDFK -> packages.id. ON DELETE CASCADE
stop_orderSMALLINTOptimizado por Google Directions API
estimated_arrivalTIME NULLCalculado al crear la ruta
actual_arrivalTIME NULLDefinido cuando el repartidor marca llegada
created_atTIMESTAMP

Constraint unico: (route_id, stop_order): dos paradas no pueden compartir el mismo orden en una ruta.


Relaciones

users --- packages (assigned_to, created_by)
users --- routes (user_id)
users --- package_status_logs (changed_by)
users --- tokens (user_id)

packages --- addresses (address_id)
packages --- route_stops (package_id)
packages --- package_status_logs (package_id)
packages --- tokens (package_id, para tracking_token)

routes --- route_stops (route_id)

Indices

CREATE INDEX idx_packages_status      ON packages (status);
CREATE INDEX idx_packages_assigned_to ON packages (assigned_to);
CREATE INDEX idx_packages_created_at  ON packages (created_at);
CREATE INDEX idx_tokens_type_expires  ON tokens (type, expires_at, revoked);
CREATE INDEX idx_logs_package_date    ON package_status_logs (package_id, changed_at);
CREATE INDEX idx_routes_date          ON routes (route_date);

Convenciones SQL

  • Todas las consultas usan prepared statements (mysql2 con placeholders ?). Sin concatenacion de strings.
  • IDs: INT UNSIGNED AUTO_INCREMENT PRIMARY KEY.
  • Timestamps: TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP.
  • No hay soft deletes en packages; el estado historico se conserva en package_status_logs.
  • addresses no se eliminan cuando se elimina un paquete (Needs verification: no hay ON DELETE CASCADE en la FK packages.address_id).

Riesgos conocidos

RiesgoDetalle
Inconsistencia de nombre de BDschema.sql crea la BD como erronka; .env.example documenta MYSQL_DATABASE=pakag. Verifica que nombre se usa en produccion antes de migrar.
Sin validacion al arranqueMYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE pueden ser undefined al arrancar sin error. El pool falla silenciosamente en la primera consulta.