From b611088d104a18adfec1e162ca56842f9a297109 Mon Sep 17 00:00:00 2001 From: Martin Bhuong Date: Tue, 19 May 2026 09:51:50 +0300 Subject: [PATCH 1/4] multi currency and finascore specs --- ...currency-mobile-wallet-aggregation-plan.md | 457 +++++++----- .trae/specs/fina-score-engine/checklist.md | 123 ++++ .trae/specs/fina-score-engine/spec.md | 680 ++++++++++++++++++ .trae/specs/fina-score-engine/tasks.md | 217 ++++++ 4 files changed, 1292 insertions(+), 185 deletions(-) create mode 100644 .trae/specs/fina-score-engine/checklist.md create mode 100644 .trae/specs/fina-score-engine/spec.md create mode 100644 .trae/specs/fina-score-engine/tasks.md 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) From 205b5fe26b1dabc60d208eabd60e788899656c8d Mon Sep 17 00:00:00 2001 From: Martin Bhuong Date: Tue, 19 May 2026 10:30:04 +0300 Subject: [PATCH 2/4] fix(utils): strip commas from formatKES inputs --- src/lib/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, From 163c2ea73fe86610b2073a5599b0f41dcca9a10a Mon Sep 17 00:00:00 2001 From: Martin Bhuong Date: Tue, 19 May 2026 10:49:43 +0300 Subject: [PATCH 3/4] style: standardize grid layouts and refine dashboard KPI styling Adjust grid class ordering across all page components to prioritize column count first, standardize gap sizes for consistent spacing between grid items. Refine KPI card padding, text sizing and internal spacing on the main dashboard to improve mobile responsiveness and visual consistency across the app. --- src/pages/Accounts.tsx | 2 +- src/pages/Bills.tsx | 2 +- src/pages/Businesses.tsx | 2 +- src/pages/Calendar.tsx | 2 +- src/pages/ChartOfAccounts.tsx | 6 +++--- src/pages/DailyPayments.tsx | 2 +- src/pages/DailySales.tsx | 2 +- src/pages/Dashboard.tsx | 12 ++++++------ src/pages/Expenses.tsx | 2 +- src/pages/Feedback.tsx | 2 +- src/pages/JournalEntries.tsx | 2 +- src/pages/Locations.tsx | 2 +- src/pages/Mpesa.tsx | 2 +- src/pages/PartnerDashboard.tsx | 2 +- src/pages/Settings.tsx | 2 +- src/pages/Suppliers.tsx | 4 ++-- src/pages/Users.tsx | 2 +- 17 files changed, 25 insertions(+), 25 deletions(-) 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." }, From cd35cd035278dae2a5cac8d02137217b24303c73 Mon Sep 17 00:00:00 2001 From: Martin Bhuong Date: Tue, 19 May 2026 10:53:49 +0300 Subject: [PATCH 4/4] minimize logging in console --- api/boot.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) 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); } });