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:
255
SINGLE_SOURCE_OF_TRUTH.md
Executable file
255
SINGLE_SOURCE_OF_TRUTH.md
Executable 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`
|
||||
Reference in New Issue
Block a user