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:
Dwindi Ramadhana
2026-06-17 20:40:00 +07:00
parent 35e93b826a
commit 6a6e74562c
401 changed files with 9517 additions and 397 deletions

255
SINGLE_SOURCE_OF_TRUTH.md Executable file
View File

@@ -0,0 +1,255 @@
# 🎯 Single Source of Truth - Tabungin Project
## 📋 Core Rules
### 1. **Currency & Exchange Rates**
#### A. Exchange Rate Format
- **Source**: External API (fetched via `fetchExchangeRates()`)
- **Format**: `{ USD: 0.0000602159, EUR: 0.000051876, ... }`
- **Meaning**: `1 IDR = X foreign currency`
- **Example**: `USD: 0.0000602159` means `1 IDR = 0.0000602159 USD`
#### B. Currency Symbols
- **Source**: `/apps/web/src/constants/currencies.ts`
- **Function**: `formatCurrency(amount, currencyCode)`
- **Features**:
- Automatic thousand separators (`useGrouping: true`)
- IDR: No decimals, id-ID locale (e.g., `Rp 100.000`)
- Other currencies: 2 decimals, en-US locale (e.g., `$ 17.90`)
- Units: 0-2 decimals, id-ID locale (e.g., `80 gram`)
### 2. **Price Format & Conversions**
#### A. Single Source Utility
**File**: `/apps/web/src/utils/walletCalculations.ts`
**Functions**:
1. `convertIDRToWalletCurrency()` - Convert IDR to wallet's native currency/units
2. `convertWalletCurrencyToIDR()` - Convert wallet's native currency/units to IDR
3. `formatWalletBalance()` - Format balance with equivalent
4. `formatAllocationAmount()` - Format allocation (IDR) with wallet equivalent
#### B. Conversion Rules
**For Money Wallets (non-IDR)**:
```typescript
// IDR → Foreign Currency
foreignAmount = idrAmount * exchangeRate
// Example: 100,000 IDR * 0.0000602159 = 6.02 USD
// Foreign Currency → IDR
idrAmount = foreignAmount / exchangeRate
// Example: 17.90 USD / 0.0000602159 = 297,264 IDR
```
**For Asset Wallets**:
```typescript
// IDR → Units
units = idrAmount / pricePerUnit
// Example: 100,000 IDR / 2,073,000 = 0.048 gram
// Units → IDR
idrAmount = units * pricePerUnit
// Example: 80 gram * 2,073,000 = 165,840,000 IDR
```
### 3. **Data Storage Rules**
#### A. Goals & Allocations
- **Storage**: Always in **IDR**
- **Fields**:
- `Goal.targetAmount`: IDR
- `Goal.currentAmount`: IDR
- `GoalAllocation.amount`: IDR
- `GoalAllocation.currency`: Always 'IDR'
- `GoalAllocation.amountInGoalCurrency`: IDR (same as amount)
#### B. Wallet Balances
- **Storage**: In **wallet's native currency/units**
- **Fields**:
- `Wallet.initialAmount`: Native currency/units
- `WalletBalance.totalBalance`: Native currency/units
- `WalletBalance.reservedBalance`: Native currency/units
- `WalletBalance.availableBalance`: Native currency/units
- For assets: `WalletBalance.totalUnits`: Units
#### C. Transactions
- **Storage**: In **wallet's native currency/units**
- **Fields**:
- `Transaction.amount`: Native currency/units
- `Transaction.type`: 'in' | 'out'
### 4. **Display Consistency**
#### A. Wallet Cards
**Primary**: Native currency/units
**Secondary**: IDR equivalent (if not IDR)
```
$ 17.90 ≈ Rp 297.264
80 gram ≈ Rp 165.840.000
```
#### B. Wallet Card Stacked Bar (UX Priority)
**Visual Order**: Available FIRST (green), then allocations (colored)
- Green bar = Available money (what user can spend)
- Colored segments = Allocated to goals (locked)
**Example**: 67% available → 67% green bar, 33% blue segments
#### C. Allocation Popover (UX Priority)
**Display Order**:
1. **Available** (most important - what user can spend)
2. Separator line
3. **Allocations** (secondary - locked for goals)
**Available Format**:
- Primary: Native currency/units
- Secondary: IDR equivalent
**Allocation Format**:
- Primary: IDR amount
- Secondary: Native currency/units equivalent
```
Available (67.0%)
$ 11.90
≈ Rp 197.264
Allocations:
---
MacbookPro (33.0%)
Rp 100.000
≈ $ 6.02
```
#### D. Wallet Table View
**Same as card view**: Native currency/units first, IDR secondary
**Setoran Column** (Total Balance):
```
$ 17.90
≈ Rp 297.264
```
**Alokasi Column** (Reserved Balance):
```
10,13 gram
≈ Rp 10
```
#### E. Goal Progress
**Always**: IDR
```
Rp 21.000.000 / Rp 25.000.000
```
### 5. **Percentage Calculations**
#### A. Wallet Card Stacked Bar
```typescript
// Convert allocation (IDR) to wallet currency
amountInWalletCurrency = convertIDRToWalletCurrency(
allocation.amount,
wallet,
exchangeRates,
pricePerUnit
);
// Calculate percentage
percentage = (amountInWalletCurrency / balance.totalBalance) * 100;
// Clamp to prevent overflow
width = Math.min(percentage, 100);
```
#### B. Reserved Percentage
```typescript
// Already in wallet's native currency
reservedPercentage = (balance.reservedBalance / balance.totalBalance) * 100;
```
### 6. **Backend Validation**
#### A. Add Allocation Flow
1. Receive amount in **IDR** from frontend
2. Convert to wallet's native currency for validation:
```typescript
amountInWalletCurrency = convertIDRToWalletCurrency(idrAmount, ...);
if (amountInWalletCurrency > wallet.availableBalance) throw Error;
```
3. Store allocation in **IDR**
4. Update wallet reserved balance in **native currency**
#### B. Spend Transaction Flow
1. Create transaction in wallet's native currency
2. If allocated, reduce goal progress (IDR)
3. Update wallet reserved balance (native currency)
---
## 🔧 Implementation Checklist
### ✅ Completed
- [x] Created `/apps/web/src/utils/walletCalculations.ts`
- [x] Updated `WalletCard.tsx` to use utility functions
- [x] Fixed percentage calculations to always sum to 100%
- [x] Fixed stacked bar to fill edge-to-edge
- [x] Fixed `formatCurrency()` to use `Intl.NumberFormat` for reliable thousand separators
- [x] Fixed backend to return asset balances in units, not IDR value
- [x] Implemented wallet-centric UX (available first, green bar)
- [x] Fixed table view calculations to match card view
- [x] Consistent allocation display with thousand separators
### 🚧 To Verify
- [ ] Test non-IDR money wallet allocations
- [ ] Test asset wallet allocations
- [ ] Test IDR wallet allocations
- [ ] Verify thousand separators in all displays
- [ ] Verify stacked bar percentages add up correctly
### 📝 Future Enhancements
- [ ] Add spend transaction flow with goal progress reduction
- [ ] Add allocation history with proper currency display
- [ ] Add multi-currency goal support (if needed)
---
## 🎨 Style Guide
### Display Format Examples
**Money (IDR)**:
```
Rp 4.490.000
```
**Money (non-IDR)**:
```
$ 17.90 ≈ Rp 297.264
```
**Asset**:
```
80 gram ≈ Rp 165.840.000
```
**Allocation (in popover)**:
```
MacbookPro (33.6%)
Rp 100.000
≈ $ 6.02
```
---
## 🐛 Common Pitfalls
1. **❌ Don't** use `balance.pricePerUnit` for money wallets - use exchange rates
2. **❌ Don't** mix IDR amounts with native currency amounts in calculations
3. **❌ Don't** forget to clamp percentages to 0-100 range
4. **❌ Don't** store allocations in wallet's native currency
5. **✅ Do** always convert IDR to native currency before comparing with wallet balance
6. **✅ Do** always use `formatCurrency()` for display
7. **✅ Do** always use utility functions from `walletCalculations.ts`