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:
- Find all wallets without a country field
- Set country based on user's current
address.country - Default to 'UNKNOWN' if user has no country
- Create the compound unique index
Migration Script
The migration script is located at: src/scripts/migrate-wallets-country.ts
What it does:
- Queries all wallets with missing or empty country field
- Looks up each user's current country from
user.address.country - Updates wallet with country (uppercase)
- Creates compound unique index
\{ user: 1, country: 1 \} - 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
- Deploy Code: Deploy updated application code
- Run Migration: Execute migration script
bun run migrate:wallet-country - Verify: Check migration summary for errors
- Validate: Query a sample of wallets to confirm country field exists
- 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:
Option 1: Quick Fix (Recommended)
- Keep code deployed
- Fix data issues with migration script adjustments
- Re-run migration if needed
Option 2: Full Rollback
- Code Rollback: Deploy previous version
- Database Rollback: Restore from backup
- Remove Index:
db.wallets.dropIndex({ user: 1, country: 1 }) - 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:
- Verify user's current country:
user.address.country - Check if wallet exists for that country
- 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:
- Check migration error logs
- Manually fix problematic records
- 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:
- Check logs:
logs/api-logs.jsonl - Run diagnostic queries (see Monitoring section)
- Review test files for expected behavior
- 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.