# Architecture ## System Overview The Booking EWA is a two-tier application: 1. **Express Backend** (Node.js, TypeScript) -- Runs on port 3001. Handles JWT authentication, session management, and proxies all API calls to the AMPECO public API. The frontend never communicates with the public API directly. 2. **React SPA Frontend** (Vite, TypeScript, Tailwind CSS) -- Runs on port 5173 in development. A single-page application using `HashRouter` for client-side navigation. Styled with `@ampeco/design-tokens` CSS custom properties and `@ampeco/ewa-ui` Tailwind plugin. ```mermaid graph LR MobileApp["Mobile App"] -->|JWT in X-Payload header| Backend["Express Backend :3001"] Backend -->|Bearer token auth| PublicAPI["AMPECO Public API"] Browser["Browser / WebView"] -->|fetch /api/*| ViteDev["Vite Dev Server :5173"] ViteDev -->|proxy| Backend ``` In production, the Vite dev server is replaced by static file serving. The built frontend assets are in `dist/`. --- ## Backend Architecture ### Entry Point **File:** `backend/src/index.ts` The Express server configures CORS (for `localhost:5173` during dev), JSON body parsing, JWT middleware, and mounts the route handler. ### JWT Middleware Flow Every incoming request passes through the JWT middleware: ```mermaid flowchart TD A[Request arrives] --> B{X-Payload header present?} B -->|Yes| C[Verify JWT with HS256] C -->|Valid| D[Set req.session from JWT payload] C -->|Invalid| E{NODE_ENV === development?} B -->|No| E E -->|Yes| F[Set req.session to dev fallback] E -->|No| G[req.session remains undefined] D --> H[next] F --> H G --> H ``` The JWT token is expected in the `X-Payload` HTTP header (not `Authorization`). This is the convention used by the mobile app when embedding the EWA in a WebView. **JWT Payload Structure** (`backend/src/types.ts`): ```typescript interface JwtPayload { iss: string; aud: string; iat: number; nbf: number; exp: number; payload: { type: string; // "ewa" timestamp: string; appVersionCode: string; parameters: { userId?: number | null; operatorId: number; appLanguage: string; // e.g. "en" appCountry: string; // e.g. "US" appTheme: 'LIGHT' | 'DARK'; designTokens?: Record; }; }; } ``` The middleware extracts `parameters` into a `SessionData` object stored on `req.session`. **Dev Fallback Session:** When `NODE_ENV=development` and no valid JWT is present, the backend injects a default session: ```typescript { userId: 775, operatorId: 1, appLanguage: 'en', appCountry: 'US', appTheme: 'LIGHT', } ``` ### API Proxy Pattern **File:** `backend/src/proxy.ts` The `proxyRequest` function forwards requests from the frontend to the AMPECO public API. It: 1. Constructs the full URL from `API_BASE_URL` + the target path. 2. Forwards query parameters (for GET requests) and maps route parameters. 3. Attaches the `Authorization: Bearer ` header. 4. Forwards the request body for non-GET methods. 5. Returns the upstream response status and body to the client. 6. Returns `502 Bad Gateway` on network errors. The frontend never sees the `API_TOKEN` -- it is server-side only. ### Route Mapping **File:** `backend/src/routes.ts` | Frontend Path | Method | Upstream Public API Path | |---------------------------------------|--------|--------------------------------------------------------------| | `GET /api/session` | -- | No proxy. Returns `req.session` directly. | | `GET /api/bookings` | GET | `/resources/bookings/v1.0` | | `GET /api/bookings/:id` | GET | `/resources/bookings/v1.0/:id` | | `POST /api/booking-requests` | POST | `/resources/booking-requests/v1.0` | | `POST /api/locations/:id/check-availability` | POST | `/actions/locations/v2.0/:id/check-booking-availability` | | `POST /dev/jwt` (dev only) | -- | No proxy. Creates and returns a dev JWT token. | ### Session Data Flow The `/api/session` endpoint does not proxy to the upstream API. It returns the session data extracted from the JWT by the middleware, which the frontend uses to: - Identify the current user (`userId`). - Apply the correct theme (`appTheme` + `designTokens`). - Set the UI language (`appLanguage`). --- ## Frontend Architecture ### Application Bootstrap **File:** `src/main.tsx` 1. i18next is initialized (`src/i18n/init.ts`). 2. Global CSS is loaded (`src/app.css` -- imports `@ampeco/design-tokens/css/variables.css` and Tailwind). 3. `` is rendered into `#root`. ### Component Tree ``` -- Fetches /api/session, applies theme, provides context -- Guards on loading/error/userId -- HashRouter with Layout wrapper -- App shell with bottom nav (Home | Book | My Bookings) -- Route: / -- Route: /create -- Route: /bookings -- Route: /bookings/:id -- Route: /bookings/:id/update -- OR (if no userId) -- -- Manual user ID entry screen ``` ### Page Routing **File:** `src/router/index.tsx` Uses `HashRouter` (URLs like `/#/bookings/123`) for compatibility with WebView environments that may not support HTML5 history API. | Route | Component | Description | |-------------------------|------------------|---------------------------------| | `/#/` | `Home` | Dashboard with upcoming bookings | | `/#/create` | `CreateBooking` | Two-step booking creation | | `/#/bookings` | `MyBookings` | Upcoming/Past booking tabs | | `/#/bookings/:id` | `BookingDetail` | Single booking detail + actions | | `/#/bookings/:id/update`| `UpdateBooking` | Edit booking time range | All routes are wrapped in the `` component which provides a fixed bottom navigation bar with three tabs: Home, Book a Charger, and My Bookings. ### State Management State is managed through React Context and local component state. There is no external state management library. **SessionContext** (`src/context/SessionContext.tsx`): - Fetches session data from `/api/session` on mount. - Applies the theme by dynamically importing `@ampeco/design-tokens` and calling `applyTheme(appTheme, designTokens)`. - Provides `session`, `loading`, `error`, and `setManualUserId` to all child components. - `setManualUserId` allows overriding the `userId` in the session (used by `UserIdFallback` in dev mode). **useSession** (`src/hooks/useSession.ts`): Convenience hook that calls `useContext(SessionContext)`. ### Data Fetching Hooks **useBookings** (`src/hooks/useBookings.ts`): - `useBookings(params)` -- Fetches a list of bookings with optional filter parameters (`userId`, `status`, date range, pagination). Loads once on mount. - `useBookingDetail(id)` -- Fetches a single booking by ID. Includes a **polling mechanism** that refreshes every 30 seconds. Polling: - Pauses when the browser tab is hidden (`document.hidden`). - Resumes immediately when the tab becomes visible. - Stops permanently when the booking reaches a terminal status (`completed`, `cancelled`, `no-show`, `failed`). **useAvailability** (`src/hooks/useAvailability.ts`): - Provides a `check(locationId, startAfter, endBefore)` function for on-demand availability queries. - Returns `slots`, `loading`, `error`, and a `reset` function. - No automatic polling. ### API Client Layer **File:** `src/api/client.ts` A thin `fetch` wrapper that: 1. Prepends `/api` to all paths (which Vite proxies to the backend). 2. Sets `Accept: application/json` and `Content-Type: application/json` headers. 3. Throws `ApiRequestError` on non-2xx responses, preserving the HTTP status and parsed response body. ```typescript export class ApiRequestError extends Error { status: number; body: Record | null; get isNotFound(): boolean; get isForbidden(): boolean; get isValidationError(): boolean; // status === 422 get isUnauthorized(): boolean; get validationErrors(): Record; // body.errors } ``` **Endpoint modules:** | Module | Functions | |---------------------------------|-----------------------------------------------------| | `src/api/bookings.ts` | `fetchBookings(params)`, `fetchBooking(id)` | | `src/api/bookingRequests.ts` | `createBookingRequest(payload)` -- handles create, update, cancel | | `src/api/availability.ts` | `checkAvailability(locationId, payload)` | The `createBookingRequest` function accepts a discriminated union payload: ```typescript type BookingRequestPayload = | { type: 'create'; userId: number; locationId: number; startAt: string; endAt: string; evseCriteria?: {...} } | { type: 'update'; bookingId: number; startAt: string; endAt: string } | { type: 'cancel'; bookingId: number } ``` ### Design Token Integration ```mermaid flowchart LR A[JWT payload] -->|designTokens + appTheme| B[SessionContext] B -->|applyTheme| C["@ampeco/design-tokens"] C -->|Sets CSS custom properties| D["document.documentElement.style"] D -->|var(--btn-primary-bg), etc.| E[All components] ``` 1. The JWT payload carries `appTheme` (`'LIGHT'` or `'DARK'`) and an optional `designTokens` map of CSS custom property overrides. 2. `SessionContext` calls `applyTheme(appTheme, designTokens)` from `@ampeco/design-tokens` after fetching the session. 3. `applyTheme` sets CSS custom properties on the document root element. 4. All components reference these variables via inline styles (e.g., `var(--btn-primary-bg, #2563eb)`) with hardcoded fallback values. 5. The base CSS variables are imported in `src/app.css` from `@ampeco/design-tokens/css/variables.css`. Key CSS custom properties used throughout the UI: | Variable | Usage | |-----------------------------|--------------------------------| | `--surface-elevation-0` | Page background | | `--surface-elevation-1` | Card/input backgrounds | | `--surface-elevation-2` | Elevated slot buttons | | `--text-primary` | Primary text color | | `--text-secondary` | Secondary/muted text | | `--btn-primary-bg` | Primary button background | | `--btn-primary-text` | Primary button text color | | `--border-default` | Border color for cards, inputs | ### Internationalization **File:** `src/i18n/init.ts` Uses `i18next` with `react-i18next`. Currently ships with English (`en`) translations only. Translation strings are organized by feature in `src/i18n/locales/en.json`. Components access translations via the `useTranslation()` hook: ```typescript const { t } = useTranslation(); // t('createBooking.title') --> "Book a Charger" ``` ### Tailwind CSS Configuration **File:** `tailwind.config.ts` - Content sources: `index.html`, all `src/**/*.{ts,tsx}`, and compiled `@ampeco/ewa-ui` dist files. - Plugins: `ewaUiPlugin` from `@ampeco/ewa-ui/plugin` (provides shared component styles). --- ## Data Flow Diagrams ### JWT to Session to Theme ```mermaid sequenceDiagram participant App as Mobile App participant WV as WebView participant BE as Express Backend participant API as AMPECO API App->>WV: Load EWA URL with X-Payload JWT header WV->>BE: GET /api/session (X-Payload: ) BE->>BE: Verify JWT (HS256) BE->>WV: 200 { userId, operatorId, appTheme, designTokens, ... } WV->>WV: SessionContext.applyTheme(appTheme, designTokens) WV->>WV: Set CSS custom properties on :root WV->>WV: Render app with themed components ``` ### Booking CRUD Flow ```mermaid sequenceDiagram participant UI as React Frontend participant BE as Express Backend participant API as AMPECO Public API Note over UI: Create Booking (2-step) UI->>BE: POST /api/locations/:id/check-availability BE->>API: POST /actions/locations/v2.0/:id/check-booking-availability API-->>BE: { data: AvailabilitySlot[] } BE-->>UI: Available slots UI->>BE: POST /api/booking-requests { type: "create", ... } BE->>API: POST /resources/booking-requests/v1.0 API-->>BE: { data: BookingRequest { status: "approved", bookingId } } BE-->>UI: Booking confirmed Note over UI: Read / Poll UI->>BE: GET /api/bookings/:id BE->>API: GET /resources/bookings/v1.0/:id API-->>BE: { data: Booking } BE-->>UI: Booking detail (polls every 30s) Note over UI: Update Booking UI->>BE: POST /api/booking-requests { type: "update", bookingId, startAt, endAt } BE->>API: POST /resources/booking-requests/v1.0 API-->>BE: { data: BookingRequest { status: "approved" } } BE-->>UI: Update confirmed Note over UI: Cancel Booking UI->>BE: POST /api/booking-requests { type: "cancel", bookingId } BE->>API: POST /resources/booking-requests/v1.0 API-->>BE: { data: BookingRequest } BE-->>UI: Cancellation confirmed ``` --- ## Type System ### Backend Types (`backend/src/types.ts`) - `JwtPayload` -- Full JWT structure with nested `payload.parameters`. - `SessionData` -- Flattened session data extracted from the JWT. ### Frontend Types (`src/types/index.ts`) - `Booking` -- Core booking entity with `id`, `userId`, `locationId`, `startAt`, `endAt`, `status`, optional `bookedEvses`, `authorizedTokens`, `accessMethods`, `sessionId`. - `BookingStatus` -- Union type: `'accepted' | 'reserved' | 'completed' | 'cancelled' | 'no-show' | 'failed'`. - `BookingRequest` -- Result of a booking request operation (`type: 'create' | 'update' | 'cancel'`, `status: 'approved' | 'rejected'`). - `AvailabilitySlot` -- EVSE availability data with `evseId` and array of `{ startAt, endAt }` slots. - `SessionData` -- Mirror of the backend `SessionData` type. --- ## Build and Deployment ### Development `npm run dev` runs two processes concurrently: 1. `vite` -- Frontend dev server on port 5173 with HMR. Proxies `/api/*` and `/dev/*` to `localhost:3001`. 2. `tsx watch backend/src/index.ts` -- Backend with hot-reload via `tsx`. ### Production Build `npm run build` executes: 1. `vite build` -- Bundles the React frontend into `dist/` (HTML, JS, CSS assets). 2. `tsc -p backend/tsconfig.json` -- Compiles the backend TypeScript into `dist/backend/`. The production deployment serves `dist/` as static files and runs the compiled backend.