Skip to content

Wallet Creation Architecture & Access Policy Integration

Current Entity Relationships

1. Identity (core.identity) - Foundation

Identity (Base ID Record)
├── id: Primary Key (auto-increment)
├── identity_type: Type of identity (agent, operator, customer, etc.)
└── No user data - purely identity record
- Purpose: Unique identity record for any entity (user, agent, operator, customer) - Shared by: User, Customer, Agent, Operator, Wallet all use same ID - Is identity created: Yes, in user onboarding/customer signup

2. User (users.users) - Authentication & Access Control

User
├── id: ForeignKey(Identity.id) → 1:1 relationship
├── username: Unique login credential
├── active: Account activation status
├── is_superuser: Admin flag
├── last_seen: For login attempt tracking
└── Relationships:
    ├── identity: Identity object (viewonly)
    ├── access_policy: AccessPolicy (via UserAccessPolicy join)
    ├── customer: Customer (optional, 1:1)
    ├── emails, phones, addresses: Contact info
    └── credentials: Passwords stored in auth.credential table
- Purpose: Enable authentication and access control - Created: Only when User authentication needed (agents, admins, some customers) - Not always created: Wallet-only customers might not have User record

3. Customer (customer.customer) - KYC & Profile

Customer
├── identity_id: ForeignKey(Identity.id) → 1:1 relationship
├── customer_type: Individual, Corporate, etc.
├── cbs_id: Integration with core banking
├── customer_number: Unique customer identifier
├── Relationships:
│   ├── profile: Profile (personal details)
│   ├── emails, phones, addresses: Contact details
│   ├── accounts: Bank accounts
│   └── files: KYC documents
- Purpose: Store customer KYC info and personal details - Created: During customer registration/onboarding - Optional User: Customer can exist without User record

4. Wallet (wallet.wallet) - Digital Account

Wallet
├── id: ForeignKey(Identity.id) → 1:1 relationship
├── wallet_number: Unique identifier (derived from phone)
├── status: active, inactive, suspended, closed
├── kyc_level: none, basic, full
├── allow_transfers: Boolean
├── allow_withdrawals: Boolean
└── Relationships:
    ├── identity: Identity object
    ├── customer: Customer (via Identity)
    ├── balances: WalletBalance records
    ├── configuration: WalletConfiguration
    └── issuer_config: IssuerWalletConfig
- Purpose: Digital transaction account - Created: When customer enables wallet (via create_wallet) - 1:1 with Identity: One wallet per identity

5. AccessPolicy (users.access_policies) - Security Rules

AccessPolicy
├── id: Primary Key
├── name: Policy name (e.g., "WALLET_CUSTOMER_PIN_REQUIRED")
├── status: active, inactive
└── Rules:
    ├── pin_restriction: PIN enforcement (required, length, complexity, expiry)
    ├── password_restriction: Password rules (length, complexity, expiry, 2FA)
    ├── login_attempt_control: Failed attempt lockout (max_attempts, lockout_duration)
    ├── otp_control: OTP settings (length, validity, delivery method)
    ├── geofencing_restriction: Location-based access
    ├── login_day_restriction: Business hours restriction
    └── policy_channels: Allowed channels (web, mobile, ussd)
- Purpose: Define security and access rules - Not created per user: Policies are templates (WALLET_CUSTOMER_PIN_REQUIRED applies to many users) - Linked to User: Via UserAccessPolicy join table

6. UserAccessPolicy (users.user_access_policy) - JOIN TABLE

UserAccessPolicy (Join Table)
├── user_id: ForeignKey(User.id)
├── access_policy_id: ForeignKey(AccessPolicy.id)
├── is_primary: Boolean (marks default policy)
└── status: active, inactive
- Purpose: Link users to their access policies - Multiple policies: User can have multiple policies (system manages priority) - Created: When assigning policy to user


Current Wallet Creation Flow (Incomplete)

What create_wallet() Does Now

def create_wallet(db, id, wallet_number=None, issuer="INTERNAL", ...):
    # Step 1: Verify identity exists
    identity = get_identity(id)

    # Step 2: Create Wallet record
    wallet = Wallet(id=id, wallet_number=wallet_number, ...)
    db.add(wallet)

    # Step 3: Create WalletConfiguration
    config = WalletConfiguration(wallet_id=id, ...)
    db.add(config)

    # Step 4: Create IssuerWalletConfig (default issuer setup)
    issuer_config = IssuerWalletConfig(wallet_id=id, issuer=issuer, ...)
    db.add(issuer_config)

    db.commit()

What's Missing

❌ User record creation (if doesn't exist)
❌ UserAccessPolicy assignment (linking to default policy)
❌ PIN credential setup (for PIN validation)
❌ Default AccessPolicy enforcement

What Needs to Happen: Enhanced Wallet Creation Workflow

Phase 1: Identity & User Setup

1. Verify Identity exists for wallet ID
   └─ If not exists: Raise error (identity must be created in onboarding first)

2. Check if User record exists for identity (id)
   └─ If NOT exists:
      └─ Create User record:
         ├── id = identity.id (use Identity ID)
         ├── active = True
         ├── is_superuser = False
         ├── other fields defaults

3. Verify Customer record exists
   └─ If not exists: This might be OK (some wallet-only customers)

Phase 2: AccessPolicy Assignment

1. Get or create default WALLET ACCESS POLICY
   └─ Query: AccessPolicy where name = "WALLET_CUSTOMER_PIN_REQUIRED"
   └─ If not exists:
      └─ Create AccessPolicy with:
         ├── name: "WALLET_CUSTOMER_PIN_REQUIRED"
         ├── pin_restriction: Required, 4 digits, 30-day expiry
         ├── login_attempt_control: 3 attempts, 30-min lockout
         ├── otp_control: Optional 2FA config
         └── status: active

2. Assign policy to user
   └─ Create UserAccessPolicy entry:
      ├── user_id = wallet ID (which == identity.id)
      ├── access_policy_id = WALLET_CUSTOMER_PIN_REQUIRED.id
      ├── is_primary = True (this is the main policy)
      └── status: active

3. Verify no other policies conflict
   └─ If user has multiple policies: get_highest_priority_policy() resolves

Phase 3: PIN Credential Setup

1. Check if PIN credential exists for user
   └─ Query: Credential where user_id=user.id AND type="pin"

2. If NOT exists:
   └─ Create PIN credential:
      ├── user_id = user.id
      ├── type = "pin"
      ├── username = wallet_number (or user identifier)
      ├── value = "" (empty or temporary - user must set on first login)
      ├── active = True
      ├── failed_attempts = 0
      ├── expires_on = datetime.now() + 30 days
      └── created_by = current_user.id

Phase 4: Wallet Creation (as now)

1. Create Wallet record (id = identity.id)
2. Create WalletConfiguration
3. Create IssuerWalletConfig

Implementation: Wallet Creation Sequence Diagram

User/System initiates wallet creation
    ├─→ create_wallet(db, identity_id, wallet_number, ...)
    ├─→ Step 1: Verify Identity exists
    │   └─→ if not found: raise error
    ├─→ Step 2: Ensure User record exists
    │   ├─→ user = get_user(id)
    │   └─→ if not found:
    │       └─→ create_user(id)  [NEW HELPER]
    ├─→ Step 3: Assign default AccessPolicy
    │   ├─→ policy = get_or_create_default_wallet_policy()  [NEW HELPER]
    │   ├─→ if not assigned:
    │   │   └─→ create_user_access_policy(user_id, policy_id)  [NEW HELPER]
    │   └─→ verify assignment successful
    ├─→ Step 4: Create PIN credential if needed
    │   ├─→ pin_cred = get_pin_credential(user_id)
    │   └─→ if not found:
    │       └─→ create_pin_credential(user_id)  [NEW HELPER]
    ├─→ Step 5: Create Wallet, Config, IssuerConfig (existing)
    │   └─→ proceed with current logic
    └─→ return Wallet with all access policies configured

New Helper Functions Needed

1. ensure_user_record_exists(db, identity_id)

def ensure_user_record_exists(db: Session, identity_id: int) -> User:
    """
    Ensures User record exists for identity.

    Creates User if not found, uses wallet defaults:
    - active = True
    - is_superuser = False
    - provider_name = "local" (wallet provider)

    Returns:
        User object (existing or newly created)
    """

2. get_or_create_default_wallet_policy(db)

def get_or_create_default_wallet_policy(
    db: Session, 
    policy_name: str = "WALLET_CUSTOMER_PIN_REQUIRED"
) -> AccessPolicy:
    """
    Gets or creates default AccessPolicy for wallet customers.

    Default configuration:
    - PIN: Required, 4-6 digits, expires in 30 days
    - Login attempts: 3 failed attempts, 30-min lockout
    - Channels: mobile, ussd (web optional)
    - 2FA: Optional

    Returns:
        AccessPolicy object
    """

3. assign_access_policy_to_user(db, user_id, policy_id)

def assign_access_policy_to_user(
    db: Session,
    user_id: int,
    policy_id: int,
    is_primary: bool = True
) -> UserAccessPolicy:
    """
    Creates UserAccessPolicy link.

    Returns:
        UserAccessPolicy entry
    """

4. create_pin_credential_for_user(db, user_id)

def create_pin_credential_for_user(
    db: Session,
    user_id: int,
    wallet_number: str = None,
    current_user: User = None
) -> Credential:
    """
    Creates empty PIN credential for user.

    User must set PIN on first login/transaction.

    Returns:
        Credential object with type="pin"
    """

Updated create_wallet() Function Signature

async def create_wallet(
    db: Session,
    id: int,
    wallet_number: Optional[str] = None,
    issuer: str = "INTERNAL",
    custom_settings: Optional[Dict] = None,
    policy_name: Optional[str] = "WALLET_CUSTOMER_PIN_REQUIRED",  # NEW
    current_user: User = None,
) -> Wallet:
    """
    Create new wallet with full setup: Identity → User → AccessPolicy → Wallet.

    Handles:
    1. User record creation if needed
    2. AccessPolicy assignment (default or custom)
    3. PIN credential setup
    4. Wallet creation
    5. Wallet configuration

    Args:
        db: Database session
        id: Wallet/Identity ID
        wallet_number: Custom wallet number
        issuer: Issuer code (default "INTERNAL")
        custom_settings: Custom wallet settings
        policy_name: AccessPolicy name to assign (uses default if None)
        current_user: Current authenticated user

    Returns:
        Created Wallet with all access policies configured
    """

Key Decision Points

Decision 1: User Record Requirement

Question: Do ALL wallet customers need a User record?

Current Design: Yes, because: - AccessPolicy is linked to User, not Identity - PIN validation needs User.last_seen for attempt tracking - Account locking updates User.active - Multi-auth workflows use User record

Alternative: Could link AccessPolicy to Identity instead - Requires schema change - Breaks existing authentication system

Decision: Keep current design - create User for all wallet customers

Decision 2: Default AccessPolicy

Question: What should PIN enforcement look like for wallets?

Option A: PIN always required for all transactions - Stricter security - Higher customer friction - Better fraud prevention

Option B: PIN required only for large amounts (configurable) - Less friction - Configurable per transaction rule - Requires transaction rule logic

Decision: Start with Option A (PIN required) - can adjust via transaction rules later

Decision 3: PIN Credential Initialization

Question: Should PIN be set immediately or on first use?

Option A: Create empty credential, user MUST set PIN on first transaction - Allows flexibility - Might cause transaction failure if user forgets

Option B: Generate temporary PIN, send to user - Ensures PIN is set - Adds SMS/email dependency - Better UX

Decision: Start with Option A (user sets on first login) - implement Option B later with 2FA system

Decision 4: Wallet Creation Timing

Question: When does wallet creation happen?

Sequence: 1. Customer registration → Identity created, Customer record created 2. Later: Customer requests wallet → User created, AccessPolicy assigned, Wallet created 3. First login → Customer prompted to set PIN

Alternative: Create all during registration (early but might be unused)

Decision: Lazy creation - wallet setup happens when customer first requests wallet


Data Flow Diagram

┌─────────────────────────────────────────────────────────────────┐
│                   Customer Onboarding Flow                      │
└─────────────────────────────────────────────────────────────────┘

Phase 1: Customer Registration (Currently exists)
├─ Create Identity (core.identity) ← identity_id
├─ Create Customer (customer.customer) ← KYC info
├─ Create Profile, Email, Phone
└─ Result: Customer profile ready but no auth/wallet

Phase 2: Wallet Activation (TO IMPLEMENT)
├─ Wallet creation triggered (mobile app / web)
├─ Ensure User exists for Identity
│  ├─ Query: SELECT * FROM users.users WHERE id = identity_id
│  └─ If missing:
│     └─ INSERT INTO users.users (id, active, is_superuser, ...)
├─ Get/Create default AccessPolicy
│  ├─ Query: SELECT * FROM users.access_policies 
│         WHERE name = 'WALLET_CUSTOMER_PIN_REQUIRED'
│  └─ If missing:
│     └─ INSERT INTO users.access_policies (name, pin_restriction, ...)
├─ Assign policy to user
│  └─ INSERT INTO users.user_access_policy (user_id, access_policy_id, is_primary)
├─ Create PIN credential
│  └─ INSERT INTO users.credentials 
│        (user_id, type='pin', active=True, failed_attempts=0, expires_on)
└─ Create Wallet and related configs
   ├─ INSERT INTO wallet.wallet (id, wallet_number, status, ...)
   ├─ INSERT INTO wallet.wallet_configuration (wallet_id, ...)
   └─ INSERT INTO wallet.issuer_wallet_config (wallet_id, ...)

Phase 3: PIN Setup (First Login)
├─ User logs in to wallet
├─ System detects PIN not set (value is empty)
├─ Prompt user to create PIN
└─ User enters PIN → validate & hash → store in credential.value

Phase 4: Subsequent Transactions
├─ User initiates transaction (topup/transfer/withdrawal)
├─ PIN required? Check AccessPolicy.pin_restriction
├─ If required: prompt PIN
├─ Validate PIN: check_pin_attempts() + verify hash
├─ Execute transaction or raise AUTH error
└─ Update User.last_seen for attempt tracking

Summary Table

Entity Created When Links To Purpose
Identity Registration Customer signup - Base ID record
Customer Registration Customer signup Identity (1:1) KYC/profile
User Wallet setup Wallet creation Identity (1:1) Auth/access control
AccessPolicy System setup First wallet creation - Template rules
UserAccessPolicy Wallet setup Policy assignment User → Policy Link rules to user
Credential (PIN) Wallet setup Wallet creation User + type Store PIN hash
Wallet Wallet setup Wallet creation Identity (1:1) Digital account
WalletConfiguration Wallet setup Wallet creation Wallet Settings
IssuerWalletConfig Wallet setup Wallet creation Wallet Issuer setup

Implementation Checklist

  • [ ] Create ensure_user_record_exists() helper
  • [ ] Create get_or_create_default_wallet_policy() helper
  • [ ] Create assign_access_policy_to_user() helper
  • [ ] Create create_pin_credential_for_user() helper
  • [ ] Update create_wallet() to call all helpers
  • [ ] Add error handling for policy/credential creation
  • [ ] Test wallet creation with access policy assignment
  • [ ] Verify PIN validation works end-to-end
  • [ ] Document PIN setup flow for frontend
  • [ ] Create system default AccessPolicy (if not exists)
  • [ ] Update wallet router to handle User creation