- 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
348 lines
8.2 KiB
Markdown
Executable File
348 lines
8.2 KiB
Markdown
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:**
|
|
```typescript
|
|
{
|
|
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:**
|
|
```typescript
|
|
{
|
|
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**
|
|
```http
|
|
GET /api/wallets/balances
|
|
Authorization: Bearer {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
[
|
|
{
|
|
"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**
|
|
```http
|
|
GET /api/wallets/:id/balance
|
|
Authorization: Bearer {token}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"walletId": "uuid",
|
|
"kind": "money",
|
|
"currency": "IDR",
|
|
"totalBalance": 5000000,
|
|
"reservedBalance": 2000000,
|
|
"availableBalance": 3000000
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🎨 **Frontend Usage:**
|
|
|
|
### **Before (Duplicated Logic):**
|
|
```typescript
|
|
// 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):**
|
|
```typescript
|
|
// 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** ✅
|
|
```typescript
|
|
// 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** ✅
|
|
```typescript
|
|
// 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)
|
|
```typescript
|
|
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)
|
|
```typescript
|
|
// 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:**
|
|
```bash
|
|
# 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:**
|
|
```bash
|
|
# 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!** 🎉
|