> For the complete documentation index, see [llms.txt](https://tyrix.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tyrix.gitbook.io/docs/script-resources/tyrix-droply/order-lifecycle.md).

# Order Lifecycle

The order state machine is the core of Droply. Every transition is server-authoritative and re-validated; the phone UI only ever reflects state it was pushed.

## States

```
pending ──▶ preparing ──▶ enroute ──▶ delivered
   │            │            │
   │            │            └──▶ cancelled   (customer offline past grace)
   │            └───────────────▶ cancelled   (customer cancel, may incur a fee)
   ├────────────────────────────▶ cancelled   (customer cancel — free)
   └────────────────────────────▶ expired     (no driver accepted in time)
```

| Status      | Meaning                                                                   |
| ----------- | ------------------------------------------------------------------------- |
| `pending`   | Placed, waiting for an employee to accept. Visible in the business queue. |
| `preparing` | A driver has claimed it and is gathering or crafting the items.           |
| `enroute`   | The driver has the full order and is travelling to the drop-off.          |
| `delivered` | Handed over. Items transferred, customer charged, payouts made.           |
| `cancelled` | Ended early — by the customer, or by the system.                          |
| `expired`   | No driver accepted within `acceptTimeoutSec`.                             |

`accepted` exists in the database enum for compatibility, but the live flow transitions `pending` straight to `preparing` on accept.

## Placing an order

`droply:placeOrder` runs this gauntlet before a row is ever written:

1. Store exists and is **open**.
2. Customer is not on the store's **blocklist**.
3. Customer has a Droply **account**.
4. Customer has no other live order (when `oneActiveOrderPerCustomer`).
5. Cart passes sanitisation — every item is on the live menu, quantities are integers within `maxQtyPerItem`, and the cart is within `maxDistinctItems`. **Prices are re-read from the menu**, never taken from the client.
6. Promo code is previewed read-only for a discount.
7. Tip is clamped to `maxTip`.
8. Drop-off resolves — live ped coordinates, or a pin validated against world bounds.
9. Customer **can afford** the total across `paymentSources`. No money moves.
10. The promo slot is claimed, then the row is inserted.

On success the customer is notified, on-duty employees of that business receive an in-game notification, a phone notification, and a live push into their Droply queue.

An **accept timer** starts for `acceptTimeoutSec` (default 10 minutes). If it fires while the order is still `pending`, the order expires.

## Accepting

An employee accepts from the business queue. The server verifies:

* The order is still `pending`.
* The caller is employed by that business (via `getEmployeeBusiness`).
* Their grade meets the storefront's `min_grade`.
* They are under `maxConcurrentPerWorker`.

The concurrency check **prunes dead entries first**, so a stale record can never permanently lock a driver out of accepting work.

On accept, the accept timer is cleared, the order flips to `preparing`, the driver's phone number is captured for the chat, and the tyrix\_business **DeliveryPanel HUD** opens showing the item checklist and the driver's projected payout.

## Preparing

The driver gathers or crafts the items. The delivery HUD tracks progress live via `ox_inventory:updateInventory`, with a 2-second backstop poller that only runs while an order is active.

An order counts as ready when **either**:

* Every line item is **loose** in the driver's inventory, **or**
* The whole order is packed inside a single **`business_box`**.

The box path is what makes large orders viable — a box is a separate stash, so its contents do not count against the carrier's weight limit. When a driver carries several qualifying boxes, Droply picks the one with the **least excess** so a box prepped for a different order cannot be shipped by accident along with its unrelated extras.

Because boxed items are invisible to a loose item count, the client asks the server to confirm readiness whenever the loose count falls short, and the HUD is normalised so the per-row counts match the "ready" header.

## Out for delivery

The driver taps **Out for Delivery**. The server re-verifies the full inventory (loose or boxed) before flipping the order to `enroute` — a modded client cannot skip ahead.

From here:

* The driver's client starts **posting its position** every `trackingIntervalMs`, gated to moves of `workerMoveGateMeters` or more.
* The customer gets a driver blip, a route, an in-app mini-map, and an ETA.
* A GPS waypoint is dropped on the customer's map at the delivery address.

The tracking relay validates on every report that the caller is the assigned driver and the order is genuinely `enroute`, throttles per driver, coalesces sub-`relayCoalesceMeters` motion, and clamps coordinates to world bounds so a spoofed position cannot be relayed.

## Handover

The driver walks up to the customer and uses the third-eye option **Hand over delivery**, visible within `handoverInteractDistance` (2.5 m). A `handoverProgressMs` progress bar plays with a give/take animation on both peds.

When several deliveries are in flight, the option resolves from the **targeted player**, not whichever order currently owns the HUD — so the driver hands over the right order by walking up to the right person.

The server then re-checks everything:

1. The order is `enroute` and the caller is the assigned driver.
2. The customer is **online and loaded**.
3. Distance is measured against the customer's **live ped**, not the recorded drop-off — a driver cannot deliver to a customer who walked away. The limit is `handoverDistance + 2.0` m. Violations are logged as exploits.
4. The driver **still has** the full order.
5. The customer **can still pay**. If not, the order is cancelled as `funds`.
6. The customer **can carry** the goods — every line item, or the box.

Only then does anything move.

## Money

**Funds are verified at order time and captured only at handover.** If an order expires, is cancelled, or never completes, the customer is never charged. There is no escrow, no hold, and no refund path to get wrong.

The handover commits in a deliberate order, items first and payment last, so a payment failure can never leave a customer holding free goods:

```
box path:    remove box from driver → add box to customer → charge customer
loose path:  per item: remove from driver → add to customer → charge customer
```

Every failure rolls back best-effort — items return to the driver, the customer is refunded in kind, and both sides are told why.

Once payment clears:

```
societyCut = floor(subtotal × commission)
workerCut  = subtotal − societyCut

business ← societyCut + (delivery fee, if deliveryFeeRecipient == 'society')
driver   ← workerCut  + tip + (delivery fee, if deliveryFeeRecipient == 'worker')
```

The business is paid into its society account; the driver is paid in cash. **The tip goes to the driver in full**, always.

## Disconnects

| Who drops | Order state                          | Result                                                                                           |
| --------- | ------------------------------------ | ------------------------------------------------------------------------------------------------ |
| Customer  | `pending` / `accepted` / `preparing` | Cancelled immediately (`customer_dc`).                                                           |
| Customer  | `enroute`                            | A `offlineGraceSec` timer starts. If they do not return, the order cancels (`customer_offline`). |
| Driver    | any                                  | The order returns to `pending` and is re-broadcast to the business. A fresh accept timer starts. |

When a driver drops, **the items stay with them** — the order simply becomes available for another employee to claim.

## Restart reconciliation

A resource restart or crash wipes all live order state — driver assignments, tracking, source ids. Those orders cannot be resumed, so on start Droply rehydrates every non-terminal order just long enough to cancel it cleanly.

This releases every promo slot, updates the database rows, frees each customer's one-active-order slot, and notifies anyone already connected. A single summary embed is logged rather than one per order, and the console prints:

```
[Droply] reconciled N orphaned order(s) after restart
```

No money is at stake, because customers are only ever charged at handover.
