# UMSB workflow backend foundation

## Scope and compatibility

This implementation is local only and is not deployed. It keeps the legacy numeric statuses, `delivery_destination`, `container_size`, `vehicle_no`, `assign_to_drivers`, and the misspelled `commision_rate`. New responses may also expose `commission_rate`.

The singular `vehicle` table is authoritative. The plural `vehicles` table remains available for tyre-history compatibility and receives a nullable mapping to the authoritative record.

## Migration order

1. Back up the database and restore it into a disposable MySQL 8 environment.
2. Apply `001_kform_container_location.up.sql`; review its report before and after.
3. Apply `002_location_master.up.sql`, then its report.
4. Apply `003_vehicle_transport_references.up.sql`, then its report.
5. Apply `004_workflow_versioning.up.sql`, then its report.
6. Apply `005_workflow_audit_idempotency_outbox.up.sql`.
7. Apply `006_driver_trip_cleanup.up.sql`, then its report. This archives duplicate rows but does not delete them.
8. Apply `007_commission_snapshot.up.sql`.
9. Run `008_foreign_keys_constraints.report.sql`. Apply `008` only after every exception is investigated and resolved.
10. Apply `009_stable_actor_mapping.up.sql`, then run its report and resolve every open mapping exception.
11. Apply `010_actor_module_snapshot.up.sql`, then run its report. Operations candidates must synchronize their own Dolibarr context at least once.
12. Apply `011_workflow_row_version_contract.up.sql`, then run its report. Both invalid-version counts must be zero.
13. Apply `012_driver_actor_mapping.up.sql`, then run its report. This retains optional compatibility with legacy physical Driver references; new task assignment does not require this mapping. Add its optional foreign key only after the report is clean and engine/type compatibility is confirmed.
14. Apply `013_transport_driver_application_assignment.up.sql`, then run its report. New Driver assignments store the application-user recipient independently; legacy `driver_id` remains nullable and optional.

Migrations are manual SQL files; this repository has no migration runner. Roll back in reverse order using the matching `.down.sql` files. Date normalization in migration 006 intentionally does not restore invalid zero dates.

## Actor provisioning

Every protected request calls Dolibarr's current-user endpoint and resolves the stable Dolibarr user ID, login, active state, and complementary module flags. `application_actors.dolibarr_user_id` is unique. Raw tokens and token hashes are not used as identities, so rotating a token keeps the same actor.

Because this database has no separate application-user master, a valid active Dolibarr user with at least one UMSB module can be provisioned just in time with `application_user_id = dolibarr_user_id`. Users without a qualifying module return HTTP 403 with `ACTOR_MAPPING_REQUIRED`. Legacy IDs are recorded in `actor_mapping_exceptions` for administrator verification; the migration does not guess from names or historical audit values.

Migration 010 stores a normalized snapshot of the six complementary module flags returned by each user's authenticated Dolibarr `/users/info` response. Operations eligibility uses `application_actors.operations_module`, keyed by stable `dolibarr_user_id`; it does not use `assigned_module`, Dolibarr groups, names, titles, or tokens. After deploying migration 010, each existing candidate should call `GET /controller/auth/context.php` once to populate the snapshot.

Transport Driver candidate discovery reads active, synchronized `application_actors.transport_driver_module` users directly, matching the Operations and Transport Manager list flow. The authorized `assign_driver` transition uses `next_assignee_id` as the task recipient and stores it in `transport_mgr_table.driver_application_user_id`. A physical `driver_id` is an optional legacy fleet reference and is never required or substituted with the application-user ID. Driver exclusion diagnostics are returned only when `UMSB_ENVIRONMENT`/`APP_ENV` is a development or test value and `UMSB_ELIGIBILITY_DEBUG=1`; production defaults never expose them.

## Endpoints

- `POST /controller/rate-management/calculate-commission.php`
- `POST /controller/rate-management/resolve-location.php`
- `GET|POST|PUT /controller/rate-management/location-requests.php`
- `GET /controller/workflow/statuses.php`
- `POST /controller/workflow/transition.php`
- `GET /controller/workflow/eligible-assignees.php?stage=driver&k_form_id=123`
- `GET /controller/workflow/task-context.php?task_id=456`
- `POST /controller/transport/validate-assignment.php`
- `GET /controller/auth/context.php`

All use DOLAPIKEY. Transition calls should also send a unique `Idempotency-Key` header.

## Task ownership policy

Every transition that processes an existing Operations, Transport Manager, Driver, or HR task requires both the stage module permission and ownership of the locked task row. Ownership compares `task_table.task_assignee` only with the server-resolved `application_actors.application_user_id`; tokens, payload identity fields, physical Driver IDs, vehicles, and next-assignee targets are never used. Administrators may view all tasks but do not bypass execution ownership. Executing another user's task requires a separate explicit, audited reassignment or administrator-override mechanism, which is not provided by the transition endpoint.

The PHP bootstrap owns endpoint CORS. Remove hosting-panel or LiteSpeed response-header rules that set `Access-Control-Allow-Origin` or `Access-Control-Allow-Headers`; otherwise those server rules can replace the allowlist and `Idempotency-Key` declaration emitted by `global_function/global2.php`. Production frontend origins are configured as a comma-separated `UMSB_CORS_ALLOWED_ORIGINS` value.

## Transition matrix

| Action | Required module | From job status | To job status | Result |
|---|---|---:|---:|---|
| assign_operations | Document | 0 | 1 | Creates Operations task |
| start_operations | Operations | 1 | 2 | Starts task |
| complete_operations | Operations | 2 | 3 | Saves Operations and creates Transport Manager task |
| start_transport_manager | Transport Manager | 3 | 4 | Starts task |
| assign_driver | Transport Manager | 4 | 5 | Validates/saves assignment and creates Driver task |
| start_driver_trip | Driver | 5 | 6 | Starts trip task |
| mark_delivered | Driver | 6/7 | 9 | Completes trip, releases resources, creates one HR task |
| start_hr_review | HR | 9 | 10 | Starts HR task |
| calculate_commission | HR | 10 | 10 | Stores immutable calculation snapshot |
| complete_hr | HR | 10 | 11 | Approves snapshot and saves legacy HR amount |
| cancel_workflow | Document/admin | 0–10 | 12 | Cancels current task/workflow |

Save and correction actions are also catalogued by `GET /workflow/statuses.php`.

## Example transition

```json
{
  "k_form_id": 123,
  "task_id": 456,
  "action": "complete_operations",
  "expected_job_status": 2,
  "expected_row_version": 4,
  "next_assignee_id": 20,
  "remarks": "Completed",
  "stage_data": {
    "manifest": true,
    "scanning": true,
    "document_status": "COMPLETED",
    "custom_approval": "2026-08-31 10:30:00",
    "port_clerk": "Redacted"
  }
}
```

The frontend will later need to send master `container_size_id`/`location_id`, use task context, pass `expected_row_version`, generate one idempotency key per user action, and retain the returned `commission_calculation_id` for `complete_hr`. Existing legacy endpoints remain available during migration.
