- 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
344 lines
11 KiB
Markdown
Executable File
344 lines
11 KiB
Markdown
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:**
|
|
```typescript
|
|
// 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:**
|
|
1. **Accurate** - Goals show real progress
|
|
2. **Prevents confusion** - Users know what they can spend
|
|
3. **Not too complex** - Can implement in 2-3 hours
|
|
4. **Flexible** - Can still release money if needed
|
|
|
|
**Implementation Steps:**
|
|
|
|
1. **Update Prisma Schema:**
|
|
```prisma
|
|
model Wallet {
|
|
// ... existing fields
|
|
reservedBalance Decimal @default(0) @db.Decimal(18, 2)
|
|
}
|
|
```
|
|
|
|
2. **Update Goals Service:**
|
|
```typescript
|
|
// 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 } }
|
|
});
|
|
```
|
|
|
|
3. **Update Transaction Validation:**
|
|
```typescript
|
|
// Check available balance instead of total balance
|
|
const availableBalance = wallet.totalBalance - wallet.reservedBalance;
|
|
if (transactionAmount > availableBalance) {
|
|
throw new Error('Insufficient available balance');
|
|
}
|
|
```
|
|
|
|
4. **Update Frontend:**
|
|
```tsx
|
|
// 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:**
|
|
|
|
1. **Do you want accurate goal tracking?**
|
|
- If YES → Implement Reserved Balance
|
|
- If NO → Keep current (Virtual Allocation)
|
|
|
|
2. **Is it okay if users can spend allocated money elsewhere?**
|
|
- If NO → Implement Reserved Balance
|
|
- If YES → Keep current
|
|
|
|
3. **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?** 🤔
|
|
|
|
1. Keep current implementation (simple but inaccurate)
|
|
2. Implement Reserved Balance (recommended)
|
|
3. Implement Actual Transfer (most accurate but complex)
|