Skip to main content

User Profiles & Accounts

What it is​

The user module owns account-level concerns: profile data, authentication settings, identity verification (BVN/NIN/biometrics), contact sync, and the user's personal wallet. It's one of the largest modules in the codebase (60+ endpoints). Contact-sync isn't covered here yet.

Core concepts​

  • Profile vs. settings vs. security are three distinct update surfaces with their own endpoints rather than one big "update user" call: PATCH /user/profile-update (name, bio, avatar, DOB, etc.), PATCH /user/settings (feature toggles like biometric auth), PATCH /user/username (username specifically, since it has uniqueness/availability concerns), PATCH /user/password/update, and separate PIN endpoints for the transaction PIN used on wallet actions.
  • This module is one of the few already on the Zod-native validation lane — its DTOs (src/modules/user/dto/user.dto.ts) are the schema that runs at request time and what the OpenAPI spec is generated from directly, no doc-only duplication needed.

How it works​

Profile update​

PATCH /user/profile-update accepts a large set of optional fields — name, bio (silently truncated to 150 chars rather than rejected), username, phone, avatar, gender, dateOfBirth, firstName/lastName, partyPlanningVibe, socialMedia, referralCode. Only send what changed; the schema is .strict(), so unknown fields are rejected outright rather than silently ignored.

dateOfBirth accepts either DD-MM-YYYY or YYYY-MM-DD and normalizes to YYYY-MM-DD internally — but empty string is explicitly allowed through as a way to clear the field.

partyPlanningVibe accepts either a single enum value or an array of up to 7 — the union exists for backward compatibility with clients still sending a single value.

Settings toggle (step-up auth pattern)​

PATCH /user/settings takes a configKey (from SettingsConfigKeysEnum) and a configValue. Most settings toggle freely, but enabling/disabling biometric_authentication requires the account password in the same request — a superRefine on the schema rejects the request if configKey is biometric auth and password is missing. This is a step-up-confirmation pattern: don't add new security-sensitive settings to this endpoint without the same password check, or you'll create a way to silently flip a security setting without re-authentication.

Identity verification (KYC)​

Verification is tracked as a set of independent flags (KycStatusFlags), not a single "verified" boolean: IDENTITY_SUBMITTED, IDENTITY_VERIFIED, BVN_VERIFIED, NIN_VERIFIED, KYC_STARTED, KYC_COMPLETE (full biometric — selfie + document). Routes are gated per-flag via isUserKycStatus([...]) middleware, so different actions can demand different levels of trust rather than an all-or-nothing gate.

BVN and NIN both follow the same initiate → verify two-step OTP pattern: POST /user/bvn/initiate sends an OTP (via the identity provider, IdentityPass) to the phone number on record for that BVN/NIN; POST /user/bvn/verify (identificationNumber + 6-digit otp) completes it. Same shape for /user/nin/initiate / /user/nin/verify. KYC_COMPLETE is a separate, heavier flow requiring a selfie + document upload processed via webhook (KYC_STARTED marks that submission was received; completion flips async).

This gating is enforced, not cosmetic — see the wallet section below for a concrete example (bank transfers require BVN_VERIFIED).

Personal wallet (PiggyVest Business-backed)​

Distinct from an event's PartyBank wallet (see the PartyBank guide) — this is the user's own wallet, used for things like promoter commission payouts and personal transfers.

  • POST /user/wallet (requires PIN) — creates the wallet.
  • GET /user/wallet/status — creation status (creation may be async/pending).
  • GET /user/wallet / GET /user/wallet/details — info and live balance.
  • GET /user/wallet/funding-account / GET /user/wallet/:walletRef/funding-account — dedicated account details for funding the wallet via bank transfer.
  • POST /user/wallet/reserve-account — provisions a regular PiggyVest "pocket" account.
  • Moving money out: POST /user/wallet/transfer/pocket (to another PiggyVest "pocket tag" — validate first via POST /user/wallet/validate-pocket-tag) or POST /user/wallet/transfer/bank (to an external bank account — requires BVN_VERIFIED KYC status, enforced by isUserKycStatus([KycStatusFlags.BVN_VERIFIED]) on the route). POST /user/wallet/verify-bank / GET /user/wallet/banks support the bank-transfer flow (resolve account name, list supported banks).
  • GET /user/wallet/cards, GET /user/transactions — display-oriented reads.

Account deletion (EU-compliant archival, not a hard delete)​

GET /user/account/deletion/terms returns the terms/acknowledgments a client should show before letting a user proceed. POST /user/account/deletion/initiate (transaction PIN + password, both required — the heaviest gate in the module) triggers archiveAndDeleteUser(): the user's data is copied into an archive record with direct identifiers hashed (emailHash/phoneHash via hashString()) rather than the account being hard-deleted outright — framed internally as "EU-compliant archival." A stale in-repo doc describes this as a "dummy implementation that doesn't perform actual deletion"; that's no longer true as of this session's check — the archival path is real and runs on every call.

Onboarding nudges ("pending tasks")​

GET /user/pending-tasks returns a small set of dismissible prompt cards (e.g. "Complete your KYC," "Enable 2FA," "Refer & Earn"), and DELETE /user/pending-tasks/:taskId dismisses one. Dismissals are stored in a process-local in-memory Map (UserService.dismissedTasks), not the database — a dismissal will not survive a server restart or be visible from a different server instance behind the load balancer. Don't rely on this being durable; if a user reports a dismissed card reappearing, this is very likely why, not a bug in the dismiss call itself.

GET /user/details also returns onboarding-guidance fields (onboardingCompleted, nextStep, nextMessage, nextMeta, nextScreen) so a client can redirect a user with an incomplete profile straight to the next required screen instead of the app inferring it client-side.

Key business rules & edge cases​

  • External bank transfers are KYC-gated by design — a user who hasn't verified their BVN can fund and hold a wallet balance, and transfer between PiggyVest pockets, but cannot move money to an external bank account. This is the concrete enforcement point for "why can't I withdraw" support questions.

  • createUserWallet and other money-movement endpoints require encryptedPinMiddleware — a transaction PIN separate from the account password, matching the same step-up pattern used by the biometric-auth setting toggle.

  • Username changes go through a dedicated endpoint (not the general profile update) with its own format rule (^[a-zA-Z0-9_]+$, 3–30 chars) — check GET /user/username/:username/availability before submitting to avoid a failed write.

  • UpdateProfileValidationRule and the others are .strict() — a client sending an extra unrecognized field gets a 422, not a silently-dropped field. If you're debugging "why did my update fail," check for typos/extra keys before assuming a value-validation issue.

  • The email and phone number "change" flows are separate initiate/verify endpoint pairs (/email/change/initiate + /verify, /phone/change/initiate + /verify) rather than a direct field update — both require an OTP-style confirmation step since they're account-recovery-sensitive.

See the API Reference — GET /user/details, PATCH /user/profile-update, PATCH /user/settings, PATCH /user/username.

Also: GET /v1/user/account/deletion/terms, POST /v1/user/account/deletion/initiate, GET /v1/user/pending-tasks, DELETE /v1/user/pending-tasks/{taskId} — not yet in the generated OpenAPI spec.

  • src/modules/user/user.route.ts, user.controller.ts
  • src/modules/user/dto/user.dto.ts — the Zod DTOs, now also the OpenAPI source
  • src/modules/user/user.openapi.ts — path registrations for the profile/settings endpoints
  • src/shared/enum/kyc.enum.ts — KycStatusFlags; src/shared/middlewares/ — isUserKycStatus, encrypted-pin.middleware
  • src/services/userWallet.service.ts — PiggyVest Business wallet integration