Manage (back-office configuration)

What it is

Everything that configures the rest of the application. ~79 components, all under /manage, with the parent route denying the two restricted groups (REST_UW_ACCESS, ADVANCE_AUDITORS) and per-screen permissions enforced inside.

The default child route is users/list.

Screen map

Route Folder Purpose

manage/batch-imports

batch-imports/

Bulk CSV imports and CLI actions. See bulk actions.

manage/users/{list,new,:loginKey/edit}

users/

Internal user administration.

manage/perm-groups/{list,new,:permGroupKey/edit}

perm-groups/

Permission groups.

manage/global-settings

global-settings/

Key/value configuration store. Has its own state slice.

manage/configs

configs/

Typed configuration records (task templates, email templates, …).

manage/event-rules/{list,add,:eventRuleId/edit,:eventRuleId/history}

event-rules/

Event rules v1. See events.

manage/event-rules-2

event-rules-2/

Event rules v2 (beta).

manage/events

events/

The raw domain-event stream with badges, filters and preview.

manage/onboarding/{list,add,:onboardingTemplateKey/edit,:onboardingTemplateKey/marketplaces}

onboarding/

Onboarding templates. See onboarding.

manage/email-config/list

email-settings/

Email templates and configuration. See emails.

manage/fee-settings/{list,add,:id/edit}

fee-settings/

Fee schedules and tiers. See fees and reserves.

manage/contracts

contracts/

Contract templates.

manage/month-end-close

month-end-close/

The accounting close screen.

—

permission-error-pop-up/

Shared "access denied" dialog used across the app.

Bulk actions and CLI

/manage/batch-imports is the bulk-operations console. BatchImportService is the only service in the codebase that spans four backends — internalApiUrl, cliApiUrl, actionsToolsUrl and b2bDocumentsApiUrl.

Import Endpoint Notes

Manual bank accounts

POST internalApiUrl deposit_accounts/csv_upload

deposit-accounts/ — CSV of supplier bank accounts.

Pre-funded cards

POST internalApiUrl payment/configs/csv_upload

pre-funded-cards/.

Bulk notes

POST actionsToolsUrl notes/bulk-update/

bulk-notes/. Parsed in the browser — see below.

EFS upload

POST cliApiUrl efs_csv_upload

Card programme data.

Supplier credit

POST internalApiUrl supplier_credit

supplier-promos/, supplier-rebates/.

Weekly purchases

POST cliApiUrl purchase_rcv (empty body)

weekly-purchases/ — triggers a job rather than uploading data.

UCC upload

ucc-upload/

Bulk UCC filing data. See underwriting.

CLI actions

cli-action/ with cli-action.service.ts (+ mock)

Arbitrary CLI operations exposed as forms.

Client-side CSV parsing

getNotes(data, user) reads the file with FileReader and builds note records in the browser:

const lines = e.target.result.split('\n');
for (let i = 1; i < lines.length; i++) {        // skips the header row
  const line = lines[i].split(',');             // naive comma split
  if (line[0]) {
    notes.push({ target_type: 'note', target_uuid: line[0],
                 body: line[1], is_pinned: line[2] === 'true', user });
  }
}

This is a naive split on , with no quote handling. A note body containing a comma is silently truncated at the comma, and any further columns shift. Column order is fixed: target_uuid, body, is_pinned. The first line is always discarded as a header.

Job monitoring

Long-running imports are executed by the B2B task manager, and progress is polled or streamed:

getLastExecution(name)     → B2bWorkflowService.getLastExecution(name)
getListOfActivities(name)  → getLastExecution(name) then getListOfActivities(task.id)

The underlying stream is server-sent events. See real-time streams.

Users

UsersService on internalApiUrl (logins and perms):

Endpoint Purpose

GET logins?permCode=…[&search=…]

Users filtered by permission code. getUsers always filters on INT_LOGIN.

GET logins/:loginKey

One user → UserModel.

PUT logins/:loginKey

Update.

DELETE logins/:loginKey

Delete.

GET logins/:loginKey/perms

A user’s permissions.

GET perms/:permCode/sub-perms

Sub-permissions of a permission (cached in an instance field after first call).

getUser(username, jwtToken) is the call AuthEffects makes at login to load the user and their perms — see authentication. checkPermission(permCode) is what SubPermissionsGuard uses.

The name-field hack

saveLogin contains an explicit, commented workaround:

const payload = {
  ...Serialize(data, UserModel),
  firstName: data.firstname,  lastName: data.lastname,
  firstname: undefined,       lastname: undefined,
};
// sorry for this hack but I don't want to broke other places where we use this model

UserModel uses firstname/lastname; the update endpoint expects firstName/lastName. Rather than change the model (used widely), the service rewrites the payload. Know this before "fixing" the model casing.

Caching

UsersService keeps three BehaviorSubject caches — usersSubject, groupsSubject, usersAndGroupsSubject — exposed as users, groups and getPreparedCombinedList(). The combined list is fetched once with combineLatest([getUsers(), getGroups()]) and cached; there is no invalidation, so a user created in this session will not appear in an assignee dropdown until reload. See state management.

Permission groups

/manage/perm-groups with perm-groups-list/, -add/, -edit/. PermissionModel / PermissionBaseModel (shared/users/permission.model.ts) and UserPermModel (user-perm.model.ts) are the models; the vocabulary is USER_PERMISSION (shared/users/user-permissions.enum.ts).

Groups nest: a permission has sub-permissions (perms/:permCode/sub-perms), which is what requireOneOfSubPermissions route data checks. Requires INT_PERMS_ADM. See authorisation.

Global settings

The most load-bearing screen in Manage: it is where feature flags are stored.

GlobalSetting (manage/global-settings/models/global-setting.model.ts):

interface GlobalSetting {
  config_key: string; config_type: string; config_value: any;
  created_at: number; updated_at: number;
  instance_id: number;
  owner_email: string; owner_uuid: string;
  updated_by: string; updated_by_name: string;
  uuid: string;
}

config_value is untyped any — its shape depends on config_key.

GlobalSettingsService on actionsToolsUrl with an x-instanceid header (env.instanceId, 8001):

Endpoint Purpose

GET settings/

All settings.

GET settings/:configKey/

One setting.

GET settings/:type/items

All settings of a type.

POST settings/

Create.

PUT settings/:configKey/

Update.

DELETE settings/:configKey/

Delete.

The service is a facade over the store slice: getAll() selects from globalSettings, dispatches GlobalSettingsFetchAll on a miss or a forced refresh, and every mutation dispatches GlobalSettingsUpdateConfig. Call the service; do not select the slice directly from a component.

Screens: new-setting/, edit-setting/, delete-setting/, and templates/ for template-valued settings.

Feature flags live here under the keys FEATURE_FLAGS_IA and FEATURE_FLAGS_EVA, with config_value = { flags: 'A,B,C', lastChange: <timestamp> }. See feature flags.

Configs

/manage/configs administers typed configuration records. ConfigsService (shared/configs/, with an abstract base and a mock) reads them through GlobalSettingsService — configs are global settings with a known config_type.

ConfigTypes:

Member config_type

TASK_PRIORITIES

task_priorities

TASK_STATUSES

task_statuses

TASK_TEMPLATE

task_template

TASK_GROUPS

task_assignee_allowed_groups

EMAIL_BACK_CONFIG

email_config

EMAIL_FRONT_CONFIG

email_frontend_config

EMAIL_FRONT_BATCH_CONFIG

email_frontend_batch_config

EMAIL_TEMPLATE

email_frontend_template

EMAIL_BASE_TEMPL

email_base_template

EMAIL_FRONTEND_AUTOMATIC_CONFIG

email_frontend_automatic_config

ConfigEmailTypes narrows the list to the six email types, and ConfigEmailTypesNames pairs each with a display name and a can_add_new flag — some email config types are singletons.

Task statuses and priorities exist twice: as TypeScript enums in crm-tools/tasks/ and as configurable records here. The enums are what the code branches on; the configs drive display and allowed values. A change in one place does not propagate to the other.

The event stream

/manage/events shows raw domain events with event-badge/, event-filters/ and event-preview/. It is backed by EventsService’s Elasticsearch query against the `em_events index on eventRulesOldUrl. The sidebar reaches it through navigateToEvents() rather than a plain routerLink.

Event rules — the reactive layer over the same stream — are documented in events.

Contracts and month-end close

Screen Detail

/manage/contracts

Contract templates via LongTermContractsService (long_term_contract/template*). See contracts.

/manage/month-end-close

A single component (month-end-close/) driving the accounting close. Note the sidebar link reuses data-test="sidebarContracts" for this item — a copy-paste bug that will confuse Cypress selectors.

Permissions

Permission Governs

INT_PERMS_ADM

Users and permission groups.

INT_GLOBAL_SETTINGS

Global settings.

INT_EMAIL_SETTINGS

Email settings.

INT_RULES_UPDATE

Event rules.

INT_REP_ADMIN

Report administration.

INT_SUPER_USER

Broad administrative access.

CRM_ADMIN

CRM administration.

DEV

DEV Admin only — see DEV Admin.

The /manage parent route denies REST_UW_ACCESS and ADVANCE_AUDITORS; finer control is per-screen and backend-enforced.

Gotchas

Issue Detail

Naive CSV parsing

Bulk notes split on , with no quoting.

Four backends in one service

BatchImportService.

firstName vs firstname

The documented hack in saveLogin.

Uninvalidated user cache

getPreparedCombinedList() caches for the session.

config_value is any

Shape depends on config_key; there is no schema.

Global settings are the flag store

Editing FEATURE_FLAGS_IA changes behaviour for everyone in the environment.

Duplicated task vocabularies

Enums in code and configs in the database.

purchase_rcv posts an empty body

It triggers a job; there is no payload to review before sending.

Two event-rule generations

Both routed and both live.