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
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
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
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
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)
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
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