diff --git a/.trae/documents/2026-05-19-multi-currency-mobile-wallet-aggregation-plan.md b/.trae/documents/2026-05-19-multi-currency-mobile-wallet-aggregation-plan.md index 641e618..7ded784 100644 --- a/.trae/documents/2026-05-19-multi-currency-mobile-wallet-aggregation-plan.md +++ b/.trae/documents/2026-05-19-multi-currency-mobile-wallet-aggregation-plan.md @@ -3,35 +3,48 @@ ## Executive Summary This plan covers two interconnected initiatives: + 1. **Multi-Currency Support** — System-wide currency architecture with ISO 4217 compliance, real-time exchange rates, and M-Pesa KES-lock 2. **Mobile Wallet Aggregation** — Refactor the monolithic M-Pesa integration into a generalized provider framework supporting M-Pesa, Airtel Money, Sasapay, and future providers Both initiatives share foundational changes (monetary schema, payment provider abstraction) and are planned sequentially across three phases. ---- +*** ## Current Architecture Assessment ### Monetary Value Handling -- All `numeric` columns use fixed `(15, 2)` — KES-centric with 2 decimal places -- Only `accounts.currency` column tracks currency (default `'KES'`, unused for conversion) -- Zero exchange rate infrastructure -- Frontend has hardcoded `formatKES()` — no currency-aware formatting -- Backend uses `decimal.js` at 15-digit precision, `ROUND_HALF_UP` + +* All `numeric` columns use fixed `(15, 2)` — KES-centric with 2 decimal places + +* Only `accounts.currency` column tracks currency (default `'KES'`, unused for conversion) + +* Zero exchange rate infrastructure + +* Frontend has hardcoded `formatKES()` — no currency-aware formatting + +* Backend uses `decimal.js` at 15-digit precision, `ROUND_HALF_UP` ### M-Pesa Integration -- **SMS-based** (not Daraja API) — parser handles 7 SMS patterns -- Tightly coupled: `mpesaTransactions` table, `mpesa-parser.ts` (duplicated server/client), `mpesa-router.ts` -- Hardcoded enums: `typeEnum = ["cash", "mpesa", "bank_account"]`, `paymentMethodEnum = ["cash", "mpesa", "bank_transfer"]` -- No Daraja webhook integration; general webhook system exists in `integrations-router.ts` (unwired) -- SMS parser duplicated across `api/mpesa-parser.ts` and `src/lib/mpesa-parser.ts` -- M-PESA is referenced in 25+ files across the codebase ---- +* **SMS-based** (not Daraja API) — parser handles 7 SMS patterns + +* Tightly coupled: `mpesaTransactions` table, `mpesa-parser.ts` (duplicated server/client), `mpesa-router.ts` + +* Hardcoded enums: `typeEnum = ["cash", "mpesa", "bank_account"]`, `paymentMethodEnum = ["cash", "mpesa", "bank_transfer"]` + +* No Daraja webhook integration; general webhook system exists in `integrations-router.ts` (unwired) + +* SMS parser duplicated across `api/mpesa-parser.ts` and `src/lib/mpesa-parser.ts` + +* M-PESA is referenced in 25+ files across the codebase + +*** ## Phase 1: Foundation (Core Data Models & Abstraction Layer) ### Goal + Establish the shared schema, abstract interfaces, and utility infrastructure that both initiatives depend on, with 90%+ unit test coverage. ### 1.1 Multi-Currency: Database Schema Changes @@ -39,6 +52,7 @@ Establish the shared schema, abstract interfaces, and utility infrastructure tha #### 1.1.1 New Tables **`supported_currencies`** + ```typescript // db/schema.ts export const supportedCurrencies = pgTable("supported_currencies", { @@ -53,6 +67,7 @@ export const supportedCurrencies = pgTable("supported_currencies", { ``` **`exchange_rates`** + ```typescript export const exchangeRates = pgTable("exchange_rates", { id: serial("id").primaryKey(), @@ -68,6 +83,7 @@ export const exchangeRates = pgTable("exchange_rates", { ``` **`business_currencies`** (per-business currency configuration) + ```typescript export const businessCurrencies = pgTable("business_currencies", { id: serial("id").primaryKey(), @@ -83,42 +99,44 @@ export const businessCurrencies = pgTable("business_currencies", { Add `currency` column to all monetary tables. For KES-only existing data, default to `'KES'`: -| Table | Columns to Add | New Columns | -|---|---|---| -| `accounts` | *(already has `currency`)* | — | -| `expenses` | `currency` | `varchar(3) default 'KES'` | -| `expense_items` | `currency` | `varchar(3) default 'KES'` | -| `bills` | `currency` | `varchar(3) default 'KES'` | -| `bill_items` | `currency` | `varchar(3) default 'KES'` | -| `bill_payments` | `currency` | `varchar(3) default 'KES'` | -| `daily_sales` | `currency` | `varchar(3) default 'KES'` | -| `daily_sale_payments` | `currency` | `varchar(3) default 'KES'` | -| `journal_lines` | `currency` | `varchar(3) default 'KES'` | -| `ledger_entries` | `currency` | `varchar(3) default 'KES'` | -| `payroll_entries` | `currency` | `varchar(3) default 'KES'` | -| `payroll_advances` | `currency` | `varchar(3) default 'KES'` | -| `suppliers` | `currency` | `varchar(3) default 'KES'` | -| `budgets` | `currency` | `varchar(3) default 'KES'` | -| `purchase_orders` | `currency` | `varchar(3) default 'KES'` | -| `purchase_order_items` | `currency` | `varchar(3) default 'KES'` | -| `items` | `currency` | `varchar(3) default 'KES'` | -| `fixed_asset_depreciation` | `currency` | `varchar(3) default 'KES'` | -| `partner_commissions` | `currency` | `varchar(3) default 'KES'` | +| Table | Columns to Add | New Columns | +| -------------------------- | ---------------------------- | -------------------------- | +| `accounts` | *(already has* *`currency`)* | — | +| `expenses` | `currency` | `varchar(3) default 'KES'` | +| `expense_items` | `currency` | `varchar(3) default 'KES'` | +| `bills` | `currency` | `varchar(3) default 'KES'` | +| `bill_items` | `currency` | `varchar(3) default 'KES'` | +| `bill_payments` | `currency` | `varchar(3) default 'KES'` | +| `daily_sales` | `currency` | `varchar(3) default 'KES'` | +| `daily_sale_payments` | `currency` | `varchar(3) default 'KES'` | +| `journal_lines` | `currency` | `varchar(3) default 'KES'` | +| `ledger_entries` | `currency` | `varchar(3) default 'KES'` | +| `payroll_entries` | `currency` | `varchar(3) default 'KES'` | +| `payroll_advances` | `currency` | `varchar(3) default 'KES'` | +| `suppliers` | `currency` | `varchar(3) default 'KES'` | +| `budgets` | `currency` | `varchar(3) default 'KES'` | +| `purchase_orders` | `currency` | `varchar(3) default 'KES'` | +| `purchase_order_items` | `currency` | `varchar(3) default 'KES'` | +| `items` | `currency` | `varchar(3) default 'KES'` | +| `fixed_asset_depreciation` | `currency` | `varchar(3) default 'KES'` | +| `partner_commissions` | `currency` | `varchar(3) default 'KES'` | Add `baseCurrency` and `baseAmount` columns to core reporting tables (for cross-currency aggregation): -| Table | New Columns | -|---|---| -| `daily_sales` | `baseCurrency varchar(3), baseAmount numeric(15,2)` | +| Table | New Columns | +| --------------- | --------------------------------------------------- | +| `daily_sales` | `baseCurrency varchar(3), baseAmount numeric(15,2)` | | `journal_lines` | `baseCurrency varchar(3), baseAmount numeric(15,2)` | -| `expenses` | `baseCurrency varchar(3), baseAmount numeric(15,2)` | -| `bills` | `baseCurrency varchar(3), baseAmount numeric(15,2)` | +| `expenses` | `baseCurrency varchar(3), baseAmount numeric(15,2)` | +| `bills` | `baseCurrency varchar(3), baseAmount numeric(15,2)` | #### 1.1.3 Frontend Types Update -- Update `src/lib/utils.ts`: Replace `formatKES()` with `formatCurrency(amount, currency, options?)` — currency-aware formatter using `Intl.NumberFormat` with the currency code -- Add `SUPPORTED_CURRENCIES` constant in `src/const.ts` (derived from API on mount, with static fallback) -- Add currency selector UI component (`CurrencySelect.tsx`) +* Update `src/lib/utils.ts`: Replace `formatKES()` with `formatCurrency(amount, currency, options?)` — currency-aware formatter using `Intl.NumberFormat` with the currency code + +* Add `SUPPORTED_CURRENCIES` constant in `src/const.ts` (derived from API on mount, with static fallback) + +* Add currency selector UI component (`CurrencySelect.tsx`) ### 1.2 Multi-Currency: Core Services @@ -160,22 +178,33 @@ class CurrencyConverter { ``` **Key behaviors:** -- In-memory cache with configurable TTL (default 300s) -- Cache-busting on admin rate update -- Stale-data safeguard: rates > 24h old trigger warning log -- Fallback chain: cache → DB → external provider -- For KES→KES, return 1.0 instantly (no DB hit) -- All conversions return `Decimal` (not `number`) per codebase convention + +* In-memory cache with configurable TTL (default 300s) + +* Cache-busting on admin rate update + +* Stale-data safeguard: rates > 24h old trigger warning log + +* Fallback chain: cache → DB → external provider + +* For KES→KES, return 1.0 instantly (no DB hit) + +* All conversions return `Decimal` (not `number`) per codebase convention #### 1.2.2 Exchange Rate Sync Service **File:** `api/lib/exchange-rate-sync.ts` -- Background job (runs hourly via `boot.ts` timer or cron endpoint) -- Fetches from configured provider (ExchangeRate-API, Open Exchange Rates) -- Upserts into `exchange_rates` table with `source`, `validFrom`/`validUntil` -- Logs sync failures to audit log -- Configurable via environment variables: +* Background job (runs hourly via `boot.ts` timer or cron endpoint) + +* Fetches from configured provider (ExchangeRate-API, Open Exchange Rates) + +* Upserts into `exchange_rates` table with `source`, `validFrom`/`validUntil` + +* Logs sync failures to audit log + +* Configurable via environment variables: + ``` EXCHANGE_RATE_PROVIDER=exchange_rate_api|open_exchange_rates|manual EXCHANGE_RATE_API_KEY=your_key @@ -188,6 +217,7 @@ class CurrencyConverter { #### 1.3.1 New Tables **`mobile_wallet_providers`** (registry) + ```typescript export const mobileWalletProviders = pgTable("mobile_wallet_providers", { code: varchar("code", { length: 20 }).primaryKey(), // 'mpesa', 'airtel_money', 'sasapay' @@ -204,6 +234,7 @@ export const mobileWalletProviders = pgTable("mobile_wallet_providers", { ``` **`mobile_wallet_transactions`** (replaces `mpesaTransactions`) + ```typescript export const mobileWalletTransactions = pgTable("mobile_wallet_transactions", { id: serial("id").primaryKey(), @@ -244,6 +275,7 @@ export const mobileWalletTransactions = pgTable("mobile_wallet_transactions", { ``` **`mobile_wallet_daily_ledger`** (replaces `dailyMpesaLedger`) + ```typescript export const mobileWalletDailyLedger = pgTable("mobile_wallet_daily_ledger", { id: serial("id").primaryKey(), @@ -268,6 +300,7 @@ export const mobileWalletDailyLedger = pgTable("mobile_wallet_daily_ledger", { ``` **`mobile_wallet_reconciliation`** (replaces `mpesaReconciliation`) + ```typescript export const mobileWalletReconciliation = pgTable("mobile_wallet_reconciliation", { id: serial("id").primaryKey(), @@ -284,6 +317,7 @@ export const mobileWalletReconciliation = pgTable("mobile_wallet_reconciliation" ``` **`provider_configs`** (per-location provider configuration) + ```typescript export const providerConfigs = pgTable("provider_configs", { id: serial("id").primaryKey(), @@ -301,18 +335,21 @@ export const providerConfigs = pgTable("provider_configs", { #### 1.3.2 Enum Changes **`typeEnum`**: Add new account types for each mobile wallet provider + ```typescript // Before: ["cash", "mpesa", "bank_account"] // After: ["cash", "mpesa", "airtel_money", "sasapay", "bank_account"] ``` **`paymentMethodEnum`**: Add new payment methods + ```typescript // Before: ["cash", "mpesa", "bank_transfer"] // After: ["cash", "mpesa", "airtel_money", "sasapay", "bank_transfer"] ``` **`paymentMethod2Enum`**: + ```typescript // Before: ["cash", "mpesa", "bank_transfer", "card"] // After: ["cash", "mpesa", "airtel_money", "sasapay", "bank_transfer", "card"] @@ -321,6 +358,7 @@ export const providerConfigs = pgTable("provider_configs", { #### 1.3.3 Data Migration Strategy Create migration script `scripts/migrate-mpesa-to-provider-framework.ts`: + 1. Seed `mobile_wallet_providers` with `{ code: 'mpesa', name: 'M-PESA', brandColor: '#C73E1D', ... }` 2. Seed `supported_currencies` with 10-15 common African currencies 3. Migrate `mpesaTransactions` → `mobile_wallet_transactions` (provider = 'mpesa', currency = 'KES') @@ -514,6 +552,7 @@ export class MpesaProvider extends BaseWalletProvider { ``` **Key behavior — KES-only currency lock:** + ```typescript // In the provider's request validation validateCurrencyConstraint(provider: string, currency: string): boolean { @@ -525,10 +564,14 @@ validateCurrencyConstraint(provider: string, currency: string): boolean { ``` The M-PESA SMS parser logic is migrated from `api/mpesa-parser.ts` into this class, with: -- All 7 SMS pattern recognizers preserved -- Currency field defaults to 'KES' -- `ParsedWalletSms` output type replaces old `ParsedMpesaSms` -- Server-side and client-side parsers now unified via shared import + +* All 7 SMS pattern recognizers preserved + +* Currency field defaults to 'KES' + +* `ParsedWalletSms` output type replaces old `ParsedMpesaSms` + +* Server-side and client-side parsers now unified via shared import #### 1.4.4 Provider Templates (Boilerplate) @@ -664,6 +707,7 @@ export async function handleWalletWebhook(payload: WalletWebhookPayload): Promis ``` Mounted in `integrations-router.ts` as a new endpoint: + ``` POST /api/integrations/webhooks/wallet/:provider ``` @@ -756,22 +800,23 @@ export type WalletProviderCode = typeof WALLET_PROVIDERS[number]['code']; ### 1.9 Unit Tests (Phase 1) -| Module | Test File | Coverage Target | -|---|---|---| -| `currency-converter.ts` | `api/lib/__tests__/currency-converter.test.ts` | 95% | -| `exchange-rate-sync.ts` | `api/lib/__tests__/exchange-rate-sync.test.ts` | 90% | -| `provider-interface.ts` | `api/lib/__tests__/provider-interface.test.ts` | 95% | -| `provider-registry.ts` | `api/lib/__tests__/provider-registry.test.ts` | 95% | -| `transaction-logger.ts` | `api/lib/__tests__/transaction-logger.test.ts` | 90% | -| `webhook-handler.ts` | `api/lib/__tests__/webhook-handler.test.ts` | 90% | -| `mpesa-provider.ts` | `api/lib/__tests__/mpesa-provider.test.ts` | 95% | -| `currency.ts` (frontend) | `src/lib/__tests__/currency.test.ts` | 95% | +| Module | Test File | Coverage Target | +| ------------------------ | ---------------------------------------------- | --------------- | +| `currency-converter.ts` | `api/lib/__tests__/currency-converter.test.ts` | 95% | +| `exchange-rate-sync.ts` | `api/lib/__tests__/exchange-rate-sync.test.ts` | 90% | +| `provider-interface.ts` | `api/lib/__tests__/provider-interface.test.ts` | 95% | +| `provider-registry.ts` | `api/lib/__tests__/provider-registry.test.ts` | 95% | +| `transaction-logger.ts` | `api/lib/__tests__/transaction-logger.test.ts` | 90% | +| `webhook-handler.ts` | `api/lib/__tests__/webhook-handler.test.ts` | 90% | +| `mpesa-provider.ts` | `api/lib/__tests__/mpesa-provider.test.ts` | 95% | +| `currency.ts` (frontend) | `src/lib/__tests__/currency.test.ts` | 95% | ---- +*** ## Phase 2: Migration (M-Pesa Refactoring & M-Pesa KES Lock) ### Goal + Migrate all existing M-Pesa functionality to the new provider framework, enforce KES-only locking, and achieve full backward compatibility with zero regression. ### 2.1 Backend Router Migration @@ -780,21 +825,21 @@ Migrate all existing M-Pesa functionality to the new provider framework, enforce **File:** `api/wallet-router.ts` (replaces `api/mpesa-router.ts`) -| Procedure | Method | Purpose | Provider-Scoped? | -|---|---|---|---| -| `transactions.list` | query | List wallet transactions with provider/location/date/status filters | Yes | -| `transactions.stats` | query | Aggregated stats by provider and currency | Yes | -| `transactions.importSms` | mutation | Parse and import SMS for SMS-capable providers | Yes (validates provider) | -| `transactions.tagToSupplier` | mutation | Link transaction to supplier | Provider-agnostic | -| `transactions.createExpenseFromTxn` | mutation | Create expense from wallet transaction | Provider-agnostic | -| `transactions.linkToAccount` | mutation | Link topup to bank account + wallet | Provider-agnostic | -| `providers.list` | query | List all active providers for a location | — | -| `providers.getConfig` | query | Get provider config for a location | Yes | -| `providers.setDefault` | mutation | Set default wallet provider for location | Yes | -| `dailyLedger.list` | query | List daily wallet ledger entries | Yes (by provider) | -| `dailyLedger.create` | mutation | Upsert daily wallet ledger | Yes (by provider) | -| `reconciliation.list` | query | List reconciliation records | Yes (by provider) | -| `reconciliation.create` | mutation | Create reconciliation record | Yes (by provider) | +| Procedure | Method | Purpose | Provider-Scoped? | +| ----------------------------------- | -------- | ------------------------------------------------------------------- | ------------------------ | +| `transactions.list` | query | List wallet transactions with provider/location/date/status filters | Yes | +| `transactions.stats` | query | Aggregated stats by provider and currency | Yes | +| `transactions.importSms` | mutation | Parse and import SMS for SMS-capable providers | Yes (validates provider) | +| `transactions.tagToSupplier` | mutation | Link transaction to supplier | Provider-agnostic | +| `transactions.createExpenseFromTxn` | mutation | Create expense from wallet transaction | Provider-agnostic | +| `transactions.linkToAccount` | mutation | Link topup to bank account + wallet | Provider-agnostic | +| `providers.list` | query | List all active providers for a location | — | +| `providers.getConfig` | query | Get provider config for a location | Yes | +| `providers.setDefault` | mutation | Set default wallet provider for location | Yes | +| `dailyLedger.list` | query | List daily wallet ledger entries | Yes (by provider) | +| `dailyLedger.create` | mutation | Upsert daily wallet ledger | Yes (by provider) | +| `reconciliation.list` | query | List reconciliation records | Yes (by provider) | +| `reconciliation.create` | mutation | Create reconciliation record | Yes (by provider) | #### 2.1.2 M-PESA Router Deprecation @@ -815,17 +860,22 @@ This ensures backward compatibility for any existing tRPC references while all n #### 2.1.3 Daily Ledger Router Migration **File:** `api/daily-ledger-router.ts` → Refactor to delegate to `wallet-router.dailyLedger`: -- Old endpoint continues to work (proxy to wallet router with `provider = 'mpesa'`) -- New unified `dailyLedger.*` procedures available + +* Old endpoint continues to work (proxy to wallet router with `provider = 'mpesa'`) + +* New unified `dailyLedger.*` procedures available #### 2.1.4 Dashboard Router Updates **File:** `api/dashboard-router.ts` Update M-PESA references in: -- `overview` procedure: Aggregate across all active wallet providers (not just M-PESA) -- `todayPayments` procedure: Include all wallet outflows (not just M-PESA) -- `mpesaFeeAnalysis` → `walletFeeAnalysis`: Multi-provider fee breakdown + +* `overview` procedure: Aggregate across all active wallet providers (not just M-PESA) + +* `todayPayments` procedure: Include all wallet outflows (not just M-PESA) + +* `mpesaFeeAnalysis` → `walletFeeAnalysis`: Multi-provider fee breakdown ### 2.2 Frontend Migration @@ -835,35 +885,45 @@ Update M-PESA references in: The `/mpesa` route continues to work as M-PESA-specific view. A new `/wallet` route provides the unified multi-provider view: -- **`/mpesa`**: Shown when user only uses M-PESA or navigates from legacy link. Fetches M-PESA data only. -- **`/wallet`**: Unified dashboard showing all active wallet providers with provider tabs/filtering. +* **`/mpesa`**: Shown when user only uses M-PESA or navigates from legacy link. Fetches M-PESA data only. + +* **`/wallet`**: Unified dashboard showing all active wallet providers with provider tabs/filtering. Key UI changes: -- Provider selector dropdown in transaction list -- Currency column displayed in transaction table -- Provider color-coded badges -- SMS import shows which provider's parser is used -- Stats panel shows per-provider and cross-provider breakdowns + +* Provider selector dropdown in transaction list + +* Currency column displayed in transaction table + +* Provider color-coded badges + +* SMS import shows which provider's parser is used + +* Stats panel shows per-provider and cross-provider breakdowns #### 2.2.2 Accounts Page **File:** `src/pages/Accounts.tsx` Update account type UI: -- Add `airtel_money` and `sasapay` account types (with brand colors and icons) -- Chart: Show wallet balances grouped by provider -- Account creation: Wire to `mobileWalletProviders` for type validation + +* Add `airtel_money` and `sasapay` account types (with brand colors and icons) + +* Chart: Show wallet balances grouped by provider + +* Account creation: Wire to `mobileWalletProviders` for type validation #### 2.2.3 Other Frontend Pages Update references across: -| Page | Change | -|---|---| -| `DailyPayments.tsx` | Show wallet payments across all providers, not just M-PESA | -| `Locations.tsx` | `defaultMpesaAccountId` → generic default wallet provider config | -| `Businesses.tsx` | Create default wallet accounts for all active providers (not just M-PESA) | -| `Reports.tsx` | `exportMpesa` → `exportWalletTransactions(provider?)` | -| `ChartOfAccounts.tsx` | Show wallet accounts grouped by provider type | + +| Page | Change | +| --------------------- | ------------------------------------------------------------------------- | +| `DailyPayments.tsx` | Show wallet payments across all providers, not just M-PESA | +| `Locations.tsx` | `defaultMpesaAccountId` → generic default wallet provider config | +| `Businesses.tsx` | Create default wallet accounts for all active providers (not just M-PESA) | +| `Reports.tsx` | `exportMpesa` → `exportWalletTransactions(provider?)` | +| `ChartOfAccounts.tsx` | Show wallet accounts grouped by provider type | ### 2.3 M-Pesa Currency Lock Implementation @@ -937,41 +997,50 @@ export async function ensureProviderCurrency( #### 2.3.2 Frontend Currency Lock UX -- When user selects M-PESA as payment method with non-KES currency: - - Show conversion disclosure modal with rate, converted amount, and any fees - - Require explicit confirmation before proceeding - - If no conversion enabled, block with error message: "M-PESA only supports KES transactions. Please convert your funds or use a different provider." +* When user selects M-PESA as payment method with non-KES currency: + + * Show conversion disclosure modal with rate, converted amount, and any fees + + * Require explicit confirmation before proceeding + + * If no conversion enabled, block with error message: "M-PESA only supports KES transactions. Please convert your funds or use a different provider." ### 2.4 Business Reset Updates **File:** `api/lib/business-reset.ts` Update to reset new tables: `mobile_wallet_transactions`, `mobile_wallet_daily_ledger`, `mobile_wallet_reconciliation` -- Preserve `mobile_wallet_providers` and `supported_currencies` (system-level, not tenant data) -- Preserve `provider_configs` (config structure, reset credentials) + +* Preserve `mobile_wallet_providers` and `supported_currencies` (system-level, not tenant data) + +* Preserve `provider_configs` (config structure, reset credentials) ### 2.5 Tests (Phase 2) -| Module | Test File | Coverage Target | -|---|---|---| -| `wallet-router.ts` | `api/__tests__/wallet-router.test.ts` | 90% | -| M-PESA backward compat | `api/__tests__/mpesa-router-backward-compat.test.ts` | 95% | -| Currency lock validation | `api/lib/__tests__/currency-lock.test.ts` | 95% | -| Frontend Wallet page | `src/pages/__tests__/Wallet.test.tsx` | 85% | -| Mpesa→Wallet migration script | `scripts/__tests__/migrate-mpesa.test.ts` | 90% | +| Module | Test File | Coverage Target | +| ----------------------------- | ---------------------------------------------------- | --------------- | +| `wallet-router.ts` | `api/__tests__/wallet-router.test.ts` | 90% | +| M-PESA backward compat | `api/__tests__/mpesa-router-backward-compat.test.ts` | 95% | +| Currency lock validation | `api/lib/__tests__/currency-lock.test.ts` | 95% | +| Frontend Wallet page | `src/pages/__tests__/Wallet.test.tsx` | 85% | +| Mpesa→Wallet migration script | `scripts/__tests__/migrate-mpesa.test.ts` | 90% | ### 2.6 End-to-End Testing -- Verify all existing M-PESA flows (list, import SMS, create expense, link topup, daily ledger, reconciliation) work identically after migration -- Verify KES-only lock: attempt to import non-KES M-PESA SMS → blocked with error -- Verify cross-provider aggregation in dashboard -- Verify all routes continue to function (no 404 due to router refactoring) +* Verify all existing M-PESA flows (list, import SMS, create expense, link topup, daily ledger, reconciliation) work identically after migration + +* Verify KES-only lock: attempt to import non-KES M-PESA SMS → blocked with error ---- +* Verify cross-provider aggregation in dashboard + +* Verify all routes continue to function (no 404 due to router refactoring) + +*** ## Phase 3: Scale (Airtel Money & Sasapay Integration) ### Goal + Integrate Airtel Money and Sasapay as first-class providers, complete UAT, and deploy with phased rollout. ### 3.1 Airtel Money Provider @@ -1002,12 +1071,13 @@ export class AirtelMoneyProvider extends BaseWalletProvider { ``` **SMS parser patterns for Airtel Money:** -| Pattern | Detection | Direction | Type | -|---|---|---|---| -| "You have received [CURRENCY] [amount] from [sender]" | `includes("you have received")` | `in` | `payment` | -| "[CURRENCY] [amount] sent to [recipient]" | `includes(" sent to ")` | `out` | `transfer` | -| "[CURRENCY] [amount] withdrawn" | `includes("withdrawn")` | `out` | `withdrawal` | -| Airtel Money cash power purchase | `includes("cashpower")` | `out` | `utility` | + +| Pattern | Detection | Direction | Type | +| -------------------------------------------------------- | ------------------------------- | --------- | ------------ | +| "You have received \[CURRENCY] \[amount] from \[sender]" | `includes("you have received")` | `in` | `payment` | +| "\[CURRENCY] \[amount] sent to \[recipient]" | `includes(" sent to ")` | `out` | `transfer` | +| "\[CURRENCY] \[amount] withdrawn" | `includes("withdrawn")` | `out` | `withdrawal` | +| Airtel Money cash power purchase | `includes("cashpower")` | `out` | `utility` | **Key difference from M-PESA:** Airtel Money operates in multiple East African currencies (KES, UGX, TZS, MWK, ZMW, RWF). The parser must detect the currency code from the SMS text. @@ -1070,11 +1140,16 @@ export class SasapayProvider extends BaseWalletProvider { ``` **Sasapay-specific features:** -- REST API with API key + secret authentication -- Webhooks with HMAC signature verification -- Supports both C2B (customer to business) and B2C (business to customer) payments -- Real-time transaction status polling -- Refund support via API + +* REST API with API key + secret authentication + +* Webhooks with HMAC signature verification + +* Supports both C2B (customer to business) and B2C (business to customer) payments + +* Real-time transaction status polling + +* Refund support via API ### 3.3 Unified Payment Selection UI @@ -1104,50 +1179,61 @@ interface WalletPaymentSelectorProps { **File:** `src/pages/WalletAdmin.tsx` or embedded in existing Settings/Admin Features: -- Multi-provider transaction feed with real-time filtering -- Provider health status (last successful transaction per provider) -- Daily settlement summaries by provider -- Reconciliation tool: match provider statements against system records -- Provider configuration: activate/deactivate, set API keys, default provider + +* Multi-provider transaction feed with real-time filtering + +* Provider health status (last successful transaction per provider) + +* Daily settlement summaries by provider + +* Reconciliation tool: match provider statements against system records + +* Provider configuration: activate/deactivate, set API keys, default provider ### 3.5 Provider Configuration API **File:** `api/wallet-management-router.ts` (admin-only, permission-protected) -| Procedure | Method | Purpose | -|---|---|---| -| `providers.configure` | mutation | Set API keys, endpoints, webhook URLs for a provider at a location | -| `providers.testConnection` | mutation | Test provider API connectivity | -| `providers.activate` | mutation | Enable/disable a provider for a location | -| `rates.manualUpdate` | mutation | Manually set exchange rate | -| `rates.sync` | mutation | Force sync rates from external provider | -| `rates.history` | query | View exchange rate history | +| Procedure | Method | Purpose | +| -------------------------- | -------- | ------------------------------------------------------------------ | +| `providers.configure` | mutation | Set API keys, endpoints, webhook URLs for a provider at a location | +| `providers.testConnection` | mutation | Test provider API connectivity | +| `providers.activate` | mutation | Enable/disable a provider for a location | +| `rates.manualUpdate` | mutation | Manually set exchange rate | +| `rates.sync` | mutation | Force sync rates from external provider | +| `rates.history` | query | View exchange rate history | ### 3.6 Multi-Currency Conversion UI **File:** `src/components/CurrencyConverterDialog.tsx` A dialog component for manual currency conversion: -- Select from currency (auto-detected from transaction) -- Select to currency (defaults to provider's supported currency) -- Shows live exchange rate with last-updated timestamp -- Shows converted amount -- Shows conversion fee if applicable -- "Apply Conversion" button with confirmation + +* Select from currency (auto-detected from transaction) + +* Select to currency (defaults to provider's supported currency) + +* Shows live exchange rate with last-updated timestamp + +* Shows converted amount + +* Shows conversion fee if applicable + +* "Apply Conversion" button with confirmation ### 3.7 Tests (Phase 3) -| Module | Test File | Coverage Target | -|---|---|---| -| `airtel-money-provider.ts` | `api/lib/__tests__/airtel-money-provider.test.ts` | 90% | -| `sasapay-provider.ts` | `api/lib/__tests__/sasapay-provider.test.ts` | 90% | -| `WalletPaymentSelector.tsx` | `src/components/__tests__/WalletPaymentSelector.test.tsx` | 85% | -| `WalletAdmin.tsx` | `src/pages/__tests__/WalletAdmin.test.tsx` | 80% | -| Airtel Money SMS parser | `api/lib/__tests__/airtel-sms-parser.test.ts` | 95% | -| Unified webhook (Sasapay) | `api/__tests__/wallet-webhook.test.ts` | 90% | -| End-to-end wallet cycle | `e2e/__tests__/wallet-cycle.test.ts` | Coverage of primary flows | +| Module | Test File | Coverage Target | +| --------------------------- | --------------------------------------------------------- | ------------------------- | +| `airtel-money-provider.ts` | `api/lib/__tests__/airtel-money-provider.test.ts` | 90% | +| `sasapay-provider.ts` | `api/lib/__tests__/sasapay-provider.test.ts` | 90% | +| `WalletPaymentSelector.tsx` | `src/components/__tests__/WalletPaymentSelector.test.tsx` | 85% | +| `WalletAdmin.tsx` | `src/pages/__tests__/WalletAdmin.test.tsx` | 80% | +| Airtel Money SMS parser | `api/lib/__tests__/airtel-sms-parser.test.ts` | 95% | +| Unified webhook (Sasapay) | `api/__tests__/wallet-webhook.test.ts` | 90% | +| End-to-end wallet cycle | `e2e/__tests__/wallet-cycle.test.ts` | Coverage of primary flows | ---- +*** ## Implementation Order & Dependencies @@ -1194,32 +1280,33 @@ Phase 3: Scale └── 3.10 Post-deployment monitoring + documentation [Operations] ``` ---- +*** ## Success Criteria Checklist -| Criterion | Measurement | Target | -|---|---|---| -| All existing M-PESA functionality operational post-migration | E2E test pass rate | 100% | -| Multi-currency conversion accuracy | Rate comparison vs source | 99.9% | -| New provider integration time | Days from template to production | < 5 business days | -| Zero-downtime deployment | Rolling update strategy | Confirmed | -| Unit test coverage (new modules) | Vitest coverage report | ≥ 90% | -| Backward compatibility | Old tRPC endpoints still work | All pass | -| M-PESA KES lock enforcement | Test: non-KES txns blocked | 100% | -| Cross-provider dashboard aggregation | Test: sums accurate across providers | Verified | +| Criterion | Measurement | Target | +| ------------------------------------------------------------ | ------------------------------------ | ----------------- | +| All existing M-PESA functionality operational post-migration | E2E test pass rate | 100% | +| Multi-currency conversion accuracy | Rate comparison vs source | 99.9% | +| New provider integration time | Days from template to production | < 5 business days | +| Zero-downtime deployment | Rolling update strategy | Confirmed | +| Unit test coverage (new modules) | Vitest coverage report | ≥ 90% | +| Backward compatibility | Old tRPC endpoints still work | All pass | +| M-PESA KES lock enforcement | Test: non-KES txns blocked | 100% | +| Cross-provider dashboard aggregation | Test: sums accurate across providers | Verified | ---- +*** ## Key Design Decisions & Rationale -| Decision | Rationale | -|---|---| -| **Store original + base currency amounts** | Enables accurate cross-provider/currency reporting without losing traceability to original transaction | -| **SMS parser stays provider-specific** | Each provider has unique SMS format; parser TDD per provider makes testing and maintenance easier | -| **Abstract class over interface** | Shared utilities (`parseDecimal`, `validateCurrency`, `logError`) reduce boilerplate across providers | -| **Provider registry as singleton** | Single source of truth for all active providers; simplifies dependency injection in routers | -| **M-PESA router → proxy pattern** | Zero risk of breaking existing API consumers; gradual migration path | -| **`numeric(18, 8)` for exchange rates** | Sufficient precision for all African currency pairs (KES→USD ~0.0075 requires high precision) | -| **`Intl.NumberFormat` for frontend formatting** | Native browser API handles all locale-specific formatting (symbol position, decimal/group separators) | -| **Per-provider `supportedCurrencies`** | Enables currency constraint validation at the provider level without hardcoding | +| Decision | Rationale | +| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| **Store original + base currency amounts** | Enables accurate cross-provider/currency reporting without losing traceability to original transaction | +| **SMS parser stays provider-specific** | Each provider has unique SMS format; parser TDD per provider makes testing and maintenance easier | +| **Abstract class over interface** | Shared utilities (`parseDecimal`, `validateCurrency`, `logError`) reduce boilerplate across providers | +| **Provider registry as singleton** | Single source of truth for all active providers; simplifies dependency injection in routers | +| **M-PESA router → proxy pattern** | Zero risk of breaking existing API consumers; gradual migration path | +| **`numeric(18, 8)`** **for exchange rates** | Sufficient precision for all African currency pairs (KES→USD \~0.0075 requires high precision) | +| **`Intl.NumberFormat`** **for frontend formatting** | Native browser API handles all locale-specific formatting (symbol position, decimal/group separators) | +| **Per-provider** **`supportedCurrencies`** | Enables currency constraint validation at the provider level without hardcoding | + diff --git a/.trae/specs/fina-score-engine/checklist.md b/.trae/specs/fina-score-engine/checklist.md new file mode 100644 index 0000000..d53d913 --- /dev/null +++ b/.trae/specs/fina-score-engine/checklist.md @@ -0,0 +1,123 @@ +# FinaScore Engine — Verification Checklist + +## Schema & Data Layer + +- [ ] All 5 new database tables (`finaScores`, `finaScoreHistory`, `finaScoreTokens`, `finaScoreAccessLog`, `finaScoreModelVersions`) exist in `db/schema.ts` with correct columns, types, constraints, and indexes +- [ ] Migration SQL file is generated under `db/migrations/` and is syntactically valid +- [ ] `npm run build` passes with the new schema definitions +- [ ] All tables have `locationId` and `businessId` for data isolation +- [ ] Indexes exist on frequently queried columns (location+business, calculation type, composite score, token) + +## Type System + +- [ ] `types.ts` contains all required interfaces (`ScoreFactor`, `FactorResult`, `ScoreContext`, `FinaScoreResult`) +- [ ] `GradeLetter` type covers all 7 grades (A+, A, B, C, D, E, F) +- [ ] `ScoreDataProvider` adapter interface is defined with all 4 data methods +- [ ] All types are exported from `api/lib/fina-score/types.ts` + +## Factor Engines + +- [ ] Cashflow Health factor correctly computes: volatility (coefficient of variation), coverage ratio, positive days ratio, trend slope, cash runway +- [ ] Payment Reliability factor correctly computes: on-time rate, avg delay, credit utilization, payment consistency, supplier concentration (HHI) +- [ ] Revenue Stability factor correctly computes: MoM growth (weighted), revenue consistency (CoV), concentration (top 3), seasonality index, DRO +- [ ] Financial Resilience factor correctly computes: current ratio, quick ratio, debt-to-revenue, cash runway days, unreconciled ratio +- [ ] Each factor handles edge cases: insufficient data, zero values, negative values, missing source data +- [ ] Each factor returns normalized 0–100 score +- [ ] Each factor has unit tests with >80% branch coverage + +## Aggregator + +- [ ] Composite score correctly computes weighted sum of 4 factors +- [ ] Composite mapped to 0–850 scale correctly (0–100 weighted → 0–850 scaled) +- [ ] Grade mapping is correct for all boundary values (781, 721, 661, 561, 461, 301, 0) +- [ ] Partial factor failure (one factor returns error) still computes with remaining factors +- [ ] Score snapshot JSON contains all metadata for historical reproducibility +- [ ] Aggregator has unit tests covering all boundary grades and partial failures + +## Engine Facade + +- [ ] `calculateScore()` orchestrates all 4 factors → aggregator → DB persistence +- [ ] `getCachedScore()` returns latest score from `finaScores` table +- [ ] `getScoreHistory()` returns paginated history from `finaScoreHistory` +- [ ] 15-minute metric cache prevents repeated DB queries within window +- [ ] Integration tests pass for full pipeline, cache behavior, concurrent requests + +## Scheduler & Triggers + +- [ ] Daily batch iterates all active locations and calculates scores +- [ ] On-demand refresh respects 15-minute debounce cooldown +- [ ] Trigger events (bill paid, large sale, token verification) initiate recalculation +- [ ] Daily batch is registered in `api/boot.ts` timer +- [ ] Scheduler tests verify batch processing, cooldown enforcement, and trigger firing + +## Access Control & Tokens + +- [ ] Token generation produces unique 64-character crypto random tokens +- [ ] Token expiry is enforced (expired tokens return 401) +- [ ] Token revocation works immediately (revoked tokens return 401) +- [ ] Optional PIN verification works correctly +- [ ] Max view limits are enforced (tokens expire after N views) +- [ ] All access events are logged to `finaScoreAccessLog` with IP, user-agent, timestamp +- [ ] Access control tests verify all security scenarios + +## API Layer + +- [ ] `GET /fina-score/current` returns latest score with dimensional breakdown +- [ ] `GET /fina-score/history` returns paginated history (with cursor/offset) +- [ ] `POST /fina-score/refresh` triggers on-demand recalculation +- [ ] `GET /fina-score/access-log` returns paginated access events for the business +- [ ] `POST /fina-score/share` generates shareable token with configurable params +- [ ] `GET /fina-score/verify/:token` returns score data for valid tokens +- [ ] `GET /fina-score/verify/:token/pdf` returns downloadable PDF +- [ ] `POST /fina-score/trigger-recalc` triggers batch recalculation +- [ ] Rate limiting: 100 req/min reads, 10 req/min mutations +- [ ] CSRF protection on all mutation endpoints +- [ ] All endpoints wired into `api/router.ts` +- [ ] Integration tests pass for all endpoints with auth, permissions, errors + +## PDF Report Generator + +- [ ] PDF contains: composite score (gauge), dimensional breakdown (radar data), trend line, factor explanations +- [ ] White-label branding works (custom logo, colors, disclaimer) +- [ ] Verification QR code is present on the report +- [ ] PDF renders correctly for all score states (high, low, partial) +- [ ] PDF generation tests pass for various data scenarios + +## Frontend Components + +- [ ] `FinaScoreDashboard.tsx` renders composite score with gauge, dimensional breakdown, and action buttons +- [ ] `FinaScoreGauge.tsx` renders circular gauge correctly for 0–850 range +- [ ] `FinaScoreRadar.tsx` renders 4-dimensional spider chart correctly +- [ ] `FinaScoreTrend.tsx` renders historical trend line chart +- [ ] `FinaScoreBreakdown.tsx` shows expandable factor cards with metrics and recommendations +- [ ] `FinaScoreShareDialog.tsx` correctly generates and displays share tokens +- [ ] `FinaScoreAccessLog.tsx` renders paginated access table +- [ ] `FinaScoreWebview.tsx` renders correctly for valid, expired, and revoked tokens +- [ ] All components are mobile-responsive and keyboard-accessible +- [ ] All components use existing shadcn/ui components for visual consistency +- [ ] New components are lazy-loaded with React.lazy + Suspense + +## Compliance + +- [ ] PII is automatically redacted from lender-facing API responses +- [ ] Token generation requires explicit business owner consent +- [ ] Tokens are revocable at any time (revoked tokens immediately invalid) +- [ ] GDPR right to erasure: deleting a business cascades to score data +- [ ] Data portability: scores exportable in JSON and PDF formats +- [ ] Regulatory dashboard exposes: score distribution, access audit, consent history + +## Performance + +- [ ] Read latency <200ms p95 for cached score retrieval +- [ ] Recalculation latency <2s p95 for on-demand scores +- [ ] Cache layer effectively reduces DB queries (hit rate >80% under load) +- [ ] Load test confirms 10,000+ concurrent API requests handled without degradation +- [ ] DB indexes are properly created for score queries + +## Spin-Off Readiness + +- [ ] `ScoreDataProvider` adapter interface is implemented and documented +- [ ] `FinaFlowDataProvider` implementation exists and passes tests +- [ ] `RestApiDataProvider` specification is documented for future standalone deployment +- [ ] OpenAPI 3.1 specification covers all public endpoints +- [ ] Configuration points documented: DB connection, auth, rate limits, cache TTL, external dependencies diff --git a/.trae/specs/fina-score-engine/spec.md b/.trae/specs/fina-score-engine/spec.md new file mode 100644 index 0000000..568aa65 --- /dev/null +++ b/.trae/specs/fina-score-engine/spec.md @@ -0,0 +1,680 @@ +# FinaScore Engine — Comprehensive Technical & Business Specification + +## Table of Contents +1. Core System & Engine Implementation +2. Research-Backed Scope Validation +3. Future Improvement & Extension Roadmap +4. Independent Platform Spin-Off Feasibility & Proposal + +--- + +## 1. Core System & Engine Implementation + +### 1.1 Why FinaScore + +FinaScore is a business credit scoring system purpose-built for Small and Medium Enterprises (SMEs) in emerging markets. It leverages existing financial data from the FinaFlow ecosystem (cashflow, accounting, payment behavior) to produce a transparent, multi-dimensional creditworthiness score that serves both business owners and lenders. + +### 1.2 What Changes + +- New `api/lib/fina-score/` engine module with factor-based scoring architecture +- New database tables: `fina_scores`, `fina_score_history`, `fina_score_access_log`, `fina_score_tokens` +- New API router: `api/fina-score-router.ts` with business-facing, lender-facing, and internal endpoints +- New frontend features: `src/features/fina-score/` with dashboard, gauge, radar chart, trend, share dialog, access log, and webview components +- Hybrid refresh scheduler (daily batch + on-demand trigger events) +- Shareable token-based webview for lenders (partner-infrastructure pattern) +- PDF report generation via existing `BusinessDocuments` infrastructure +- **BREAKING**: None — entirely additive, no existing schema or API changes + +### 1.3 Functional Requirements + +#### 1.3.1 Credit Data Ingestion + +The system SHALL ingest structured financial data from the following existing FinaFlow tables: + +| Data Domain | Source Tables | Refresh Cadence | +|---|---|---| +| Cashflow Activity | `daily_sales`, `mobile_wallet_transactions`, `daily_ledger`, `accounts` | Daily batch + on-demand | +| Payment Behavior | `bills`, `bill_payments`, `expenses`, `purchase_orders` | Daily batch + on-demand | +| Revenue Stability | `daily_sales`, `journal_lines` (revenue accounts), `partner_commissions` | Daily batch + on-demand | +| Financial Resilience | `accounts`, `bills`, `journal_entries`, `ledger_entries` | Daily batch + on-demand | + +The data ingestion pipeline SHALL: + +- Extract data for the trailing 90–180 day window for each scoring calculation +- Normalize multi-currency amounts to the business's base currency using the existing `CurrencyConverter` service +- Filter out deleted records (`deletedAt IS NOT NULL`) +- Respect location-level data isolation (all queries scoped by `locationId`) +- Cache aggregated metrics for 15 minutes to reduce repeated database load + +#### 1.3.2 Multi-Model Risk Calculation Engine + +The scoring engine SHALL implement four independent factor models, each calculating a normalized score (0–100) and grade letter (A+ through F): + +**Cashflow Health (Weight: 35%)** +- Daily inflow/outflow volatility (coefficient of variation over 90 days) +- Coverage ratio (avg daily inflow ÷ avg daily outflow) +- Positive cashflow days ratio (percentage of days with net positive cashflow) +- Cashflow trend direction (slope of 30-day moving average) +- Minimum cash runway in days + +**Payment Reliability (Weight: 25%)** +- On-time payment rate (percentage of bills paid on or before due date) +- Average payment delay (mean days early or late) +- Credit utilization ratio (outstanding bills ÷ average monthly revenue) +- Payment consistency (standard deviation of payment timing) +- Supplier concentration (Herfindahl-Hirschman Index of supplier diversity) + +**Revenue Stability (Weight: 25%)** +- Month-over-month growth rate (weighted average of last 3 months) +- Revenue consistency (coefficient of variation of monthly revenue) +- Revenue concentration (percentage from top 3 sources) +- Seasonality index (variance from rolling monthly average) +- Days revenue outstanding (average time from sale to payment) + +**Financial Resilience (Weight: 15%)** +- Current ratio (liquid assets ÷ short-term liabilities) +- Quick ratio ((cash + equivalents) ÷ current liabilities) +- Debt-to-revenue ratio (total liabilities ÷ annualized revenue) +- Cash runway (cash balance ÷ avg daily operating costs) +- Unreconciled transaction ratio + +#### 1.3.3 Composite Score Aggregation + +The engine SHALL compute: + +- **Composite Score**: Weighted sum of the four factor scores, mapped to a 0–850 scale +- **Overall Grade**: Letter grade derived from the composite score +- **Dimensional Scores**: Individual 0–100 scores for transparency and drill-down + +**Grade Mapping:** + +| Composite Range | Grade | Meaning | +|---|---|---| +| 781–850 | A+ | Exceptional — low risk, strong financial health | +| 721–780 | A | Strong — stable finances, reliable payment history | +| 661–720 | B | Good — generally stable, minor concerns | +| 561–660 | C | Fair — moderate risk, some warning signs | +| 461–560 | D | Watch — elevated risk, needs attention | +| 301–460 | E | Concerning — high risk, significant issues | +| 0–300 | F | Critical — severe financial distress | + +#### 1.3.4 Real-Time Scoring API + +The API SHALL expose the following endpoints: + +``` +# Business-facing endpoints +GET /fina-score/current → Latest score with dimensional breakdown +GET /fina-score/history → Score trend over time (paginated) +POST /fina-score/refresh → Manually trigger on-demand recalculation +GET /fina-score/access-log → Lender access history (paginated) +POST /fina-score/share → Generate shareable access token + +# Lender/Third-party endpoints +GET /fina-score/verify/:token → Authenticated webview with score report +GET /fina-score/verify/:token/pdf → Downloadable PDF score report + +# Internal/Scheduler endpoints +POST /fina-score/trigger-recalc → Trigger batch recalculation +``` + +**Performance SLAs:** +- Sub-200ms latency for cached score retrieval (p95) +- Sub-2s latency for on-demand score recalculation (p95) +- 99.99% uptime for read endpoints +- Support for 10,000+ concurrent API requests + +#### 1.3.5 Compliance Modules + +The system SHALL implement: + +- **Automated PII Redaction**: All access logs and shareable tokens SHALL strip PII from API responses that are not explicitly authorized for the requesting party +- **Consent Management**: Shareable tokens SHALL require explicit business owner consent via the `/fina-score/share` endpoint, with configurable expiry (default 7 days, max 30 days). Tokens SHALL be revocable at any time. +- **Regulatory Reporting**: An internal dashboard SHALL expose: + - Score distribution reports (aggregate, no PII) + - Access audit trails with timestamps, parties, and data accessed + - Consent revocation history + - Data retention compliance metrics + +### 1.4 Non-Functional Requirements + +| Requirement | Target | Measurement | +|---|---|---| +| Uptime (core scoring reads) | 99.99% | Monthly uptime calculation | +| Read latency (p95) | <200ms | APM tracing | +| Write/recalc latency (p95) | <2s | APM tracing | +| Concurrent API requests | 10,000+ | Load testing | +| Data encryption (at rest) | AES-256 | DB encryption config | +| Data encryption (in transit) | TLS 1.3 | Network config | +| PII redaction latency | <50ms added | Integration testing | +| Audit log retention | 7 years | DB retention policy | + +### 1.5 Technical Architecture + +#### 1.5.1 Cloud-Native Microservices Design + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ FinaFlow Monolith (Phase 1) │ +│ │ +│ ┌─────────────────────┐ ┌──────────────────────────────────────┐ │ +│ │ HTTP Router │ │ FinaScore Engine Module │ │ +│ │ (Hono.js/tRPC) │ │ api/lib/fina-score/ │ │ +│ │ │ │ │ │ +│ │ /fina-score/* │──│ index.ts → Facade & orchestration │ │ +│ └──────────┬───────────┘ │ types.ts → Type definitions │ │ +│ │ │ factors/ │ │ +│ │ │ ├── cashflow-health.ts │ │ +│ ┌──────────▼───────────┐ │ ├── payment-reliability.ts │ │ +│ │ Partner Invite │ │ ├── revenue-stability.ts │ │ +│ │ Infrastructure │ │ └── financial-resilience.ts │ │ +│ │ (token/access) │ │ aggregator.ts → Composite & grade │ │ +│ └───────────────────────┘ │ scheduler.ts → Batch & triggers │ │ +│ │ access-control.ts → Token mgmt │ │ +│ ┌───────────────────────┐ │ report-generator.ts → PDF output │ │ +│ │ DB Layer │ └──────────────────────────────────────┘ │ +│ │ (Drizzle ORM) │ │ +│ └───────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +#### 1.5.2 Data Pipeline Architecture + +``` +┌────────────┐ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ +│ Source │ │ Metric │ │ Score Engine │ │ Storage │ +│ Tables │──→│ Extractor │──→│ (Factor Calc) │──→│ (Scores + │ +│ │ │ (SQL + agg) │ │ (Node.js) │ │ History) │ +└────────────┘ └──────────────┘ └──────────────────┘ └──────────────┘ + │ + ▼ + ┌──────────────┐ + │ Cache Layer │ + │ (15-min TTL) │ + └──────────────┘ +``` + +#### 1.5.3 Model Versioning Framework + +Each factor calculation SHALL include a `model_version` field in the score snapshot. The version follows semver: + +- **Major**: Weight restructuring or new factor introduction +- **Minor**: Formula refinement or data source addition +- **Patch**: Bug fix or calibration adjustment + +Version metadata SHALL be stored in a `fina_score_model_versions` table: + +```typescript +export const finaScoreModelVersions = pgTable("fina_score_model_versions", { + id: serial("id").primaryKey(), + version: varchar("version", { length: 20 }).notNull(), + factors: jsonb("factors").notNull(), // Full weight/config for each factor + changelog: text("changelog"), + isActive: boolean("is_active").default(false).notNull(), + deployedAt: timestamp("deployedAt").defaultNow().notNull(), +}); +``` + +Every score calculation SHALL reference `modelVersion` so historical scores remain reproducible. + +#### 1.5.4 Audit Logging + +All score calculations and data access events SHALL be logged to: +1. `fina_score_access_log` — lender access events (who, when, token, IP, user-agent) +2. `audit_log` — existing FinaFlow audit trail for system-level events (score recalculations, token generation/revocation, model version changes) + +### 1.6 Database Schema + +```typescript +// ─── Score snapshots (daily + on-demand) ─── +export const finaScores = pgTable("fina_scores", { + id: serial("id").primaryKey(), + locationId: bigint("locationId", { mode: "number" }).notNull(), + businessId: bigint("businessId", { mode: "number" }).notNull(), + + compositeScore: integer("composite_score").notNull(), // 0-850 + + // Dimensional scores (0-100) + cashflowHealthScore: integer("cashflow_health_score").notNull(), + paymentReliabilityScore: integer("payment_reliability_score").notNull(), + revenueStabilityScore: integer("revenue_stability_score").notNull(), + financialResilienceScore: integer("financial_resilience_score").notNull(), + + // Letter grades + cashflowGrade: varchar("cashflow_grade", { length: 2 }).notNull(), + paymentGrade: varchar("payment_grade", { length: 2 }).notNull(), + revenueGrade: varchar("revenue_grade", { length: 2 }).notNull(), + resilienceGrade: varchar("resilience_grade", { length: 2 }).notNull(), + overallGrade: varchar("overall_grade", { length: 2 }).notNull(), + + // Calculation metadata + calculationType: varchar("calculation_type", { length: 10 }).notNull(), // 'daily' | 'on_demand' + modelVersion: varchar("model_version", { length: 20 }).notNull(), + scoreSnapshot: jsonb("score_snapshot"), // Full breakdown + calculationDurationMs: integer("calculation_duration_ms"), + + dataRangeStart: date("data_range_start"), + dataRangeEnd: date("data_range_end").notNull(), + + lenderAccessCount: integer("lender_access_count").default(0).notNull(), + + calculatedAt: timestamp("calculatedAt").defaultNow().notNull(), + createdAt: timestamp("createdAt").defaultNow().notNull(), +}, (table) => ({ + locationBusinessIdx: index("idx_fina_scores_loc_biz").on(table.locationId, table.businessId), + calcTypeIdx: index("idx_fina_scores_calc_type").on(table.calculationType), + compositeScoreIdx: index("idx_fina_scores_composite").on(table.compositeScore), +})); + +// ─── Score history (trend tracking, append-only) ─── +export const finaScoreHistory = pgTable("fina_score_history", { + id: serial("id").primaryKey(), + locationId: bigint("locationId", { mode: "number" }).notNull(), + businessId: bigint("businessId", { mode: "number" }).notNull(), + compositeScore: integer("composite_score").notNull(), + scoreSnapshot: jsonb("score_snapshot"), + modelVersion: varchar("model_version", { length: 20 }).notNull(), + calculatedAt: timestamp("calculatedAt").defaultNow().notNull(), +}, (table) => ({ + locBizDateIdx: index("idx_fina_score_history_loc_biz_date").on(table.locationId, table.businessId, table.calculatedAt), +})); + +// ─── Shareable access tokens ─── +export const finaScoreTokens = pgTable("fina_score_tokens", { + id: serial("id").primaryKey(), + locationId: bigint("locationId", { mode: "number" }).notNull(), + businessId: bigint("businessId", { mode: "number" }).notNull(), + token: varchar("token", { length: 64 }).notNull().unique(), + pinCode: varchar("pin_code", { length: 6 }), // Optional PIN for extra security + recipientName: varchar("recipient_name", { length: 255 }), // Lender/partner name + recipientEmail: varchar("recipient_email", { length: 320 }), // For audit tracking + maxViews: integer("max_views").default(5).notNull(), + currentViews: integer("current_views").default(0).notNull(), + isRevoked: boolean("is_revoked").default(false).notNull(), + expiresAt: timestamp("expiresAt").notNull(), + createdBy: bigint("createdBy", { mode: "number" }).notNull(), + createdAt: timestamp("createdAt").defaultNow().notNull(), + revokedAt: timestamp("revokedAt"), +}); + +// ─── Lender access log ─── +export const finaScoreAccessLog = pgTable("fina_score_access_log", { + id: serial("id").primaryKey(), + locationId: bigint("locationId", { mode: "number" }).notNull(), + businessId: bigint("businessId", { mode: "number" }).notNull(), + tokenId: bigint("tokenId", { mode: "number" }).references(() => finaScoreTokens.id), + accessedBy: varchar("accessed_by", { length: 255 }), // From token recipient + accessType: varchar("access_type", { length: 20 }).notNull(), // 'lender_portal' | 'shared_link' | 'api_direct' + ipAddress: varchar("ip_address", { length: 45 }), + userAgent: text("user_agent"), + action: varchar("action", { length: 20 }).notNull(), // 'viewed' | 'downloaded_pdf' + viewedAt: timestamp("viewedAt").defaultNow().notNull(), +}, (table) => ({ + locBizIdx: index("idx_fina_score_access_loc_biz").on(table.locationId, table.businessId), + tokenIdx: index("idx_fina_score_access_token").on(table.tokenId), +})); +``` + +### 1.7 Compliance Requirements + +#### 1.7.1 GDPR Compliance +- Business owners SHALL have the right to access their scoring data via the dashboard +- Right to erasure: Deleting a business location SHALL cascade-delete all associated score data +- Data portability: Score reports SHALL be exportable in JSON and PDF formats +- Consent records SHALL be maintained for all score sharing events + +#### 1.7.2 PCI DSS Alignment +- While FinaScore does not process raw card data, score calculations may reference financial transaction volumes. All data in transit SHALL use TLS 1.3, and all stored data SHALL use AES-256 encryption. +- Access logs SHALL be retained for audit purposes for a minimum of 12 months (extendable to 7 years for regulatory compliance). + +#### 1.7.3 CCPA Compliance +- California residents (where applicable) SHALL be able to opt out of score data being shared with third parties +- A "Do Not Share" flag SHALL be available on the business profile settings + +#### 1.7.4 Local Financial Regulatory Frameworks +- For operations in Kenya (CBK), Uganda (BOU), Tanzania (BOT), and other markets: + - Score methodology SHALL be documented and made available to regulators on request + - Score model changes SHALL be logged with version history for regulatory review + - Algorithmic fairness testing SHALL be performed quarterly to detect demographic bias + +### 1.8 Error Handling & Resilience + +| Scenario | Behavior | +|---|---| +| Source data unavailable (table down) | Return last cached score with `dataStaleness` warning | +| Individual factor calculation fails | Return partial score with `failedFactors[]` array | +| Token expired or revoked | Return 401 with `code: "TOKEN_EXPIRED"` or `code: "TOKEN_REVOKED"` | +| Rate limit exceeded | Return 429 with retry-after header | +| Concurrent recalculation request | Debounce: if recalc in progress, return 409 with `code: "CALCULATION_IN_PROGRESS"` | +| Multi-currency conversion failure | Skip currency conversion, use original amounts with warning flag | + +--- + +## 2. Research-Backed Scope Validation + +### 2.1 Industry Benchmark Analysis + +#### 2.1.1 Traditional Credit Scoring (FICO) + +| Feature | FICO Score | FinaScore | +|---|---|---| +| Market Focus | Consumer credit (US-centric) | SME business (emerging markets) | +| Data Sources | Credit bureau reports (payment history, amounts owed, credit history length, new credit, credit mix) | Cashflow, payment behavior, revenue, financial resilience | +| Score Range | 300–850 | 0–850 (familiar mapping) | +| Transparency | Proprietary algorithm (opaque) | Fully transparent (all factors shown) | +| Update Cadence | Monthly (bureau refresh) | Daily + on-demand | +| SME Coverage | Limited (thin credit files) | Built for SME data ecosystem | + +**Key Differentiation**: FinaScore serves the 65% of SMEs globally that lack traditional credit bureau files. By using operational cashflow data instead of credit history, FinaScore opens credit access to underserved businesses. + +#### 2.1.2 Alternative Scoring Platforms + +| Platform | Focus | FinaScore Delta | +|---|---|---| +| **Experian Business** | Credit bureau + public records | FinaScore uses real-time operational data, not historical bureau records | +| **CredoLab** | Smartphone-based digital scoring | FinaScore uses actual financial data, not behavioral proxies | +| **LenddoEFL** | Psychometric + digital footprint | FinaScore uses verifiable accounting data | +| **Cignifi** | Mobile phone usage patterns | FinaScore uses platform-native financial data | +| **Nova Credit** | Cross-border credit history | FinaScore targets domestic SMEs with operational data | + +#### 2.1.3 Unique Value Propositions + +1. **Platform-Native Data**: FinaScore is embedded in the accounting tool businesses already use — no separate onboarding or data sharing +2. **Transparent, Actionable Breakdown**: Businesses see which factors hurt their score and get specific improvement recommendations +3. **Lender-Verified Access**: Shareable tokens with audit trails build trust between businesses and lenders +4. **Real-Time Adaptation**: Scores update daily and on-demand, reflecting current financial reality (not 3-month-old bureau data) +5. **Emerging Market Optimization**: Built from the ground up for markets where cashflow data is more reliable than credit bureau data + +### 2.2 Market Research: SME & Underbanked Credit Scoring + +#### 2.2.1 Market Size & Opportunity + +| Metric | Value | Source | +|---|---|---| +| Global SME credit gap | $5.7 trillion | IFC (International Finance Corporation) | +| SMEs with unmet credit needs | 65% (approx. 130M businesses) | World Bank | +| SMEs in emerging markets lacking credit | 50%+ | McKinsey Global Institute | +| African SME financing gap | $331 billion | African Development Bank | +| Digital lending market CAGR | 25.6% (2024–2030) | Grand View Research | + +#### 2.2.2 Target Use Case Validation + +**Primary Use Case: SME Internal Financial Health Assessment** +- Business owners need an objective, quantified measure of financial health +- Current alternatives: manual ratio calculation, expensive accountants, or no analysis at all +- FinaScore provides: automatic, daily-updated, 4-dimensional analysis with improvement guidance + +**Secondary Use Case: SME Credit Access for Lenders** +- Lenders need reliable, verifiable SME credit data +- Current alternatives: expensive manual due diligence, collateral-based lending, or rejecting SMEs outright +- FinaScore provides: standardized, verifiable, token-shareable score with full audit trail + +**Tertiary Use Case: Supply Chain & Trade Credit** +- Suppliers need to assess buyer creditworthiness before extending trade credit +- FinaScore provides: a standardized reference that reduces information asymmetry + +### 2.3 Technical Risk Assessment & Mitigation + +| Risk | Description | Severity | Mitigation | +|---|---|---|---| +| **Model Bias** | Score systematically disadvantages certain business types, locations, or industries | High | Quarterly fairness testing; factor weights reviewed by domain experts; demographic parity analysis | +| **Data Drift** | Model accuracy degrades over time as business patterns change | Medium | Automated monitoring of score distribution shifts; monthly recalibration; model versioning | +| **Cyber Threats** | Unauthorized access to score data or token hijacking | High | End-to-end encryption; token expiry (max 30 days); optional PIN codes; rate limiting on verify endpoints | +| **Gaming/Manipulation** | Businesses artificially inflate scores by altering accounting data | Medium | Auditable input data (all source data is double-entry); anomaly detection on data patterns | +| **Regulatory Change** | New financial regulations require scoring methodology changes | Low | Modular factor architecture allows factor addition/removal without overhaul; regulatory dashboard | +| **Adverse Selection** | Only high-risk businesses use FinaScore, skewing the model | Low | Score is optional feature; broad FinaFlow user base provides diverse data | + +--- + +## 3. Future Improvement & Extension Roadmap + +### 3.1 12-Month Milestones (Foundation & Core Adoption) + +| Quarter | Milestone | Deliverables | +|---|---|---| +| **Q1** (Months 1–3) | MVP Launch | Core 4-factor engine, daily batch scheduler, composite score, business dashboard, basic PDF report | +| **Q2** (Months 4–6) | Shareable Score & Lender Access | Token-based webview, access log dashboard, lender API, share dialog UI, PDF with QR verification | +| **Q3** (Months 7–9) | Enhanced Analytics | Score trend charts, improvement recommendations per factor, industry benchmark comparison (anonymized), email notifications on score changes | +| **Q4** (Months 10–12) | Alternative Data Foundation | FinaBill integration (invoice payment data → Payment Reliability), M-Pesa/Daraja API data enrichment, basic configurable factor weights | + +**Success Metrics (12 months):** +- 1,000+ active business scores (daily or weekly viewing) +- 50+ lender/partner organizations using score verification +- 500+ score shares generated +- <5% error rate in score predictions (validated against actual repayment data from pilot lenders) + +### 3.2 24-Month Milestones (Expansion & Alternative Data) + +| Quarter | Milestone | Deliverables | +|---|---|---| +| **Q1** (Months 13–15) | Alternative Data Integration | Telecom data (airtime recharge patterns as reliability proxy), utility bill payment data integration, e-commerce transaction data ingestion API | +| **Q2** (Months 16–18) | Niche Scoring Models | Gig worker scoring model (irregular income patterns), micro-business scoring (reduced data requirements), cryptocurrency asset lending score (wallet analysis) | +| **Q3** (Months 19–21) | Multi-Tenant Enterprise | Enterprise client onboarding portal, custom model fine-tuning per enterprise, white-label branding for score reports, dedicated API rate tiers | +| **Q4** (Months 22–24) | Predictive Scoring | ML-based default probability prediction (in addition to deterministic score), cashflow forecasting integration, "Score Simulator" (what-if analysis for business owners) | + +**Success Metrics (24 months):** +- 10,000+ active business scores +- 200+ enterprise/lender clients +- 85%+ client retention rate +- 15% improvement in loan approval rates for FinaScore-verified SMEs (pilot data) + +### 3.3 36-Month Milestones (Platform Maturity & Ecosystem) + +| Quarter | Milestone | Deliverables | +|---|---|---| +| **Q1** (Months 25–27) | Self-Service Model Training | Internal data science team UI for training/validating custom models, A/B testing framework for scoring models, automated data drift monitoring | +| **Q2** (Months 28–30) | API Ecosystem & Marketplace | Public developer portal with API documentation, SDK libraries (Python, Node.js, PHP, Java), webhook notifications for score changes | +| **Q3** (Months 31–33) | Regulatory Compliance Suite | Automated regulatory reporting dashboards, cross-border data transfer compliance, AI governance framework & documentation | +| **Q4** (Months 34–36) | Ecosystem Network Effects | Federated scoring (cross-institution data sharing with consent), industry-specific benchmark reports, FinaScore Certification program for lenders | + +**Success Metrics (36 months):** +- 50,000+ active business scores +- 500+ enterprise/lender clients +- FinaScore accepted as credit assessment tool by 3+ regulated financial institutions +- Independent revenue stream from standalone platform + +### 3.4 Platform Extension Proposals + +#### 3.4.1 Multi-Tenant Access +- Each tenant (enterprise/lender) SHALL have isolated configuration: custom factor weights, grade mappings, and report branding +- Tenant onboarding SHALL follow the existing partner allocation infrastructure pattern +- Usage-based billing SHALL be tracked per tenant (API calls, score views, PDF downloads) + +#### 3.4.2 Custom Model Fine-Tuning +- Enterprise clients SHALL be able to adjust factor weights within defined guardrails (±20% from baseline) +- Advanced clients SHALL be able to upload custom factors via a plugin interface +- All customizations SHALL be versioned and auditable + +#### 3.4.3 White-Label Branding +- Enterprise clients SHALL be able to brand score reports with their own logo, colors, and company name +- The verify endpoint SHALL accept a `brand` parameter to render white-labeled versions +- PDF generator SHALL support custom header/footer, disclaimer text, and color schemes + +--- + +## 4. Independent Platform Spin-Off Feasibility & Proposal + +### 4.1 Business Case + +#### 4.1.1 Market Size Analysis + +| TAM Segment | Market Size | FinaScore Addressable Share | +|---|---|---| +| Global SME Lending Technology | $12.4B (2025) | $500M (SME credit scoring tools) | +| Alternative Credit Scoring | $3.8B (2025) | $800M (platform-native scoring) | +| Embedded Finance Platforms | $7.2B (2025) | $300M (white-label scoring) | +| **Total Addressable Market** | **$23.4B** | **$1.6B (FinaScore TAM)** | + +#### 4.1.2 Revenue Model Projections + +| Revenue Stream | Pricing Model | Year 1 | Year 3 | Year 5 | +|---|---|---|---|---| +| Usage-Based API | $0.10–$0.50 per score query (tiered by volume) | $120K | $1.2M | $4.8M | +| Enterprise License | $2,000–$10,000/month (custom models + white-label) | $180K | $1.8M | $5.4M | +| Custom Model Dev | $15,000–$50,000 per custom model (one-time) | $75K | $300K | $750K | +| Regulatory Reporting | $500–$2,000/month per institution | $30K | $240K | $600K | +| Data Insights Reports | $200–$1,000 per report (benchmarking) | $20K | $180K | $360K | +| **Total Projected Revenue** | | **$425K** | **$3.72M** | **$11.91M** | + +#### 4.1.3 Competitive Positioning + +| Competitive Advantage | FinaScore Strength | Barrier to Entry | +|---|---|---| +| **Platform-Native Data** | Scores generated from app data with zero additional effort | High — requires both financial platform + scoring engine | +| **Emerging Market Focus** | Built for cashflow-based markets (Africa, SE Asia, LatAm) | Medium — regional knowledge + local partnerships | +| **Transparent Scoring** | Factor-level breakdown with improvement guidance | Low — competitors could copy, but first-mover advantage | +| **Lender-Verified Access** | Token-based sharing with full audit trail | Medium — technical but feasible | +| **Real-Time Updates** | Daily + event-driven refreshes | Medium — requires event infrastructure | + +### 4.2 Technical Refactoring for Spin-Off + +#### 4.2.1 Core Dependency Abstraction + +The following abstractions SHALL be implemented to decouple the FinaScore engine from the FinaFlow monolith: + +| Component | Current Dependency | Abstraction Target | +|---|---|---| +| Data Source | Direct Drizzle queries to FinaFlow DB | DataProvider interface with adapter pattern | +| User/Auth | FinaFlow session cookies | JWT-based auth with separate API keys | +| Currency Conversion | `api/lib/currency-converter.ts` | Abstracted as external dependency (configurable provider) | +| PDF Generation | `api/lib/business-documents.ts` | Self-contained report generator | +| Rate Limiting | `api/lib/rate-limit.ts` | Self-contained rate limiter | +| Audit Logging | `api/lib/audit.ts` | Standalone audit service | + +#### 4.2.2 DataProvider Interface + +```typescript +// api/lib/fina-score/adapters/data-provider.ts +export interface ScoreDataProvider { + getCashflowData(params: { + locationId: number; + startDate: Date; + endDate: Date; + }): Promise; + + getPaymentData(params: { + locationId: number; + startDate: Date; + endDate: Date; + }): Promise; + + getRevenueData(params: { + locationId: number; + startDate: Date; + endDate: Date; + }): Promise; + + getResilienceData(params: { + locationId: number; + startDate: Date; + endDate: Date; + }): Promise; +} +``` + +Two implementations: +1. `FinaFlowDataProvider` — Direct DB queries (Phase 1, in-monolith) +2. `RestApiDataProvider` — HTTP-based data ingestion (Phase 2, standalone) + +#### 4.2.3 Tiered Access Control System + +| Tier | Features | Target | +|---|---|---| +| **Free** | Self-service score view, basic PDF, 1 share token/month | SME businesses | +| **Pro** | Unlimited share tokens, score history, trend charts, improvement recommendations | SME businesses | +| **Business** | Custom factor weights, API access, white-label PDF, 3 team members | Growing businesses | +| **Enterprise** | Full API, custom models, dedicated support, SLA guarantees, regulatory reports | Lenders, banks, fintechs | + +#### 4.2.4 Self-Service Developer Portal + +- REST API with OpenAPI 3.1 specification +- SDK packages (npm, pip, composer, gradle) +- Interactive API playground (Swagger UI) +- Webhook subscriptions for score change events +- Usage analytics dashboard for API consumers + +### 4.3 Legal & Operational Framework + +#### 4.3.1 Data Ownership Agreements + +- **Business Data**: The business owner retains full ownership of their financial data. FinaScore acts as a data processor. +- **Score Data**: The calculated score is jointly owned — the business for self-assessment, FinaScore for the derived intellectual property. +- **Usage Data**: Anonymous aggregate usage data (score distributions, factor correlations) belongs to FinaScore for model improvement. +- **Data Processing Agreement (DPA)**: Required for all enterprise clients accessing business score data. + +#### 4.3.2 Liability Limitations + +- FinaScore SHALL provide a "score" not a "credit decision" — lenders SHALL be required to accept terms explicitly stating that FinaScore is not a credit bureau or financial advisor. +- Disclaimer SHALL appear on all score reports: "This score is for informational purposes only and does not constitute financial advice, credit approval, or guarantee of repayment." +- Liability SHALL be capped at 12 months of subscription fees (standard SaaS limitation). +- Professional liability insurance (errors & omissions) SHALL be maintained at minimum $2M coverage. + +#### 4.3.3 Cross-Border Data Transfer + +- Data residency SHALL be maintained within the region of origin (e.g., African SME data stays in African data centers) +- Standard Contractual Clauses (SCCs) SHALL be used for EU adequacy decisions +- Local data protection registration SHALL be obtained in each operating jurisdiction (e.g., Kenya's ODPC, Uganda's NITA-U) + +### 4.4 Go-To-Market Strategy + +#### 4.4.1 Pilot Programs (Months 1–6 post-spin-off) + +| Pilot Partner | Integration Type | Success Criteria | +|---|---|---| +| **Partner Bank A** (Microfinance) | API-based score query for loan origination | 500+ score queries, 90% API uptime | +| **Partner Fintech B** (Digital Lender) | Webview-based verification for instant loans | 200+ score shares, <2s verification time | +| **Partner Platform C** (Supply Chain) | Embedded scoring in supplier onboarding | 100+ supplier scores, 80% adoption rate | +| **Partner Accounting Firm D** | White-label score reports for clients | 50+ branded reports, positive NPS | + +#### 4.4.2 Developer Outreach & Community + +- Monthly developer webinars on scoring methodology +- Open-source sample integrations (GitHub repositories) +- Developer challenge/hackathon (best FinaScore integration wins $10K) +- Technical blog series: "Building Credit Scoring for SMEs in Emerging Markets" + +#### 4.4.3 Cloud Marketplace Distribution + +| Marketplace | Listing Type | Requirements | +|---|---|---| +| **AWS Marketplace** | SaaS subscription (hourly/monthly) | CloudFormation integration, IAM roles | +| **Azure Marketplace** | Managed application | ARM template, Azure AD integration | +| **Google Cloud Marketplace** | SaaS with private offers | GCP service account, Cloud Run deployment | +| **M-Pesa Daraja API Marketplace** | API product listing | Safaricom partnership agreement | + +### 4.5 Financial Projections (Spin-Off Scenario) + +| Metric | Year 1 | Year 2 | Year 3 | +|---|---|---|---| +| Development Cost | $350K | $200K | $150K | +| Operations Cost | $120K | $250K | $400K | +| Marketing & Sales | $80K | $150K | $250K | +| **Total Costs** | **$550K** | **$600K** | **$800K** | +| **Revenue** | **$425K** | **$1.2M** | **$3.72M** | +| **Gross Margin** | -$125K | $600K | $2.92M | +| **Cumulative Cash Flow** | -$125K | $475K | $3.395M | +| **Break-Even** | Month 14–16 | | | + +--- + +## Appendix A: Key Assumptions & Constraints + +1. FinaScore is additive to existing FinaFlow infrastructure — no breaking changes to existing functionality +2. Multi-currency support (Phase 1 of the wallet aggregation plan) is a prerequisite for cross-currency score normalization +3. All data used for scoring is already available within the FinaFlow ecosystem (no external data partnerships required for MVP) +4. Regulatory compliance is scoped to Kenyan (CBK), Ugandan (BOU), and Tanzanian (BOT) frameworks initially, with expansion planned +5. The FinaBill platform integration will follow in Q3–Q4 of the 12-month roadmap +6. All pricing figures are in USD and should be adjusted for local market conditions + +## Appendix B: Glossary + +| Term | Definition | +|---|---| +| Composite Score | 0–850 aggregate score derived from weighted factor scores | +| Dimensional Score | Individual 0–100 score for each of the 4 factors | +| Factor | A scoring dimension (Cashflow Health, Payment Reliability, Revenue Stability, Financial Resilience) | +| Grade Letter | A+ through F letter grade mapping for easy interpretation | +| Share Token | Cryptographic token enabling secure, time-limited score sharing | +| On-Demand Recalculation | Real-time score refresh triggered by specific events or manual request | +| Data Drift | Degradation in model accuracy due to changes in underlying data patterns | +| White-Label | Custom-branded score reports for enterprise/lender clients | diff --git a/.trae/specs/fina-score-engine/tasks.md b/.trae/specs/fina-score-engine/tasks.md new file mode 100644 index 0000000..4b4ecdf --- /dev/null +++ b/.trae/specs/fina-score-engine/tasks.md @@ -0,0 +1,217 @@ +# Tasks — FinaScore Engine Implementation + +## Overall Task Sequencing +Implementation follows a bottom-up order: schema → engine core → API layer → frontend → sharing/webview → infrastructure. Each phase builds on the previous, and testing is integrated at every level. + +--- + +### Phase 1: Foundation (Weeks 1–2) + +- [ ] **Task 1**: Create database schema for FinaScore + - Define and add to `db/schema.ts`: + - [ ] `finaScores` table with composite score, dimensional scores, grades, calculation metadata + - [ ] `finaScoreHistory` table (append-only trend tracking) + - [ ] `finaScoreTokens` table (shareable access tokens with expiry, PIN, view limits) + - [ ] `finaScoreAccessLog` table (lender access audit trail) + - [ ] `finaScoreModelVersions` table (model versioning metadata) + - [ ] Run `npm run build` to validate Drizzle schema compilation + - [ ] Create `db/migrations/` SQL migration file + - **Sub-Agent**: Use `backend-architect` to add schema definitions following Drizzle ORM patterns + +- [ ] **Task 2**: Create FinaScore type system and interfaces + - [ ] Create `api/lib/fina-score/types.ts` with: + - `ScoreFactor` interface, `FactorResult`, `ScoreContext`, `FinaScoreResult` + - `GradeLetter` type, `ScoreDataPoint` interfaces + - Adapter interfaces (`ScoreDataProvider`) + - **Sub-Agent**: Use `backend-architect` + +### Phase 2: Scoring Engine Core (Weeks 3–5) + +- [ ] **Task 3**: Implement Cashflow Health factor + - [ ] Create `api/lib/fina-score/factors/cashflow-health.ts` + - [ ] Implement: daily inflow/outflow volatility (coefficient of variation), coverage ratio, positive cashflow days ratio, cashflow trend (30-day moving average slope), minimum cash runway + - [ ] Queries against `daily_sales`, `mobile_wallet_transactions`, `daily_ledger`, `accounts` + - [ ] Returns normalized 0–100 score + - [ ] Write unit tests covering: normal cashflow, volatile cashflow, negative cashflow, insufficient data edge case + - **Sub-Agent**: Use `backend-architect` + +- [ ] **Task 4**: Implement Payment Reliability factor + - [ ] Create `api/lib/fina-score/factors/payment-reliability.ts` + - [ ] Implement: on-time payment rate, average payment delay, credit utilization ratio, payment consistency std dev, supplier concentration (Herfindahl-Hirschman Index) + - [ ] Queries against `bills`, `bill_payments`, `expenses`, `purchase_orders` + - [ ] Returns normalized 0–100 score + - [ ] Write unit tests covering: perfect payment history, consistently late payments, no bills (graceful degradation), single supplier concentration + - **Sub-Agent**: Use `backend-architect` + +- [ ] **Task 5**: Implement Revenue Stability factor + - [ ] Create `api/lib/fina-score/factors/revenue-stability.ts` + - [ ] Implement: month-over-month growth (weighted 3-month), revenue consistency (coefficient of variation), revenue concentration (top 3 sources %), seasonality index, days revenue outstanding + - [ ] Queries against `daily_sales`, `journal_lines` (revenue accounts), `partner_commissions` + - [ ] Returns normalized 0–100 score + - [ ] Write unit tests covering: steady growth, seasonal business (restaurant/hotel), declining revenue, new business (limited data) + - **Sub-Agent**: Use `backend-architect` + +- [ ] **Task 6**: Implement Financial Resilience factor + - [ ] Create `api/lib/fina-score/factors/financial-resilience.ts` + - [ ] Implement: current ratio, quick ratio, debt-to-revenue, cash runway (days), unreconciled transaction ratio + - [ ] Queries against `accounts`, `bills`, `journal_entries`, `ledger_entries` + - [ ] Returns normalized 0–100 score + - [ ] Write unit tests covering: strong balance sheet, high debt load, negative cash runway, unreconciled transactions + - **Sub-Agent**: Use `backend-architect` + +- [ ] **Task 7**: Implement Score Aggregator + - [ ] Create `api/lib/fina-score/aggregator.ts` + - [ ] Implement: weighted composite score (0–850), grade letter mapping, score snapshot (JSON) generation + - [ ] Grade mapping: 781–850→A+, 721–780→A, 661–720→B, 561–660→C, 461–560→D, 301–460→E, 0–300→F + - [ ] Handle partial results (one factor fails → still compute with remaining) + - [ ] Write unit tests covering: all weights at extremes, mid-range scores, partial factor failure, grade boundary values + - **Sub-Agent**: Use `backend-architect` + +- [ ] **Task 8**: Implement Engine Facade + - [ ] Create `api/lib/fina-score/index.ts` — public API facade + - [ ] `calculateScore(context)`: orchestrates all 4 factors → aggregator → persist + - [ ] `getCachedScore(locationId, businessId)`: return latest from `finaScores` + - [ ] `getScoreHistory(locationId, businessId)`: return paginated history + - [ ] Implement 15-minute metric cache to reduce DB load + - [ ] Write integration tests: full score calculation pipeline, cache hits/misses, concurrent recalc debounce + - **Sub-Agent**: Use `backend-architect` + +### Phase 3: Hybrid Refresh Scheduler (Week 6) + +- [ ] **Task 9**: Implement Scheduler & Trigger System + - [ ] Create `api/lib/fina-score/scheduler.ts` + - [ ] Daily batch: Iterate all active locations, calculate scores, archive to history + - [ ] On-demand trigger: Recalculate when `POST /refresh` called, debounce (15 min cooldown) + - [ ] Wake-up triggers: bill paid, large sale recorded, lender token verification + - [ ] Register daily batch in `api/boot.ts` timer + - [ ] Write tests: batch processes all locations correctly, on-demand respects cooldown, triggers fire correctly + - **Sub-Agent**: Use `backend-architect` + +### Phase 4: Access Control & Token System (Week 7) + +- [ ] **Task 10**: Implement Access Control + - [ ] Create `api/lib/fina-score/access-control.ts` + - [ ] Token generation: 64-char crypto random token, configurable expiry (7–30 days), optional PIN + - [ ] Token verification: validate not expired, not revoked, under max view limit + - [ ] Token revocation: immediate invalidation, logged to audit + - [ ] Access logging: IP, user-agent, timestamp, action type + - [ ] Write tests: token generation uniqueness, expiry enforcement, revocation, PIN verification, max view limits + - **Sub-Agent**: Use `backend-architect` + +### Phase 5: API Layer (Week 8) + +- [ ] **Task 11**: Create FinaScore API Router + - [ ] Create `api/fina-score-router.ts` + - [ ] Business endpoints: + - `GET /fina-score/current` — latest score with dimensional breakdown + - `GET /fina-score/history` — paginated history + - `POST /fina-score/refresh` — manual recalc trigger + - `GET /fina-score/access-log` — lender access history + - `POST /fina-score/share` — generate shareable token + - [ ] Lender endpoints: + - `GET /fina-score/verify/:token` — authenticated webview data + - `GET /fina-score/verify/:token/pdf` — PDF download + - [ ] Internal endpoint: + - `POST /fina-score/trigger-recalc` — scheduler trigger + - [ ] Rate limiting: 100 req/min for reads, 10 req/min for mutations + - [ ] CSRF protection on all mutation endpoints + - [ ] Wire into `api/router.ts` + - [ ] Write integration tests: all endpoints with auth, permission checks, rate limiting, error scenarios + - **Sub-Agent**: Use `backend-architect` + +### Phase 6: PDF Report Generator (Week 9) + +- [ ] **Task 12**: Implement PDF Report Generator + - [ ] Create `api/lib/fina-score/report-generator.ts` + - [ ] Generate PDF with: composite score (gauge visualization), dimensional breakdown (radar chart data), trend data, factor explanations + - [ ] Leverage existing `api/lib/business-documents.ts` infrastructure + - [ ] Support white-label branding (logo, colors, custom disclaimer) + - [ ] Include verification QR code on report + - [ ] Write tests: PDF generation with various data states, brand customization, error handling for missing data + - **Sub-Agent**: Use `backend-architect` + +### Phase 7: Frontend (Weeks 10–12) + +- [ ] **Task 13**: Build FinaScore Dashboard Components + - [ ] Create `src/features/fina-score/FinaScoreDashboard.tsx` — main score card for business dashboard + - [ ] Render composite score with gauge visualization and overall grade + - [ ] Show dimensional breakdown with grade letters for each factor + - [ ] Add quick-action buttons: Refresh, Share, View History + - [ ] Integrate into existing Dashboard page or dedicated nav section + - **Sub-Agent**: Use `frontend-engineer` + +- [ ] **Task 14**: Build FinaScore Gauge & Radar Components + - [ ] `src/features/fina-score/FinaScoreGauge.tsx` — circular gauge showing 0–850 + - [ ] `src/features/fina-score/FinaScoreRadar.tsx` — spider/radar chart for 4 dimensions + - [ ] Use existing charting infrastructure (recharts from chart-data.ts) + - [ ] Mobile-responsive, accessible (ARIA labels) + - **Sub-Agent**: Use `frontend-engineer` + +- [ ] **Task 15**: Build FinaScore Trend & Breakdown + - [ ] `src/features/fina-score/FinaScoreTrend.tsx` — historical trend line chart + - [ ] `src/features/fina-score/FinaScoreBreakdown.tsx` — expandable factor detail cards with raw metrics + - [ ] Show improvement recommendations per factor where scores are low + - **Sub-Agent**: Use `frontend-engineer` + +- [ ] **Task 16**: Build Share Dialog & Access Log + - [ ] `src/features/fina-score/FinaScoreShareDialog.tsx` — modal for generating share tokens + - [ ] Fields: recipient name/email, expiry, optional PIN + - [ ] Token display with copy-to-clipboard and direct email link + - [ ] `src/features/fina-score/FinaScoreAccessLog.tsx` — paginated table of access events + - [ ] Show: who viewed, when, how many times, PDF downloads + - **Sub-Agent**: Use `frontend-engineer` + +- [ ] **Task 17**: Build Lender Webview + - [ ] `src/features/fina-score/FinaScoreWebview.tsx` — lender-facing secure page + - [ ] Route: `/fina-score/verify/:token` + - [ ] Public route (no auth required — authenticated via token) + - [ ] Show: composite score, dimensional breakdown, professional PDF download + - [ ] Mobile-optimized (lenders view on phones) + - [ ] Error states: expired token, revoked token, invalid token + - **Sub-Agent**: Use `frontend-engineer` + +### Phase 8: Infrastructure & Compliance (Week 13) + +- [ ] **Task 18**: Implement Compliance Features + - [ ] Automated PII redaction in access log API responses + - [ ] Consent management (token generation requires explicit consent, revocable) + - [ ] GDPR right to erasure cascade (delete business → delete scores) + - [ ] Data portability export (JSON + PDF) + - [ ] Regulatory dashboard data (score distribution, access audit, consent history) + - **Sub-Agent**: Use `backend-architect` + +- [ ] **Task 19**: Performance Optimization & Load Testing + - [ ] Implement 15-min metric cache layer + - [ ] Add DB indexes for score queries + - [ ] Run load test: simulate 10,000 concurrent API requests + - [ ] Verify: read latency <200ms p95, recalc latency <2s p95 + - [ ] Optimize factor queries (materialized view for frequently-accessed metrics if needed) + - **Sub-Agent**: Use `backend-architect` + +### Phase 9: Spin-Off Preparation (Week 14) + +- [ ] **Task 20**: Abstract Core Dependencies for Spin-Off + - [ ] Create `ScoreDataProvider` adapter interface + - [ ] Implement `FinaFlowDataProvider` (direct DB queries) + - [ ] Document `RestApiDataProvider` specification for future standalone deployment + - [ ] Create OpenAPI 3.1 specification for public API + - [ ] Document all configuration points for standalone deployment (env vars, DB connection, auth) + - **Sub-Agent**: Use `backend-architect` + +--- + +# Task Dependencies + +- [Task 1] has no dependencies (schema foundation) +- [Task 2] depends on [Task 1] +- [Tasks 3–6] depend on [Task 2] (can be done in parallel) +- [Task 7] depends on [Tasks 3, 4, 5, 6] +- [Task 8] depends on [Task 7] +- [Task 9] depends on [Task 8] +- [Task 10] has no dependencies on engine (can be done in parallel with Phase 2) +- [Task 11] depends on [Tasks 8, 10] +- [Task 12] depends on [Task 8] +- [Tasks 13–17] depend on [Task 11] (can be done in parallel) +- [Task 18] depends on [Task 11] +- [Task 19] depends on [Tasks 8, 11] +- [Task 20] depends on [Task 8] (can be done in parallel with frontend) diff --git a/api/boot.ts b/api/boot.ts index 52ac6fe..2b7a278 100644 --- a/api/boot.ts +++ b/api/boot.ts @@ -89,8 +89,8 @@ app.use("/api/trpc*", trpcRateLimiter, apiLimiter); app.use("/api/trpc*", async (c) => { const method = c.req.method; - const url = c.req.url; - console.log(`[trpc-server] --> ${method} ${url}`); + const path = new URL(c.req.url).pathname; + console.log(`→ ${method} ${path}`); try { const response = await fetchRequestHandler({ endpoint: "/api/trpc", @@ -98,14 +98,10 @@ app.use("/api/trpc*", async (c) => { router: appRouter, createContext, }); - const clone = response.clone(); - const bodyText = await clone.text().catch(() => ""); - console.log( - `[trpc-server] <-- ${method} ${url} -> ${response.status} body=${bodyText.slice(0, 300)}`, - ); + console.log(`← ${method} ${path} ${response.status}`); return response; } catch (err) { - console.error(`[trpc-server] <-- ${method} ${url} ERROR:`, err); + console.error(`✗ ${method} ${path}`, err); return c.json({ error: "Internal server error" }, 500); } }); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 3e1330c..f9209a6 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -6,7 +6,7 @@ export function cn(...inputs: ClassValue[]) { } export function formatKES(amount: string | number): string { - const num = typeof amount === "string" ? parseFloat(amount) : amount; + const num = typeof amount === "string" ? parseFloat(amount.replace(/,/g, "")) : amount; if (isNaN(num)) return "KES 0.00"; return `KES ${num.toLocaleString("en-KE", { minimumFractionDigits: 2, diff --git a/src/pages/Accounts.tsx b/src/pages/Accounts.tsx index ec10809..a594c76 100644 --- a/src/pages/Accounts.tsx +++ b/src/pages/Accounts.tsx @@ -263,7 +263,7 @@ export function Accounts() { Add Account
-
+
void; busines
-
+
setSelectedDate(e.target.value)} className="w-auto" />
-
+
Bills Due

{formatKES(billsTotal)}

{payments?.billPayments?.length ?? 0} bills

Expenses

{formatKES(expensesTotal)}

{payments?.expenses?.length ?? 0} entries

Payroll

{formatKES(payrollTotal)}

{payments?.payroll?.length ?? 0} periods

diff --git a/src/pages/DailySales.tsx b/src/pages/DailySales.tsx index f7541bb..fb0dc55 100644 --- a/src/pages/DailySales.tsx +++ b/src/pages/DailySales.tsx @@ -410,7 +410,7 @@ export function DailySales() { )} {/* Card View */} -
+
{sales?.map((sale) => { const isExpanded = expandedSale === sale.id; const locationName = locations?.find(l => l.id === sale.locationId)?.name ?? "Unknown"; diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index ceae449..42db7b3 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -315,14 +315,14 @@ function KpiCard({ }) { return ( - +
-
-

+

+

{title}

{formatKES(value)}

-

{subtitle}

+

{subtitle}

-
{icon}
+
{icon}
diff --git a/src/pages/Expenses.tsx b/src/pages/Expenses.tsx index 9d5a86a..a98b1bf 100644 --- a/src/pages/Expenses.tsx +++ b/src/pages/Expenses.tsx @@ -538,7 +538,7 @@ export function Expenses() {
{/* Per-Branch Breakdown */} -
+
{locations?.map(loc => { const locBalance = accountBalances?.byLocation[loc.id] ?? 0; const locIncome = prevDayIncome?.byBranch.find(b => b.locationId === loc.id)?.total ?? "0"; diff --git a/src/pages/Feedback.tsx b/src/pages/Feedback.tsx index 120aab8..7a21a72 100644 --- a/src/pages/Feedback.tsx +++ b/src/pages/Feedback.tsx @@ -104,7 +104,7 @@ export function Feedback() { )}
-
+
{questionnaires?.map(q => ( diff --git a/src/pages/JournalEntries.tsx b/src/pages/JournalEntries.tsx index 22ef95b..29f63ea 100644 --- a/src/pages/JournalEntries.tsx +++ b/src/pages/JournalEntries.tsx @@ -365,7 +365,7 @@ function JournalEntryForm({ onSuccess, businessId }: { onSuccess: () => void; bu return ( -
+
-
+
{locations?.map(loc => { const mpesaAcct = accounts?.find(a => a.id === loc.defaultMpesaAccountId); const cashAcct = accounts?.find(a => a.id === loc.defaultCashAccountId); diff --git a/src/pages/Mpesa.tsx b/src/pages/Mpesa.tsx index 3deae6e..55df11d 100644 --- a/src/pages/Mpesa.tsx +++ b/src/pages/Mpesa.tsx @@ -255,7 +255,7 @@ export function Mpesa() { Transaction Fee Analysis -
+
{feeAnalysis.feesByType?.map((ft: { txnType: string; totalFees: string; count: number }) => (

{ft.txnType}

diff --git a/src/pages/PartnerDashboard.tsx b/src/pages/PartnerDashboard.tsx index 10e560b..8ccc46f 100644 --- a/src/pages/PartnerDashboard.tsx +++ b/src/pages/PartnerDashboard.tsx @@ -68,7 +68,7 @@ export function PartnerDashboard() {
{/* Summary Cards */} -
+
Clients

{clients?.length ?? 0}

diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 2383bdc..4a8de37 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -336,7 +336,7 @@ export function Settings() { -
+
{allPlans.map((planKey) => { const plan = PLAN_DETAILS[planKey]; const isCurrent = subscription?.plan === planKey; diff --git a/src/pages/Suppliers.tsx b/src/pages/Suppliers.tsx index 22011a3..19861a6 100644 --- a/src/pages/Suppliers.tsx +++ b/src/pages/Suppliers.tsx @@ -156,7 +156,7 @@ export function Suppliers() { {tab === "suppliers" && ( <> -
+
@@ -343,7 +343,7 @@ export function Suppliers() { {alerts && alerts.length > 0 && (

Price Alerts ({alerts.length})

-
+
{alerts.map((alert, i) => (

{alert.itemName}

diff --git a/src/pages/Users.tsx b/src/pages/Users.tsx index 34775c1..f854d87 100644 --- a/src/pages/Users.tsx +++ b/src/pages/Users.tsx @@ -348,7 +348,7 @@ export function Users() { Role Reference -
+
{[ { role: "Owner", desc: "Full access to everything. Can reset all transactions." }, { role: "Admin", desc: "Full access except owner-only actions like transaction reset." },