- Add goals feature (models, migrations, API, web pages) - Add reserved/centralized wallet balance service - Add wallet detail page and overview components - Add new UI components (progress, multi-select, FAB) - Remove stray empty -H/-d files from working tree
8.2 KiB
Executable File
8.2 KiB
Executable File
✅ Centralized Wallet Balance Service - Implementation Complete!
Date: October 22, 2025
Status: Implemented & Ready to Test
🎯 Problem Solved:
Before:
- ❌ Balance calculated in multiple places (Goals service, AddMoneyDialog, etc.)
- ❌ Inconsistent calculations
- ❌ Didn't respect wallet kind (money vs asset)
- ❌ No proper unit/currency handling
- ❌ Duplicated code everywhere
After:
- ✅ Single source of truth for wallet balance
- ✅ Respects wallet kind (money vs asset)
- ✅ Proper currency/unit symbols
- ✅ Centralized calculation logic
- ✅ Reusable across the entire app
📊 How It Works:
Wallet Balance Service (wallet-balance.service.ts)
For Money Wallets:
{
walletId: "uuid",
kind: "money",
currency: "IDR",
totalBalance: 5000000, // initialAmount + sum(in) - sum(out)
reservedBalance: 2000000, // Reserved for goals
availableBalance: 3000000 // totalBalance - reservedBalance
}
For Asset Wallets:
{
walletId: "uuid",
kind: "asset",
unit: "gram",
totalUnits: 100, // initialAmount + sum(in) - sum(out)
pricePerUnit: 1000000, // Current price per unit
totalValue: 100000000, // totalUnits * pricePerUnit
totalBalance: 100000000, // Same as totalValue
reservedBalance: 20000000, // Reserved for goals (in value)
availableBalance: 80000000 // totalValue - reservedBalance
}
🔧 API Endpoints:
1. Get All User Wallet Balances
GET /api/wallets/balances
Authorization: Bearer {token}
Response:
[
{
"walletId": "uuid-1",
"kind": "money",
"currency": "IDR",
"totalBalance": 5000000,
"reservedBalance": 2000000,
"availableBalance": 3000000
},
{
"walletId": "uuid-2",
"kind": "asset",
"unit": "gram",
"totalUnits": 100,
"pricePerUnit": 1000000,
"totalValue": 100000000,
"totalBalance": 100000000,
"reservedBalance": 20000000,
"availableBalance": 80000000
}
]
2. Get Single Wallet Balance
GET /api/wallets/:id/balance
Authorization: Bearer {token}
Response:
{
"walletId": "uuid",
"kind": "money",
"currency": "IDR",
"totalBalance": 5000000,
"reservedBalance": 2000000,
"availableBalance": 3000000
}
🎨 Frontend Usage:
Before (Duplicated Logic):
// In AddMoneyDialog
const txResponse = await axios.get('/api/transactions', { params: { walletId } });
let balance = wallet.initialAmount || 0;
txResponse.data.forEach(tx => {
if (tx.direction === 'in') balance += tx.amount;
else balance -= tx.amount;
});
// In Overview page
const txResponse = await axios.get('/api/transactions', { params: { walletId } });
let balance = wallet.initialAmount || 0;
txResponse.data.forEach(tx => {
if (tx.direction === 'in') balance += tx.amount;
else balance -= tx.amount;
});
// In Wallets page
// ... same code again!
After (Centralized):
// Anywhere in the app
const balances = await axios.get('/api/wallets/balances');
// Use it!
balances.data.forEach(balance => {
console.log(`${balance.currency || balance.unit}: ${balance.availableBalance}`);
});
💰 Proper Currency/Unit Display:
Money Wallet:
Total Balance: Rp 5,000,000
Reserved for Goals: -Rp 2,000,000
Available to Allocate: Rp 3,000,000
Asset Wallet (Gold):
Total Units: 100 gram
Price per Unit: Rp 1,000,000/gram
Total Value: Rp 100,000,000
Reserved for Goals: -Rp 20,000,000
Available to Allocate: Rp 80,000,000
🔄 Integration Points:
1. Goals Service ✅
// Before: Manual calculation
const transactions = await this.prisma.transaction.findMany(...);
let balance = wallet.initialAmount || 0;
// ... 20 lines of calculation
// After: Use centralized service
const walletBalance = await this.walletBalanceService.calculateBalance(walletId);
if (amount > walletBalance.availableBalance) {
throw new Error('Insufficient balance');
}
2. Add Money Dialog ✅
// Before: Fetch transactions and calculate manually
// After: Use centralized API
const balances = await axios.get('/api/wallets/balances');
const wallet = balances.find(b => b.walletId === selectedWalletId);
// Shows: Total, Reserved, Available
3. Future: Overview Page (TODO)
const balances = await axios.get('/api/wallets/balances');
const totalAvailable = balances.reduce((sum, b) => sum + b.availableBalance, 0);
const totalReserved = balances.reduce((sum, b) => sum + b.reservedBalance, 0);
4. Future: Wallets Page (TODO)
// Show balance for each wallet card
const balances = await axios.get('/api/wallets/balances');
wallets.forEach(wallet => {
const balance = balances.find(b => b.walletId === wallet.id);
// Display: Total / Reserved / Available
});
📁 Files Created/Modified:
Backend:
apps/api/src/wallets/
├── wallet-balance.service.ts ✅ NEW - Centralized calculation
├── wallets-balance.controller.ts ✅ NEW - API endpoints
├── wallets.module.ts ✅ Updated - Export service
└── wallets.service.ts (Unchanged)
apps/api/src/goals/
├── goals.service.ts ✅ Updated - Use centralized service
└── goals.module.ts ✅ Updated - Import WalletsModule
Frontend:
apps/web/src/components/pages/goals/
└── AddMoneyDialog.tsx ✅ Updated - Use /api/wallets/balances
✅ Benefits:
1. Single Source of Truth
- All balance calculations in one place
- Consistent across the entire app
- Easy to maintain and update
2. Respects Wallet Types
- Money wallets: Show currency (IDR, USD, etc.)
- Asset wallets: Show units (gram, shares, etc.)
- Proper value calculation for assets
3. Performance
- Can fetch all balances in one request
- No need to fetch transactions multiple times
- Optimized queries
4. Extensibility
- Easy to add new balance types
- Can add caching later
- Can add real-time updates
🧪 Testing:
Test Money Wallet:
# Get balances
curl http://localhost:3001/api/wallets/balances \
-H "Authorization: Bearer YOUR_TOKEN"
# Expected:
{
"walletId": "...",
"kind": "money",
"currency": "IDR",
"totalBalance": 5000000,
"reservedBalance": 2000000,
"availableBalance": 3000000
}
Test Asset Wallet:
# Get balances
curl http://localhost:3001/api/wallets/balances \
-H "Authorization: Bearer YOUR_TOKEN"
# Expected:
{
"walletId": "...",
"kind": "asset",
"unit": "gram",
"totalUnits": 100,
"pricePerUnit": 1000000,
"totalValue": 100000000,
"totalBalance": 100000000,
"reservedBalance": 0,
"availableBalance": 100000000
}
🚀 Next Steps:
Phase 1: Update Existing Pages (TODO)
- Update Overview page to use
/api/wallets/balances - Update Wallets page to use
/api/wallets/balances - Update Transactions page to use
/api/wallets/balances - Remove all manual balance calculations
Phase 2: Add Caching (Future)
- Cache balance calculations
- Invalidate cache on transaction create/update/delete
- Add Redis for distributed caching
Phase 3: Real-time Updates (Future)
- WebSocket for balance updates
- Push notifications when balance changes
- Live balance updates in UI
📝 Summary:
What We Built:
- ✅ Centralized
WalletBalanceService - ✅ Respects wallet kind (money vs asset)
- ✅ Proper currency/unit handling
- ✅ Reserved balance calculation
- ✅ Available balance calculation
- ✅ API endpoints for easy access
- ✅ Integrated with Goals service
- ✅ Updated Add Money dialog
What's Different:
- No more duplicated balance calculations
- Consistent balance across the app
- Proper support for assets (not just money)
- Single API call to get all balances
What's Next:
- Update other pages to use centralized service
- Remove old manual calculations
- Add caching for performance
The wallet balance system is now centralized and respects all wallet types! 🎉