Appearance
06 - Master data and constants
Source: wt-v2-prototype-core at commit 3d70475a. Record counts come from the local database tms_local_v2 (11 Nov 2026) and say what the demo data looks like, not what production will hold.
Master data here means reference records that change rarely and that transactions (delivery orders, plans) read from. The companion file 03-schema.md has the full field tables; this file says who creates each item, who reads it and what to know before using it.
Overview
| # | Item | Collection | Who creates / edits | Records locally |
|---|---|---|---|---|
| 1 | Store | stores | Admin API only | 2 |
| 2 | Vehicle type | vehicle_types | Admin and store operator API | 0 |
| 3 | Vehicle | vehicles | Admin API only | 0 |
| 4 | User | users | Boot seed, admin API | 12 |
| 5 | Role | none (fixed list in code) | Code release | 4 roles in code |
| 6 | Application | applications | Boot seed, admin API | 3 |
| 7 | Plan template | plan_templates | Admin and store operator API | 0 |
| 8 | Settings | _settings | Boot-time code defaults, admin API | 6 records |
| 9 | Storage setting | record storage in _settings | Same as settings | 1 record |
| 10 | Template | templates | Admin API | 2 |
| 11 | Export config | export_configs | Admin API | 0 |
| 12 | Field catalog | none (built at boot) | Computed from schemas | not stored |
| 13 | Schedule | schedules | Code declaration plus admin API | 5 (QA rows) |
Nothing in the Kafka intake creates master data. The only Kafka input is the delivery order message (do_consume), and it only reads store to fill the source_store snapshot.
All admin endpoints are under /v1/... and need the role shown in the "API" column of each section.
1. Store
| Question | Answer |
|---|---|
| What it is for | A depot or shop where deliveries start; its opening and closing time bound the planning day. |
| Created or edited by | Admin API: POST /store, PUT /store/:code, GET /store, GET /store/:code (role admin). No seed, no Kafka. |
| Read by | Delivery order intake (fills source_store and source_geo from source_store.code), plan creation (copies { id, code, name, location } into plan.store and computes start_date and end_date), script and export entity store. |
| Records locally | 2 (001 "Store 001", 002 "Store 002") |
| Key fields | code (unique), name, open_time and close_time ("HH:mm"), location (latitude, longitude), loading_bay. |
| Watch out | Changing a store does not update the copies already inside plans or delivery orders. loading_bay is stored but nothing reads it. |
2. Vehicle type
| Question | Answer |
|---|---|
| What it is for | Class of truck: capacity per round and the time it spends loading and unloading. Drives the optimiser. |
| Created or edited by | Admin API: POST /type/vehicle, PUT /type/vehicle/:code, GET (roles admin, store_operator). No seed. |
| Read by | Plan creation (copies capacity, loading_time, unloading_time into each shift; fails if a template names a type that no longer exists), vehicle create / update (snapshot). |
| Records locally | 0 |
| Key fields | code (unique), name, capacity { weight kg, volume m3 }, loading_time (min, default 0), unloading_time (min per order, default 0; the planner uses 20 when this is 0). |
3. Vehicle
| Question | Answer |
|---|---|
| What it is for | A physical truck identified by plate number. |
| Created or edited by | Admin API: POST /vehicle, PUT /vehicle/:code, GET (role admin). No seed. |
| Read by | Only its own API and the script entity vehicle. The planner does not read vehicles yet: plan shifts are built from vehicle types, and shift.vehicle stays empty. |
| Records locally | 0 |
| Key fields | code (required, unique), plate_number, vehicle_type snapshot { id, code, name }. Columns status, type, delivery_support_type, loading_time, unloading_time exist but are never written. |
| Watch out | code is required by the schema but optional in the request; the default-code line is a comparison, so a request without code fails. |
4. User
| Question | Answer |
|---|---|
| What it is for | Login accounts: admins, store operators, drivers, and an internal system actor. |
| Created or edited by | Boot seed (two accounts, below), admin API POST /user (role admin; assignable roles admin, store_operator, driver), PUT /user/me (any signed-in user edits own name; role cannot be changed). Login stamps last_logged_in_at. |
| Read by | Login and token issue (auth service), role guard on every route, activity log actor, plan (driver and actor references), script entity user. |
| Records locally | 12 (8 active admins, 2 active store operators, 1 active driver, 1 internal system user; most are QA accounts) |
| Key fields | user_id (uuid, unique), username (unique), first_name, last_name, role, state, password (hash, hidden in output), system. |
Boot seeds
| Account | Created when | Role / state | Purpose |
|---|---|---|---|
system | Module init, before any other hook | system_admin, inactive, system: true | The actor recorded for work no request started (Kafka consumer, schedule tick, boot). Cannot log in. |
system_admin | Application bootstrap, only if missing | admin, active | First admin. Password comes from the environment variable DEFAULT_SYSTEM_ADMIN_PASSWORD (value not shown here). |
5. Role
| Question | Answer |
|---|---|
| What it is for | Decides which API routes a user may call. |
| Created or edited by | Code only. There is no role collection or role schema in v2: the four roles are the UserRole enum and each controller states its roles with @Roles([...]). |
| Read by | Auth guard, user create validation, application roles validation, activity log actor. |
| Records locally | The local db still has a roles collection with 9 rows (code, name) copied from the v1 clone. v2 code never reads it. Do not treat it as part of the model. |
| Values | See the constants section below. |
6. Application
| Question | Answer |
|---|---|
| What it is for | A client (admin web, driver app, integration) that exchanges its key pair for an access token. Decides which roles may sign in through it and whether 2FA or a registered device is required. |
| Created or edited by | Boot seed creates default if missing; admin API POST /application, PUT /application/:code, PATCH /application/revoke (new secret), DELETE /application/:code. The list hides the default row. |
| Read by | Auth service when issuing a token (looks up by app_key), role check on sign-in. |
| Records locally | 3 (default, qa_app_ac13, qa_app_driver) |
| Key fields | name (unique), code (unique), app_key (unique, 32 chars), secret_key (44 chars; masked in API output, value not shown here), enabled, roles[], enable_2fa, require_device. |
| Boot seed | default: enabled, roles [admin]. Key and secret come from DEFAULT_APP_KEY and DEFAULT_SECRET_KEY when set, otherwise random. |
7. Plan template
| Question | Answer |
|---|---|
| What it is for | Recipe for a plan day: how many trucks of each vehicle type work and between which hours. |
| Created or edited by | Admin and store operator API: POST /template/plan, PUT /template/plan/:code, GET (roles admin, store_operator). No seed. |
| Read by | Plan creation: copies { id, code, name } into plan.plan_template and expands each entry into one shift per truck (amount rows, labelled <vehicle_type>-<n>). |
| Records locally | 0 |
| Key fields | code (uuid generated by service), name, shifts[] = { vehicle_type (code), amount >= 1, start_time, end_time }. |
| Watch out | Editing a template does not change plans already created from it. Deleting a vehicle type breaks plan creation from any template that names it. |
8. Settings (records and keys)
| Question | Answer |
|---|---|
| What it is for | Runtime configuration that operations can change without a deploy: log retention, login OTP, file storage, Kafka connection and topics, Google keys, script limits. |
| How it works | Code declares each record and key with a type and a default. At boot every declared record is inserted if missing (values: {}, rev: 1), then all records are loaded into an in-memory cache on each pod. A key absent from values reads as its code default. A stored value that fails validation is ignored and the code default is used (with a warning). Pods reload every 60 s and on a Redis publish after each save. |
| Created or edited by | Boot code (records and defaults); admin API: GET /settings, GET /settings/:code, PUT /settings/:code (send current rev; a stale rev returns 409), POST /settings/:code/reset (back to code default; encrypted keys cannot be reset). Role admin. Every change writes an activity_logs row; secret changes record only that the key changed. |
| Read by | The owning module through SettingsService.get(key) or getSecret(key). |
| Records locally | 6 (log, authen, storage, kafka, google, script) |
| Secrets | Keys marked secret are encrypted at rest and shown as a mask (••••) by the API. Values are not listed here. |
Setting keys and code defaults
| Record (owner) | Key | Code default | Secret | Used for |
|---|---|---|---|---|
log (log) | activity_logs | { days: 60 } | no | Retention of audit rows. Allowed range 1 to 365 days. |
log | kafka_logs | { days: 60 } | no | Retention of Kafka message log. |
log | google_logs | { days: 30 } | no | Retention of Google call log. |
authen (auth) | enable_universal_otp | false | no | Accept a fixed OTP list at login (test use). |
authen | universal_otp | none | yes | JSON list of accepted codes. |
storage (storage_setting) | max_file_size_mb | 10 (1 to 100) | no | Upload size limit. |
storage | allowed_extensions | [] | no | Extra extensions. |
storage | provider | aws_s3 | no | Only provider supported. |
storage | bucket | "" | no | Empty means "storage not configured" and uploads return 503. |
storage | region | "" | no | |
storage | prefix | "" | no | |
storage | kms_key_id | "" | no | |
storage | credential_mode | iam_role | no | iam_role or static. |
storage | access_key_id | "" | no | Used only in static mode. |
storage | secret_access_key | none | yes | Used only in static mode. |
kafka (kafka) | bootstrap_servers | environment KAFKA_BROKERS split by comma, else localhost:9092 | no | Broker list. |
kafka | authen_type | PLAINTEXT | no | PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL. |
kafka | authen_setting | {} | no | SASL mechanism, username, CA / cert PEM, reject-unauthorised flag. |
kafka | enabled | true | no | Master switch. |
kafka | events | the six declared events (table below) with their default topic and enabled: true | no | Ops can change topic name or switch an event off. An event missing from the list takes the declared default. |
kafka | sasl_password | none | yes | |
kafka | ssl_key_pem | none | yes | |
google (google) | google_map_api_key | "" | no | Browser map key. Not encrypted on purpose: it is returned to any visitor by the public route GET /settings/public. |
google | route_optimization_service_account | none | yes | Service account JSON text for Google Route Optimization. |
script (script_runner) | max_calls | 20 | no | Database calls a script may make per run. |
script | max_docs | 1000 | no | Documents a script may read per run. |
script | wall_ms | 2000 | no | Time limit per run. |
script | heap_mb | 64 | no | Memory limit per run. |
script | export_state_kb | 64 | no | State a script may keep between export batches. |
script | max_bulk_records | 50 | no | Records per bulk call. |
Local drift to know about: the log record also holds kafka_api_logs and google_api_logs keys, and the storage record holds allowed_image_extensions, endpoint and force_path_style. Current code declares none of them, so they are ignored.
9. Storage setting
| Question | Answer |
|---|---|
| What it is for | Where uploaded files go (S3 bucket, region, key prefix, KMS key, credentials) and the upload limits. |
| Created or edited by | It has no schema or collection of its own. It is the settings record storage (owner storage_setting, display name "Storage"); edit it through the settings API above. |
| Read by | StorageSettingService builds the S3 client (rebuilt when the record changes) for the attachment module. S3_ENDPOINT (environment) overrides the host for local testing. Attachment purposes limit file types per use (constants section). |
| Records locally | 1 record in _settings. The old v1 storage_settings collection (1 row) is also present and unused by v2. |
| Key fields | See the storage rows in the settings table. |
10. Template (print, email, SMS)
| Question | Answer |
|---|---|
| What it is for | A document layout rendered from one entity: print sheet, email, or SMS text. |
| Created or edited by | Admin API: GET /templates, GET /templates/:id, POST /templates, PUT /templates/:id (send revision), POST /templates/render (preview). No seed. |
| Read by | Template render service, which builds the context for the chosen entity, runs the template script and fills the body. |
| Records locally | 2 (qa-dispatch-sheet print, qa-sms-fixture sms; both on entity plan) |
| Key fields | code (unique), name, channel, entity, page (paper, orientation, margin, font), subject (email), script, body, revision. |
11. Export config
| Question | Answer |
|---|---|
| What it is for | An admin-defined spreadsheet export for one entity: which columns, number formats, optional script and footer sums. |
| Created or edited by | Admin API: GET /export_configs, GET /export_configs/:id, POST /export_configs, PUT /export_configs/:id (send revision; stale revision returns 409). No seed. |
| Read by | The export run, with the field catalog validating every column path. |
| Records locally | 0 |
| Key fields | name, entity, columns[] (type field, text or script; path, header, format, align, width, footer_sum), script, script_reads, revision. |
| Number formats allowed | #,##0, #,##0.00, #,##0.000 |
12. Field catalog
| Question | Answer |
|---|---|
| What it is for | List of the entities and fields that exports and templates may use, so an admin cannot name a field that does not exist. |
| Created or edited by | Nobody. Built at application bootstrap by reading the mongoose schemas of the registered script entities, dropping internal fields and anything the read API hides (for example user.password). |
| Read by | Export config validation, template validation, admin UI field picker through GET /export_fields and GET /export_fields/:entity (role admin). It can follow a ref, so source_store_id.name is valid. |
| Records locally | Not stored. Entities offered: delivery_order, delivery_order_item, vehicle, plan, user, store (six). |
| Key fields | Per field: name (array children written parent[].child), type, ref, array, system. |
13. Schedule
| Question | Answer |
|---|---|
| What it is for | Recurring jobs (cron). |
| Created or edited by | A feature module declares a schedule in code (code, cron, missed-slot policy, retry count); the boot sync upserts it. Admin API: GET /schedules, enable, disable, set or clear cron_override (role admin). Code owns the cron; admin owns enabled and cron_override. |
| Read by | The schedule ticker (every 15 s), which enqueues a job per due slot. |
| Records locally | 5, all qa_* test rows. No feature module declares a real schedule yet. |
| Key fields | code, cron, cron_override, enabled, missed_slot_policy, retry_count, next_run_at. |
Enum and constant lists that act as master data
These live in code (release to change), but the screens, the Kafka messages and the planner depend on them.
Delivery order
| List | Values | Where used |
|---|---|---|
| Status | PENDING, PLANNED, IN_TRANSIT, DELIVERED, FAILED, CANCELLED | delivery_order.status. A status change enqueues the OMS and staff-app updates. Only PENDING is set by v2 code. |
| Type | DELIVERY, PICKUP, RETURN | delivery_order.type |
| Delivery type | STANDARD, ON_DEMAND | delivery_order.delivery_type |
| Channel codes routed to OMS | DONJAI, CHOKCHAI, HLH | Choose whether do_update_oms is sent (chanel.chanel_code) |
| Channel codes routed to staff app | SPO, OD, HLH | Choose whether do_update_staffapp is sent |
| Running number | DO + 8 digits (counter delivery_order); tasks TK + 8 digits (counter task) | delivery_no, task_no when none supplied |
A DO whose channel code is in neither list is stored but no outbound update is queued (a warning is logged).
Plan
| List | Values | Where used |
|---|---|---|
| Plan status | pending, planned, generated | plan.status (stop edits need planned; generated blocks optimise) |
| Plan operation type | fixed, re_generate | Enum only; not stored on any schema |
| Stop type | DEPARTURE, DELIVERY, RETURN | plan.tasks[].stops[].type |
| Violation codes | EXCEED_SHIFT_WINDOW, EXCEED_WEIGHT, EXCEED_VOLUME, LATE_AT_STOP, STALE_ETA (the type file also names ROUND_OVERLAP, INSUFFICIENT_TURNAROUND) | plan.tasks[].violations[].code |
| Plan history actions | CREATE, RE_PLAN, RECALCULATE, MOVE_STOP, ASSIGN_STOP, UNASSIGN_STOP, RESET | plan_history.action; scope values all, dirty |
| Trip colours | #2563eb, #16a34a, #ea580c, #9333ea, #0891b2, #dc2626, #ca8a04, #4f46e5 | One per task index, shared by Gantt, map and list |
| Planner knobs | Timezone Asia/Bangkok; same-address precision 4 decimals (about 11 m); default unloading 20 min; cost per km 15, per hour 250, fixed per round 500; avoid tolls and highways; load scale 1000 for Google | plan.constants.ts, google.constants.ts |
Unallocated reason codes
Written to plan.unallocated[].reason_codes, with Thai text in reason_label.
| Code | Meaning (Thai label in code) |
|---|---|
MANUALLY_UNASSIGNED | A person removed the order from the plan; keeps it out of the next re-plan |
NO_VEHICLE | No vehicle can take it |
DEMAND_EXCEEDS_VEHICLE_CAPACITY | Weight or volume above every vehicle |
CANNOT_BE_PERFORMED_WITHIN_VEHICLE_DISTANCE_LIMIT | Too far |
CANNOT_BE_PERFORMED_WITHIN_VEHICLE_DURATION_LIMIT | Not enough working time |
CANNOT_BE_PERFORMED_WITHIN_VEHICLE_TRAVEL_DURATION_LIMIT | Travel time over the limit |
CANNOT_BE_PERFORMED_WITHIN_VEHICLE_TIME_WINDOWS | No round meets the promised time |
VEHICLE_NOT_ALLOWED | Locked to a vehicle that cannot take it |
NO_GEO_LOCATION | No destination coordinate |
| (fallback text) | Used when Google returns no reason |
Users, devices, applications
| List | Values | Where used |
|---|---|---|
| User role | admin, store_operator, driver, system_admin | user.role, application.roles, @Roles(...) on controllers. system_admin only for the seeded internal user; assignable by API: the other three. |
| User state | staged, active, inactive, locked_out, suspended | user.state. Login requires active. |
| Device platform | android, ios | device.platform |
| Fixed names | Default application name Default Application, code default; system username system; seeded admin username system_admin | Boot seeds |
| Key lengths | App key 32 chars, secret 44 chars, access token 44 chars, token lifetime 1 year | Auth |
Files
| List | Values | Where used |
|---|---|---|
| Attachment purpose | admin_file: private, extensions jpg, jpeg, png, webp, pdf, max 20 MB | Upload rules per purpose (attachment.purposes.ts) |
| Attachment status | pending, scanning, ready, failed | attachment.status |
| Scan status | pending, clean, infected | attachment.scan_status |
| Visibility | private | attachment.visibility |
| Storage provider and credential mode | aws_s3; iam_role, static | Storage setting |
| Limits | Upload URL valid 5 min, download URL 15 min, max 20 pending per user, 30 upload URLs per minute, pending expires after 6 min, scanning stale after 5 min | attachment.constants.ts |
Templates and exports
| List | Values |
|---|---|
| Template channel | print, email, sms |
| Paper | A4, A3, A5, Letter |
| Orientation | portrait, landscape |
| Export column type | field, text, script |
| Number formats | #,##0, #,##0.00, #,##0.000 |
| Script limits (code fallback when the setting is missing) | calls 20, docs 1000, wall 2000 ms, heap 64 MB, export state 64 KB, bulk 50; forbidden filter operators $where, $function, $accumulator, $expr; database functions find, get, count |
Kafka events (master list of topics)
Declared in kafka.constants.ts; the kafka.events setting can rename a topic or disable an event.
| Event | Direction | Default topic |
|---|---|---|
do_consume | consume | bigc.lastmiles.do-post |
do_update_oms | produce | lastmiles.oms.do-update |
do_update_staffapp | produce | lastmiles.staffapp.do-update |
load_update | produce | lastmiles.staffapp.load-update |
task_update | produce | lastmiles.staffapp.task-update |
driver_update | produce | lastmiles.staffapp.driver-update |
Only do_consume, do_update_oms and do_update_staffapp have code behind them today. Consumer group prefix: environment KAFKA_GROUP_PREFIX, else KAFKA_CLIENT_ID, else bigc-tms-core-v2. Kafka log status values: sent, consumed, failed; direction in, out.
Jobs and schedules
| List | Values |
|---|---|
| Job status | PENDING, RUNNING, DEAD |
| Job lane | fast, long |
| Delivery mode | at_most_once, at_least_once |
| Partitions and batches | 64 partitions; fast batch 100, long batch 10 |
| Retry delays | 1, 2, 4, 8, 16 minutes (6 attempts in total) |
| Leases | Partition lease 30 s with 10 s heartbeat; long job lease 5 min with 1 min heartbeat |
| Missed-slot policy | run_once, skip (crons running every 5 minutes or faster default to skip) |
| Schedule defaults | Retry count 3; tick every 15 s; slot late after 60 s counts as missed; cron timezone fixed at UTC+7 |
| Activity actor types | user, system |
| Activity actions in code | do-create, do-update, setting-change, and the plan history actions above |