Cardholder Registration
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).
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}/personwith name, email, DOB, gender, nationality. Returns apersonUuid. - Create user in Frontegg —
POST /identity/resources/vendor-only/users/v1with email, password, role, tenant, and metadata containing thepersonUuid. - 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.
OAuth Callback + Person Guard
After Frontegg auth, the browser returns to /oauth/callback/default.
CallbackComponent
- Frontegg: OAuth redirect lands. Frontegg sets the access token in the session. The callback component activates.
- FE: Read redirect URL. Card Balance app reads
SSKey.OAUTH_CALLBACK_REDIRECT_URLfrom localStorage (default/cards). Incomm app checks user metadata forfirst_login_redirect_urlinstead. - 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.
| Guard | Applied to | Logic |
|---|---|---|
| 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) |
Person Details Completion
MissingPersonDetailsDialogComponent at /complete-person-details. Guarded: requires auth + no existing personUuid.
Flow
- FE: Pre-populate from email. Sets email from
authService.user?.email. Attempts to fetch existing KYC inquiry data viaGET /cambrist/cpm/kyc-inquiry/personal-data?email={email}to pre-fill name, DOB, gender. - FE: User completes form. Fields: firstName, lastName, email, dateOfBirth, gender, nationality, privacyPolicy. Countries loaded from
CountryCodeService. - .NET:
POST /existing-users/person. CallsUserManagementService.existingUserCreatePerson(). Backend creates the person record and associates it with the Frontegg user, settingpersonUuidin user metadata. - FE: Navigate to
/cards. Person is created. TheexistingUserPersonGuardnow passes (personUuid exists), granting access to the app.
Card Lookup
CardLookupComponent. The user enters a card's external reference and last-four digits.
Flow — onClickLookup()
- FE:
validateLookup(externalId, lastFour). CallsLandingLookupServicewhich resolves the card and checks its state. Returns aLookupValidationwith a status and optional action callback. - .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 fullPaymentCardincludingrequiresKycandkycLocked. - Fetch person-card link from core-service —
GET /core/api/v1/{partner}/cards/{cardId}/link. ReturnsCardLinkPerson. - Validate locally — checks card status (rejects NotActivated, Lost, Stolen, Expired), checks for active blocks, checks card design against a local blacklist table.
- Fetch card from axis-service —
- 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. - FE:
showRequiredRegistration(externalId). If the card has no linked person: stores the card'sexternalIdinsessionStorage[KYC_CARD]and navigates to/cards/kycto begin the KYC flow.
KYC Verification + Card Linking
Conditional — only when the card's design requires KYC. CardKycComponent at /cards/kyc. Guarded by fronteggAuthGuard.
Initialization — DefaultCardKycInitBehaviour
- .NET:
GET /users/current— fetch the authenticated user's profile data. - FE: Card lookup — resolve the card from
sessionStorage[KYC_CARD]viacardLookupService.findCard(externalId). - FE: Get card value + currency via
cardValueCurrencyService. - .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 singlelevelstring. This endpoint has no auth requirement. Frontend falls back toLEVEL_1on error. - FE: Configure form.
KycLevelConfiguratorServicesets 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:
// 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
| Level | Fields | Trigger |
|---|---|---|
| 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.
- Gate — sanction check (frontend-only). Checks
countryOfBirthagainst sanctioned list: RU, BY. If matched, shows EU sanctions dialog and sends alert email. Blocking. - .NET:
PUT /users/current— update person. Sends phone, address, country, nationality, birth country, gender. Blocking. - .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 responsestatus === 'PASSED'. - Link card — .NET receives
POST /cambrist/core/person/link, adds personUuid, forwards to core.
- Submit KYC — .NET receives
- .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. - .NET (fire-and-forget):
POST /enrolled-cards— enroll card. Creates anEnrolledCardrecord locally. Checks for duplicate enrollment globally (409 if card already enrolled by any user). Errors swallowed.
Card Enrollment (Without KYC)
When a card's design does not require KYC, enrollment happens directly — no verification gate.
The .NET Enrolled-Cards API
| Method | Endpoint | Auth | Purpose |
|---|---|---|---|
| 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.
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.
| Method | Frontend 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) |