# 🎯 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`