Collecting an order: ready, arrival and handover¶
What happens to an order the customer collects, from the moment it's placed to the moment it's
handed over (or cancelled because nobody came). It applies to every collection order: the single
"collect from us" option every plan has (collect-default, see
Locations) and collection from any store
(Click and Collect).
One service, PickupService (commerce-core/.../order/collection/), moves a collection order on.
The admin order page, the fulfiller portal, the Badger POS
app, the customer's arrival page and the hold-expiry job all go through it, so the order, the
customer's emails and the store's task lists can't disagree.
The lifecycle¶
| Stage | Set by | What the customer gets |
|---|---|---|
| To prepare | Placing the order | The order confirmation page and email say where to collect from and show their collection code; the page adds "We'll email you when it's ready to collect, usually by 13:12 today" (from the ready-by time, in the shop's time zone) |
| Ready | Admin Ready for collection, portal Mark ready to collect, POS Ready | The "ready to collect" email (once), with the code and a link to the arrival page |
| On the way / here | The customer, on the arrival page | Nothing; the store is told |
| Collected | Admin Collected, portal Customer has collected, POS handover | A short thank-you note (orderCollected). No dispatch email |
| Cancelled | The hold running out, or a manual cancellation | The usual cancellation email, if the site sends them |
Ready and collected ride on the pack and dispatch pipelines, so stock, payment capture, fulfilment states and the order list behave as they do for an order that is sent.
Where an order has got to is stored on the order as collection (Order.getCollectionDetails()):
its stage, location code, collection code, when it should be ready by, when it was made ready, how
long it is held (holdUntil), when it was collected, what has been picked per line, the customer's
arrival notice and when the reminder went. A submit-pipeline step (startCollectionProcessor) sets
it up as the order is placed, before the confirmation email; an order placed before this existed
gets it the first time anything touches it.
Ready by is the time of placing plus the preparation time checkout promised
(DeliveryOptionBean.collectionPrepMinutes), else the location's preparation time
(collectionSettings.prepMinutes). An order collected on a later day or in a slot (see Collection and
delivery slots) is ready by, and ready from
(collection.readyFrom), that day's opening time or the slot's start: if staff mark it ready
sooner, the "ready to collect" email waits until then (readyEmailDueAt, sent by the hold job) and
the hold runs from then. Held until is the moment it was made ready plus the
location's hold days (collectionSettings.holdDays, default 7).
Collection codes¶
Each collection order gets a random six-character code made of letters and digits nobody confuses
(no 0/O/Q, 1/I/L, 2/Z, 5/S or 8/B). It's printed in the confirmation,
ready and reminder emails ("Show this code when you collect"). At handover, staff either check the
code (typed codes are compared ignoring case, spaces and hyphens) or check the name on the order.
The code is shown to staff in the POS app; it is never logged.
The arrival page¶
The ready email links to /collect/{token}: a page with no sign-in where the customer sees where to
collect (address, phone, opening hours with today marked, directions, the store's collection
instructions) and can say:
- I'm on my way, choosing when they'll arrive (5 to 60 minutes);
- I'm here. At a location with the
KERBSIDEcapability they also give their bay or space and their car, and are asked to stay in it; elsewhere it just tells the store they're at the counter. Once they've said they're here, On your way? is no longer offered.
While the order is being prepared or is ready, the page also shows the collection code to give at the counter.
It only works while the order is ready and not yet collected or cancelled; otherwise it says where
the order has got to. The page shows the order number, the collection code and the store, never the
customer's name or items (the link is the customer's alone, like the email that carries it), is noindex, Cache-Control: private, no-store and Referrer-Policy: no-referrer. Text
comes from collect.arrival.* message keys (Nova and Bootstrap 4 templates under
templates/{nova,bootstrap}/collect/arrival.html).
The link. The token is base64url(orderId|expiry).base64url(HMAC-SHA256), signed with a random
secret per site (collection-link-signing-key, created on first use and never shown;
CollectionLinkTokenService). It names the order and nothing else, can't be used on another shop,
and expires a day after the hold ends. Links are minted on whichever node marks the order ready and
checked on the storefront, which is why the key lives in site config rather than an environment
variable.
What happens. The notice is written to the order with a guarded, targeted update (only while
it's ready), recorded in its activity, and broadcast as a CollectionArrivalEvent on the
fulfilment-events-exchange fanout. The event carries ids, the state, the ETA and the bay, never
the name or vehicle.
The fulfiller portal¶
The portal's board has a Customers collecting panel listing, for the user's fulfillers (or the whole site for an administrator), orders whose customer said in the last three hours that they're on their way (with the time) or here (with bay and vehicle). An arrival event raises the portal's alert banner ("Customer here for #1042") and refreshes the panel in place, over the same server-sent events stream as new orders.
On the board, an order's Mark ready to collect and Customer has collected record the collection stage too, as do the admin's per-fulfiller buttons.
POS tasks¶
The Badger POS app shows a store's collection orders as tasks, through /v1/pos/{locationId}/...
(the store's code or Stripe Terminal location id; the agent must be allowed to work there). See
Mobile POS and the API reference. The same
task list can include web orders the store sends (ship tasks).
Push notifications¶
Every device registered at a store with an Expo push token (PUT /v1/pos/{locationId}/devices/{id})
is sent:
data.type |
When | Says |
|---|---|---|
new-store-task |
A collection order is placed for the store (taskType: COLLECTION) |
"New order to prepare: order #1001 is to be collected from Leeds" |
new-store-task |
A web order is placed with items the store sends (taskType: SHIP) |
"New order to send: order #1002 is to be sent from Leeds" |
customer-on-the-way |
The customer says they're coming | "Order #1001: arriving in about 15 min" |
customer-arrived |
The customer says they're here | "Order #1001: bay 3" |
Each message is {to, title, body, sound: "default", channelId: "default", priority: "high", data:
{type, taskType, orderId, orderNumber, locationCode, url: "badgerpos://tasks/<orderId>"}}. Messages carry
the order number and bay at most, never the customer's name or vehicle: they pass through Expo,
Apple and Google and show on a lock screen.
The worker's PosPushListener reads the events from its own durable queue on the fulfilment
events exchange (pos-push-events, shared by every worker, so each event is pushed once) and
PosPushService sends them to https://exp.host/--/api/v2/push/send in batches of 100. Tokens
Expo reports as DeviceNotRegistered are removed from their device. Only ids and counts are
logged.
Push is off unless the global setting POS Push Notifications (pos-push-enabled) is on, so
development and test stacks never call Expo. If the Expo project requires an access token, set
Expo Access Token (pos-expo-access-token, a secret) or the EXPO_ACCESS_TOKEN environment
variable on the worker.
Emails¶
| Template | When | Notes |
|---|---|---|
orderPlaced |
Placed | Shows the collection code |
orderReadyForCollection |
Ready, once | Code and the arrival link |
orderCollectionReminder |
A day before the hold ends, once | Code, arrival link, until when it's kept. Not sent for a hold of a day or less |
orderCollected |
Handed over, once | Thank-you note; no dispatch email is sent |
All are queued to the worker like the other order emails; see Transactional email.
When nobody comes¶
The worker's CollectionHoldJob runs every 15 minutes (badger.collection.hold-schedule) and, for
each site:
- Reminds customers whose ready order's hold ends within a day (
orderCollectionReminder). - Cancels ready orders whose hold has ended, through
OrderCancellationService: the payment is refunded or released, the stock goes back where it was taken (the collection store), the order records the reason ("Not collected: kept for collection at leeds until ..."), and the customer gets the site's cancellation email if it sends them.
Several workers can run it at once: each reminder and cancellation is first claimed on the stored
order with a guarded findAndModify. A cancellation that fails (a refund refused, say) is logged
and tried again an hour later.
Code¶
| Piece | Where |
|---|---|
PickupService, PickupServiceImpl, CollectionLinkTokenService, CollectionException |
commerce-core/.../order/collection/ |
CollectionDetails, CollectionArrival, CollectionOrderRepository |
common/.../order/ |
Placing: StartCollectionProcessor, AnnounceCollectionTaskProcessor |
commerce-core/.../pipeline/impl/ |
Events: CollectionArrivalEvent, CollectionTaskEvent, FulfilmentEventPublisher |
commerce-core/.../fulfilment/events/ |
| Arrival page | web-mvc/.../render/CollectArrivalController.java, templates/{nova,bootstrap}/collect/arrival.html |
| Portal | FulfilmentEventListener, FulfilmentEventHub.deliverArrival, FulfilmentWorkService.arrivals |
| POS tasks | PosTaskService, PosTaskView (commerce-core/.../pos/), PosController |
| Push and hold job | worker/.../pos/push/ (PosPushListener, PosPushService, ExpoPushClient), worker/.../order/collection/CollectionHoldJob.java |
Known limits¶
- Orders placed before this release have no collection details until something touches them, so they don't appear in a store's POS task lists.
- Picks and arrival notices are targeted writes, but readying and handing over save the whole order; a pick scanned at the very moment another till marks the order ready can be lost.