Store operations: status, special hours and capacity¶
Running a store day to day: closing it for a while, opening on different hours for a bank holiday, and (on Enterprise) limiting how much online work it takes so a busy or short-staffed store doesn't get overwhelmed. Everything here is about locations; none of it needs setting up until a shop wants it.
| Feature | Who | Switch |
|---|---|---|
| Temporarily closed | Any shop with locations | none |
| Holiday and special hours | Any shop with locations | none |
| Daily capacity for click and collect, ship-from-store and store delivery | Enterprise (storeFulfilment) |
Store capacity limits (store-capacity-enabled, off) |
| Ready from another day and collection and delivery slots | Enterprise | Store capacity limits; see Collection and delivery slots |
| Never go over a store's limit | Enterprise | Never go over a store's limit (store-capacity-hard-limit, off) |
Both settings are under Settings › Orders › Fulfilment.
Temporarily closed¶
A location can be closed for now (a refit, a burst pipe, no staff this afternoon) with a reason and, optionally, a time it opens again. While it is closed:
- no web order is sent from it: sourcing never uses a closed location;
- customers can't choose it for collection at checkout, and the product page and basket say "Leeds is closed for now: choose another store to collect from" to anyone whose chosen store it is; with the single pickup from the main location (every plan), the pickup isn't offered at all;
- its store page shows a Temporarily closed banner with the reason and when it reopens, and the store locator doesn't show it as open today;
- orders it already has stay with it.
It reopens by itself once the reopening time passes. Nothing has to run for that: the closure is
simply ignored from then on (StoreOperationsService.isClosed), so the admin, portal and POS show
it open again. Close and reopen from the location's Status card in the admin, Store today in
the fulfilment portal, the POS app, or MCP.
The admin REST API's PUT /v1/admin/locations/{code} is a partial update and never touches status,
special hours, capacity, slots or the service area. Closing, reopening and setting a day's limit
change only those fields
(LocationService.updateStatus and setDayCapacityOverride, single-field database updates), so
someone saving the location's other details in the admin at the same moment doesn't lose the
change, and doesn't have theirs overwritten.
Stock at a closed store doesn't count towards the stock the shop advertises while it is closed, so an item that only a closed store has shows as out of stock rather than failing at payment. The product page and basket pick this up straight away; closing or reopening (including the automatic reopen when the closure ends) changes the set of locations that network availability is worked out from, so stored figures are recomputed. Search's in-stock filter catches up at its next rebuild.
Holiday and special hours¶
Holiday and special hours on the location lists dates whose hours differ from the week's: closed all day, or open on different times (a closing time before the opening time runs past midnight), with a note such as "Boxing Day". They replace that date's weekly hours everywhere:
- the store page lists the next fortnight's special hours under the opening hours;
- the store locator's "Open today" uses them;
- the store's structured data carries them as schema.org
specialOpeningHoursSpecification(a closed date opens and closes at 00:00), so search engines show holiday hours; - with capacity on, a date that is closed all day has a capacity of 0.
Capacity (Enterprise)¶
Badger Commerce Enterprise
Capacity needs the storeFulfilment capability and Store capacity limits turned on. Without
them limits can't be set (the admin shows what Enterprise adds), and with the capability but the
switch off, limits are saved but ignored and nothing is counted.
Each location can limit how many orders a day it takes, separately for:
- Click and collect: orders customers collect there;
- Ship from store: web orders packed and sent from there. Counted at stores, and at any other location given a ship-from-store limit (a warehouse with no limit isn't counted).
- Store delivery: orders the location's own vans deliver, counted at a location with store delivery slots (or a store delivery limit).
Capacity is counted in orders, not items: an order with three lines sent from Leeds is one order at Leeds. An order split between Leeds and York is one at each.
Setting limits¶
- Usual daily limits: a limit per day of the week for each kind of work. Blank is no limit, which is every location's starting point.
- Changes for particular dates: a limit for a run of dates in place of the usual one (a short-staffed week, the run-up to Christmas). Where changes overlap, the shortest wins (a one-day change beats the week around it); for equal lengths, the later start. 0 takes no orders.
- Today: staff change today's limit from the portal or the POS app ("more", "fewer", "no more today", "back to usual"). That's a one-day change for today.
The limit for a day is, in order: 0 if the special hours close the store that day; the shortest change covering the day; the usual limit for that weekday; otherwise none.
Days are the store's own dates in its time zone (the location's time zone, else the shop's, else
Europe/London), so "today" at a store in New York starts at its midnight. The POS app gets that
zone as timeZone (an IANA name such as Europe/London) on GET /v1/pos/{locationId}/status, to
work out the end of the store's day itself.
Lowering a limit below what is already booked doesn't move any orders: the day just has none left.
How orders are routed¶
When a shopper pays, sourcing (see Where web orders ship from) works with each candidate's ship-from-store capacity on the day the order would be dispatched from it: today before its cut-off, else its next dispatch day (see Delivery promise; "today" below means that day):
- Closed locations are never candidates (with or without capacity).
- Room first, within priority. Among candidates with the same sending preference
(
sourcingPriority), the one with the most room left today goes first, measured as a share of its limit; no limit counts as the most room. A lower sending preference still wins outright. - Full stores are passed over when the stores with room can send everything.
- An item only a full store has still goes there (the limit is soft), unless Never go over a store's limit is on, in which case that item can't be sent and the order is refused as sold out. When a full store is needed anyway, the usual "one parcel if possible" rule applies over all the stores: if the full store can send the whole order, it sends all of it.
Worked examples, with item A at Leeds and York and item B only at York:
| Situation | Result |
|---|---|
| Both have room, Leeds more | York sends A and B: one parcel beats splitting A to Leeds, and York's capacity is spent on this order either way |
| York is full | York still sends A and B (soft limit): it is the only source of B, so it is over its limit by this one order either way, and the customer gets one parcel |
| York is full, hard limit on | The order is refused as sold out |
| Both stores have A and B, Leeds 8 of 10 used, York 2 of 10 | York sends the order |
| Leeds full, York has room, both have A and B | York sends the order |
Collection at checkout: a store with no collections left today is offered Ready from its next day with room (and hours, and time to prepare it) within a week, and booked on that day; only a store with no such day isn't offered, and the product page and basket then say it is "fully booked". Stores can also offer collection slots, and own-van delivery windows: see Collection and delivery slots.
Booking and releasing¶
Capacity is booked when the stock is taken at payment (StockAllocationService.allocate): one
ship-from-store booking per location the order is sent from, on the day it is dispatched from
there, or one collection booking at the store it is collected from, on the day it is ready (that
store's today, or the "ready from" day checkout offered). An order with a slot
has its slot booked instead, before its stock is taken: the booking counts against the window and
the day. The
dispatch day is the one the order was promised when its delivery promise is
from that location, else the location's next dispatch day by its courier collection times, hours
and cut-off (DispatchService.dispatchDate), so an order after the cut-off, at the weekend or on a
bank holiday counts against the next day the store sends anything, not today. It is given back when the stock is
(a checkout that fails after payment, the release job for orders never placed) and when the order is
cancelled (whether or not the stock is put back). A store that fills up between the shopper
choosing and paying (another order took its last slot) is handled by the hard-limit setting:
- Off (default): the store takes the order anyway, booked over its limit. The limit is applied when choosing; booking at payment never refuses an order whose stock is already taken.
- On: the lines planned for the full store are planned once more without it. Where another location can send them, their stock is taken there, put back at the full store, and the other location is booked within its limit. A line that nowhere else can send stays at the full store and is booked over the limit (logged as a warning), because refusing a paid order at this point would be worse. So "never go over" holds when choosing, and at payment it is broken only for a line no other location can send.
Every booking is one ledger row per order, location, kind of work and day
(capacity_bookings, its id built from all five), so booking twice changes nothing and releasing
twice gives back one slot. The day's count (capacity_counters) only goes up while it is below the
limit, in one conditional database update, so two servers can't both take a store's last slot.
Ledger rows and counters expire 60 days after their day.
A booking adds to the count before writing its ledger row, and a release deletes its row before
taking one off the count, so a server stopping between the two leaves a count one out. Every 30
minutes the worker's CapacityReconcileJob puts today's and tomorrow's counts (and their slot
window counts) right from the ledger
(bookings further ahead, such as Friday evening's on Monday, are put right once their day is
tomorrow)
(count = number of bookings) at every active location that counts capacity, on every active site
that manages it (StoreOperationsService.reconcile / reconcileUpcoming). A correction only
applies if the count hasn't moved since it was read, and a booking caught half-way through is
put right on the next pass. Corrections are logged at WARN with the location, kind, day and
both numbers (badger.capacity.reconcile-schedule sets the schedule).
Couriers¶
Courier capacity is assumed to be unlimited: the ship-from-store limit is the store's packing capacity, which is the practical limit for most shops. Courier collection limits are left for a later, specialist phase.
Where staff manage it¶
| Where | What |
|---|---|
| Admin › Shop Settings › Locations › location | Status (close for now with reason and reopening time, or open again), Holiday and special hours, Capacity (usual limits, dated changes, and a two-week load chart), Collection & delivery slots. The location list shows closed locations and, with capacity on, today's load |
Fulfilment portal › Store today (/fulfilment/store) |
For staff of the fulfiller a store is linked to (Location.fulfillerId; admins see every store): open or closed now, today's hours, close for an hour, two, four, the rest of the day or until reopened, and today's load with More, Fewer, No more today and Back to usual, and for a store with slots each window's load today |
POS app (/v1/pos/{locationId}/status, with the store's timeZone and, for a store with slots, today's windows' load as slots; /v1/pos/{locationId}/capacity/today) |
Read by anyone who sells at the store. Changing it needs canManageStore on the agent profile (/v1/pos/agents/me): agents trusted to adjust stock (canAdjustStock), and admins without an agent record |
| MCP | locations get adds today's hours, closure, special hours, load and courier collection times; manageLocations setStatus (closed, reason, until), setCapacity (capacityType, capacity: 'MON 20', '2026-12-24 40', '2026-12-01..2026-12-24 60', 'today 5', or [] to clear), and save takes specialHours, courierCollection and pickBufferMinutes |
Site data transfer¶
Special hours and capacity limits travel with a site export. Whether a store is temporarily closed doesn't: an import keeps the target's own status. Bookings never travel.
Code¶
| Piece | Where |
|---|---|
| Model | Location (status, closedReason, closedUntil, hoursExceptions, capacity), HoursException, LocationCapacity, CapacityRule, CapacityOverride, CapacityType, CapacityDay, StoreStatus in common/.../locations/ |
| Service | StoreOperationsService (common), StoreOperationsServiceImpl (commerce-core/.../locations/) |
| Ledger | CapacityBooking, CapacityCounter, CapacityRepository (commerce-core/.../locations/capacity/); CapacityReconcileJob (worker/.../inventory/) |
| Routing | CapacityRouting and SourcingServiceImpl (commerce-core/.../inventory/sourcing/); booking and release in StockAllocationServiceImpl and RestockCancelledOrderProcessor; the dispatch date from DispatchService (commerce-core/.../delivery/) |
| Collection | CollectionPointService.availability, LocalPickupService.PickupOffer.unavailable, fragment/collectOffer |
| Admin | LocationAdminController, templates/admin/locations/ |
| Portal | StoreTodayController, FulfilmentPortalAdvice, templates/fulfilment/store.html |
| POS | PosStoreController (tag POS Store in rest/docs/api-spec/api.yaml) |
| MCP | LocationTools |
| Storefront | StoreController (ClosedNotice, SpecialDay), StoreLocatorServiceImpl.openToday, StoreStructuredDataServiceImpl |