Skip to content

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 KERBSIDE capability 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:

  1. Reminds customers whose ready order's hold ends within a day (orderCollectionReminder).
  2. 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.