Files
tabungin/CENTRALIZED_WALLET_BALANCE.md
Dwindi Ramadhana 6a6e74562c 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
2026-06-17 20:40:00 +07:00

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! 🎉