Docs Hub Data Simo route

web-shop-data-simo

Vodafone UK · Data SIM Only (SIMO) e-commerce journey · Consumer & Business segments

This is the front-facing web application that powers Vodafone's Data-SIM-Only (SIMO — SIM Only, data variant) purchase journey. It lets customers and business users browse data plans, pick add-ons, and complete checkout for a SIM card that provides mobile data with no voice calls included. The app supports both brand-new customers (acquisition) and existing Vodafone subscribers upgrading or migrating their tariff.

In plain English Think of this as the Vodafone online shop — but only for data-only SIM cards. A customer visits the page, picks a data plan (e.g. 20GB/month), optionally adds extras, then proceeds to checkout. The app talks to several backend services to load the available plans, save the customer's choices, and hand off to the payment flow.
React 17
Frontend Framework
TypeScript
Language
MobX 6
State Management
Contentful
CMS (headless)
Vite 3
Build Tool
Express
Node.js Server
~240+
Source-Web Components
Jest + Cypress
Test Stack
⚠️ These figures reflect the state of the codebase when this site was generated (June 2026). The code may have changed since — cross-check before relying on any numbers.

Guided Tour — Start Here

If you're new to this repo, read these sections in order. Each one builds on the last.

1
Architecture Diagram See how the browser, server, proxy, and backend services connect in a single picture. This is the mental model for everything else.
2
CMS — Contentful Almost all user-visible text, images, and marketing content is managed in Contentful, not in this repo. Learn what's hardcoded vs. CMS-driven before touching copy.
3
Backend Endpoints This app calls four distinct backend services. Know which service owns which data before debugging a broken journey.
4
HATEOAS Navigation The purchase journey does NOT use hardcoded API paths for state transitions. Understand HATEOAS or you will be confused by how plans get selected.
5
State Management All purchase state lives in MobX stores. Know the store shape before adding or debugging any feature.
6
Gotchas & Traps Read before you ship. Several non-obvious behaviours are waiting to bite you, especially around auth, the DXL proxy, and Contentful key naming.

Who Are You?

Jump straight to what you need most.

🆕
New Joiner
Just joined the team. Start here to get the app running locally and understand the big picture.
🔌
Backend / API Engineer
Working on the purchase APIs or content service. See every endpoint the frontend calls.
✏️
Content / CMS Owner
Managing Contentful entries. See which content keys this app consumes and how they map to the page.
🔐
Auth / Security Engineer
Working on IDM, login flows, or the DXL proxy. See the full auth model and token flow.
🧪
QA / Test Engineer
Find the test strategy, test file locations, and how to run unit and E2E tests.
📊
DevOps / SRE
Monitoring, logging, feature flags, and environment configuration all in one place.

Tech Stack

TL;DR: React 17 with TypeScript on the frontend, Express.js for server-side rendering, MobX for state, Vite for bundling, Contentful for content, and a suite of Vodafone-internal (@vfuk) and Vodafone design system (@source-web) packages.

In plain English This app runs on two levels simultaneously. The server (Node.js + Express) renders the initial HTML so users and search engines see real content instantly. Then React takes over in the browser for smooth, interactive page updates without full page reloads. This pattern is called Server-Side Rendering (SSR).
CategoryTechnologyVersionPurpose
Frontend FrameworkReact17.0.2Component rendering, UI interactions
LanguageTypeScript4.9.5Type safety across client and server
State ManagementMobX + mobx-react6.13.7Observable purchase journey state
Build ToolVite3.2.11Dev server, bundling, code splitting
ServerExpress.js4.21.2SSR, API proxying, middleware pipeline
CMSContentfulMarketing copy, page content, signposting
HTTP ClientAxios1.16.0API calls (wrapped via @vfuk middleware)
RoutingReact Router DOM5.3.4Client-side page navigation
CSSSCSS + Styled Components6.1.16Component and global styling
Internationalisationi18next + react-i18nextString translations
Document Headreact-helmet5.2.1SEO meta tags, page titles
AnalyticsTealium (internal)User journey event tracking
Feature FlagsLaunch DarklyProgressive feature rollout
MonitoringDatadog RUMReal-user performance monitoring
Unit TestsJest + Enzyme29.7.0 / 3.11.0Component and service unit tests
E2E TestsCypress10.11.0Full journey browser tests
Code QualityESLint + SonarQubeLinting and static analysis
Design System@source-webvariousVodafone UI component library (240+ components)
Internal Packages@vfuk/*variousVodafone middleware, auth, proxy, logging

Notable Internal Packages

PackageWhat it does
@vfuk/web-middleware-dxl-proxyDXL (Digital Experience Layer) — the API gateway proxy that adds auth headers and routes backend calls
@vfuk/web-middleware-idmIDM (Identity Management) middleware — handles session token validation
@vfuk/web-middleware-login-redirectIntercepts unauthenticated requests and redirects to the login page
@vfuk/lib-web-feature-flaggingLaunch Darkly integration for feature flags
@vfuk/lib-web-fe-loggerFrontend logging utility (wraps console + Datadog)
@vfuk/utils-middleware-content-api-transformerTransforms Contentful API responses into a consistent internal format
@vfuk/lib-web-analyticsTealium analytics event dispatcher

Architecture Diagram

TL;DR: The browser talks to an Express server, which proxies API calls through DXL (the Vodafone API gateway) to backend microservices. Content is fetched from Contentful. All routing back to services uses AWS API Gateway.

flowchart LR A["🌐 Browser\n(React SPA)"] -->|"① Page request"| B["🖥 Express Server\n(Node.js SSR)"] B -->|"② Render HTML"| A A -->|"③ API calls"| C["🛡 DXL Proxy\n(@vfuk/web-middleware-dxl-proxy)"] C -->|"④ Authenticated request"| D["☁️ AWS API Gateway\n(eu-west-1)"] D -->|"⑤ Purchase API"| E["📦 SIMO-Purchase\n(v2 — active)"] D -->|"⑤ Legacy API"| F["📦 Data-SIMO-Purchase\n(v1 — legacy)"] D -->|"⑥ Auth"| G["🔑 Auth Service\n(IDM)"] D -->|"⑦ Content"| H["📝 Content Service\n→ Contentful"]
Press Play to walk through the request flow step by step.

Walking Through the Diagram

  1. ① Page request — A user visits /data-only-sim. Their browser sends an HTTP GET to the Express server.
  2. ② Render HTML — The Express server renders React to HTML server-side (SSR), returning a fully-formed page to the browser. This makes the first paint fast and SEO-friendly.
  3. ③ API calls — Once the browser loads the JavaScript bundle, React hydrates and the purchase journey begins making API calls as the user interacts (selecting a plan, adding extras, etc.).
  4. ④ Authenticated request — All API calls go through the DXL Proxy middleware, which attaches authentication headers (session token, API key) before forwarding to AWS API Gateway.
  5. ⑤ Purchase APIs — AWS Gateway routes to either the new SIMO-Purchase service (v2) or the legacy Data-SIMO-Purchase service (v1) depending on the journey type and feature flags.
  6. ⑥ Auth — The Auth Service (IDM — Identity Management) handles login, session validation, and two-factor authentication (2FA).
  7. ⑦ Content — The Content Service fetches marketing copy, plan descriptions, and signposting text from Contentful, transforming it into a format the frontend can render directly.

Purchase Journey — Request Flow

TL;DR: A purchase journey is a server-managed state machine. The frontend creates a journey, fetches available plans, the user selects one, extras are optionally added, and the journey concludes. State transitions are driven by HATEOAS links (see HATEOAS section) not hardcoded URLs.

In plain English Think of a "journey" like a shopping cart with memory. When you arrive, a new cart is created on the server. Each time you make a choice (pick a plan, add an extra), the app sends an update to the server, which replies with the new cart state AND tells the app what you can do next. The app never decides the next step on its own — it always follows the server's instructions.
flowchart TD A["User visits /data-only-sim"] --> B["POST /journeys — Create journey"] B --> C["GET /plans — Load available plans"] C --> D{"New or\nexisting customer?"} D -->|New| E["POST actions/new-or-existing-customer"] D -->|Existing| E E --> F["GET /plans/{planId} — User selects plan\nPOST /package"] F --> G{"Add extras?"} G -->|Yes| H["GET /plans/{planId}/extras\nPOST /package/extras"] G -->|No| I["GET /subscription-summary"] H --> I I --> J["Proceed to checkout\n(external system)"]

Journey State Transitions

The journey progresses through these states. Each state is driven by an action call to the backend:

StepActionHATEOAS Link Key
Identify customer typePOST actions/new-or-existing-customerselect-existing-customer / select-new-customer
Select a planPOST /packageselect-plan
Accept upsell offerPOST /packageselect-plan-upsell
Add an extraPOST /package/extrasselect-extra
Remove an extraDELETE /package/extras/{id}remove-extra
Keep existing packagePOST actions/keep-or-replace-packageselect-keep-package
Replace existing packagePOST actions/keep-or-replace-packageselect-replace-package
Switch customer segmentPOST actions/keep-or-change-segmentsync-jrny-seg-to-bskt
Empty basketGET /empty-basketempty-basket

Server-Side Rendering (SSR)

TL;DR: The Express server renders the initial React tree to HTML on first load. After that, the browser takes over and React updates the page dynamically without full reloads.

In plain English Server-Side Rendering is like getting a pre-printed menu rather than waiting for the waiter to type one up in front of you. The server does the heavy lifting of building the HTML once, sends it instantly, and then React makes it interactive. This makes pages load faster and appear correctly in Google's search results.

Key Server Entry Points

FileRole
src/server/production/server.tsExpress server bootstrap and middleware wiring
src/server/common/config/core.config.tsCore server configuration (ports, paths, env vars)
src/server/common/config/dxlProxy.config.tsDXL proxy configuration — API gateway URL, API key, allowed endpoints
src/server/common/config/whitelist.config.tsEndpoint allowlist — only listed paths can be proxied to the backend
src/server/common/config/contentAPITransformer.config.tsContentful transformer configuration — space names and transformation rules
src/client/index.tsxReact client bootstrap — hydrates SSR HTML

CMS — Contentful Integration

TL;DR: Yes — this app is heavily CMS-driven. Almost all user-visible text, images, signposting panels, and marketing content is fetched from Contentful (a headless CMS). The app never hardcodes product copy directly in React components.

In plain English A headless CMS (Content Management System) is a content database without a built-in front end. Think of it like Google Docs for website content — editors update text and images there, and this app fetches and displays whatever is currently in Contentful. Changing page copy does not require a code deploy; a content editor just updates the Contentful entry.

Contentful Dependencies

Content API Endpoint

GET /api/content-service/v2/content
    ?contentEntryKey=dataonlysim_consumer
    &contentType=marketingContent
    &spaceName=consumer

The contentEntryKey identifies the specific Contentful entry. The spaceName is either consumer or business, selecting the correct Contentful space.

Contentful Content Keys

The following keys are used to fetch content entries. Each key maps to a Contentful entry that an editor manages:

KeyWhat it controlsSegment
dataonlysim_consumerMain consumer page content — hero, intro, plan descriptionsConsumer
dataonlysim_businessMain business page contentBusiness
contactUsFlyoutContact Us side panel contentBoth
signposting_consumerSignposting panels — directional banners pointing users to related productsConsumer
signposting_consumer_secondlineSignposting for secondary line (existing customer adding a second SIM)Consumer
dataonlysim_essentials"What's included" / essentials section copyBoth
dataonlysim_charges_list_1Additional charges list (e.g. roaming, overage)Both
dataonlysim_needtoknow_list_1"Need to know" information panelBoth
devicedetailsmbb_about_speedAbout mobile broadband speeds sectionBoth
dataonlysim_plansPlan listing content / descriptionsBoth
dataonlysim_bestdealslistBest deals list copyBoth
dataonlysim_bestdealsBest deals section heading/introBoth
dataonlysim_breadcrumbBreadcrumb navigation labelsBoth
dataonlysim_mid_contract_riseMid-contract price rise notice (legal/regulatory)Both
mcpr_price_increaseMCPR (Mid-Contract Price Rise) pricing increase contentBoth

Contentful Layer Types

Content is organised into layers. Each layer type controls a different region of the page:

LayerPurpose
marketingContentMain page sections — heroes, banners, plan copy
postBodyMarketingContentContent displayed after the main body content
channelAppChannel-specific configuration and overrides (e.g. eShop vs. app)
pageSeoPage title, meta description, Open Graph tags

Content Store

Fetched Contentful content is stored in the MobX DataSimoContentStore. This store holds all CMS content for the current page session and makes it available to any component that needs it.

Content Key → Component Mapping

TL;DR: Every content key ends up on screen somewhere inside the DataSimoTemplate tree (or a store/helper it feeds). Some keys have a dedicated store getter; others are queried ad hoc by the component that needs them.

In plain English This table answers "if a content editor changes entry X in Contentful, what part of the page changes?" Follow the row for the key you care about across to the "Consuming Component" and "File Path" columns to find exactly where it's rendered.
Content Entry KeyStore Property / GetterConsuming Component(s)File PathWhat it renders
dataonlysim_consumer / dataonlysim_businesscontent (via consumerContent / businessContent)Root payload — every component below reads from thisn/aBase page content object, switched by isBusiness
contactUsFlyoutconsumerContent.contactUs / businessContent.contactUsContactUssrc/client/templates/DataSimoTemplate/DataSimoTemplate.tsx:106-109"Get in touch" flyout panel content
signposting_consumer + signposting_consumer_secondlinesignpostingContentSimpleNotification + RawHtmlWrapper (inline in template)src/client/templates/DataSimoTemplate/DataSimoTemplate.tsx:54-60Second-line SIM signposting banner — only when journey type is secondline and segment is consumer
dataonlysim_mid_contract_risepriceRiseContentMidContractPriceRisesrc/client/templates/DataSimoPlansTemplate/DataSimoPlansTemplate.tsx:56Mid-contract price rise disclosure above the plan list (gated by feature flag)
mcpr_price_increasemcprContentPlanCardListPlanCardsrc/client/components/molecules/PlanCardList/PlanCardList.tsx:32MCPR (Mid-Contract Price Rise) title shown per plan card
dataonlysim_breadcrumb_{segment}breadcrumbContentDataSimoBreadcrumbssrc/client/templates/DataSimoTemplate/components/DataSimBreadcrumbs/DataSimoBreadcrumbs.tsx:20,31JSON-LD BreadcrumbList schema + visual breadcrumb trail
dataonlysim_essentialsPlanDetailsStore.getContentForPlan('whatsincluded')PlanDetailsOverlayModal (via PlanCardDetailsModal)src/client/components/molecules/PlanDetailsOverlayModal/PlanDetailsOverlayModal.tsx:20-40"What's included" tab in the plan details modal
dataonlysim_charges_list_1PlanDetailsStore.getContentForPlan('additionalcharges')PlanDetailsOverlayModalsrc/client/components/molecules/PlanCardDetailsModal/PlanCardDetailsModal.tsx:62-66"Additional Charges" tab
dataonlysim_needtoknow_list_1PlanDetailsStore.getContentForPlan('whatyouneedtoknow')PlanDetailsOverlayModalsrc/client/components/molecules/PlanCardDetailsModal/PlanCardDetailsModal.tsx:67-71"What you need to know" tab
devicedetailsmbb_about_speedPlanDetailsStore.getContentForPlan('aboutspeed')PlanDetailsOverlayModalsrc/client/components/molecules/PlanCardDetailsModal/PlanCardDetailsModal.tsx:72-76"About speed" tab
dataonlysim_plansPlanDetailsStore.getFields() (container lookup only)— no direct render —src/client/stores/PlanDetailsStore/PlanDetailsStore.ts:71Pure indirection: the entry whose nested list holds the four keys above
dataonlysim_bestdealslist_{segment}_{journeyType}Ad hoc lookup via contentStore.content.marketingContentHeroBannerIconSnippetListsrc/client/components/organisms/HeroBanner/HeroBanner.tsx:58-73Icon/heading/text snippet list under the hero banner
dataonlysim_bestdeals_{segment}_{journeyType}_skinnybannerAd hoc lookup via contentStore.content.marketingContentHeroBannersrc/client/components/organisms/HeroBanner/HeroBanner.tsx:37-93Hero banner background image + heading
marketingContent layer (slot1 tag)getMarketingContent()MarketingComponentsrc/client/templates/DataSimoTemplate/DataSimoTemplate.tsx:73Marketing slot above the fold
postBodyMarketingContent layer (slot3 tag)getPostBodyMarketingContent()MarketingComponentsrc/client/templates/DataSimoTemplate/DataSimoTemplate.tsx:123Marketing slot at the bottom of the page
pageSeo layerseoDataDataSimoSeoTagsHeadTagssrc/client/templates/DataSimoTemplate/components/DataSimoSeoTags/DataSimoSeoTags.tsx<head> SEO / OpenGraph / Twitter meta tags
.tracking (raw field, not in key config)consumerContent.tracking / businessContent.trackingNot a componentformatTrackingProps helpersrc/client/stores/helpers/formatTrackingProps.ts:7Builds the Tealium analytics tracking payload
.filterOptionsContent (raw field)content.filterOptionsContentgenerateSecureNetBenefitItemBenefitItemssrc/client/components/molecules/CardBenefitsSlot/components/BenefitItems/helpers/generateSecureNetBenefitItem/generateSecureNetBenefitItem.ts:6-10"Secure Net" benefit item on a plan card
.planCardEntertainmentContent (raw field)content.planCardEntertainmentContentPlanCardAccordionModalsrc/client/components/molecules/PlanCardAccordionModal/PlanCardAccordionModal.tsx:50-51Entertainment benefit modal — header + accordion
content (root, generic)store.contentStore.contentSubscriptionSummaryrenderInclusiveProductssrc/client/components/molecules/SubscriptionSummary/SubscriptionSummary.tsx:32,72,80Inclusive products/benefits list in the post-purchase summary
Indirection pattern Only 8 of the 15 CONTENTFUL_KEY_CONFIG keys have a dedicated getter on DataSimoContentStore (consumerPage, businessPage, contactUsFlyout, signposting_consumer, signposting_consumer_secondline, midContractPriceRise, mcprContent, dataonlysim_breadcrumb). The remaining 7 keys (dataonlysim_essentials, dataonlysim_charges_list_1, dataonlysim_needtoknow_list_1, devicedetailsmbb_about_speed, dataonlysim_plans, dataonlysim_bestdealslist, dataonlysim_bestdeals) are queried ad hoc by PlanDetailsStore and HeroBanner, which call findContentByEntryKey directly against contentStore.content rather than going through a store getter. Keep this in mind when adding a new key — there's no single consistent pattern to follow.
Correctness
SimoStore calls a content-store method that doesn't exist
src/client/stores/SimoStore/SimoStore.ts:284 calls this.contentStore.loadPlanDetailsContent(), but DataSimoContentStore has no method by that name (confirmed by a full-repo search — the only references are in SimoStore.ts and its test file). Unless this is mocked away in tests, it will throw TypeError: ... is not a function at runtime. Worth a bug ticket before relying on this code path.

Backend Endpoints

TL;DR: The app calls four distinct backend service groups — Content, SIMO-Purchase (new), Data-SIMO-Purchase (legacy), and Auth/Authorization. All calls are proxied through the DXL proxy, which adds authentication headers and routes to AWS API Gateway.

In plain English The DXL proxy is like a reception desk. Every call the app wants to make to a backend goes through the reception desk first. The receptionist checks credentials, stamps the request, and forwards it to the right department. The app never talks to backend services directly.
Important Only endpoints listed in the server-side allowlist (src/server/common/config/whitelist.config.ts) can be proxied. Any call to an unlisted path will be blocked by the DXL proxy. If you add a new backend endpoint, you must add it to the whitelist.
Content API
GET
/api/content-service/v2/content
Fetch a Contentful content entry. Query params: contentEntryKey, contentType, spaceName
GET
/content/asset
Legacy content asset endpoint (images, documents from older content layer)
SIMO-Purchase API v2 — Active service (new journeys)
GET
/simo-purchase/paym/v2/{sessionId}/journeys
List all journeys for this platform session
POST
/simo-purchase/paym/v2/{sessionId}/journeys
Create a new purchase journey — call this first when a user starts the flow
GET
/simo-purchase/paym/v2/{sessionId}/journeys/latest
Retrieve the most recent in-progress journey (used to resume a session)
GET
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}
Get a specific journey by ID including its current state and HATEOAS links
DELETE
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}
Delete / abandon a journey
POST
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}
Update journey metadata
GET
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/plans
Fetch all available data plans for this journey
GET
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/plans/{planId}
Get details for a specific plan
GET
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/plans/{planId}/extras
Fetch available add-ons (extras) for a specific plan
GET
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/plans/{planId}/accessories
Fetch available accessories bundled with a plan
GET
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/package
Get the current selected package (plan + extras)
POST
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/package
Select a plan — adds plan to the basket
DELETE
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/package
Remove the selected plan from the basket
POST
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/package/extras
Add an extra/add-on to the basket
DELETE
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/package/extras/{extraId}
Remove a specific extra from the basket
POST
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/package/accessories
Add an accessory to the basket
DELETE
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/package/accessories/{accessoryId}
Remove a specific accessory from the basket
POST
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/package/businessApps
Add a business application add-on (business segment only)
GET
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/businessApps
List available business apps for this journey
DELETE
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/package/businessApps/{businessAppId}
Remove a specific business app from the basket
GET
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/subscription-summary
Retrieve the final order summary before checkout handoff
GET
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/offers-and-buy-options
Fetch OTB (Offers To Buy) — personalised offers for existing customers
GET
/simo-purchase/paym/v2/{sessionId}/empty-basket
Clear the entire basket for this session
POST
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/actions/new-or-existing-customer
Declare whether this is a new or returning Vodafone customer — affects available plans
POST
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/actions/keep-or-change-segment
Consumer/business segment switch — keep current or change to a different segment
POST
/simo-purchase/paym/v2/{sessionId}/journeys/{journeyId}/actions/keep-or-replace-package
Existing customer choice — keep current package or replace it with the new selection
GET
/simo-purchase/actuator/health
Health check endpoint for this service
GET
/simo-purchase/actuator/vhealth
Vodafone extended health check (includes dependency status)
Data-SIMO-Purchase API v1 — Legacy service
Legacy Warning This is the older service (v1). The newer SIMO-Purchase v2 service is preferred for new journeys. Both may be active simultaneously depending on feature flags.
GET
/data-simo-purchase/{sessionId}/{segment}/paym/journeys
List journeys — note the {segment} path param (consumer/business)
POST
/data-simo-purchase/{sessionId}/{segment}/paym/journeys
Create a new journey
GET
/data-simo-purchase/{sessionId}/{segment}/paym/journeys/latest
Get latest journey
GET
/data-simo-purchase/{sessionId}/{segment}/paym/journeys/{journeyId}
Get specific journey
PATCH
/data-simo-purchase/{sessionId}/{segment}/paym/journeys/{journeyId}
Partially update journey state
POST
/data-simo-purchase/{sessionId}/{segment}/paym/journeys/{journeyId}
Full update of journey
GET
/data-simo-purchase/{sessionId}/{segment}/paym/journeys/{journeyId}/plans
Fetch available plans
GET
/data-simo-purchase/{sessionId}/{segment}/paym/journeys/{journeyId}/plans/{planId}
Fetch specific plan details
POST
/data-simo-purchase/{sessionId}/{segment}/paym/journeys/{journeyId}/package
Select a plan (add to basket)
PATCH
/data-simo-purchase/{sessionId}/{segment}/paym/journeys/{journeyId}/package
Update the package selection
DELETE
/data-simo-purchase/{sessionId}/{segment}/paym/journeys/{journeyId}/package
Remove the selected package
POST
/data-simo-purchase/{sessionId}/{segment}/paym/journeys/{journeyId}/actions/new-or-existing-customer
Declare customer type
POST
/data-simo-purchase/{sessionId}/{segment}/paym/journeys/{journeyId}/actions/sync-journey-segment
Sync journey segment (consumer/business) — v1 equivalent of keep-or-change-segment
GET
/data-simo-purchase/actuator/health
Health check
GET
/data-simo-purchase/actuator/vhealth
Vodafone extended health check
Auth Service (IDM — Identity Management)
GET
/auth/session
Retrieve the current user session — used to check if logged in
POST
/auth/actions/authenticate
Authenticate the user (initial login)
POST
/auth/actions/sign-out
Sign the user out and clear session
POST
/auth/2fa/registeredNumbers
Fetch phone numbers registered for 2FA (Two-Factor Authentication)
POST
/auth/2fa/actions/sendOtp
Send an OTP (One-Time Passcode) to a registered number for 2FA
GET
/auth/accounts
List all Vodafone accounts linked to this user
GET
/auth/accounts/{accountId}/subscriptions
List all active SIM subscriptions on a given account
POST
/auth/journey
Auth journey initiation (e.g. starting a login flow)
Authorization Service (token-based access)
GET
/authorization/authorizationToken
Fetch an authorization token for the current session
GET
/authorization/accessToken
Fetch an access token for API calls
GET
/authorization/2fa/registeredNumbers
2FA registered phone numbers (authorization service variant)
POST
/authorization/2fa/action/sendOtp
Send OTP via authorization service
GET
/authorization/accounts
List accounts via authorization service
GET
/authorization/accounts/{accountId}/subscriptions
List subscriptions via authorization service
POST
/authorization/journey
Authorization journey initiation

HATEOAS Navigation

TL;DR: HATEOAS (Hypermedia As The Engine Of Application State) means the backend tells the frontend what actions are available at each step. The frontend never hardcodes "what to call next" — it reads the links from the last API response and follows them.

In plain English HATEOAS is like a GPS that gives you the next turn only after you've made the current one. You don't get the whole route upfront — after each step the server tells you "here are the roads you can take from here." The app reads those options and shows only valid choices to the user. This means the backend controls the flow, and the frontend just renders whatever options it's given.

How it works in code

Each API response from the purchase service includes a links array. Each link has a rel (relation type) that tells the frontend what that link means. The frontend extracts the link by its rel and calls it to progress the journey.

// Example API response structure
{
  "journeyId": "abc-123",
  "status": "PLAN_SELECTED",
  "links": [
    { "rel": "get-extras",    "href": "/simo-purchase/.../plans/xyz/extras", "method": "GET" },
    { "rel": "empty-basket",  "href": "/simo-purchase/.../empty-basket",      "method": "GET" },
    { "rel": "get-subscription-summary", "href": "/simo-purchase/.../subscription-summary", "method": "GET" }
  ]
}

The frontend looks up the link by rel and calls the href — it never constructs these URLs itself.

HATEOAS Link Reference

Link relWhat it means
get-plansLoad the available plan listing
select-planConfirm the user's plan choice
select-plan-upsellAccept an upsell offer on top of the selected plan
reject-plan-upsellDecline the upsell offer and keep the original plan
select-existing-customerDeclare this user is an existing Vodafone customer
select-new-customerDeclare this user is a new customer
get-offers-and-buy-optionsLoad OTB (personalised offers) for existing customers
set-journey-typeSet the journey type (acquisition, upgrade, etc.)
get-subscription-summaryLoad the order summary before checkout
get-extrasLoad available add-ons for the selected plan
get-business-appsLoad available business app add-ons
select-extraAdd an extra to the basket
remove-extraRemove an extra from the basket
select-business-appAdd a business app to the basket
remove-business-appRemove a business app from the basket
select-keep-packageExisting customer keeps their current package
select-replace-packageExisting customer replaces their current package
reset-journeyReset the journey back to the beginning
set-segment-typeSet consumer vs. business segment
sync-jrny-seg-to-bsktKeep journey segment (sync journey → basket direction)
sync-bskt-seg-to-jrnyChange journey segment (sync basket → journey direction)
empty-basketClear the entire basket
switch-accountSwitch to a different Vodafone account
sign-inTrigger the login flow

State Management — MobX

TL;DR: All application state is managed with MobX 6. Stores hold journey state, plan selections, content data, auth state, and UI state. Components observe stores and re-render automatically when state changes.

In plain English MobX is a state management library — think of it as a shared whiteboard that all components can read and write on. When anything on the whiteboard changes, every component watching that part of the board automatically updates itself. There's no manual "refresh" step needed.

Key Stores

StoreWhat it holdsLocation
DataSimoContentStoreAll Contentful CMS content for the current page — marketing copy, signposting, SEO datasrc/client/stores/DataSimoContentStore/
Journey StoreCurrent purchase journey state — selected plan, extras, journey ID, HATEOAS linkssrc/client/stores/
Auth StoreUser authentication state — logged in/out, session token, account detailssrc/client/stores/

MobX Pattern Used

The codebase uses MobX with mobx-react (the React bindings). Components are wrapped with observer() to make them reactive to store changes. Stores use makeAutoObservable or makeObservable for declaring which properties are observable.

// Typical pattern — component reacts to store changes automatically
import { observer } from 'mobx-react'

const PlanSelector = observer(({ store }) => {
  // Re-renders whenever store.selectedPlan changes
  return <div>{store.selectedPlan?.name}</div>
})

Store Access Pattern

Stores are typically injected via React Context or passed as props from the top-level page component. There is no global singleton store — stores are created per-request on the server side to avoid state leaking between users.

SSR Store Isolation Because the app renders server-side, stores must be created fresh per HTTP request. Sharing a store instance across requests would cause one user's state to leak into another user's page. This is a common SSR gotcha — see the Gotchas section for more.

Auth & Identity

TL;DR: Authentication is handled by IDM (Identity Management) — a Vodafone internal service. The frontend uses middleware packages (@vfuk/web-middleware-idm, @vfuk/web-middleware-login-redirect) to validate sessions and redirect unauthenticated users.

In plain English IDM (Identity Management) is the Vodafone "bouncer" service. Before certain pages or API calls are allowed, IDM checks whether the user has a valid session (like a wristband from a concert). If they don't, they're sent to the login page. After login, a session token is issued — this token travels with every API request to prove the user is authenticated.

Auth Flow

  1. User lands on the purchase page
  2. The @vfuk/web-middleware-idm middleware checks for a valid session cookie
  3. If no valid session: @vfuk/web-middleware-login-redirect redirects to the Vodafone login page
  4. After login, IDM issues a session token (stored in a cookie)
  5. All subsequent API calls are made with this token in the headers (added by the DXL proxy)
  6. For high-security actions, a 2FA (Two-Factor Authentication) challenge is triggered via POST /auth/2fa/actions/sendOtp

Auth Endpoints Summary

Two Auth Services There are two separate services with overlapping capabilities — /auth and /authorization. The /auth service handles session and login. The /authorization service handles token issuance for API access. Both may be called during a single user session.

Key Auth Middleware Packages

PackageRole
@vfuk/web-middleware-idmValidates the user's session token on every request
@vfuk/web-middleware-login-redirectRedirects unauthenticated users to the login page
@vfuk/utils-middlewares-auth-shop-polyfillsAuth polyfills specific to eShop flows
@vfuk/web-middleware-dxl-proxyAttaches auth token to all outbound API calls
cookie-parserParses session cookies from incoming requests

Routing

TL;DR: React Router 5 manages client-side navigation. The app has two base paths — /data-only-sim for consumer and /business/data-only-sim for business. Routes are defined in src/client/routes.tsx.

PathSegmentPurpose
/data-only-simConsumerConsumer data SIM entry page
/business/data-only-simBusinessBusiness data SIM entry page

The server is mounted at the path prefix /data-only-sim. The channel ID used in API calls is eShop-data-simo.

Plan Filtering & Sorting

TL;DR: The data model (FilterStore) supports six filter dimensions and the API supports a sortBy parameter, but only one filter — commitment period (contract length) — actually has a UI control. Everything else is defined in the types and analytics config but has no rendered control anywhere in the app today.

In plain English Think of the filter system like a remote control with six buttons printed on it, but only one button is actually wired up to the TV — the other five are there but don't do anything if you press them, because no one has built the on-screen menu for them yet.

Filter dimensions defined in IFilters

FilterStore.types.ts defines the full shape of filters the backend journey response can carry:

Filter keyUI Control?ComponentNotes
duration (commitment period)Yes — activePlanFilterWrapper (desktop, radio buttons) / PlanFilterWrapperMobile (mobile, select menu)The only filter with a working UI. Drives filterStore.selectedCommitmentPeriodId and the commitment-period tabs on the plans template.
dataAllowanceNo UI— none —Type defined, and an analytics key exists (dataAllowance'data' in filterConfigForAnalytics.ts), but no component reads or renders it.
entertainmentNo UI— none —Analytics key entertainmentPromotion exists, but this is distinct from the "Entertainment benefit" shown on plan cards (that's static plan content, not a filter toggle).
monthlyPriceNo UI— none —Analytics buckets exist (0-15, 16-25, 26-30, 31-40) in filterConfigForAnalytics.ts, suggesting a price-range filter was planned, but nothing renders it.
readyWith5gNo UI— none —Type-only. No analytics key, no component reference found anywhere in src/client.
salesDealsNo UI— none —Analytics key salesPromotion'sale/deals' exists in filterConfigForAnalytics.ts, but no rendered toggle.
Don't assume a filter works just because the type exists IFilters is the full contract the backend can send back on a journey, and filterConfigForAnalytics.ts maps several of these to Tealium tracking keys as if selection were possible — but PlanFilterWrapper / PlanFilterWrapperMobile only ever read filterStore.commitmentPeriodRadioList (built from filters.duration). If you want to add a working "sort by data allowance" or "5G only" toggle, the type and analytics plumbing already exist — you'd be adding the missing UI control and wiring it to filterStore.setFilters / a new action, not inventing the filter dimension from scratch.

Sorting — sortBy

TL;DR: Sorting is not a UI control either. It only works as a deep-link query parameter — if a URL like ?sortBy=monthlyPrice.asc is used to land on the page, the value is forwarded to the backend's HATEOAS link query parameters. There is no on-page sort dropdown.

// src/client/services/dataSimOnlyService/helpers/deeplinkingQueryOverride/deeplinkingQueryOverride.ts
if (paramKey === 'sortBy') {
  const isDefault = ['default', 'recommended'].includes(deepLinkQueryParams[paramKey])
  hateoasLink.queryParameters[paramKey] = isDefault ? '' : deepLinkQueryParams[paramKey]
}

Any sortBy value present in the page's URL query string is copied onto the outgoing HATEOAS link before it's called — default and recommended are normalised to an empty string (the backend's default ordering).

Deep-link sortBy valueAnalytics labelMeaning
recommendedDefault/recommended order (normalised to empty)
monthlyPrice.ascmonthly_price:low_highCheapest plans first
monthlyPrice.descmonthly_price:high_lowMost expensive plans first
dataAllowance.ascdata:low_highSmallest data allowance first
dataAllowance.descdata:high_lowLargest data allowance first
Summary — what you can change today without new backend work Only duration (commitment period) has a working UI. sortBy works today too, but only via URL query string, not a visible control. The other five filter dimensions and any on-page sort dropdown would need new frontend components wired to the existing FilterStore / generateFilterPayload plumbing — the backend contract and analytics tracking already anticipate them.

Module & Component Inventory

TL;DR: The app is split into client-side React components/pages, server-side Express middleware, shared services, and MobX stores. Most UI components come from the @source-web design system.

Client-Side Structure
DirectoryContents
src/client/pages/DataSimo/Top-level page components (consumer and business variants)
src/client/templates/HeaderFooterTemplate/Page shell template — wraps pages with header and footer
src/client/components/Shared React components used across pages
src/client/stores/MobX stores — all application state
src/client/services/API service functions — HTTP calls to backend
src/client/helpers/Pure utility functions and request helpers
src/client/constants/constants.tsAll app-wide constants — content keys, HATEOAS links, route paths
src/client/routes.tsxReact Router route configuration
src/client/index.tsxBrowser entry point — React hydration
Server-Side Structure
DirectoryContents
src/server/production/server.tsExpress server entry point and middleware pipeline
src/server/common/config/All server configuration — proxy, whitelist, content transformer, core
src/server/common/config/whitelist.config.tsEndpoint allowlist for the DXL proxy
src/server/common/config/dxlProxy.config.tsDXL proxy settings — AWS gateway URL, API key
src/server/common/config/contentAPITransformer.config.tsContentful transformer config
Key Services
FilePurpose
src/client/services/dataSimOnlyService/dataSimOnlyService.tsMain purchase API service — all journey, plan, and package calls
src/client/services/contentServiceV2/getAssetModelV2.tsContentful content fetching service
src/client/helpers/makeAPIRequest/makeAPIRequest.tsLow-level API request helper — wraps Axios with consistent error handling
Environment Config Files

Environment-specific configuration is in the .env/ directory:

  • .env/common.env — shared settings across all environments
  • .env/local-prod.env — local dev pointing at production backends
  • .env/int1.env — integration environment 1
  • .env/qc1.env — QC (Quality Control) environment 1

Key environment variables:

VariablePurpose
AWS_GATEWAY_DAL_URLAWS API Gateway base URL (e.g. https://m3zqs725ki.execute-api.eu-west-1.amazonaws.com)
DAL_URLDAL (Data Abstraction Layer) direct URL for non-gateway calls
AWS_GATEWAY_DAL_API_KEYAPI key sent with every call to AWS Gateway

Getting Started

TL;DR: Clone, install with npm, set up your .env file, and run npm run dev. You'll need Node.js and access to the Vodafone internal npm registry for @vfuk and @source-web packages.

Internal Registry Required The @vfuk/* and @source-web/* packages are hosted on Vodafone's internal npm registry (Artifactory). You must be on the Vodafone network or VPN and have registry credentials configured before running npm install.

Prerequisites

Install & Run

# Install dependencies (requires VPN + Vodafone registry credentials)
npm install

# Start development server
npm run dev

# Build for production
npm run build

# Start production server (after build)
npm start

Testing

# Run unit tests (Jest + Enzyme)
npm test

# Run tests in watch mode
npm run test:watch

# Run E2E tests (Cypress)
npm run cypress:open    # Interactive browser mode
npm run cypress:run     # Headless CI mode

Linting & Type Checking

# ESLint
npm run lint

# TypeScript type check (no emit)
npm run type-check

# Stylelint (CSS/SCSS)
npm run stylelint

Local Environment Setup

Copy the appropriate .env/ file for your target environment. For local development against a real environment, use .env/local-prod.env or .env/int1.env and ensure your AWS_GATEWAY_DAL_URL and AWS_GATEWAY_DAL_API_KEY are set.

Feature Flags

TL;DR: Feature flags are managed via Launch Darkly, accessed through the @vfuk/lib-web-feature-flagging package. Flags gate features behind toggles that can be turned on or off per environment without a code deploy.

In plain English A feature flag is like a light switch in the code. When the switch is off, users don't see the new feature even though the code is already deployed. The product or engineering team can flip the switch at any time through the Launch Darkly dashboard — no code change needed. This is used to roll out risky features gradually or to run A/B tests.

Feature flags are particularly relevant to controlling which purchase API version is used — the v1 legacy service vs. v2 SIMO-Purchase service. Before changing any flow, check whether it's gated behind a feature flag.

Observability & Logging

TL;DR: Logs go to Datadog via @vfuk/lib-web-fe-logger. Real-user monitoring (RUM) runs via Datadog RUM. Analytics events go to Tealium.

ToolPurposePackage
Datadog RUMBrowser performance monitoring — page load times, JS errors, user sessionsDatadog SDK
@vfuk/lib-web-fe-loggerStructured logging — errors and events are sent to Datadog log management@vfuk/lib-web-fe-logger
TealiumUser journey analytics — button clicks, page views, purchase funnel events@vfuk/lib-web-analytics
SonarQubeStatic code analysis — code quality and security issue reportingCI/CD integration

Health Check Endpoints

Both backend services expose health endpoints. These are used by load balancers and monitoring systems to check if the service is alive:

Testing

TL;DR: Unit tests use Jest + Enzyme. End-to-end tests use Cypress 10. SonarQube provides static analysis in CI/CD.

TypeToolVersionLocation
Unit / ComponentJest + Enzyme29.7.0 / 3.11.0Co-located with source files (*.test.tsx, *.spec.ts)
End-to-EndCypress10.11.0cypress/ directory
Static AnalysisSonarQubeRuns in CI pipeline
Type CheckingTypeScript compiler4.9.5npm run type-check
Enzyme is legacy Enzyme 3 has no official support for React 17 hooks. The codebase uses the unofficial enzyme-adapter-react-17. If you encounter test failures with hooks or functional components, this incompatibility may be the cause. React Testing Library would be the modern replacement.

Gotchas & Traps

Non-obvious behaviours and known limitations. Read before shipping.

Correctness
HATEOAS: Never hardcode API paths for state transitions
The purchase journey uses HATEOAS — the server tells you what URL to call next via links in each response. Do not construct journey action URLs manually. If you hardcode a path like /journeys/{id}/actions/new-or-existing-customer, it will work in testing but silently break when the backend changes its URL structure. Always extract the URL from the relevant rel link in the last API response.
Security
Endpoint allowlist: new backend paths must be added to the whitelist
The DXL proxy only forwards requests to paths listed in src/server/common/config/whitelist.config.ts. If you add a new backend endpoint and forget to add it to this allowlist, the proxy will silently block the call with no useful error message to the client. Always check the whitelist when a new API integration is not working.
Correctness
Two purchase API versions are both live — feature flags control which is used
The codebase supports both the legacy data-simo-purchase (v1) service and the newer simo-purchase (v2) service. Feature flags in Launch Darkly determine which is active per environment. A bug fix in one version may not apply to the other. Before debugging a journey issue, confirm which API version is active in the target environment.
Correctness
MobX stores must be created per-request server-side — never share across requests
Because the app uses SSR (server-side rendering), store instances are created per HTTP request on the server. If a store is accidentally instantiated as a module-level singleton (outside the request handler), it will be shared between concurrent user requests — meaning user A's basket contents could appear for user B. Always create stores inside the request handler or React tree root, never at module level.
Correctness
Contentful content key naming is fragile — a typo silently returns empty content
Content is fetched using string keys like dataonlysim_consumer. If a key doesn't match exactly what's in Contentful, the API returns an empty result with no error — the page section just renders blank. There's no compile-time validation of content keys. When adding or renaming a Contentful entry, verify the key matches exactly with the constants in src/client/constants/constants.ts.
Testing
Enzyme 3 has unofficial React 17 support — hook-heavy components may not test cleanly
The project uses enzyme-adapter-react-17, which is a community adapter not officially supported by the Enzyme team (who halted React 17/18 support). Tests involving hooks, useEffect, or useState can behave unexpectedly. If a unit test fails for no obvious reason, try converting it to use React Testing Library (@testing-library/react) which has full React 17 support.
Correctness
The v1 API has a segment path param; v2 does not
The legacy v1 data-simo-purchase API includes {segment} (consumer/business) as a URL path parameter: /data-simo-purchase/{sessionId}/{segment}/paym/journeys. The new v2 simo-purchase API does not — segment is determined from the journey context instead. If you're building a service layer that supports both, handle this difference explicitly rather than sharing URL-construction logic.
Performance
Vite 3 is an older version — some newer Vite features or plugins may not be compatible
The project uses Vite 3.2.11, which is several major versions behind the current release. Plugin APIs changed significantly between Vite 3 and Vite 5. If you add a Vite plugin, check it explicitly supports Vite 3 — "Vite compatible" on npm often means Vite 4+.