HTTP layer and services

Service conventions

A service is a thin @Injectable wrapper over one backend concern. The canonical shape:

@Injectable()
export class SupplierEmailPreferencesService {
  private readonly apiUrl: string;

  constructor(private appConfig: AppConfigService, private http: HttpClient) {
    this.apiUrl = appConfig.env.internalApiUrl;      // never hardcoded
  }

  getPreferences(supplierKey: string): Observable<EmailPreference[]> {
    return this.http
      .get<EmailPreferenceDto[]>(`${this.apiUrl}suppliers/${supplierKey}/email_preferences`)
      .pipe(map((dto) => dto.map(toEmailPreference)));   // shape mapping in the service
  }
}

Rules:

  1. Base URLs come from AppConfigService. appConfig.env.<someApiUrl> — one of the ~15 keys in IEnvironment. Never a literal host.

  2. Return Observable<T>. No promises, with one exception: FeatureFlagsService uses async/await and .toPromise() internally because flag initialisation is awaited by a guard.

  3. Map the response shape inside the service. Components should receive models, not raw API payloads.

  4. One concern per service. SupplierService is the counter-example — it has grown to cover details, tokens, reports, balances, advances, health and statements; do not extend it further.

Base URL keys and their meaning

env key Backend

internalApiUrl

The primary internal API (database_v2/internal). Suppliers, payments, fees, reports, users, permissions, lookups, marketplace links, UCC, advances.

cliApiUrl

The CLI service (database_v2/cli) — bulk/scripted operations.

supWidgetsApiUrl

Supplier widget endpoints used by onboarding.

authToolsApiUrl

Supplier auth tokens and SMS number provisioning.

actionsToolsUrl

CRM activity (notes, tasks, files) and global settings.

eventLoggerUrl

Domain event stream and the Elasticsearch changelog.

eventRulesOldUrl

Event rules v1 plus its Elasticsearch index.

messagingUrl

Outbound email/SMS.

workflowApiUrl + workflowGciId

B2B task manager — long-running jobs and the SSE progress stream.

b2bDocumentsApiUrl, pybDocumentsApiUrl, documentsApiUrl, s3FilesProviderApi

Four document/file backends, used by different features.

marketplaceApiUrl

Marketplace service (supplier-marketplace data).

automaticMarketplaceApiUrl

Automatic underwriting tool — real-time validation and UW data.

evaApiUrl

The EVA micro-front-end origin (iframe src, not an HTTP API).

supplierUrl, supplierReferralUrl

Supplier self-service site (login-as-supplier) and referral link base.

Which service uses which is enumerated in services and endpoints; the wider picture is in the integration landscape.

The interceptor chain

Two interceptors are registered in AppModule, in this order:

{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor,            multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: FetchedRequestsInterceptor, multi: true },

and a third is contributed by NgHttpLoaderModule via useExisting.

AuthInterceptor

src/app/auth/auth-interceptor.ts

  • Reads the auth slice with take(1) on every request.

  • If hasCorrectToken(authState), clones the request with the X-Session-Token: <jwt> header. Header name is exported as AUTH_HEADER_NAME.

  • Catches HttpErrorResponse; on 401 with a token present it logs and dispatches Logout({ message: 'Auth token expired, please try to login again.' }), then rethrows.

  • If there is no valid token, the request passes through unmodified — which is how the login call itself works (it sends its own Authorization: Basic header).

Because the interceptor selects the store per request, an in-flight token change is picked up by subsequent requests but not by requests already dispatched. Nothing refreshes a token: expiry is detected by RouteAccessResolver on navigation and by the 401 trap here.

FetchedRequestsInterceptor

src/app/shared/interceptors/fetched-requests.interceptor.ts dispatches FetchRequest(req) for every request and forwards it untouched. The errors reducer keeps the last 50 requests, JSON.stringify-ed, in state.

This is purely a debugging aid — it is what the error-handling UI shows as recent request context. It also means every request body passes through the store, so avoid dispatching sensitive payloads through paths you would not want in a DevTools snapshot.

PendingInterceptorService

src/app/shared/http-loader/pending-interceptor.service.ts (a vendored ng-http-loader):

  • Counts in-flight requests and pushes true/false to pendingRequestsStatus, which drives SpinnerComponent / PreloaderDirective.

  • Additionally toasts any response with status >= 500 as "<status>: <message>", then rethrows.

So a 5xx produces a toast here, and any error the calling code passes to ErrorVisualizationService produces a second one. Duplicate toasts on server errors are expected behaviour, not a bug to chase.

CachingInterceptor — present but not registered

src/app/shared/cache/caching-interceptor.ts implements read-through caching for GET requests against HttpCacheAbstract. It is not in AppModule’s `HTTP_INTERCEPTORS list, so it is inactive globally; HttpCacheAbstract → HttpCacheLocalStorage is still provided and can be injected directly.

If you enable it, note the 5-minute TTL and MD5 cache key in HttpCacheLocalStorage, and that it caches all GETs including permission- and supplier-scoped ones. See caching and persistence.

Service abstractions and mocks

Eleven abstract base classes define seams that specs and alternative implementations plug into:

Abstract class Live implementation

HttpCacheAbstract

HttpCacheLocalStorage (bound in AppModule)

LookupAbstractService

LookupRepository (bound in AppModule)

UsersAbstractService

UsersService

ConfigsAbstractService

ConfigsService

TasksAbstractService

TasksService

MarketplacesAbstractService

MarketplaceRepository

PaymentBatchesAbstractService

PaymentBatchRepositoryService

PaymentBatchDownloaderAbstractService

PaymentBatchDownloaderService

ReportsAbstractService

ReportsRepositoryService

LongTermContractsAbstractService

LongTermContractsService

Alongside these, ~30 services ship a *-mock.service.ts sibling (supplier-mock.service.ts, crm-mock.service.ts, tasks-mock.service.ts, payment-config-mock.service.ts, b2b-document-mock.service.ts, …). They are the test doubles; specs provide them by token. They must never be imported from production code paths — see testing.

The ServiceUpdatePartialInterface (shared/interfaces/service-update-partial-interface/) declares saveItemPart(itemKey, itemPart, …​params). Services that back the supplier workspace’s card-by-card editing implement it, which is what lets the UPDATE_MODEL_PART effect call any of them uniformly. See partial editing.

Error mapping

Services do not surface raw HttpErrorResponse objects to components. Three patterns are in use, oldest first:

Pattern Where

catchError(err ⇒ ErrorObservable.create(err))

Older services (SupplierService, LookupRepository). A deep legacy RxJS import; equivalent to throwError.

catchError(err ⇒ observableThrowError(err)) with a toast

Repository services (PaymentBatchRepositoryService toasts "Cannot fetch Payment Batches" then rethrows).

Return an _ERROR action from an effect

Store-backed flows. The component reads the error out of state.

For user-facing presentation, everything funnels through ErrorParserService → ErrorVisualizationService, which understands the internal API’s error envelope (error.meta.errors[].message) as well as plain { message } shapes. Details in error handling.

Request shaping conventions

Query parameters

Built with HttpParams and .set()/.append() chaining, never string concatenation. Grid-driven endpoints take limit/offset plus arbitrary filter keys (BalanceApprovalService.getData copies a DataTableQueryParams object into params, skipping limit/offset).

Cache-busting

refreshTimestamp=Date.now() is appended when a caller asks for a refresh (SupplierService.fetchSupplierDetails).

Custom headers

Several backends need extra headers, set per service rather than in an interceptor: X-InstanceId (event rules, global settings — value from env.instanceId, 8001), X-EventLogger-InstanceId, X-EventLogger-Type, X-EventLogger-TargetUUID, X-Actions-TargetUUID (CRM). CrmService keeps the target-UUID header in sync by subscribing to the store in its constructor.

File uploads

FormData built by a shared helper (BatchImportService.getFormData), posted to *_csv_upload endpoints.

Downloads

responseType: 'text' for report dumps, file-saver for writing the file. See CsvExportService and PaymentBatchDownloaderService.

MonitorService.getHeaders() contains a hardcoded Authorization: Token <literal> and X-EventLogger-InstanceId: 8003 for the analytics event-logger call. It is the one place a credential is embedded in source. Treat it as known debt; do not copy the pattern.