Cardholder Registration

liveEnd-to-end cardholder journey from first visit through card enrollment. The Angular frontend talks only to the .NET card-balance-api.· 2026-05-30
0

The Whole Lifecycle

A cardholder's journey from first visit through card enrollment. The Angular frontend talks only to the .NET card-balance-api — it never calls core-service or axis-service directly. The .NET API orchestrates across those backends: creating users via Frontegg + core, translating KYC level responses, and managing enrollment state locally. Frontend service names like CambristCoreCardService are misleading — the /cambrist/* URL paths all route to the .NET API, which delegates and transforms.

The lifecycle flows through six stages: /account/create (name, email, password, DOB) → Frontegg OAuth (email verify + login) → person details (guard-driven completion) → card lookup (find card by ext ref) → KYC + link (conditional: verify → link → enroll) → enrolled (card in dashboard).

1

Account Creation

Public route — no auth required. CreateAccountComponent at /account/create.

Form Fields

  • firstName — min 2, max 50
  • lastName — min 2, max 50
  • email — email format
  • password — 8–16 chars, upper + lower + digit + special
  • passwordConfirm — must match
  • dateOfBirth — formatted yyyy-MM-dd
  • gender — optional (M/F)
  • nationality — optional
  • locale — auto-set from navigator.language
  • privacyPolicy — required true

Flow — onSubmit()

Step 1 (FE): Validate + disable form. Client-side validation. Form is disabled to prevent double-submit.

Step 2 (.NET):POST /users/current — orchestrates three systems. Sends firstName, lastName, email, password, gender, nationality, dateOfBirth, locale. The .NET API orchestrates the full creation:

  • Check email in Frontegg — calls ManagementUserFindByEmail. Returns 409 if already exists.
  • Create person in core-service — POST /core/api/v1/{partner}/person with name, email, DOB, gender, nationality. Returns a personUuid.
  • Create user in Frontegg — POST /identity/resources/vendor-only/users/v1 with email, password, role, tenant, and metadata containing the personUuid.
  • Assign to Frontegg applications — parallel calls to assign the user to each configured app.
  • Set active tenant in Frontegg.
  • Save UserPersonLink — inserts {userUuid, personUuid} into the local DB, bridging Frontegg identity to core-service person.

Step 3 (FE): Success → confirmation dialog. Shows toast + dialog telling the user to check their email. Stores redirect URL in sessionStorage.

Step 4 (Frontegg):loginWithRedirect(). Hands off to Frontegg hosted login for email verification. User clicks the verification link, then authenticates.

2

OAuth Callback + Person Guard

After Frontegg auth, the browser returns to /oauth/callback/default.

CallbackComponent

  1. Frontegg: OAuth redirect lands. Frontegg sets the access token in the session. The callback component activates.
  2. FE: Read redirect URL. Card Balance app reads SSKey.OAUTH_CALLBACK_REDIRECT_URL from localStorage (default /cards). Incomm app checks user metadata for first_login_redirect_url instead.
  3. FE: Navigate. Routes to the stored URL. The root layout's existingUserPersonGuard fires on the target route.

The Guard Pair

Two guards control access to the app vs. the person-completion form. Together they form a loop: you can't use the app without a personUuid, and you can't re-visit the form once you have one.

GuardApplied toLogic
existingUserPersonGuard AppLayoutComponent (root) if not authenticated -> allow; if no metadata -> redirect /complete-person-details; if metadata but no personUuid -> redirect /complete-person-details; if personUuid exists -> allow
existingUserPersonGoodGuard /complete-person-details if no metadata -> allow (needs to complete); if metadata but no personUuid -> allow; if personUuid exists -> redirect /cards (already done)
3

Person Details Completion

MissingPersonDetailsDialogComponent at /complete-person-details. Guarded: requires auth + no existing personUuid.

Flow

  1. FE: Pre-populate from email. Sets email from authService.user?.email. Attempts to fetch existing KYC inquiry data via GET /cambrist/cpm/kyc-inquiry/personal-data?email={email} to pre-fill name, DOB, gender.
  2. FE: User completes form. Fields: firstName, lastName, email, dateOfBirth, gender, nationality, privacyPolicy. Countries loaded from CountryCodeService.
  3. .NET:POST /existing-users/person. Calls UserManagementService.existingUserCreatePerson(). Backend creates the person record and associates it with the Frontegg user, setting personUuid in user metadata.
  4. FE: Navigate to /cards. Person is created. The existingUserPersonGuard now passes (personUuid exists), granting access to the app.
4

Card Lookup

CardLookupComponent. The user enters a card's external reference and last-four digits.

Flow — onClickLookup()

  1. FE:validateLookup(externalId, lastFour). Calls LandingLookupService which resolves the card and checks its state. Returns a LookupValidation with a status and optional action callback.
  2. .NET:GET /lookup/{externalId} — card fetch + enrichment. The .NET API does real work here:
    • Fetch card from axis-service — POST /api/cpm/v1/partners/{partner}/cards/findOne?accessLevel=3. Returns the full PaymentCard including requiresKyc and kycLocked.
    • Fetch person-card link from core-service — GET /core/api/v1/{partner}/cards/{cardId}/link. Returns CardLinkPerson.
    • Validate locally — checks card status (rejects NotActivated, Lost, Stolen, Expired), checks for active blocks, checks card design against a local blacklist table.
  3. FE: Branch on result. Authenticated + person linked: navigate to /cards. Anonymous + person linked: navigate to /cards/{externalId}/balance. Anonymous + no link: show login dialog. No person link: show registration-required dialog.
  4. FE:showRequiredRegistration(externalId). If the card has no linked person: stores the card's externalId in sessionStorage[KYC_CARD] and navigates to /cards/kyc to begin the KYC flow.
5

KYC Verification + Card Linking

Conditional — only when the card's design requires KYC. CardKycComponent at /cards/kyc. Guarded by fronteggAuthGuard.

Initialization — DefaultCardKycInitBehaviour

  1. .NET:GET /users/current — fetch the authenticated user's profile data.
  2. FE: Card lookup — resolve the card from sessionStorage[KYC_CARD] via cardLookupService.findCard(externalId).
  3. FE: Get card value + currency via cardValueCurrencyService.
  4. .NET → core:GET /cambrist/core/cards/{cardId}/kyc-level?amount={amount}. Determines the required KYC level. The .NET API forwards to core, then translates core's two-field response (requiresKyc + kycLevel) into a single level string. This endpoint has no auth requirement. Frontend falls back to LEVEL_1 on error.
  5. FE: Configure form. KycLevelConfiguratorService sets field requirements based on level and country. LEVEL_2_A / LEVEL_2_B add national ID and source-of-funds fields.

How LEVEL_NONE Is Produced

Core-service returns two fields: { requiresKyc: boolean, kycLevel: string|null }. The .NET API collapses them into a single level string for the frontend:

csharp
 // DefaultCambristCoreCards.FindKycLevel() var curLevel = (kycRequired ? kycLevel : "LEVEL_NONE") ?? "LEVEL_1"; // core returns requiresKyc: true + kycLevel: "LEVEL_1" -> "LEVEL_1" // core returns requiresKyc: true + kycLevel: null -> "LEVEL_1" (null coalesce) // core returns requiresKyc: false + kycLevel: null -> "LEVEL_NONE"

KYC Levels

LevelFieldsTrigger
LEVEL_NONE No KYC verification -- person data + card link only .NET returns this when core says requiresKyc: false. Not a core-service concept. In practice only reachable via a config race.
LEVEL_1 Phone, address, country, nationality, birth country, gender Default for verification-required cards
LEVEL_2_A L1 + source of funds Higher-value cards or escalation from L1
LEVEL_2_B L1 + national ID + source of funds Escalation from L2A; country-specific ID rules
LEVEL_3 (defined but not used in frontend) --

Submit Chain — DefaultCardKycSubmitBehaviour

Five steps, executed sequentially. A failure at any blocking step aborts the chain. Every backend call goes through the .NET API, which injects the personUuid (from the JWT) and partnerId (from config) — the frontend never sends these.

  1. Gate — sanction check (frontend-only). Checks countryOfBirth against sanctioned list: RU, BY. If matched, shows EU sanctions dialog and sends alert email. Blocking.
  2. .NET:PUT /users/current — update person. Sends phone, address, country, nationality, birth country, gender. Blocking.
  3. .NET → core: KYC submit + card link. Two-part call:
    • Submit KYC — .NET receives POST /cambrist/core/person/kyc, adds personUuid from JWT, forwards to core. Checks response status === 'PASSED'.
    • Link card — .NET receives POST /cambrist/core/person/link, adds personUuid, forwards to core.
    Auto-escalation: if KYC is rejected, the error is parsed for a next level. If it differs from the current level, the entire step retries recursively at the higher level (LEVEL_1 → LEVEL_2_A → LEVEL_2_B).
  4. .NET → axis (fire-and-forget):PUT /cambrist/cpm/owners/{ownerId}/personal/conditional — sends name, email, phone, DOB, gender, address. Feature-flag gated (enablePersonalDataUpdate). Errors swallowed.
  5. .NET (fire-and-forget):POST /enrolled-cards — enroll card. Creates an EnrolledCard record locally. Checks for duplicate enrollment globally (409 if card already enrolled by any user). Errors swallowed.
6

Card Enrollment (Without KYC)

When a card's design does not require KYC, enrollment happens directly — no verification gate.

The .NET Enrolled-Cards API

MethodEndpointAuthPurpose
POST /enrolled-cards FronteggUser Enroll a card -- saves {cardId, cardNumber} to UserRegisteredCards
GET /enrolled-cards/cards FronteggUser List enrolled cards (paginated) -- filtered by authenticated user's sub claim
DELETE /enrolled-cards/{externalId} FronteggUser + PersonLink Soft-delete enrollment + unlink card from person in core-service

Authorization model: all endpoints use the JWT sub claim (Frontegg user ID) to scope data. Users can only see and manage their own enrolled cards — the WHERE clause always includes UserId = token.sub. DELETE additionally requires the FronteggUserPersonLink policy.

Duplicate prevention: POST checks for an existing card with the same ExternalCardNumber across all users — returns 409 Conflict if found.

DELETE is two-phase: first calls core-service to unlink the card from the person, then soft-deletes the local row (sets DateRemoved). Core 404s are tolerated — migration compatibility.

7

All Endpoints Touched

Every frontend HTTP call hits the .NET card-balance-api. The "delegates to" column shows where the .NET API forwards. .NET adds personUuid (from JWT) and partnerId (from config) to every delegated call — the frontend never sends these.

MethodFrontend calls (.NET)Delegates to.NET does
POST /users/current Frontegg + core + local Orchestrates: email check -> create person in core -> create user in Frontegg -> assign apps -> save UserPersonLink locally
POST /existing-users/person core + Frontegg Legacy path for users created outside /users/current
GET /users/current local Returns user profile from local DB
PUT /users/current local Updates user profile in local DB
GET /lookup/{externalId} axis + core Fetches card from axis, joins person-link from core, validates status + design blacklist + ownership
GET /cambrist/core/cards/{cardId}/kyc-level core Translates core's {requiresKyc, kycLevel} -> single level string. No auth required.
POST /cambrist/core/person/kyc core Adds personUuid, forwards to core. Auth: FronteggUser + PersonLink
POST /cambrist/core/person/link core Adds personUuid, forwards to core. Auth: FronteggUser + PersonLink
GET /cambrist/core/person/cards core Adds personUuid, forwards. Wraps response in PersonCardPair
PUT /cambrist/cpm/owners/{id}/personal/conditional axis Feature flag gate (enablePersonalDataUpdate) -> 403 if disabled, else forwards to axis
POST /enrolled-cards local only Local DB insert. Global duplicate check (409). No delegation.
GET /enrolled-cards/cards local only Local DB query, scoped to JWT sub. No delegation.
DELETE /enrolled-cards/{externalId} axis + core + local Unlinks card in core, soft-deletes local row. Tolerates core 404s.
GET (dead) /cambrist/cpm/kyc-inquiry/personal-data local DB query Queries local KycInquiry table by email -- but no data exists (dead feature)
8

Known Issues