Docs Hub Checkout route

Overview

web-shop-checkout is Vodafone's route-driven checkout and order-completion app for online shopping journeys outside VOXI. The repo combines a React + MobX client, a shared route/config layer, and Express/Vite server runtimes that proxy backend APIs, auth, flags, and AIM mocks.

Primary path prefixes

Normal checkout runs under /secure-checkout. Seamless migration mirrors the route tree under /customer-transfer/secure-checkout.

Main runtime split

Browser load goes through index.tsx, app.tsx, routes.tsx, Wrapper, then either the panel-driven checkout shell or a standalone route.

What changes behavior

JourneyStore, PanelsStore, CheckoutStore, FeatureFlagStore, and ContentStore jointly determine which panels, copy, routes, and modals exist at runtime.

Where the UI diverges

The repo owns the main accordion checkout, the order-processing poller, accepted/conditional/declined outcome pages, 3DS validation, device-loan routes, and an assisted-channel summary route.

This docs site is grounded in the current code paths in src/client/routes.tsx, AppInit, the MobX stores under src/client/app/stores, and the server runtimes under src/server.

Tech stack

The app is a React 18 checkout running on a MobX singleton-store model, fronted by React Router and backed by Express/Vite middleware, Contentful, LaunchDarkly, Adobe Target, and AIM.

Getting started

The local developer path in the README is: install dependencies, copy env templates, generate the local certificate, run the HTTPS server, add ?debug, and then set or generate a basketId.

Recommended local mock flow Fastest path for UI and panel work.
  1. Run yarn install.
  2. Copy the env templates called out in the README and fill real values where needed.
  3. Run yarn certgen once to generate the localhost certificate.
  4. Start the app with yarn start.
  5. Open https://localhost:8000/secure-checkout?debug.
  6. Use the DebugUtil or browser cookies to set a valid basketId.
Live API and environment notes Needed for end-to-end or environment-specific debugging.
  • Node 22 and Yarn are expected; the repo uses Volta.
  • The README calls out Bastion access, correct SSH config, and a valid LaunchDarkly auth token.
  • .env files are intentionally not committed. Use the provided .example files as templates.
  • Cypress flows expect a production-like server to be running first, typically with yarn start:prod.

Runtime architecture

There are three runtime layers to keep in mind: client boot and route ownership, MobX-driven journey/panel orchestration, and the server middleware stack that injects auth, flags, proxies, and mocks.

Client boot

index.tsx starts the session, then CheckoutApp creates the router, while Wrapper injects theme, i18n, suspense, and the lazy debug panel.

AppInit and stores

AppInit initialises flags, checkout/content/basket readiness, then journey detection, JSON schemas, panel instances, and analytics before route UIs render as ready.

Panel system

PanelsStore chooses a journey-specific interface list from journeyInterfaces.ts, creates one PanelStore per step, and drives hidden/summary/active transitions with MobX reactions.

Route specialisations

The main accordion checkout is only one surface. Order processing, order completion, 3DS, loan review/signing, and assisted-channel summary are separate route owners with their own runtime loops.

Route and runtime map

The graph below follows the real runtime hand-off from browser boot into the route tree, AppInit, checkout/panel orchestration, order submission, processing, and final outcome pages.


            
Key path rule: all primary routes live under systemCfg.pathPrefix, which is currently /secure-checkout. Seamless migration mirrors part of the tree under /customer-transfer/secure-checkout.

Render breakdown

This is the runtime ownership map from route shells down into meaningful children. The groups are ordered from shared owners to deep panel families and standalone routes.

State management

The app uses MobX singleton stores rather than React context for shared state. The strictest rules live in CheckoutStore, which calls configure({ enforceActions: "always" }), while other stores derive branch conditions or expose domain-specific data slices.

Source of truth

CheckoutStore holds checkout data, basket IDs, order flags, redirect state, and many computed values panels read directly.

Branch engine

JourneyStore derives logged-in, bingo, PAYG+, broadband, migration, business, and network-trial modes from checkout data and flags.

Panel coordinator

PanelsStore turns the journey model into visible panel instances, active-step order, and summary/hidden/error transitions.

Core request lifecycle

A successful checkout submission is not a single request. The normal path is checkout bootstrap, basket hydration, panel-specific PUT/POST submissions, optional payment/3DS hand-off, order-state polling, and then a refreshed order-complete render.

Server and runtime layer

The repo ships two server entrypoints that share most of the same middleware chain: a production Express runtime and a Vite dev server. Both are responsible for route prefixing, auth/session middleware, flags, proxying, and AIM support.

Content, config, and flags

Content and toggles arrive through multiple channels. Contentful-backed content comes through ContentService and is consumed by both newer stores and a few tactical legacy calls. Feature flags arrive server-side through LaunchDarkly and client-side through Adobe Target on window.vfukTnt.

Contentful wiring How copy and dynamic CMS content enter the runtime.
  • ContentStore.initStore() gates notification/order-complete fetches behind flags.
  • PanelsStore.setPanelCopy() still owns panel-copy fetching and parsing.
  • CheckoutStore.initStore() still contains tactical Contentful fetch calls for notifications, order-complete content, and state notifications.
  • The journey-specific Contentful key is mostly derived from JourneyStore.contentfulJourneyType.
Debug and AIM Why ?debug matters in local development.
  • README explicitly expects local work to use ?debug.
  • AIM middleware proxies checkout, basket, payment redirect, and session endpoints.
  • The debug panel can change mocks, flags, login state, analytics tracing, and redirect scenarios.

Component inventory

The inventory below is broad on purpose: it covers route owners, panel orchestration, key panels, stores, and service/runtime modules that materially affect behavior.

File structure and aliases

The repo is split between src/client, src/server, shared route config, worker transforms, Cypress/Jest harnesses, and AIM mock payloads. Path aliases in tsconfig.json are used heavily across the codebase.

Tooling and testing

Most local workflows are script-driven through package.json, with Jest for unit coverage, Cypress for E2E and visual runs, and Sonar/ESLint/TypeScript for quality gates.

Unresolved areas

A few parts of the runtime are intentionally external or only partially visible from this repository. They are worth calling out so new engineers know where code evidence ends.

Federated basket deep dive

The checkout does not render its own order-summary UI. Instead it delegates that work to an independently deployed micro-frontend — the federated basket — loaded at runtime via the Vodafone Federated Module system (@vf/federated-react, @vf/federated-core). This section explains exactly what "federated" means here, how the basket data flows in, and which parts of the checkout it affects.

What is the federated basket?

Vodafone's online properties use a micro-frontend architecture where different squads own different UI surfaces. The basket is maintained by a separate team and deployed independently as a remote bundle. When web-shop-checkout needs to show an order summary (in ReviewYourOrder and on the FakeFederatedBasket dev route), it pulls that remote bundle from a known URL (/basket/federated) and mounts it inside the page. The checkout never contains the basket rendering code; it only acts as a host.

How the bundle is loaded

Remote entry point

The federated module is fetched from /basket/federated. In local dev this URL is proxied so the basket team's own dev server (or a mock) can respond. In production the path resolves to the basket squad's deployed CDN asset.

Mount mechanism

@vf/federated-react wraps the remote bundle in a React component. ReviewYourOrder renders that component — named vfuk-BasketContainer — inside its JSX tree. If the remote bundle has not yet loaded, a loading skeleton is shown instead.

Isolation

The federated bundle executes in its own JS scope. It brings its own React version, its own CSS, and its own state. The checkout cannot directly call functions inside it or read its internal store. The only contract is the props interface and the event bus.

Dev route

A /fake-federated-basket route (registered only when systemCfg.isDev is true) mounts a local placeholder so developers can work on the review panel without needing the real basket bundle running.

Data flowing into the federated basket

The basket needs to know what is in the order so it can display a correct summary. The checkout owns that data — it is fetched from the backend and stored in CheckoutStore.basketData. Two mechanisms send that data to the federated component:

Props pass-throughDirect data at mount time.

When ReviewYourOrder mounts vfuk-BasketContainer it passes checkoutStore.basketData as a prop (typically called basketData or equivalent). This gives the federated component its initial snapshot of the order — products, prices, discounts, delivery charges, and totals — without any further network call.

Event busRuntime updates after mount.

The checkout emits a basket:data event through eventService.emit() whenever CheckoutStore.updateBasketData() is called — for example after a panel submission changes delivery options or a promo is applied. The federated basket listens for this event and re-renders its summary accordingly. This keeps the displayed totals in sync without a full page reload.

What CheckoutStore.basketData contains

basketData is the raw basket payload fetched from GET /api/basket/v2/basket/{basketId} and reshaped by the basket web worker. It is the same data that BasketStore uses to build payment-panel breakdowns and order-complete summaries. Key sections include:

  • Products / packages — handset, SIM, broadband, or accessory lines with their individual prices.
  • Discounts and promotions — applied offer codes and their monetary effect.
  • Delivery charges — shipping cost for the selected delivery method.
  • Upfront and recurring totals — the amounts surfaced in the payment panel and in the review summary.
  • Trade-in value — deducted from upfront costs when a trade-in is present.

Which checkout sections involve the federated basket

ReviewYourOrder panel

The primary host. The federated basket renders the full order summary card inside this panel. The checkout wraps it in its own panel chrome (header, errors, T&C checkbox, submit button) but the product listing is entirely basket-team code.

BasketStore

Consumes the same raw basket payload via the web worker to produce the breakdownSections and totals that the UpfrontPayment panel and order-complete screens consume. The basket store and the federated component read the same source of truth but render different views of it.

CheckoutStore.updateBasketData()

Called after every panel submission that could affect basket content (delivery changes, address changes, promotions). It re-fetches from the basket API and re-emits the basket:data event so the federated basket re-renders automatically.

FakeFederatedBasket (dev only)

A local stub route at /fake-federated-basket that mounts a simplified basket mock. Used during development when the real basket bundle is unavailable or when testing checkout layout without a live basket deployment.

Key takeaway for new engineers: if the order summary is wrong, blank, or not updating after a panel submit, the fault is almost always in one of three places: the basketData that CheckoutStore holds, the updateBasketData() call chain after a panel submit, or the federated basket bundle itself (which is a separate deployment and may need its own investigation with the basket team).

Panel — About You

The first panel in almost every journey. It collects the customer's identity — personal details, customer type, and journey-specific fields — and is the earliest branch point in the checkout. The optimised variant (OptimisedAboutYou) is an Adobe Target experiment with the same backend contract.

Component

src/client/app/components/panels/AboutYou/AboutYou.tsx
Optimised variant: OptimisedAboutYou/OptimisedAboutYou.tsx

Store

aboutYouStore (or optimisedAboutYouStore for the CRO variant). Both delegate submission to panelStore.submit().

Visibility trigger

Always the first visible panel. Shown for every journey type. Cannot be skipped unless the customer is already fully authenticated and the journey interface excludes it.

API call

MethodPathPurpose
PUT/api/checkoutInfo/v2/{checkoutId}/personalDetailsSaves personal details and customer type to the backend checkout session.

What the front-end sends

FieldTypeConditionSource
customerTypestringAlwaysRadio button selection: Consumer, SoleTrader, LimitedCompany, Partnership
titlestringAlwaysDropdown (Mr, Mrs, Ms, Dr, …)
firstNamestringAlwaysText input
lastNamestringAlwaysText input
emailstringAlwaysText input — validated for uniqueness before submit via checkUsernameEmailUniqueness()
phoneNumberstringAlwaysText input
dateOfBirthstring (ISO)Consumer onlyDate picker (day / month / year dropdowns)
nationalitystringConsumer onlyDropdown selection
employmentStatusstringConsumer, credit journeysDropdown (Employed, Self-Employed, Retired, …)
companyNamestringBusiness customer typesText input
currentNetworkProviderstringNetwork Trial HBB journeys onlyDropdown

Required fields to continue

  • All fields marked "Always" must be populated and pass JSON Schema validation.
  • The backend validates the payload against a schema fetched via GET /checkoutInfo/v2/{checkoutId}/personalDetails/schema during app init.
  • The backend response must return a statusInfo.status that is not an error for the panel to transition to summary state.
  • Email uniqueness is checked client-side first via POST /authorization/v2/action/doesUsernameExist — a non-unique email blocks submission before the panel PUT is even attempted.
OptimisedAboutYou difference: the field set and backend endpoint are identical. The only change is the UI layout (field order, grouping) and which MobX store drives validation. It is activated by an Adobe Target flag (optimisedAboutYouPanel) and is only available for Limited Company / Partnership customer types in specific journey interface sets.

Panel — Affordability

A confirmation-style panel — not an input form. The customer reads an affordability summary table and checks a box to confirm their circumstances have not changed since they first applied. It is used in Bingo (device finance) journeys where a credit decision already exists but a formal affordability affirmation is required before the loan documents can be shown.

Component

src/client/app/components/panels/Affordability/Affordability.tsx

Store

yourFinancesStore — the same store that drives YourFinances. The affordability table it renders reads loanAgreementStore for loan copy.

Visibility trigger

Shown for Bingo / device-finance journeys when the journey interface list includes it. Hidden for income-check journeys (which use YourFinances instead).

API call

MethodPathPurpose
PUT/api/checkoutInfo/v2/{checkoutId}/affordabilityDetailsSubmits the customer's affordability confirmation to the backend.

What the front-end sends

FieldTypeValueSource
noChangeInCircumstancesbooleantrueCheckbox — must be ticked by the user. The field is only submitted when true; leaving it unchecked blocks the continue button.

Required fields to continue

  • The only required field is noChangeInCircumstances: true.
  • The "Agree & continue" button is disabled until the checkbox is ticked.
  • Backend must return a non-error status on the affordabilityDetails object in the checkout response.

Panel — Address Details

Collects up to three years of UK address history. The front-end uses a premises lookup service to let customers find their address by postcode, then records how long they lived at each address and their residential status. A sibling panel — AddressCheck — handles a simpler variation for authenticated users who need to confirm (rather than enter) their address.

Components

AddressDetails/AddressDetails.tsx — full address history entry.
AddressCheck/AddressCheck.tsx — confirm existing account address.

Store

addressesStore — manages multiple address entries, date-moved-in validation, and the premises lookup interaction. AddressCheck reads from the same store.

Visibility trigger

Required for anonymous and credit-check journeys. AddressCheck replaces it for authenticated users on PAYM or home-moves journeys. Both panels are absent from journey interfaces that do not require a billing address history (e.g., some PAYG+ flows).

API calls

MethodPathPurpose
GET/api/premise/address?postcode={}&houseNumber={}Address lookup via premiseService — called when the customer types a postcode. Returns a list of matching premises to pick from.
PUT/api/checkoutInfo/v2/{checkoutId}/personalDetailsSaves the full address history (residential addresses, dates, residential status) to the checkout session.

What the front-end sends (personalDetails PUT)

FieldTypeConditionSource
addresses[].flatNumberstringIf applicablePre-filled from premises lookup or typed manually
addresses[].buildingNumberstringAlways (primary address)Premises lookup result
addresses[].buildingNamestringIf applicablePremises lookup result
addresses[].streetstringAlwaysPremises lookup result
addresses[].citystringAlwaysPremises lookup result
addresses[].postcodestringAlwaysCustomer-entered postcode used for the lookup
addresses[].countrystringAlwaysDefaulted to GB or selected from dropdown for non-UK addresses
addresses[].dateMovedInstring (YYYY-MM)AlwaysMonth/year picker in the panel
addresses[].residentialStatusstringAlways for primary addressDropdown: Owner, Tenant, Living With Parents, …
addresses[].currentAddressbooleanAlwaysSet to true for the first entry

Required fields to continue

  • A minimum of three years of continuous address history must be provided. If the current address covers fewer than three years, additional address entries are required.
  • Each address entry must have a valid dateMovedIn, postcode, street, and city.
  • The primary address must have a residentialStatus.
  • Backend validates the full payload and returns errors per-address if any entry is incomplete.
  • AddressCheck variant: the customer confirms a pre-populated address from their account; the only additional required field is dateMovedIn for each address entry. The same personalDetails PUT is used.

Panel — Delivery

Lets the customer choose how and where their order is delivered. There are three panel variants covering different product types: standard Delivery for handsets and SIMs, BroadbandDelivery for router/equipment shipping, and RedDelivery for PAYG+ orders that need a combined delivery and billing address step. All three write to the same backend key (deliveryOptions) via the same PUT endpoint.

Components

Delivery/Delivery.tsx — standard handset / SIM delivery.
BroadbandDelivery/BroadbandDelivery.tsx — broadband router shipping.
RedDelivery/RedDelivery.tsx — PAYG+ combined delivery + billing address.

Stores

deliveryStore, addressDeliveryStore (home delivery selection), broadbandDeliveryStore, redDeliveryStore.

Visibility trigger

Shown when the basket contains a physical product (handset, router, SIM). eSIM-only or activation-only journeys may skip this panel entirely or show a reduced broadband-activation variant that does not involve an address.

API calls

MethodPathPurpose
PUT/api/checkoutInfo/v2/{checkoutId}/deliveryOptionsInitial call to fetch available delivery methods for a given postcode (and later to save the customer's choice). The same endpoint is used for both "get methods" and "save selection".
GET/api/premise/address?postcode={}Address lookup used by RedDelivery and BroadbandDelivery when the customer searches for a delivery address different from their billing address.

What the front-end sends (deliveryOptions PUT)

FieldTypeValue / source
deliveryNamestringHardcoded 'DeliverToAny' for home delivery. 'ClickAndCollect' for store pick-up.
deliveryAddress.flatNumberstringFrom selected premises result or previously entered address
deliveryAddress.buildingNumberstringFrom premises lookup
deliveryAddress.buildingNamestringFrom premises lookup (if applicable)
deliveryAddress.streetstringFrom premises lookup
deliveryAddress.citystringFrom premises lookup
deliveryAddress.postcodestringCustomer-entered search postcode
deliveryAddress.countrystringDefaulted 'GB'
deliverySlotIdstringSelected time-slot ID from the list of available slots returned by the first PUT call (if slot selection is available)

RedDelivery additional fields

FieldTypeSource
billingAddress.*objectEither re-used from billing address (checkbox ticked) or separately searched via premises lookup
useBillingAddressForDeliverybooleanCheckbox in the RedDelivery panel UI

Required fields to continue

  • A delivery method must be selected (home delivery or click & collect).
  • A valid delivery address must be chosen from the premises lookup results — free-text address entry is not permitted on this panel.
  • If PAC/STAC switching restrictions apply (e.g., NI restriction), a modal error is shown and the panel cannot be submitted until the issue is resolved or the modal is dismissed with an alternative action.
  • BroadbandDelivery: if the journey is activation-only (no physical router), this requirement is relaxed — the panel submits without address fields.

Panel — Monthly Payment

Collects the customer's Direct Debit bank account details so that recurring monthly charges can be set up. It is a thin shell around ActivePanel (bank details form) and Summary (the completed state). Editing this panel can also re-open the Spend Manager panel when bill-capping state is linked.

Component

src/client/app/components/panels/MonthlyPayment/MonthlyPayment.tsx

Store

monthlyPaymentStore. The panel's active/summary toggle is also driven by checkoutStore.showMonthlyPaymentActive.

Visibility trigger

Shown when the basket contains a product with a monthly recurring charge (PAYM, SIMO migration, broadband bundles). Not shown for PAYG+ or upfront-only journeys.

API call

MethodPathPurpose
PUT/api/checkoutInfo/v2/{checkoutId}/bankPaymentSaves the bank account details for Direct Debit setup.

What the front-end sends

FieldTypeSource
accountTypestringDropdown: Personal or Business
accountHolderNamestringText input
sortCodestringText input (6 digits, formatted as XX-XX-XX)
accountNumberstringText input (8 digits)
paymentDaynumberDropdown: preferred payment day of the month

Required fields to continue

  • All five fields above are required.
  • Sort code and account number are validated against format rules client-side before submission.
  • Backend performs a Modulus check on the sort code / account number combination and will return an error if the bank account is not valid.
  • If Spend Manager is also in an active state (e.g., the user navigated back to edit), both panels must be re-submitted before checkout can progress.

Panel — Payment

The upfront / card-payment panel. It handles one-off charges such as a handset upfront cost, a PAYG+ SIM purchase, or a Bingo device deposit. Because this panel touches raw payment-card data it uses a remote payment iframe for PCI compliance — no card numbers are ever handled by the checkout JavaScript directly.

Component

src/client/app/components/panels/PaymentPanel/UpfrontPayment.tsx

Store

upfrontPaymentStore. Payment options (saved cards) are read from checkoutStore.checkoutData.paymentDetails.

Visibility trigger

Shown when the basket includes an upfront payment amount greater than zero. Hidden for zero-upfront journeys (e.g., SIM-only with no activation fee). PAYG+ uses a slightly different sub-layout (HybridSection) within the same component.

API calls

MethodPathPurpose
PUT/api/checkoutInfo/v2/{checkoutId}/payment/{activeId}Updates the selected payment card / method choice.
PUT/api/checkoutInfo/v2/{checkoutId}/payment/{paymentId}/initiateCardPaymentInitiates a card payment session — called when the customer selects a card type that requires the iframe. Returns the iframe URL and a payment session token.

What the front-end sends

FieldTypeSource
selectedPaymentIdstringRadio button — ID of the card selected from the list of stored cards surfaced by checkout data
saveCardbooleanCheckbox — whether to save the card for future purchases (only shown for new cards)

Payment iframe flow

  • When a card requiring 3DS or new-card entry is selected, initiateCardPayment is called. The response contains an iframe URL.
  • The checkout renders that URL in an <iframe> — the remote page (owned by the payment provider) collects the card number, expiry, and CVV.
  • The iframe posts a window.makeGetWebPayment() callback on success, which the checkout intercepts to trigger order submission or 3DS redirect.
  • No card data is ever sent through the checkout's own APIs. This is intentional PCI DSS compliance.

Required to continue

  • A payment card must be selected (existing saved card or new card entered in the iframe).
  • If 3DS authentication is required, the customer is redirected to /secure-checkout/card-authentication and must complete the bank challenge before order submission continues.
  • A backend error on paymentDetails.statusInfo will surface an inline panel error and block progression.

Panel — Spend Manager

An optional panel that appears after Monthly Payment for logged-in customers. It lets them set a bill cap per package to protect against unexpected overage charges. Customers can configure caps immediately or defer to later — both choices are valid completions.

Component

src/client/app/components/panels/SpendManager/SpendManager.tsx

Store

spendManagerStore. Also reads journeyStore to determine whether the panel should be visible at all.

Visibility trigger

Only shown for logged-in customers on monthly-payment journeys where spend management features are enabled. Anonymous or PAYG+ journeys do not see this panel.

API call

MethodPathPurpose
PUT/api/checkoutInfo/v2/{checkoutId}/billCappingSaves the customer's bill-cap selections (or their choice to defer) to the checkout session.

What the front-end sends

FieldTypeSource
billCaps[].packageIdstringPackage identifier from checkout data
billCaps[].capAmountnumber | nullDropdown per package: Off / £5 / £10 / £25 / £50. null means "off".
deferredSetupbooleantrue when the customer clicks "No, maybe later" — bypasses the selector and submits with no caps set.

Required to continue

  • The customer must either select a cap level for each package or click "No, maybe later". There is no other blocking requirement.
  • Multi-plan journeys render one BillCapSelector row per active package; all rows must have a selection before the continue button enables.

Panel — Your Account (Create Your Account)

Shown for anonymous customers who do not yet have a My Vodafone account. It combines account credentials (email, password, PIN), accessibility preferences, marketing consent, and optional confirmation-letter preferences into a single step. The Preferences panel is a lighter variant for authenticated customers who already have an account but still need to set accessibility and consent preferences.

Component

CreateYourAccount/CreateYourAccount.tsx — anonymous account creation.
Preferences/Preferences.tsx — authenticated preference update.

Stores

accountPanelStore, accountSetupStore, pinSetupStore, accessibilityStore. Preferences uses preferenceStore + accessibilityStore.

Visibility trigger

Anonymous journeys only (PAYG+, broadband, standard anonymous PAYM checkout). Logged-in customers see the lighter Preferences panel instead, or skip account setup entirely for upgrade journeys.

API calls

MethodPathPurpose
POST/authorization/v2/action/doesUsernameExistPre-submit uniqueness check on the email / username before the panel PUT is attempted.
POST/api/checkoutInfo/v2/checkUsernameEmailExistSecondary availability check (used in some journeys as an additional validation layer).
PUT/api/checkoutInfo/v2/{checkoutId}/personalDetailsSaves email/username and account credentials to the checkout session.
PUT/api/checkoutInfo/v2/{checkoutId}/accessibilityDetailsSaves accessibility needs and marketing/personalisation consent flags.

What the front-end sends (personalDetails PUT)

FieldTypeSource
emailstringText input — checked for uniqueness first
passwordstring (hashed)Password field — hashed client-side before any network call; the raw password is never sent
pinstring4-digit PIN input
confirmationLetterPreferencestringRadio: email / post (shown on some journey variants)

What the front-end sends (accessibilityDetails PUT)

FieldTypeSource
accessibilityNeedsstring[]Multi-select checkboxes: hearing, visual, mobility, etc.
marketingConsent.emailbooleanCheckbox
marketingConsent.smsbooleanCheckbox
personalisationConsentbooleanCheckbox

Required to continue

  • Email must be unique (checked via the doesUsernameExist endpoint before the main submit).
  • Password must meet complexity rules (validated client-side via JSON schema from the validation service).
  • PIN must be exactly 4 digits.
  • Marketing consent fields must be explicitly set (opt-in or opt-out — an unset state is not accepted).
  • Both the personalDetails and accessibilityDetails PUTs must succeed before the panel transitions to summary.

Panel — Review Your Order

The final panel before order submission. It hosts the federated basket component (the order summary), surfaces any pending panel errors, and exposes the Terms & Conditions checkbox and the "Place order" button. Clicking "Place order" hands control to OrderSubmissionRedirectService, which navigates to either the processing screen or (if 3DS is required) the card-authentication screen.

Component

src/client/app/components/panels/ReviewYourOrder/ReviewYourOrder.tsx

Store / dependencies

reviewYourOrderStore, checkoutStore.basketData, panelsStore.isReadyForSubmission, federated basket module (vfuk-BasketContainer).

Visibility trigger

Always the last panel in every journey. Becomes active only after all preceding panels have reached their summary state (panelsStore.isReadyForSubmission === true).

API calls

MethodPathPurpose
PUT/api/checkoutInfo/v2/{checkoutId}/personalDetailsFinal validation pass — ensures personalDetails.validated is true and defaults any missing fields (e.g., customerType defaults to 'Consumer') before order submission.
POST/api/checkoutInfo/v2/{checkoutId}/submit (via OrderSubmissionService)The actual order submission call — triggers the backend to process the full checkout and begin fulfilment. After this call the user is redirected to /secure-checkout/processing.

What is checked before "Place order" is enabled

  • panelsStore.isReadyForSubmission must be true — every panel ahead of Review Your Order must be in its summary (completed) state.
  • The Terms & Conditions checkbox must be ticked.
  • The federated basket must have loaded without an error (if it fails, a blocking message is shown).

Post-submission flow

  • If no 3DS is needed → navigates to /secure-checkout/processing.
  • If 3DS is required → navigates to /secure-checkout/card-authentication, the customer completes the bank challenge, then the window.makeGetWebPayment() callback re-enters the submission flow.
  • OrderProcessingService.beginProcessing() polls GET /api/checkoutInfo/v2/{checkoutId}/orderState on the processing screen until a terminal state (accepted / conditional / declined) is reached, then routes to the appropriate /secure-checkout/submitted|conditional|error page.

All API requests

Every HTTP request this service makes to backend systems, organised by domain. The base path for all checkout endpoints is /api/checkoutInfo/v2/{checkoutId}. The {checkoutId} is created on first load and stored in session storage; all subsequent panel submits reference it. All requests go through DataService.request(), which adds AIM tracking keys in development mode.

Checkout lifecycle

Method Path Caller Description
POST /api/checkoutInfo/v2/ CheckoutApiService.createCheckout(basketId) Creates a new checkout session. Sends basketId (read from cookie). Returns a CheckoutData object containing the checkoutId used by all subsequent calls.
GET /api/checkoutInfo/v2/{checkoutId}/ CheckoutApiService.getCurrentCheckoutState() Re-fetches the full checkout state. Called on page reload or after a redirect to resume an in-progress checkout.
GET /api/checkoutInfo/v2/{checkoutId}/{schemaName}/schema CheckoutApiService.fetchSchema()ValidationService Fetches the JSON Schema for a named panel (e.g., personalDetails, affordabilityDetails). Called during AppInit for all panels in the current journey.
POST /api/checkoutInfo/v2/{checkoutId}/submit OrderSubmissionService.submitOrder() Submits the completed checkout for fulfilment. Called by ReviewYourOrder when the customer clicks "Place order". Triggers the backend to begin order processing.
GET /api/checkoutInfo/v2/{checkoutId}/orderState OrderProcessingService.beginProcessing() Polled repeatedly on the processing screen until a terminal order state is reached (accepted / conditional / declined).

Panel data submissions

Method Path Panel Description
PUT /api/checkoutInfo/v2/{checkoutId}/personalDetails About You, Address Details, AddressCheck, BillingAddress, RedDelivery, Create Your Account, Review Your Order (final pass) The most widely-used panel endpoint. Multiple panels submit to this path but send different field subsets — the backend merges them into the single personalDetails object.
PUT /api/checkoutInfo/v2/{checkoutId}/affordabilityDetails Affordability, Your Finances Saves income/expenditure or the no-change-in-circumstances confirmation. Both panels write to the same key with different payloads depending on journey type.
PUT /api/checkoutInfo/v2/{checkoutId}/deliveryOptions Delivery, BroadbandDelivery, RedDelivery Fetches available delivery methods (first call by getDeliveryMethods()) and then saves the customer's delivery selection and address.
PUT /api/checkoutInfo/v2/{checkoutId}/bankPayment Monthly Payment Saves Direct Debit bank account details (sort code, account number, account holder name, payment day).
PUT /api/checkoutInfo/v2/{checkoutId}/billCapping Spend Manager Saves bill-cap selections per package or records that the customer chose to defer setup.
PUT /api/checkoutInfo/v2/{checkoutId}/payment/{activeId} Payment (UpfrontPayment) Updates the selected payment card / method for the upfront charge.
PUT /api/checkoutInfo/v2/{checkoutId}/payment/{paymentId}/initiateCardPayment Payment (UpfrontPayment) Initiates a card payment session and returns the iframe URL. Called when the customer selects a card option that requires an iframe entry or 3DS challenge.
PUT /api/checkoutInfo/v2/{checkoutId}/accessibilityDetails Create Your Account, Preferences Saves accessibility needs and marketing / personalisation consent flags.

Utility and lookup endpoints

Method Path Caller / panel Description
GET /api/premise/address?postcode={}&houseNumber={} premiseService — Address Details, Billing Address, Your Company, Red Delivery Address lookup by postcode (and optional house number). Returns a list of matching addresses from Royal Mail PAF. Used anywhere the customer must enter a UK address.
GET /api/utility/company?registrationNumber={}&legalStatus=LC CheckoutApiService.getCompanyList() — Your Company (Limited Company) Looks up a company by Companies House registration number for Limited Company customers.
GET /api/utility/company?companyName={}&legalStatus=P&postalCode={} CheckoutApiService.getCompanyList() — Your Company (Partnership) Looks up a company by name and postcode for Partnership customers.
POST /authorization/v2/action/doesUsernameExist CheckoutApiService.checkUsernameEmailUniqueness() — About You, Create Your Account Checks whether an email address is already registered as a Vodafone username. Called before the personal details PUT to prevent duplicate account creation.
POST /api/checkoutInfo/v2/checkUsernameEmailExist CheckoutApiService.checkUsernameEmailAvailability() — Create Your Account Secondary availability check used in some journey variants as an additional validation layer before account creation.

Basket and content endpoints

Method Path Caller Description
GET /api/basket/v2/basket/{basketId} CheckoutStore.updateBasketData() Fetches the current basket contents. Called on init and after any panel submission that may change basket totals (delivery change, address change). Result is passed through the basket web worker and stored in checkoutStore.basketData.
GET /api/content/v2/checkout/{journeyType} (Contentful-backed) ContentServiceContentStore, PanelsStore Fetches Contentful-backed copy for notifications, panel headers, and order-complete content. The journeyType key is derived from JourneyStore.contentfulJourneyType.
Method colour key used in this section: GET = read-only fetch  ·  POST = create / action  ·  PUT = update panel state. All requests are proxied through the Express/Vite server middleware stack and include AIM tracking keys in development mode.