Skip to main content

Country-Specific Wallet Migration Guide

Overview

The wallet system has been updated to support country-specific wallets. Each user can now have multiple wallets - one per country. points earned in a country are isolated to that country's wallet.

Key Changes

1. Wallet Model

  • New Field: country (String, required, uppercase, default: 'UNKNOWN')
  • New Index: Compound unique index on (user, country)
  • Users can have multiple wallets, one per country

2. Behavior

  • Isolated Balances: points in one country are NOT visible/usable in another
  • Automatic Creation: New wallet created when user changes country
  • Current Country: All operations use user's current country from user.address.country
  • Preserved History: Old country wallets remain with their balances

Backward Compatibility

Default Behavior

  • New wallets without explicit country → 'UNKNOWN'
  • Existing wallets without country field → Treated as 'UNKNOWN'
  • Users without country → Use 'UNKNOWN' wallet

Migration Required

Before deploying to production, you MUST migrate existing wallets:

# Run migration script
bun run migrate:wallet-country

This will:

  1. Find all wallets without a country field
  2. Set country based on user's current address.country
  3. Default to 'UNKNOWN' if user has no country
  4. Create the compound unique index

Migration Script

The migration script is located at: src/scripts/migrate-wallets-country.ts

What it does:

  1. Queries all wallets with missing or empty country field
  2. Looks up each user's current country from user.address.country
  3. Updates wallet with country (uppercase)
  4. Creates compound unique index \{ user: 1, country: 1 \}
  5. Provides detailed summary with success/error counts

Running the Migration

# Connect to production database
export MONGO_URI="mongodb://your-production-db"

# Run migration
bun run migrate:wallet-country

Migration Output Example:

Starting wallet country migration...
Found 1234 wallets without country field
✓ Updated wallet 507f1f77bcf86cd799439011 for user 507f191e810c19729de860ea with country: US
✓ Updated wallet 507f1f77bcf86cd799439012 for user 507f191e810c19729de860eb with country: UK
...

=== Migration Summary ===
Total wallets processed: 1234
Successfully updated: 1234
Errors: 0

Creating compound unique index on (user, country)...
✓ Index created successfully

Migration completed!

Testing

Run Unit Tests

# Test wallet model and country-specific logic
bun run test:wallet

# Test wallet controller endpoints
bun run test:wallet-controller

# Run all tests
bun test

Test Coverage

The tests cover:

  • ✅ Wallet creation with country
  • ✅ Multiple wallets per user (different countries)
  • ✅ Country-specific queries
  • ✅ Balance isolation
  • ✅ Country change scenarios
  • ✅ Backward compatibility
  • ✅ Edge cases (empty country, case sensitivity)
  • ✅ Real-world scenarios (topups, debits, refunds)

API Changes

User Endpoints

GET /api/v1/wallet/me

Before: Returns user's single wallet After: Returns wallet for user's current country

// Response when user is in US
{
"data": {
"_id": "...",
"user": "...",
"country": "US",
"availableZishPoints": 100,
"ledger": [...]
}
}

// Response when user changes to UK (and has no UK wallet yet)
{
"data": null
}

Admin Endpoints

GET /admin/wallets/:userId

New Behavior: Returns ALL wallets for user across countries

{
"user": "507f191e810c19729de860ea",
"wallets": [
{
"_id": "...",
"user": "507f191e810c19729de860ea",
"country": "US",
"availableZishPoints": 100,
"ledger": [...]
},
{
"_id": "...",
"user": "507f191e810c19729de860ea",
"country": "UK",
"availableZishPoints": 200,
"ledger": [...]
}
]
}

GET /admin/wallets/:userId?country=US

New: Get wallet for specific country

{
"user": "507f191e810c19729de860ea",
"country": "US",
"availableZishPoints": 100,
"ledger": [...]
}

PUT /admin/wallets/:userId

Updated: Requires country in request body or uses user's current country

// Request
{
"country": "US", // Optional, defaults to user's current country
"availableZishPoints": 150
}

Use Cases

Use Case 1: Plan Purchase

User in US buys a plan → points credited to US wallet

// User purchases plan while in US
const user = await User.findById(userId)
const country = user.address?.country || 'UNKNOWN' // "US"

let wallet = await Wallet.findOne({ user: userId, country: "US" })
wallet.availableZishPoints += 100
await wallet.save()

// Result: US wallet has 100 points

Use Case 2: User Changes Country

User moves from US to UK → New wallet created for UK

// User updates country
user.address.country = "UK"
await user.save()

// New wallet created for UK
await Wallet.create({
user: userId,
country: "UK",
availableZishPoints: 0,
ledger: []
})

// Result:
// - US wallet: 100 points (preserved)
// - UK wallet: 0 points (new)

Use Case 3: Tournament Join

User in UK joins tournament → Entry fee debited from UK wallet

const user = await User.findById(userId)
const country = user.address?.country // "UK"

const wallet = await Wallet.findOne({ user: userId, country: "UK" })
if (!wallet) throw new Error("No wallet for your current country")

wallet.availableZishPoints -= entryFee
await wallet.save()

// Result: UK wallet debited, US wallet unchanged

Use Case 4: Refund

Tournament cancelled → Refund goes to user's current country wallet

const user = await User.findById(userId)
const country = user.address?.country // Current country

const wallet = await Wallet.findOne({ user: userId, country })
wallet.availableZishPoints += refundAmount
await wallet.save()

// Result: Refund to current country wallet

Deployment Checklist

Pre-deployment

  • Review migration script
  • Test migration on staging database
  • Backup production database
  • Run all tests: bun test
  • Verify no duplicate (user, country) combinations exist

Deployment Steps

  1. Deploy Code: Deploy updated application code
  2. Run Migration: Execute migration script
    bun run migrate:wallet-country
  3. Verify: Check migration summary for errors
  4. Validate: Query a sample of wallets to confirm country field exists
  5. Monitor: Watch for any wallet-related errors in logs

Post-deployment

  • Verify compound index exists: db.wallets.getIndexes()
  • Spot check wallets have correct country codes
  • Monitor error logs for wallet issues
  • Test key flows (plan purchase, tournament join, payouts)

Rollback Plan

If issues arise after deployment:

  1. Keep code deployed
  2. Fix data issues with migration script adjustments
  3. Re-run migration if needed

Option 2: Full Rollback

  1. Code Rollback: Deploy previous version
  2. Database Rollback: Restore from backup
  3. Remove Index:
    db.wallets.dropIndex({ user: 1, country: 1 })
  4. Remove Country Field (Optional):
    db.wallets.updateMany({}, { $unset: { country: "" } })

Monitoring

Key Metrics to Watch

  • Wallet queries by country
  • Failed wallet operations (country not found)
  • Users with multiple country wallets
  • UNKNOWN country wallet usage

Useful Queries

// Count wallets per country
db.wallets.aggregate([
{ $group: { _id: "$country", count: { $sum: 1 } } },
{ $sort: { count: -1 } }
])

// Find users with multiple country wallets
db.wallets.aggregate([
{ $group: { _id: "$user", countries: { $push: "$country" }, count: { $sum: 1 } } },
{ $match: { count: { $gt: 1 } } }
])

// Find UNKNOWN country wallets
db.wallets.find({ country: "UNKNOWN" }).count()

// Check for missing country field (should be 0 after migration)
db.wallets.find({ country: { $exists: false } }).count()

Troubleshooting

Issue: Duplicate Key Error

Symptom: Error creating wallet - duplicate key for (user, country) Cause: Wallet already exists for that user+country combination Solution: Query existing wallet instead of creating new one

let wallet = await Wallet.findOne({ user: userId, country })
if (!wallet) {
wallet = await Wallet.create({ user: userId, country, ... })
}

Issue: User Cannot See Balance

Symptom: User has balance but API returns null wallet Cause: User changed country, no wallet exists for new country Solution:

  1. Verify user's current country: user.address.country
  2. Check if wallet exists for that country
  3. Create new wallet if needed (balance starts at 0)

Issue: Migration Failed for Some Wallets

Symptom: Migration shows errors for specific wallets Cause: User not found or data inconsistency Solution:

  1. Check migration error logs
  2. Manually fix problematic records
  3. Re-run migration for failed records

FAQs

Q: What happens to existing balances after migration? A: All existing balances are preserved. Wallets are assigned the user's current country.

Q: Can users transfer balance between countries? A: No. This is by design. Balances are isolated per country.

Q: What if a user doesn't have a country set? A: Their wallet uses 'UNKNOWN' as the country code.

Q: Can admin panel see all wallets? A: Yes. Admin endpoints can query all wallets for a user or specific country.

Q: What happens if user changes country back to original? A: They will see their original wallet with preserved balance.

Q: Do I need to update the mobile app? A: The mobile app should work as-is. The API returns the wallet for user's current country transparently.

Support

For issues or questions:

  1. Check logs: logs/api-logs.jsonl
  2. Run diagnostic queries (see Monitoring section)
  3. Review test files for expected behavior
  4. Contact engineering team

Summary

Backward Compatible: Default country handles legacy wallets ✅ Migration Script: Automated migration for existing data
Comprehensive Tests: 30+ test cases covering all scenarios ✅ Clear Isolation: Balances are strictly per-country ✅ Admin Visibility: Admins can see all country wallets ✅ Rollback Ready: Clear rollback procedures documented

The country-specific wallet system is production-ready with full backward compatibility and comprehensive testing.