Click and collect from your stores¶
Customers can choose to collect an online order from any of your stores, against that store's own stock. Checkout offers each store that has everything in the basket, product pages and the basket say whether the shopper's chosen store has it, and the stock is taken at that store when the shopper pays.
This page covers choosing a store, up to the order being placed. What happens after that (getting the order ready, the customer arriving, handing it over) is described with the rest of the order lifecycle and on the Locations page.
Badger Commerce Enterprise
Collection from several stores is part of Enterprise (the clickAndCollect capability, with
multiLocationInventory; see Subscription Entitlements). Every
plan can still offer the single collection from your main location
(local-pickup-enabled), described in
Customers collecting from your shop. A shop
that hasn't turned collection from stores on sees no change at all.
Turning it on¶
Collection from stores is on only when all three hold (LocationService.isStoreCollectionActive):
- Customers can collect from any of your stores (
click-and-collect-enabled, Settings › Orders › Fulfilment) is on. It is off by default, so having the entitlement never changes a shop by itself. - The shop's plan includes
clickAndCollect. - Multiple stock locations is active (
multi-location-enabledandmultiLocationInventory), because each store sells its own stock.
The quickest way is the question at the top of the Locations screen: … and customers can collect from any of my stores turns on both multiple stock locations and collection from stores. Without Enterprise the answer is shown locked, with an Enterprise badge, and is refused if posted anyway. Choosing any other answer turns collection from stores off.
Then, on each store customers can collect from, tick Customers collect orders here
(CLICK_AND_COLLECT) and give it a street address. Its Collection settings say how long staff
need to get an order ready (prepMinutes, default two hours), how long a ready order is kept, and
instructions for the customer.
Collection points. With collection from stores on, every active location marked
CLICK_AND_COLLECT with an address (first line and postcode) is a collection point. Each has a
store page, whether or not it is listed on your store pages. The main location is one only if it is
marked too: the single pickup gives way to the stores. With it off, the only collection point is
the main location, when local-pickup-enabled is on and it has an address, exactly as on every
plan. The decision lives in CollectionPointService.
Which stores checkout offers¶
Checkout offers Collect from
A store that is temporarily closed isn't offered either, and the product page and basket tell a shopper whose chosen store it is ("closed for now"). With Store capacity limits on (Enterprise), a store that is full, closed or too late to prepare the order today is offered Ready from Wednesday 15 October instead (its next ready day within a week), and only one with no such day is left out ("fully booked"); a store with collection slots asks the shopper to choose a time. See Collection and delivery slots and Store operations.
Something only a store has¶
A product no warehouse (no SHIP_FROM location) has, but a collection store does, can still be
bought: listings show it in stock, the product page enables Add to basket, and its stock line
says Collect only: not available for delivery (stock.collectOnly; on a product with variants,
the line for the picked variant says so). The quantity is limited to what one store has, since a
collection comes from one store. At checkout a basket holding it is offered no delivery options,
only the stores that have every line (LocalPickupService.isCollectionOnly, read live, checked by
DeliveryServiceImpl.findDeliveryMethods). A shop that takes orders for things it has run out of
(allowOOSAddToOrder) keeps delivery, as it sends those when restocked. A basket whose lines are
each only at different stores has no option at all until the shopper removes one.
With collection from stores off (every Lite shop), stock only a store holds doesn't count: the product is out of stock, exactly as before.
Each option¶
Each option is free and shows the store's address, Ready in about 2 hours (from the store's
preparation time, rounded up to whole hours; Ready from Wednesday 15 October when it can only be
ready on a later day, or a slot picker for a store with collection slots) and its collection
instructions. Its code is
collect-<location code>, and the order records the store in deliveryMethod.collectionLocationCode.
Collection is never chosen for the shopper: it needs no delivery address, so it's offered before
one is typed, and choosing it asks for a billing address instead.
The step-by-step checkout (a site without the unified checkout, or without Stripe) lists the same
options in its delivery step, with the Ready in about 2 hours line in place of the generic
delivery estimate (DeliveryChooserExtension puts it on the model as readyIn, by option code).
It has no store picker, so every store that can supply the basket is listed.
Delivery or collection first. With collection from stores on, the unified checkout puts the choice (headed Delivery or collection, with "Choose a store to collect from, or enter your address below for delivery") above the delivery address, so someone collecting never starts on an address. Choosing a store hides the address. Until an address finds delivery options, the list also offers Have it delivered instead, which brings the address back; Pay then waits until a delivery option is picked (the store stays the order's choice on the server until then, since a delivery choice can't be blank). The basket summary's row reads Collection rather than Delivery while a store is chosen.
Many stores. With up to four stores to offer, each is its own option in the delivery list. With more, the list shows one Collect from a store choice. Choosing it picks the shopper's own store straight away when that store can supply the basket; otherwise it opens a compact picker inside the checkout: a search box (store name, address or postcode) and a list of stores, the shopper's own first and then by name. The stores are buttons, so the picker works with Tab and Enter and on a phone, and nothing is chosen while moving through the list. Change store opens it again.
"My store"¶
A shopper can pick the store they usually collect from. It is remembered in the bdgr_store
cookie: just the store page's address (its slug), for 90 days, Secure, HttpOnly,
SameSite=Lax. It says which branch someone prefers, not who they are, so it holds no personal data
and needs no login. A store that stops taking collections is ignored wherever the cookie is read.
Shoppers choose it from:
- Make this my store on a collection store's page (
/stores/{slug}); - Change store (or Choose a store to check stock) on a product page or the basket. This
opens the store picker over the page: a panel from the right on wider screens, a bottom sheet on
phones (a native
<dialog>: Escape, the backdrop or the close button shut it, and focus goes back to the link). It lists the stores customers collect from that can take a collection (LocalPickupService.pickerStores: not temporarily closed or fully booked within the week, a free slot for a slotted store), the shopper's own first, each with Choose this store as its main button and the store's page as a link under it. A store that can only have an order ready on a later day says so (Ready from Wednesday 15 October). Coming from a product page it lists only the stores that have that product (the variant in the page's?v=, else any enabled variant), and from the basket or checkout only those with everything in the basket, each marked In stock here (StoreController.pickerStock, display stock); if none has it every store is listed, marked Not in stock here. The search and Use my location work there too. Choosing posts in the background and swaps in the page's new collection line and header store link without leaving the page. The panel is/stores/picker?return=<page>(the list without a page around it,stores/picker, never cached); the script lives infragment/collectOffer. Without JavaScript, or if the panel can't load, the link goes to the same picker as a page,/stores?choose=1&return=<page>.
Both post to /stores/my-store, which sets the cookie and sends the shopper back to the page they
came from. Only a path on the shop is followed (one starting with a single /, without
backslashes or control characters); anything else goes to the store's page instead, so the picker
can't be used to send someone to another site.
On the product page and basket¶
With collection from stores on, the collection line under the add-to-basket button and in the basket says what the shopper's store has:
| Shopper's store | Product page | Basket |
|---|---|---|
| Chosen, has it | Collect from Leeds: in stock · Change store | The same, when the store has every line in the quantity in the basket |
| Chosen, has it, ready on a later day (Enterprise capacity) | Collect from Leeds: in stock (ready from Wednesday 15 October) · Change store | The same |
| Chosen, hasn't | Collect from Leeds: not in stock there · Change store | The same, when any line is short |
| None chosen | Choose a store to check stock, linking to the picker | The same |
| No store that can take a collection has it | Not currently available for click & collect, nothing to pick | The same, when no such store has every line |
The line sits under a Free click & collect heading, directly under the add-to-basket button. On Depot the stock line above the button then reads In stock for delivery / Only 3 left for delivery, since a store may have more than the warehouses.
On a product with variants, before one is picked, the store counts as having it when it has any
enabled variant; once one is picked, that variant. These lines read display stock, which may be a
few seconds behind; checkout reads it live. A shop without collection from stores keeps
Delivery, or free collection from
The wording is worked out in Java (LocalPickupService.offer / offerAnyOf, put on the model by
ProductController and the BasketActions extension as collectOffer) and drawn by the shared
fragment/collectOffer fragment in every theme. The text comes from the collect.store.*,
stores.choose.*, stores.myStore.* and checkout.delivery.collect.* message keys.
Stock is taken at the collection store¶
When the shopper pays, the stock for a collection order is taken at the store they collect from
(StockAllocationServiceImpl), never sourced from a warehouse or another store:
- Every line that would be sent is taken at
collectionLocationCode. Lines with nothing to hand over are taken at the main location, as on any order. - Each line records the store as
sourceLocationCode, and thestockTakenrecord names it, so a cancellation or return puts the stock back there. - If the store has a fulfiller, the lines are handed to it, as sourcing does, so the fulfilment queue, portal and notifications route the order to that store.
- If the store has sold out of something since checkout offered it, the order isn't placed, the card hold is released and the shopper is told the item has just sold out, unless the site allows out-of-stock orders, in which case what could be taken is kept and the order goes ahead.
With multiple stock locations active, this applies to any collection order, including one collected from the main location. With them off (every Lite shop), collection orders are taken at the main location exactly as before.
Code¶
| Piece | Where |
|---|---|
| The switch and gate | ConfigKeyDefaults.CLICK_AND_COLLECT_ENABLED, LocationService.isStoreCollectionActive / canCollectFromStores |
| Collection points, store stock checks | CollectionPointService (commerce-core/.../delivery/) |
| Collect-only stock (stock level, stock line, no delivery) | SkuAvailability.level() / isCollectOnly(), AvailabilityServiceImpl, ProductController (collectOnly), VariantDisplayer (collectOnly per SKU), LocalPickupService.isCollectionOnly |
| Checkout options, product page and basket line | LocalPickupService, DeliveryServiceImpl, CollectionReadyIn (the ready-in line), UnifiedCheckoutServiceImpl, DeliveryChooserExtension (step checkout), UnifiedCheckoutExtension (shopper's store, picker threshold), nova/js/extensions/unifiedCheckout/checkout.js |
| My store cookie, return path check | MyStoreCookie (commerce-core/.../delivery/) |
| Store picker and "Make this my store" | StoreController (?choose=1, POST /stores/my-store), templates/{nova,bootstrap}/stores/ |
| Allocation at the store | StockAllocationServiceImpl.allocateAtStore |
| Locations screen answer | LocationAdminController (MULTI_LOCATION_AND_COLLECT), templates/admin/locations/list.html |