Domain Model

This page describes the full data model of pakAG as defined in schema.sql. All seven tables, their fields, foreign keys, and the business rules encoded in the application layer are documented here.


Entity overview

addresses ──────────────────────────────────────┐
                                                 │ address_id
users ──┬──────────────────────────────────── packages ──┬── package_status_logs
        │ assigned_to / created_by                       │
        │                                                 │ package_id
        └── routes ──── route_stops ───────────┘
              │ route_id    │ package_id
              └─────────────┘
                                    tokens (user_id OR package_id)
users          1 ──< routes          (one user, many routes — one per date)
routes         1 ──< route_stops     (one route, many stops)
packages       1 ──< route_stops     (one package, appears in many stops across routes)
packages       1 ──< package_status_logs
packages       1 ──  addresses       (each package has one delivery address)
packages       1 ──< tokens          (tracking token per package)
users          1 ──< tokens          (refresh / reset / activation tokens per user)
users          1 ──< packages        (assigned_to — distributor)
users          1 ──< packages        (created_by — admin)

Entities

addresses

Stores geocoded delivery addresses. Coordinates are resolved at package creation time via the Google Geocoding API.

ColumnTypeNullableNotes
idINT UNSIGNEDnoAuto-increment primary key
streetVARCHAR(255)noStreet name and number
cityVARCHAR(100)noCity name
postal_codeVARCHAR(20)noPostal/ZIP code
countryVARCHAR(100)noDefaults to 'España'
latitudeDECIMAL(10,7)noGeocoded latitude
longitudeDECIMAL(10,7)noGeocoded longitude
created_atTIMESTAMPnoRow creation time

[!NOTE] Application code aliases latitude/longitude to lat/lng with SQL AS clauses. When writing new queries, always use latitude AS lat to stay consistent with TypeScript types.


users

Represents both admins and distributors. The role column is the single source of truth for authorization.

ColumnTypeNullableNotes
idINT UNSIGNEDnoAuto-increment primary key
nameVARCHAR(150)noDisplay name
emailVARCHAR(255)noUnique; used for login
password_hashVARCHAR(255)nobcrypt hash
roleENUM('admin','distributor')noDefault 'distributor'
is_activeBOOLEANnoDefault TRUE. New users created by admin start as FALSE until they activate their account
created_atTIMESTAMPno
updated_atTIMESTAMPnoAuto-updated on any change

Roles

RoleDescription
adminFull access: manage users, packages, routes, view logs, see daily summaries
distributorLimited access: see own assigned packages and daily route, update package status, record stop arrivals

Business rule: An admin cannot delete themselves (DELETE /api/users/remove enforces this check in the service layer).

Account activation flow

1. Admin creates user (is_active = FALSE, password = DEFAULT_USER_PASSWORD hashed)
2. Backend generates activate_account_token (24h TTL)
3. Email sent to user with activation link
4. User submits new password → is_active = TRUE, token revoked

packages

The central entity. A package moves through a defined lifecycle and is optionally assigned to a distributor and placed on a route.

ColumnTypeNullableNotes
idINT UNSIGNEDnoAuto-increment primary key
tracking_codeVARCHAR(50)noUnique short code, e.g. PAK-20260001
recipient_nameVARCHAR(150)noName of the recipient
recipient_emailVARCHAR(255)noEmail used for delivery notifications
weight_kgDECIMAL(6,3)noWeight in kilograms
descriptionTEXTyesOptional package contents description
statusENUM(...)noCurrent lifecycle status (see below)
estimated_deliveryDATEyesSet when the package is added to a route
address_idINT UNSIGNEDnoFK → addresses.id
assigned_toINT UNSIGNEDyesFK → users.id (distributor); NULL when unassigned. SET NULL on user delete
created_byINT UNSIGNEDyesFK → users.id (admin); SET NULL on user delete
created_atTIMESTAMPno
updated_atTIMESTAMPnoAuto-updated

Package status lifecycle

                ┌──────────────────────────────────────────┐
                │                                          │
  [created] → pending → assigned → in_transit → delivered │
                                        │                  │
                                        ├──→ undelivered   │
                                        │                  │
                                        └──→ failed        │

                └──────────────────────────────────────────┘
StatusMeaningWho sets it
pendingPackage created, not yet assigned to a distributorSystem (on create)
assignedAssigned to a distributor and added to a routeAdmin (route creation)
in_transitDistributor has started the route for that dayDistributor
deliveredSuccessfully delivered to recipientDistributor
undeliveredDelivery attempted but failed; may retryDistributor
failedFinal failure — no further delivery attemptsDistributor

Side effects on status change (handled by packageStatusSideEffects.service.ts):

TransitionEmail sent
pending → assigned”Your package has been assigned” to recipient
assigned → in_transit”Your package is on the way” to recipient
in_transit → delivered”Your package has been delivered” to recipient
in_transit → undelivered”Delivery attempt failed” to recipient
in_transit → failed”Delivery failed” to recipient

Every transition is also recorded as a row in package_status_logs.


tokens

A unified token table handling four distinct token types. Each token is an opaque hex string.

ColumnTypeNullableNotes
idINT UNSIGNEDnoAuto-increment primary key
tokenVARCHAR(512)noUnique opaque token value (hex, 32 bytes)
typeENUM(...)noOne of four types (see below)
user_idINT UNSIGNEDyesFK → users.id. Used for refresh_token, reset_pwd_token, activate_account_token. CASCADE delete
package_idINT UNSIGNEDyesFK → packages.id. Used for tracking_token. CASCADE delete
expires_atTIMESTAMPnoExpiry time; checked on every token lookup
revokedBOOLEANnoDefault FALSE; set to TRUE on use or logout
created_atTIMESTAMPno

Token types

TypeLinked toTTLPurpose
refresh_tokenuser_idJWT_REFRESH_EXPIRES_DAYS daysRotated on each /api/auth/refresh call; revoked on logout
tracking_tokenpackage_idTRACKING_EXPIRES_DAYS daysPublic opaque token used in GET /api/tracking/[token]
reset_pwd_tokenuser_id24 hoursSent via email for password reset
activate_account_tokenuser_id24 hoursSent via email to activate a newly created account

[!NOTE] Tracking tokens are public — they are embedded in emails sent to recipients and can be shared freely. They reveal only package status and address, not internal IDs.


package_status_logs

Immutable audit log of every package status change.

ColumnTypeNullableNotes
idINT UNSIGNEDnoAuto-increment primary key
package_idINT UNSIGNEDnoFK → packages.id. CASCADE delete
old_statusENUM(...)yesPrevious status; NULL for the initial creation log
new_statusENUM(...)noStatus after the change
changed_byINT UNSIGNEDyesFK → users.id; SET NULL on user delete
notesTEXTyesOptional free-text note from the actor
changed_atTIMESTAMPnoTime of the change

Business rule: Rows in this table are never updated or deleted (except via cascading package deletion). They form an append-only audit trail.


routes

A route is a single distributor’s delivery plan for a specific calendar date. Each distributor can have at most one route per day (enforced by UNIQUE KEY uq_route_user_date).

ColumnTypeNullableNotes
idINT UNSIGNEDnoAuto-increment primary key
user_idINT UNSIGNEDyesFK → users.id (distributor). SET NULL on user delete
route_dateDATEnoDelivery date for this route
statusENUM('planned','in_progress','completed')noDefault 'planned'
created_atTIMESTAMPno
updated_atTIMESTAMPnoAuto-updated

Route statuses

StatusMeaning
plannedRoute created by admin; distributor has not started yet
in_progressDistributor has started the route
completedAll stops finished for the day

Business rules:

  • Route creation calls the Google Directions API with optimize:true to reorder stops for minimum travel time.
  • Maximum 20 stops per route.
  • “Carryover” packages — packages with status assigned from previous days belonging to the distributor — are automatically prepended to new routes.
  • If a distributor has unfinished stops from a previous day, GET /api/routes/continueFromPast returns them. POST /api/routes/continueFromPast migrates them into today’s route.

route_stops

Junction table linking a route to its ordered package stops, with arrival time tracking.

ColumnTypeNullableNotes
idINT UNSIGNEDnoAuto-increment primary key
route_idINT UNSIGNEDnoFK → routes.id. CASCADE delete
package_idINT UNSIGNEDnoFK → packages.id. CASCADE delete
stop_orderSMALLINTno1-based position in the optimized route
estimated_arrivalTIMEyesEstimated arrival time at this stop (calculated from Google Maps leg durations)
actual_arrivalTIMEyesRecorded by the distributor when they arrive at the stop
created_atTIMESTAMPno

UNIQUE KEY uq_stop_order (route_id, stop_order) ensures no two stops share the same position in a route.

Business rule: Stop reordering is available via PATCH /api/stops/reorder (admin only). The distributor records their actual arrival time via PATCH /api/stops/updateArrival.


Database indexes

IndexTableColumnsPurpose
idx_packages_statuspackagesstatusFast filtering by status (e.g. daily summary, list by status)
idx_packages_assigned_topackagesassigned_toFast lookup of a distributor’s packages
idx_packages_created_atpackagescreated_atDate-range filtering on package lists
idx_tokens_type_expirestokenstype, expires_at, revokedEfficient token validation queries
idx_logs_package_datepackage_status_logspackage_id, changed_atFast log history per package
idx_routes_dateroutesroute_dateFast lookup of routes by date