checkpoint: goals feature, wallet balance, and goals/wallet detail UI
- 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
This commit is contained in:
347
CENTRALIZED_WALLET_BALANCE.md
Executable file
347
CENTRALIZED_WALLET_BALANCE.md
Executable file
@@ -0,0 +1,347 @@
|
||||
# ✅ 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!** 🎉
|
||||
Reference in New Issue
Block a user