- 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
11 KiB
Executable File
11 KiB
Executable File
Goals & Wallet Behavior - How Money Works
📊 Current Implementation: Virtual Allocation (No Freeze)
Your Scenario:
Initial State:
Wallet A: Rp 5,000,000
Wallet B: Rp 2,000,000
Goal: MacbookPro M4 (Target: Rp 4,000,000)
After Adding Money to Goal:
Action 1: Add Rp 2,000,000 from Wallet A to Goal
Action 2: Add Rp 1,000,000 from Wallet B to Goal
What Happens:
Current Behavior (Virtual Allocation):
┌─────────────────────────────────────────────────────────────┐
│ Wallet A │
├─────────────────────────────────────────────────────────────┤
│ Balance: Rp 5,000,000 │
│ Status: STILL AVAILABLE │
│ │
│ ❌ Money is NOT frozen │
│ ❌ Money is NOT deducted │
│ ✅ You can still spend all Rp 5,000,000 │
│ │
│ Note: Goal allocation is just a "tracking record" │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Wallet B │
├─────────────────────────────────────────────────────────────┤
│ Balance: Rp 2,000,000 │
│ Status: STILL AVAILABLE │
│ │
│ ❌ Money is NOT frozen │
│ ❌ Money is NOT deducted │
│ ✅ You can still spend all Rp 2,000,000 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Goal: MacbookPro M4 │
├─────────────────────────────────────────────────────────────┤
│ Target: Rp 4,000,000 │
│ Current: Rp 3,000,000 (75%) │
│ │
│ Allocations: │
│ - From Wallet A: Rp 2,000,000 │
│ - From Wallet B: Rp 1,000,000 │
│ │
│ ⚠️ This is just a TRACKING record │
│ ⚠️ Money still exists in wallets │
└─────────────────────────────────────────────────────────────┘
⚠️ Problem with Current Implementation:
Issue: Double Counting
You can spend the same money twice:
Scenario:
1. Add Rp 2,000,000 from Wallet A to Goal ✅
2. Goal shows Rp 2,000,000 saved ✅
3. Spend Rp 2,000,000 from Wallet A for something else ✅
4. Wallet A now has Rp 3,000,000
5. But Goal still shows Rp 2,000,000 saved! ❌
Result: Goal tracking becomes inaccurate!
💡 Recommended Solutions:
Option 1: Virtual Allocation (Current - Simple but Inaccurate)
How it works:
- Goal allocation is just a record
- Money stays in wallet
- No restrictions on spending
Pros:
- ✅ Simple to implement (already done)
- ✅ Flexible - can spend money anytime
- ✅ No complex logic needed
Cons:
- ❌ Can spend allocated money elsewhere
- ❌ Goal progress becomes inaccurate
- ❌ User might think they saved money but actually spent it
Best for:
- Simple goal tracking
- Users who just want to see progress
- Not critical if goals are just aspirational
Option 2: Reserved Balance (Recommended)
How it works:
- When you allocate money to a goal, it's "reserved"
- Wallet shows: Total Balance vs Available Balance
- Reserved money can't be spent on transactions
- Can only be used for the goal or released back
Example:
Wallet A:
├── Total Balance: Rp 5,000,000
├── Reserved for Goals: Rp 2,000,000
└── Available Balance: Rp 3,000,000 ← Only this can be spent
When creating transaction:
- System checks Available Balance (not Total Balance)
- If you try to spend Rp 4,000,000 → Error: Insufficient available balance
Implementation:
// Add to Wallet model
interface Wallet {
totalBalance: number;
reservedBalance: number; // ← New field
availableBalance: number; // = totalBalance - reservedBalance
}
// When adding money to goal
1. Check wallet.availableBalance >= amount
2. Create goal allocation
3. Update wallet.reservedBalance += amount
4. Update goal.currentAmount += amount
// When removing allocation
1. Delete allocation
2. Update wallet.reservedBalance -= amount
3. Update goal.currentAmount -= amount
// When creating transaction
1. Check wallet.availableBalance >= amount (not totalBalance)
2. If sufficient, allow transaction
Pros:
- ✅ Accurate goal tracking
- ✅ Prevents double-spending
- ✅ Clear separation of allocated vs available money
- ✅ User sees exactly how much they can spend
Cons:
- ⚠️ More complex implementation
- ⚠️ Need to update wallet model
- ⚠️ Need to update transaction validation
Option 3: Actual Transfer (Most Strict)
How it works:
- Create a special "Goal Wallet" for each goal
- When allocating money, create a transaction that transfers money
- Money is physically moved from Wallet A → Goal Wallet
- Goal Wallet balance = Goal progress
Example:
Before:
Wallet A: Rp 5,000,000
Goal Wallet (MacbookPro): Rp 0
Add Rp 2,000,000 to goal:
1. Create transaction: Wallet A → Goal Wallet (Rp 2,000,000)
2. Wallet A: Rp 3,000,000
3. Goal Wallet: Rp 2,000,000
Result:
- Money is actually moved
- Wallet A balance decreases
- Goal Wallet balance increases
Pros:
- ✅ Most accurate
- ✅ Real money movement
- ✅ Easy to understand
- ✅ Can see goal money as a separate wallet
Cons:
- ⚠️ Creates many wallets
- ⚠️ More complex to manage
- ⚠️ Harder to "un-allocate" money
📊 Comparison:
| Feature | Virtual (Current) | Reserved Balance | Actual Transfer |
|---|---|---|---|
| Accuracy | ❌ Low | ✅ High | ✅ Very High |
| Complexity | ✅ Simple | ⚠️ Medium | ❌ Complex |
| Flexibility | ✅ High | ⚠️ Medium | ❌ Low |
| User Understanding | ⚠️ Confusing | ✅ Clear | ✅ Very Clear |
| Double Spending | ❌ Possible | ✅ Prevented | ✅ Prevented |
| Implementation Time | ✅ Done | ⚠️ 2-3 hours | ❌ 4-6 hours |
🎯 My Recommendation:
Use Option 2: Reserved Balance
Why:
- Accurate - Goals show real progress
- Prevents confusion - Users know what they can spend
- Not too complex - Can implement in 2-3 hours
- Flexible - Can still release money if needed
Implementation Steps:
- Update Prisma Schema:
model Wallet {
// ... existing fields
reservedBalance Decimal @default(0) @db.Decimal(18, 2)
}
- Update Goals Service:
// When adding allocation
const wallet = await prisma.wallet.findUnique({ where: { id: walletId } });
const availableBalance = wallet.totalBalance - wallet.reservedBalance;
if (amount > availableBalance) {
throw new Error('Insufficient available balance');
}
// Update reserved balance
await prisma.wallet.update({
where: { id: walletId },
data: { reservedBalance: { increment: amount } }
});
- Update Transaction Validation:
// Check available balance instead of total balance
const availableBalance = wallet.totalBalance - wallet.reservedBalance;
if (transactionAmount > availableBalance) {
throw new Error('Insufficient available balance');
}
- Update Frontend:
// Show both balances
<div>
<p>Total: {formatCurrency(wallet.totalBalance)}</p>
<p>Reserved: {formatCurrency(wallet.reservedBalance)}</p>
<p>Available: {formatCurrency(wallet.availableBalance)}</p>
</div>
🤔 What Should We Do?
Questions for you:
-
Do you want accurate goal tracking?
- If YES → Implement Reserved Balance
- If NO → Keep current (Virtual Allocation)
-
Is it okay if users can spend allocated money elsewhere?
- If NO → Implement Reserved Balance
- If YES → Keep current
-
Do you want to prevent double-spending?
- If YES → Implement Reserved Balance
- If NO → Keep current
My suggestion: Implement Reserved Balance because:
- Goals become meaningful (not just aspirational)
- Users won't be confused about their actual available money
- Prevents the "I thought I saved money but I spent it" problem
- Still flexible (can release reserved money if needed)
Current State Summary:
Right now with your example:
Wallet A: Rp 5,000,000 (can spend all of it)
Wallet B: Rp 2,000,000 (can spend all of it)
Goal: Shows Rp 3,000,000 saved
⚠️ But if you spend from wallets, goal progress doesn't decrease!
⚠️ Goal is just a "wish list" not actual savings
With Reserved Balance:
Wallet A:
Total: Rp 5,000,000
Reserved: Rp 2,000,000
Available: Rp 3,000,000 ← Can only spend this
Wallet B:
Total: Rp 2,000,000
Reserved: Rp 1,000,000
Available: Rp 1,000,000 ← Can only spend this
Goal: Rp 3,000,000 saved (accurate!)
✅ Goal progress is protected
✅ Can't accidentally spend goal money
What would you like to do? 🤔
- Keep current implementation (simple but inaccurate)
- Implement Reserved Balance (recommended)
- Implement Actual Transfer (most accurate but complex)