Saltar al contenido principal

Customers and delivery orders in the PWA (feature 065)

Feature 065 is the PWA side of three backend features: the customer aggregate (062), the address form (063) and delivery orders with routing (064). It replaces the old customer list, profile and quick-create screens, and adds delivery capture, call-center routing and the two owner settings screens.

The design record is in specs/065-delivery-customers-pwa/: spec.md (FR-…, SC-…), research.md (F…, D…, B1–B7, residuals), contracts/, tasks.md (every task has a "Done" note) and checklists/ (handoffs, planted defects, timing).

Screens​

RouteWhat it isMain code
/forms/CustomerPagePhone-first search and listpages/customers/CustomerListPage.tsx, components/customers/CustomerSearch.tsx
/customers/:idCustomer view: core and phones, addresses, billing and emailspages/customers/CustomerProfilePage.tsx, components/customers/CustomerView.tsx
/restaurant/delivery/newStart a delivery order: customer, details, branchpages/restaurant/DeliveryCapturePage.tsx, components/delivery/DeliveryCaptureFlow.tsx
/restaurant/delivery/:orderIdThe order: cart for the fulfilling branch, details, send, change branchpages/restaurant/DeliveryOrderPage.tsx
/restaurant/originated-ordersOrders this location sent to branches, livepages/restaurant/OriginatedOrdersPage.tsx
/settings/address-formBusiness profile and location overridespages/settings/address-form/
/settings/order-routingCall centers and served branchespages/settings/order-routing/OrderRoutingSettingsPage.tsx

The retail customer selector (components/common/CustomerSelector.tsx) and the loyalty panel use the same search and dialogs. No other screen keeps its own customer or address editor.

What the user may do: capabilities (B1)​

GET /customers/capabilities?businessId= reports the caller's own grants: customer.{read,create,update,delete}, addressForm.manage, orderRouting.manage, remoteOrder.create. hooks/customers/useCustomerCapabilities.ts reads it and fails closed: until it answers, and on error, every flag is false. Screens hide actions, never just disable them, when a flag is false. The server enforces the same rules; the flags only shape the UI.

Versioned writes​

Every customer, delivery, profile, override and routing write sends If-Match: "<version>". CORS does not expose ETag, so the version is read from the last response body. A first save is "0".

  • services/customers/customerApi.ts is the only customer writer in the PWA. Creates send no version and never the legacy flat fields; everything else sends one (customerApi.test.ts).
  • hooks/customers/useCustomerWrite reads the version from the query cache, writes the returned view back, and turns a *_VERSION_CONFLICT into the shared conflict dialog (components/customers/VersionConflictDialog.tsx). After "Reload", the user's unsaved input stays visible read-only beside the fresh data (UnsavedInputPanel).
  • Since B5 the server refuses an unversioned PATCH, status change or delete (428 CUSTOMER_VERSION_REQUIRED).
  • lib/api-error.ts (parseApiError) reads both error shapes: 062/063 put details at the top level, 064 nests them under details.

Phones in results (B4)​

Search and duplicate results carry phoneDisplay, masked on the server (••••-6975) except when the whole number was typed. Full numbers appear only on the customer view, the delivery step, the delivery order page and the order ticket (FR-003a).

The address dialog​

components/customers/address/buildAddressFormModel.ts turns the location's resolved form (GET /address-form?locationId=) and the stored address into rows: primary fields in configured order, the rest under "More fields" with an A–Z option (kept per device), and stored values the location hides shown read-only so they survive a save. refreshOnOpen refetches the form when the dialog opens, keyed on the location; a regression test guards a past refetch loop.

Business-provided labels go through components/customers/localizedLabel.ts: the user's language, then Spanish, then English, with an empty string treated as missing.

Delivery capture​

  • One page in sections: customer, delivery details, branch. Defaults (default address, primary phone, default billing profile) are preselected. "Open cart" creates the order with its details in one call. An email can be added on the details step and is stored on the customer; the snapshot does not include it.
  • Unfinished capture (components/delivery/deliveryCaptureStore.ts): kept in localStorage under flowpos:delivery-capture:{userId}:{businessId}:{locationId}, ids and order-only notes only, never customer data. Discarded after 30 minutes idle, on sign-out, on a location change, and once the order exists. A resumed id the customer no longer has is dropped and the user is asked again.
  • Branch step (components/delivery/BranchStep.tsx, B2 read GET …/order-routing/branches): a call center must choose, with nothing preselected; a branch that routes lists itself first, preselected; everyone else never sees it. A failed read blocks the order (fail closed), so deploy the backend first.
  • Order page: the cart is the existing order-taking panel, which reads menus and prices for order.locationId (the fulfilling branch). Details are editable while the order is a draft; an address, phone or billing profile added in place is saved at once. After submit the page shows the snapshot. Routed orders skip the station-health check. The cash-session guard applies only when the order is fulfilled here. The address row and the order-tab strip show the labelled place. Details, or a double-click on the row, lists every stored field; Edit opens the address dialog. Edit, or a double-click, opens the phone or email dialog and saves the customer at once. The strip stays read-only, including after submit, because it reads the frozen snapshot. The email stays off the order.
  • Change branch (ChangeBranchControl): the first call never acknowledges a cart clear; on CART_CLEAR_NOT_ACKNOWLEDGED the user confirms with the item count, and only then is it repeated with acknowledgeCartClear: true.
  • NIT/CUI warning: inline above Q2,500 when the business issues FEL (buyerIdThreshold on the delivery read, B6), using the shared requiresBuyerId.

Served-branch reads (B3)​

A call-center agent is not assigned to the branches, so the global LocationAccessGuard would refuse the branch's menu and prices. @AllowServedBranchRead() opts a read route into one extra rule: when the guard would refuse the request's locationId, it lets it through only if the caller holds RemoteOrder Create in that business and is assigned to a location whose routing serves that active branch.

  • Decorated routes, exactly: GET /locations/:locationId/menus and POST /api/v1/pricing/resolve. The other panel calls carry no top-level locationId (list and reasons in research § B3).
  • The routed-branch lookup is a port (roles/domain/served-branch-read.port.ts) with a Kysely adapter. The guard only uses it when app.module passes it; a guard built without it behaves exactly as before.
  • roles/infrastructure/__tests__/served-branch-read-wiring.spec.ts fails if the decorated set changes, if the decorator lands on a class, or if app.module stops passing the wiring.

Originated orders​

The server filters the list by origin and status only: a branch filter would name a locationId the agent is not assigned to, and widening the guard would expose the branch's whole order list. The page hides orders the origin fulfils itself and filters by branch over each loaded page (50). It refreshes on order.created / order.updated summaries for this origin. A branch the origin no longer serves shows "Branch no longer routed". The menu entry is shown only where the location serves a branch (filterMenuByAccess hiddenPaths).

Owner settings​

  • Address form: presets with a preview computed from ProfileView.presetFields (B7), the field table, labels and custom fields; per-location overrides with inherited / overridden / locked markers and resets. Turning off a shown field says its stored values are kept and still print.
  • Order routing: call-center switch and served branches. The location itself is never a candidate; a call center without branches cannot be saved.

Translations​

All strings are in i18n/locales/{es,en}.json under customers, addressForm, deliveryOrder and orderRouting. Spanish uses tuteo, and the delivery word is "orden", as in the rest of the restaurant module. i18n/locales/feature-065-keys.test.ts fails if a key a 065 screen uses is missing from either locale.

Deploy order​

  1. Backend B1–B4, B6, B7, B2, B3 (all additive).
  2. Remote Config pwaMenu.json (new menu entries).
  3. The PWA.
  4. B5 last: required versions and staff-mode legacy addresses. An older PWA build would start getting 428 and validation refusals, so B5 waits until the new PWA is live everywhere.