Docs

Engineering · Report · Running on the preview VM, not in production

Multilocation Business APIs

Twenty-four web endpoints list a business's records across every location the signed-in user may read. Each one answers for the whole business, or for the locations picked in the location filter. The mobile app's endpoints are unchanged.

24 endpointsread onlyweb only24 of 24 pass on preview VM
branch codex/multilocation-ui @ d1fa11f17b · PR #23863 (draft) · measured 2026-10-05

Status

PartState
Shared location scope (who may read which locations)Pushed. Running on the preview VM.
24 list endpointsPushed. Running on the preview VM.
Contract for all 24 (requests, responses, errors, cache keys)Written. Status: proposed.
Unit tests10,433 pass across the nine libraries touched and the controller tests. 3 scheduling tests fail; they compare the server's form-answer table with the client app's widget code, which this work did not change.
Live check on the preview VM24 of 24 endpoints pass. 17 were compared against real rows; 7 have no data on the VM.
ProductionNot deployed. The branch is 93 commits behind develop and the PR is a draft.
Web pages reading these endpointsNot started.
Writes by businessNot built. Creating and editing stay on each location's existing endpoints.

Database side of this work: Multilocation Database Ownership.

Shape

Every endpoint has the same address form and the same answer form.

Address
GET /business/:businessEntityId/<resource>. The business comes first. The word "multilocation" is in no path.
Location filter
locationEntityIds, a comma-separated list. Left out, the answer covers every location the user may read.
Paging
page and limit (1 to 200, default 20). A page past the end returns the last page.
Answer
items, totalCount, totalPages, currentPage, and locationEntityIds: the locations the answer was read over.

Each item carries its location. A definition shared by several locations (a service, a coupon) is one item with a locations list. Something that happened at one place (a sale, an appointment) is one item with a single location. Records are never merged.

Who sees what

A location is included only when the user could open that location's own list for the same records. The check uses the same permission the per-location page uses, so the business view shows nothing the location pages would hide.

CaseResult
User has the permission at two of three locationsThe answer covers those two.
User asks for a location they cannot read403. The request is refused, not silently narrowed.
User has the permission at no location of the business403.
A row belongs to another businessNever returned.
A row has no owner recorded yetReturned when it sits at a location in the answer.

Per-row rules carry over. Client contact details are hidden row by row using the user's permissions at that client's own location. A chart restricted to certain jobs stays hidden from staff who do not hold that job at the chart's location.

Four record types have no location permission todayBundles, gift cards, gift card designs and products are gated only by role or feature flag on their existing per-location pages. The business lists keep that as it is: they cover every location the user is assigned to. Tightening this needs new permissions, which is a separate change.

Endpoints

All paths start with /business/:businessEntityId.

Catalogue

PathListsFilters
/servicesActive servicessearch, categoryId
/services/categoriesCategories, each with its service countsearch
/bundlesBundlessearch
/addonsAdd-onssearch, isActive
/addon-groupsAdd-on groupssearch, isActive
/resourcesRooms and equipmentisActive
/resource-groupsResource groupsisActive
/productsRetail and back-bar productsfilterSearch, filterIsActive, filterIntent

Packages, memberships and gift cards

PathListsFilters
/packagesPackage and membership templatesstatus (active or archived), billingType, search
/packages/soldPackages clients have boughtstatus
/memberships/soldMemberships clients holdstatus
/gift-cardsIssued gift cardsstatus, source, search, dateFrom, dateTo, recipientClientId
/gift-card-designsGift card designsownership

Clients and team

PathListsFilters
/clientsClientssearchKey, source
/staffTeam members with their location assignmentsincludeArchived

Sales and appointments

PathListsFilters
/salesSalesstatus, search, dateFrom, dateTo
/bookingsAppointmentsstatus, search, startDate, endDate, providerId

Promotions

PathListsFilters
/offersOffersstatus, search
/couponsCouponsstatus, search, origin, autoGenType
/auto-coupon-configsAutomatic coupon rulesnone

Forms and charts

PathListsFilters
/formsForms and chartskind, search, status, managementView
/form-tagsForm tagsnone
/dot-phrasesDot phrasesnone
/form-submissionsCharts waiting for sign-offformId, from, to, assignedToMe

Verification

Run on 2026-10-05 against the preview VM (one business, two locations), signed in as the business owner.

Checked on every endpoint: the answer has exactly the five fields; no record repeats; no item exposes the owner column; every item names a location in the answer; narrowing to one location returns only that location's rows; a page past the end returns the last page; a location outside the business is refused; every filter is accepted; an unknown status is refused.

EndpointRowsCompared with
Services28A direct database count: 28.
Categories6Each category's service count equals the services filtered by that category.
Clients62The two per-location lists: 32 and 30.
Sales125The two per-location lists: 65 and 60.
Appointments124The two per-location lists: 64 and 60. Filtered to one staff member: 27, same as that location's list.
Team1515 member rows. A member assigned to both locations is one item with two locations.
Products11 product row.
Bundles, coupons, forms, form tags, dot phrases, resources, resource groups0One test record created at a location through the existing API. It appeared in the business list and the location's list, at that location only, and left both after delete.
Add-ons, add-on groups2The same test. An archived record stays in both lists.
Packages0The same test. An archived template moves to the archived tab.
Offers0A draft created through the API is hidden from both the business list and the location's web list. See Open decisions.
Sold packages, sold memberships, gift cards, gift card designs, automatic coupon rules, charts waiting for sign-off0Nothing. The VM has no such rows. The requests succeed and return an empty page; no row content was compared.

Not tested liveA user with the permission at only some locations, contact hiding on client rows, and the job restriction on charts. The owner sees everything, so these three are covered by unit tests only. Speed on production-sized data is also unmeasured.

What is different from the per-location lists

  • Rows are slimmer. Values worked out for one location (display price, staff coverage, stock on hand, a website status) are left out.
  • Unknown filter values are refused with a 400. Several per-location lists ignore them or fail.
  • Search treats % and _ as plain characters.
  • Every list has one fixed order, newest first.
  • Offers that need ops review are not listed.

Remaining work

1

Rebase onto develop

The PR conflicts with develop and CI has not run on it.

93 commits behind

2

Web pages

Move each Your business page onto its endpoint. Hook names and cache keys are in the contract.

per page

3

Domain documentation

The forms pack is updated. Services, clients, staff, bookings, payments and inventory packs still need their entries before the PR can merge.

6 areas

4

Query plans

The preview database is small. Searches and counts across many locations have not been measured on real volumes.

production-sized data

5

Writes by business

Creating a record for several locations at once still goes through each location's endpoint.

not designed

Open decisions

DecisionWhy it matters
Show rows with no owner recorded?Today they are shown. Hiding them empties a location's lists until its backfill has run.
History of a location that changed businessSales and clients stamped with the previous business are left out of the new business's lists. Appointments carry no owner, so they are shown. The two disagree.
Permissions for bundles, gift cards, designs and productsThey have none per location today. Any staff member assigned to a location reads its gift card codes.
A record shared to a second locationIt appears in the business list for that location, but some per-location detail pages still look only at the record's first location and answer "not found". Nothing creates such a share yet.
Sales permissionReading sales is gated by the permission to create a sale. A separate read permission does not exist.
What the Offers list should holdThe web offers list hides offers that go through ops review, and every offer created today is of that kind. The business list mirrors the web list, so it holds only older self-serve offers.
Tags and dot phrases for staff who only fill chartsBoth lists need the manage permission, so those staff are refused.
Sign-off from the business queueSign-off stays per location, 50 items at a time. A selection across locations has to be split by the page.

Each group's full list of open questions is in the contract, docs/specs/multilocation-business-apis-contract.md.