Keepfig v1.0
AI-powered invoice extraction and financial management platform
Complete SaaS solution with Next.js 16 frontend, Go backend microservices, document extraction, and intelligent data processing.
π Quick Start
Prerequisites
- Docker & Docker Compose
- Go 1.25+
- Node.js 18+
- PostgreSQL 15+
- Redis 7+
Installation
# Clone repository
git clone https://github.com/yourusername/invoice-pro.git
cd invoice-pro
# Install dependencies
./deploy.sh install
# Start development
docker-compose up -d
Access Points
| Service | URL | Purpose |
|---|---|---|
| Frontend | http://localhost:3000 | Next.js web app |
| Backend API | http://localhost:8080 | gRPC-Web gateway |
| PostgreSQL | localhost:5432 | Primary database |
| Redis | localhost:6379 | Cache & rate limiting |
ποΈ Architecture
System Overview
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Frontend (Next.js 16) β
β β’ App Router, TypeScript, Tailwind CSS β
β β’ Connect-RPC gRPC client β
β β’ React Query for state management β
ββββββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β HTTP/gRPC-Web
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Backend Services (Go) β
β ββββββββββββββββββββββββββββββββββββββββββββ β
β β Rate Limiter (Redis) β β
β ββββββββββββββββ¬ββββββββββββββββββββββββββββ β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββ β
β β Authentication (JWT) β β
β ββββββββββββββββ¬ββββββββββββββββββββββββββββ β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββ β
β β Business Logic Handlers β β
β β β’ Accounting Service β β
β β β’ Import Service β β
β β β’ Integration Service β β
β β β’ AI Agent Service β β
β ββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β
ββββββββββββββΌβββββββββββββ
βΌ βΌ βΌ
βββββββββββ ββββββββββ ββββββββββββ
βPostgreSQLβ β Redis β β AI APIs β
β(Primary) β β(Cache) β β(OpenAI) β
βββββββββββ ββββββββββ ββββββββββββ
π¦ Data Import System Complete
Overview
Universal CSV/Excel import with AI-powered column mapping. Supports any provider (Paystack, Stripe, Flutterwave, Square) with automatic field detection and transformation.
Key Features
- β CSV & Excel (.xlsx, .xls) file upload
- β AI-powered column mapping with 70-95% accuracy
- β Template matching for instant imports (100% confidence, zero AI cost)
- β Pre-seeded templates for Stripe and Paystack
- β Value transformations (divide_by, currency conversion, value_mapping)
- β Duplicate detection via ExternalTransactionSource
- β Multi-step wizard UI with progress tracking
- β Integration-aware imports (standalone + from integrations page)
Import Strategy
| Method | Speed | Cost | Accuracy |
|---|---|---|---|
| Template Matching | Instant | FREE | 100% |
| Rule-Based Patterns | Instant | FREE | 70-95% |
| AI Fallback | ~2s | ~$0.02/file | 85% |
| 90-day Cache | Instant | FREE | 95% hit |
Database Schema
import_sessions
- id, tenant_id, filename, status
- total_rows, processed_rows, error_rows
- integration_id, provider, template_id
import_field_mappings
- session_id, source_column, target_entity, target_field
- mapping_strategy (template/ai/rules)
- confidence_score, transformations (JSONB)
import_errors
- session_id, row_number, field_name, error_message
import_templates
- name, description, provider
- column_mappings (JSONB), is_public
mapping_cache
- cache_key (hash of columns), mapping_result (JSONB)
- hit_count, expires_at
API Endpoints (gRPC)
UploadImportFile- Upload CSV/Excel, start analysisGetImportMappings- Get AI/template mappingsUpdateFieldMapping- Manual mapping adjustmentConfirmMappings- Lock mappings, optionally save templateExecuteImport- Process file, create payments/entriesGetImportSession- Session status & progressListImportSessions- All imports with paginationGetImportErrors- Row-level error detailsListImportTemplates- Available templatesCreateImportTemplate- Save mappings for reuseDeleteImportTemplate- Remove templateCancelImport- Abort running import
Frontend Components
frontend/src/
βββ app/dashboard/data-imports/
β βββ page.tsx (list all imports)
β βββ new/page.tsx (import wizard)
β βββ [id]/page.tsx (import details)
β βββ templates/page.tsx (manage templates)
βββ components/data-imports/
β βββ import-wizard.tsx (multi-step wizard)
β βββ upload-step.tsx
β βββ review-step.tsx
β βββ execute-step.tsx
βββ hooks/
β βββ use-imports.ts (8 React hooks)
βββ lib/api/
βββ imports.ts (ImportsAPI client)
Usage Example
// Upload file
const session = await ImportsAPI.uploadFile({
tenantId: 'uuid',
file: csvFile,
integrationId: 'uuid', // optional
provider: 'stripe', // optional
templateId: 'uuid', // optional (instant import)
})
// Get mappings (AI or template)
const { mappings } = await ImportsAPI.getMappings(tenantId, sessionId)
// Adjust if needed
await ImportsAPI.updateMapping({
tenantId,
sessionId,
mappingId: 'uuid',
targetField: 'amount',
isUserConfirmed: true
})
// Execute import
await ImportsAPI.executeImport(tenantId, sessionId)
β‘ Workflow Automation Production Ready
Overview
Visual workflow builder with scheduled execution, AI-powered automation, and seamless integration management. Build complex business processes with drag-and-drop nodes that execute automatically on configurable intervals.
Key Features
- β Visual drag-and-drop workflow builder (V2)
- β Scheduled execution with cron-based automation
- β Workflow activation/pause controls with status tracking
- β
Template variable substitution with
{{variable}}syntax - β Loop node for array iteration with context setting
- β Condition node with 12 comparison operators
- β Transform node for data mapping and manipulation
- β Real Paystack API integration with transaction sync
- β Integration-aware workflow builder with validation
- β 11 node executors with comprehensive functionality
- β Status-based execution control (draft/active/paused)
Workflow Architecture
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Workflow Builder (Frontend V2) β
β β’ Drag-and-drop canvas with zoom/pan β
β β’ Node palette (triggers, actions, logic) β
β β’ Configuration sidebar β
β β’ Integration validation before save β
β β’ Activate/Pause buttons with status badge β
ββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββ
β Save β status: draft
β Activate β /api/workflows/{id}/activate
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Workflow API Routes (Next.js) β
β β’ POST /api/workflows - Create/update β
β β’ POST /activate - Set active + create schedule β
β β’ POST /pause - Set paused + disable schedule β
ββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββ
β gRPC: activateWorkflow, pauseWorkflow
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Backend Workflow Services (Go) β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
β β Workflow Scheduler (robfig/cron/v3) β β
β β β’ Auto-starts with app lifecycle β β
β β β’ Loads active schedules from DB β β
β β β’ Converts polling intervals to cron β β
β β β’ Executes workflows on schedule β β
β β β’ Checks workflow status before execution β β
β β β’ Refresh loop every 5 minutes β β
β ββββββββββββββ¬ββββββββββββββββββββββββββββββββ β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
β β Workflow Runner (Graph Executor) β β
β β β’ Resolves {{template}} variables β β
β β β’ Executes nodes in dependency order β β
β β β’ Manages execution context β β
β β β’ Updates run counts and timestamps β β
β ββββββββββββββ¬ββββββββββββββββββββββββββββββββ β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
β β Node Executors (11 types) β β
β β β’ Triggers: paystack_scheduled_sync β β
β β β’ Actions: create_payment, create_contact β β
β β β’ Logic: condition, loop, transform β β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
Scheduled Execution System
Workflows can be activated to run automatically on configurable intervals. The scheduler uses cron expressions and tracks execution history.
Polling Intervals
| Interval | Cron Expression | Use Case |
|---|---|---|
| Every 15 minutes | */15 * * * * | Real-time transaction sync |
| Every 30 minutes | */30 * * * * | Frequent updates |
| Every hour | 0 * * * * | Hourly reports |
| Every 6 hours | 0 */6 * * * | Periodic sync |
| Daily (midnight) | 0 0 * * * | Daily reconciliation (recommended) |
Workflow Lifecycle
// 1. Save workflow (status: draft)
POST /api/workflows
{
"name": "Auto-import Paystack",
"nodes": [...],
"connections": [...]
}
β Workflow saved, NOT scheduled
// 2. Activate workflow (creates schedule)
POST /api/workflows/{id}/activate
{
"id": "workflow-uuid"
}
β Response:
{
"workflow": { "status": "active", ... },
"scheduleId": "schedule-uuid",
"cronExpression": "0 0 * * *",
"nextRunAt": "2025-11-29T00:00:00Z"
}
β Status badge shows "β Active" (green)
β Scheduler begins executing daily
// 3. Pause workflow (disables schedule)
POST /api/workflows/{id}/pause
{
"id": "workflow-uuid"
}
β Response:
{
"workflow": { "status": "paused", ... },
"success": true
}
β Status badge shows "βΈβΈ Paused" (yellow)
β Schedule preserved but disabled
// 4. Re-activate (re-enables existing schedule)
POST /api/workflows/{id}/activate
β Same schedule reactivated
β Status returns to "active"
Scheduler Implementation
// WorkflowScheduler (Go)
type WorkflowScheduler struct {
cron *cron.Cron
db *database.DB
scheduleMap map[string]cron.EntryID
workflowRunner *WorkflowRunner
}
// Start - Called on app startup
func (s *WorkflowScheduler) Start(ctx context.Context) {
s.cron.Start()
s.LoadActiveSchedules(ctx)
go s.refreshLoop(ctx) // Reload every 5 min
}
// ConvertPollingIntervalToCron
func ConvertPollingIntervalToCron(interval string) string {
cronMap := map[string]string{
"15": "*/15 * * * *", // Every 15 minutes
"30": "*/30 * * * *", // Every 30 minutes
"60": "0 * * * *", // Every hour
"360": "0 */6 * * *", // Every 6 hours
"1440": "0 0 * * *", // Daily at midnight
}
return cronMap[interval]
}
// CreateOrUpdateScheduleForWorkflow
func (s *WorkflowScheduler) CreateOrUpdateScheduleForWorkflow(
workflowID, tenantID uuid.UUID,
pollingInterval string
) (*models.WorkflowSchedule, error) {
cronExpr := ConvertPollingIntervalToCron(pollingInterval)
// Check if schedule exists
var existing models.WorkflowSchedule
if found {
// Update cron and re-add to scheduler
existing.CronExpression = cronExpr
existing.IsActive = true
db.Save(&existing)
s.RemoveSchedule(existing.ID)
s.AddSchedule(ctx, &existing)
return &existing, nil
}
// Create new schedule
return s.CreateSchedule(workflowID, tenantID, cronExpr, "UTC")
}
// executeScheduledWorkflow - Run workflow on schedule
func (s *WorkflowScheduler) executeScheduledWorkflow(
ctx context.Context,
schedule *models.WorkflowSchedule
) {
// Check workflow status
var workflow models.Workflow
db.Where("id = ?", schedule.WorkflowID).First(&workflow)
if workflow.Status != "active" {
log.Printf("Skipping workflow %s: status is '%s'",
schedule.WorkflowID, workflow.Status)
return
}
// Update last_run_at, calculate next_run_at
now := time.Now()
nextRun := cronSchedule.Next(now)
db.Model(schedule).Updates(map[string]interface{}{
"last_run_at": now,
"next_run_at": nextRun,
})
// Execute workflow
s.workflowRunner.ExecuteWorkflow(
ctx,
schedule.WorkflowID,
schedule.TenantID,
models.TriggerTypeSchedule,
nil
)
}
Frontend Activation Controls
// workflow-builder-v2.tsx
const [currentWorkflow, setCurrentWorkflow] = useState<{
id: string
name: string
status?: string // 'draft' | 'active' | 'paused'
}>(null)
const activateWorkflow = async () => {
const response = await fetch(
`/api/workflows/${currentWorkflow.id}/activate`,
{ method: 'POST' }
)
const data = await response.json()
showToast(
`Workflow activated! Next run: ${new Date(data.nextRunAt).toLocaleString()}`,
"success"
)
setCurrentWorkflow({ ...currentWorkflow, status: 'active' })
}
const pauseWorkflow = async () => {
await fetch(`/api/workflows/${currentWorkflow.id}/pause`, { method: 'POST' })
showToast("Workflow paused", "success")
setCurrentWorkflow({ ...currentWorkflow, status: 'paused' })
}
// UI - Status badge
{currentWorkflow?.status && (
<span className={`badge ${
currentWorkflow.status === 'active' ? 'badge-success' :
currentWorkflow.status === 'paused' ? 'badge-warning' :
'badge-secondary'
}`}>
{currentWorkflow.status === 'active' ? 'β Active' :
currentWorkflow.status === 'paused' ? 'βΈβΈ Paused' :
'β Draft'}
</span>
)}
// UI - Activate/Pause button
{currentWorkflow?.status === 'active' ? (
<Button onClick={pauseWorkflow}>Pause</Button>
) : (
<Button onClick={activateWorkflow}>Activate</Button>
)}
Node Executors
| Category | Node Type | Purpose |
|---|---|---|
| Triggers | paystack_sync | Fetch transactions from Paystack API |
| Actions | create_payment | Create payment record in database |
| create_contact | Create customer/vendor contact | |
| create_journal | Create journal entry for accounting | |
| link_external | Link to external transaction source | |
| send_email | Send notification email | |
| Logic | condition | If/else branching with operators |
| loop | Iterate over arrays, set context per item | |
| transform | Data mapping and transformation |
Template Variable System
Use {{variable}} syntax to reference data from previous nodes. Supports dot notation for nested fields.
// Template syntax examples
{{current_transaction.amount}} // Access nested field
{{loop_item.customer.email}} // Access from loop context
{{paystack_transactions[0].status}} // Array indexing
// Context variables
{{loop_item}} // Current item in loop
{{loop_index}} // Current iteration index (0-based)
{{loop_total}} // Total items in array
{{loop_is_first}} // Boolean: first iteration
{{loop_is_last}} // Boolean: last iteration
Condition Operators
| Operator | Example | Use Case |
|---|---|---|
| equals | status == "success" | Exact match |
| not_equals | type != "refund" | Exclusion |
| greater_than | amount > 10000 | Numeric threshold |
| less_than | amount < 1000 | Lower bound |
| contains | email contains "@gmail" | Substring match |
| starts_with | reference starts_with "PAY" | Prefix match |
| ends_with | name ends_with ".pdf" | Suffix match |
| is_empty | description is_empty | Null/empty check |
Integration-Aware Workflow Builder
When creating workflows that require integrations (Paystack, Stripe, etc.), the system automatically validates that integrations are connected before saving.
User Flow
- User drags Paystack Scheduled Sync node onto canvas
- Node configuration shows alert: "β οΈ Integration Required - Connect Paystack Integration β"
- User configures polling interval and transaction filter
- User clicks Save Workflow
- Backend validates that Paystack integration exists and is connected
- If missing: Returns 400 error with provider name
- Frontend shows alert: "Please connect Paystack integration before saving"
- User clicks link β Redirects to integrations page
- User connects Paystack with API key
- Returns to workflow builder and saves successfully β
Backend Validation
// API Route: /api/workflows (POST)
// Extract integration nodes
const integrationNodes = nodes.filter(node =>
node.nodeType === 'paystack_scheduled_sync' ||
node.nodeType === 'stripe_scheduled_sync'
)
// Check each integration
for (const node of integrationNodes) {
const provider = node.nodeType.replace('_scheduled_sync', '')
const integrations = await accountingClient.listIntegrations({})
const hasIntegration = integrations.some(
int => int.provider === provider && int.status === 'connected'
)
if (!hasIntegration) {
return NextResponse.json({
error: 'Missing required integrations',
missingIntegrations: [{ provider }],
message: `Please connect ${provider} before saving`
}, { status: 400 })
}
}
Frontend Node Configuration
// Paystack Sync Node Config UI
{selectedNode.nodeType === "paystack_scheduled_sync" && (
<>
<div className="bg-blue-50 border border-blue-200 rounded p-3">
<AlertCircle className="text-blue-600" />
<span className="font-semibold">Integration Required</span>
<p>This workflow requires a connected Paystack integration.</p>
<a href="/integrations">Connect Paystack Integration β</a>
</div>
<select>
<option value="15">Every 15 minutes</option>
<option value="60">Every hour</option>
<option value="1440">Daily (recommended)</option>
</select>
</>
)}
Example Workflow: Auto-Import Paystack Transactions
{
"name": "Auto-import Paystack Transactions",
"nodes": [
{
"id": "node_1",
"nodeType": "paystack_scheduled_sync",
"config": {
"polling_interval": "1440",
"from_date": "{{today - 7 days}}",
"to_date": "{{today}}",
"transaction_filter": "success_only"
}
},
{
"id": "node_2",
"nodeType": "loop",
"config": {
"array_path": "paystack_transactions.transactions"
}
},
{
"id": "node_3",
"nodeType": "condition",
"config": {
"field": "{{loop_item.amount}}",
"operator": "greater_than",
"value": 10000
}
},
{
"id": "node_4",
"nodeType": "create_payment",
"config": {
"amount": "{{loop_item.amount}}",
"reference": "{{loop_item.reference}}",
"customer_email": "{{loop_item.customer.email}}",
"payment_type": "received",
"payment_method": "card"
}
},
{
"id": "node_5",
"nodeType": "create_journal",
"config": {
"description": "Payment received: {{loop_item.reference}}",
"lines": [
{
"account_code": "1010",
"debit": "{{loop_item.amount}}"
},
{
"account_code": "4010",
"credit": "{{loop_item.amount}}"
}
]
}
}
],
"connections": [
{ "sourceId": "node_1", "targetId": "node_2" },
{ "sourceId": "node_2", "targetId": "node_3" },
{ "sourceId": "node_3", "targetId": "node_4", "type": "true_branch" },
{ "sourceId": "node_4", "targetId": "node_5" }
]
}
Workflow Execution Flow
- Scheduler triggers workflow (daily at midnight)
- Paystack Sync Node fetches last 7 days of transactions via API
- Loop Node iterates over each transaction, sets
{{loop_item}} - Condition Node checks if amount > β¦10,000
- Create Payment Node creates payment record with resolved values
- Create Journal Node generates double-entry journal entry
- Workflow completes, logs execution summary
Paystack API Integration
Real HTTP integration to api.paystack.co/transaction with pagination, date filtering, and Bearer authentication.
// Paystack Sync Executor (Go)
func (e *PaystackSyncExecutor) Execute(ctx context.Context, ...) {
// 1. Get integration credentials
integration := getIntegration(tenantID, "paystack")
// 2. Fetch transactions from Paystack
transactions := fetchPaystackTransactions(
integration.SecretKey,
config.FromDate,
config.ToDate,
100 // perPage
)
// 3. Transform API response
for _, txn := range transactions {
amount := txn.Amount / 100 // kobo to naira
processedTxns = append(processedTxns, {
"amount": amount,
"reference": txn.Reference,
"status": txn.Status,
"customer": {
"email": txn.Customer.Email,
"name": txn.Customer.FirstName + " " + txn.Customer.LastName
}
})
}
// 4. Store in execution context
return map[string]interface{}{
"paystack_transactions": {
"transactions": processedTxns,
"total": len(processedTxns)
}
}
}
Performance
- Template Resolution: ~50Β΅s per variable
- Node Execution: ~200Β΅s per node (avg)
- Paystack API: ~1.5s per 100 transactions
- Loop Iteration: ~100Β΅s per item
- Total Workflow: ~5s for 1000 transactions
Files
| File | Lines | Purpose |
|---|---|---|
| workflow_scheduler.go | 337 | Cron scheduler with lifecycle management |
| workflow_runner.go | 380 | Graph execution engine (stubs removed) |
| executors/paystack_sync.go | 210 | Paystack API integration |
| executors/loop.go | 145 | Array iteration with context |
| executors/condition.go | 217 | If/else branching logic |
| executors/transform.go | 180 | Data transformation |
| workflows.go | 851 | gRPC handlers (activate/pause added) |
| accounting.proto | 2660+ | Proto definitions (activate/pause RPCs) |
| workflow-builder-v2.tsx | 2245 | Visual builder with activation controls |
| /api/workflows/.../activate | 29 | Activation endpoint |
| /api/workflows/.../pause | 27 | Pause endpoint |
π§ Email System Production Ready
Overview
Template-based email system with Sparkpost integration, dynamic recipients, file attachments, and workflow automation. All emails use HTML templates with automatic plain text generation.
Key Features
- β Template-only email system (no custom HTML)
- β Sparkpost API integration with sandbox mode
- β Dynamic recipient resolution (tenant_admin, user, contact, custom)
- β File attachments (CSV, PDF, receipts)
- β
Variable substitution with
{{variable}}syntax - β Multiple recipients (To, CC, BCC)
- β Auto-generated plain text from HTML
- β Beautiful responsive email templates
- β Workflow integration via send_email node
- β Schema-driven frontend config UI (composable, dynamic, no hardcoded conditions)
Email Architecture
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Workflow Email Node (send_email) β
β β’ Template selection (required) β
β β’ Recipient configuration β
β β’ Subject with variables β
β β’ Attachment configuration β
ββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Email Executor (Go Backend) β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
β β 1. Resolve Recipients β β
β β β’ tenant_admin β Tenant owner β β
β β β’ user β From workflow context β β
β β β’ contact β From payment/transaction β β
β β β’ custom β Static email list β β
β ββββββββββββββ¬ββββββββββββββββββββββββββββββββ β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
β β 2. Prepare Template Data β β
β β β’ Flatten payment/contact data β β
β β β’ Add workflow metadata β β
β β β’ Merge custom data from config β β
β β β’ Replace {{variables}} β β
β ββββββββββββββ¬ββββββββββββββββββββββββββββββββ β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
β β 3. Render Template (html/template) β β
β β β’ Load from templates/emails/ β β
β β β’ Execute with template data β β
β β β’ Generate plain text version β β
β ββββββββββββββ¬ββββββββββββββββββββββββββββββββ β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
β β 4. Generate Attachments β β
β β β’ payment_receipt β PDF/HTML β β
β β β’ csv_export β CSV from data β β
β β β’ financial_report β Comprehensive PDF β β
β β β’ Base64 encode for API β β
β ββββββββββββββ¬ββββββββββββββββββββββββββββββββ β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
β β 5. Send via Sparkpost API β β
β β β’ POST /transmissions β β
β β β’ Multiple recipients (To/CC/BCC) β β
β β β’ Attachments with base64 encoding β β
β β β’ Sandbox mode for development β β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
Built-in Email Templates
| Template | Purpose | Key Variables |
|---|---|---|
payment_received |
Payment confirmation | amount, currency, reference, transaction_date |
workflow_success |
Workflow completion | workflow_name, execution_id, nodes_executed, duration |
workflow_failed |
Workflow failure alert | workflow_name, failed_node, error_message |
financial_report |
Financial summary | revenue, expenses, net_income, top_expenses, top_revenue |
Recipient Resolution Types
| Type | Description | Configuration |
|---|---|---|
tenant_admin |
Send to tenant owner/administrator | {"type": "tenant_admin"} |
user |
Extract from workflow context | {"type": "user", "email_field": "user.email", "name_field": "user.name"} |
contact |
From payment/transaction data | {"type": "contact", "email_field": "created_payment.contact_email"} |
custom |
Static email list | {"type": "custom", "emails": ["admin@company.com"]} |
Email Node Configuration Example
// Payment notification with receipt
{
"type": "send_email",
"config": {
"template": "payment_received",
"recipients": {
"type": "contact",
"email_field": "created_payment.contact_email",
"name_field": "created_payment.contact_name",
"cc_tenant": true // CC tenant admin
},
"subject": "Payment Received - {{reference}}",
"attachments": [
{
"type": "payment_receipt",
"filename": "receipt_{{reference}}.pdf"
}
],
"data": {
"recipient_name": "{{contact_name}}",
"dashboard_url": "https://app.company.com"
}
}
}
Attachment Types
| Type | Generated Content | Use Case |
|---|---|---|
payment_receipt |
HTML/PDF receipt | Payment confirmations |
csv_export |
CSV from data array | Transaction reports, data exports |
financial_report |
Comprehensive PDF | Monthly/quarterly reports |
Variable Substitution
Use {{variable}} syntax with dot notation for nested values:
// Automatically available variables
{{workflow_id}} // Current workflow ID
{{execution_id}} // Current execution ID
{{tenant_name}} // Tenant company name
{{current_date}} // YYYY-MM-DD format
{{current_datetime}} // RFC3339 format
{{dashboard_url}} // Dashboard base URL
// Flattened payment data (from created_payment)
{{amount}} // Payment amount
{{currency}} // Payment currency (USD, NGN, etc.)
{{reference}} // Payment reference
{{description}} // Payment description
{{payment_method}} // Payment method (card, bank_transfer)
{{transaction_date}} // Transaction date
{{payment_id}} // Payment UUID
// Flattened contact data (from created_contact)
{{contact_name}} // Contact full name
{{contact_email}} // Contact email
{{contact_phone}} // Contact phone
// Nested access from workflow context
{{created_payment.metadata.custom_field}}
{{loop_item.customer.name}}
Environment Configuration
# Sparkpost API Configuration
SPARKPOST_API_KEY=your_sparkpost_api_key
SPARKPOST_API_URL=https://api.sparkpost.com/api/v1
# Email Defaults
EMAIL_FROM_ADDRESS=noreply@yourdomain.com
EMAIL_FROM_NAME=Keepfig
# Environment (affects sandbox mode)
ENVIRONMENT=production # development enables sandbox mode
Sparkpost Setup Steps
- Get API Key: Log in to Sparkpost dashboard β API Keys β Create New Key
- Verify Domain: Account β Sending Domains β Add Domain
- Add DNS Records: Copy DKIM and SPF records to Namecheap DNS
- DKIM: TXT record like
scph1234._domainkeyβv=DKIM1; k=rsa; p=... - SPF: TXT record @ β
v=spf1 include:sparkpostmail.com ~all - Verify: Wait 24-48h for DNS propagation, click Verify in Sparkpost
- Test: Use sandbox mode (ENVIRONMENT=development) for testing
Pricing
- β First 500 emails/month: FREE
- β 501-100K emails: $0.20 per 1,000 emails
- β Very cost-effective for transactional emails
Schema-Driven Configuration UI
The workflow builder uses a dynamic, composable configuration system instead of hardcoded conditions for each node type. This makes it easy to add new node types without frontend code changes.
How It Works
- Backend Schema: Node types define
config_schemain JSON (e.g.,actions.json) - Dynamic Renderer:
DynamicConfigRenderercomponent reads the schema and generates UI - Field Types: text, textarea, select, boolean, number, array, object (nested configs)
- Conditional Fields:
required_whenshows/hides fields based on parent values - Template Metadata: Each email template option includes
required_data,suggested_recipients,supports_attachments
Example: send_email Config Schema
{
"template": {
"type": "select",
"label": "Email Template",
"required": true,
"options": [
{
"value": "payment_received",
"label": "Payment Received",
"description": "Payment confirmation with receipt",
"required_data": ["amount", "currency", "reference"],
"suggested_recipients": ["contact"],
"supports_attachments": ["payment_receipt"]
}
]
},
"recipients": {
"type": "object",
"properties": {
"type": {
"type": "select",
"options": [
{ "value": "tenant_admin", "label": "Tenant Owner/Admin" },
{ "value": "user", "label": "User from Context" },
{ "value": "contact", "label": "Contact from Payment" },
{ "value": "custom", "label": "Custom Email List" }
]
},
"email_field": {
"type": "text",
"required_when": ["user", "contact"],
"placeholder": "team_member.email"
}
}
}
}
Benefits
- β No Frontend Code Changes: Add new node types by updating JSON only
- β Consistent UI: All nodes use same styling and behavior
- β Self-Documenting: Schema includes labels, descriptions, placeholders
- β Type-Safe: Field types ensure correct data format
- β Conditional Logic: Show/hide fields based on other values
- β Maintainable: Eliminates 1000+ lines of hardcoded if/else conditions
frontend/src/components/workflow/dynamic-config-renderer.tsx
Common Workflow Patterns
1. Payment Notification Flow
paystack_sync β create_payment β send_email (payment_received)
β
Store payment in DB
β
Email customer with receipt
2. Workflow Status Notifications
// Success notification
[workflow completes] β send_email (workflow_success to admin)
// Failure notification
[workflow fails] β send_email (workflow_failed to admin)
3. Scheduled Reports
[cron: daily at midnight]
β
generate_report_data
β
send_email (financial_report with CSV attachment)
4. Conditional Notifications
create_payment
β
condition (amount > 10000)
β (true)
send_email (high_value_payment alert to CFO)
π Rate Limiting Complete
Overview
Multi-tier token bucket rate limiting with Redis. Protects API from abuse with automatic retry and user-friendly feedback.
Architecture
- β Global limiting (10k req/min)
- β Tenant limiting (1k req/min)
- β User limiting (200 req/min)
- β Method-specific limits (AI: 100/min, uploads: 200/min, bulk: 50/min)
- β Frontend: Automatic retry with exponential backoff
- β Frontend: Toast notifications with countdown
Default Limits
| Tier | Requests/Min | Burst |
|---|---|---|
| Global | 10,000 | 100 |
| Tenant | 1,000 | 50 |
| User | 200 | 20 |
| AI Chat | 100 | 10 |
| File Upload | 200 | 20 |
| Bulk Ops | 50 | 5 |
Backend Implementation
// Go - Rate limiter with token bucket
rateLimiter := middleware.NewRateLimiter(redisClient, config)
// gRPC interceptor chain
grpcSrv := grpc.NewServer(
grpc.ChainUnaryInterceptor(
rateLimiter.Unary(), // Rate limit FIRST
authInterceptor.Unary(), // Auth second
),
)
Frontend Integration
// Automatic retry on rate limit
import { withRateLimitRetry } from '@/lib/rate-limit-handler'
const result = await withRateLimitRetry(async () => {
return await fetch('/api/endpoint')
})
// Retries 3x with backoff: 1s β 2s β 4s
// Toast notification
import { useRateLimitToast } from '@/components/ui/rate-limit-toast'
const { toast, showToast } = useRateLimitToast()
if (isRateLimited) {
showToast("Rate limit reached, retrying in 3s...", 3)
}
Performance
- Latency: ~187Β΅s per check
- Throughput: 100k+ checks/sec
- Memory: ~200 bytes per key
- Tests: 8/8 passing
π€ AI Chatbot Enhanced
Overview
Natural language interface for financial queries. Powered by OpenAI with context-aware function calling.
Features
- β Natural language queries ("Show me Q3 revenue")
- β Context-aware responses with tenant data
- β Function calling for database queries
- β Conversation history with session management
- β Real-time streaming responses
Available Functions
| Function | Purpose |
|---|---|
| get_dashboard_stats | Revenue, expenses, profit overview |
| list_transactions | Recent transactions with filters |
| get_revenue_trend | Time-series revenue analysis |
| get_expense_breakdown | Category-wise expense breakdown |
| search_payments | Find specific payments |
| get_mrr_report | Monthly recurring revenue metrics |
Usage
// Frontend - Chat with AI
import { accountingClientBrowser } from '@/lib/connect-rpc-browser'
const stream = accountingClientBrowser.chatWithAI({
tenantId: 'uuid',
sessionId: 'uuid',
userMessage: 'What was my revenue last month?'
})
for await (const response of stream) {
console.log(response.assistantMessage)
}
π Integrations Active
Supported Providers
| Provider | Type | Features |
|---|---|---|
| Paystack | API + CSV | Live sync, transaction history, webhooks |
| Stripe | CSV | Balance history import, template matching |
| Flutterwave | CSV | Transaction export import |
| Square | CSV | Payment export import |
| Generic | CSV | Any provider with AI mapping |
Integration Architecture
βββββββββββββββββββββββββββββββββββββββββββ
β Integration Manager β
βββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββββββ ββββββββββββββββ β
β β API Sync β β CSV Import β β
β β (Paystack) β β (All) β β
β ββββββββ¬ββββββββ ββββββββ¬ββββββββ β
β β β β
β βΌ βΌ β
β βββββββββββββββββββββββββββββββββββ β
β β ExternalTransactionSource β β
β β (Deduplication & Linking) β β
β βββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββββββββββββββ β
β β Payment & Journal Entry β β
β βββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββ
Dual Entry Points
- From Integrations Page: Click "Import Data" on connected integration β Pre-fills integrationId
- Standalone: Data Imports β New Import β Provider selection dropdown
π Pitch Deck Generation Active
Overview
AI-generated investor pitch decks with live financial metrics. Connect data sources, answer questions, get presentation-ready slides.
Features
- β AI-powered slide generation from financial data
- β Real-time metrics (MRR, burn rate, runway)
- β Customizable templates and branding
- β Export to PDF/PowerPoint
- β Version history and collaboration
Data Sources
- Dashboard stats (revenue, expenses, profit)
- Transaction history and trends
- MRR and subscription metrics
- User-provided business context
- Uploaded presentation assets (logos, charts)
βοΈ Backend Services
Service Architecture
backend/
βββ cmd/
β βββ server/
β βββ main.go (gRPC server, interceptors)
βββ internal/
β βββ accountingserver/ (20+ gRPC handlers)
β β βββ accounts.go
β β βββ payments.go
β β βββ journalentries.go
β β βββ reconciliation.go
β β βββ ai_agent.go
β β βββ ...
β βββ services/ (Business logic)
β β βββ import_parser_service.go
β β βββ import_mapper_service.go
β β βββ import_service.go
β β βββ ai_agent_service.go
β β βββ ...
β βββ middleware/
β β βββ ratelimit.go (Rate limiting)
β β βββ auth.go (JWT validation)
β βββ database/
β β βββ database.go (GORM setup)
β βββ models/
β βββ models.go (50+ database models)
βββ proto/
βββ accounting.proto (gRPC definitions)
Key Services
| Service | Purpose | LOC |
|---|---|---|
| ImportParserService | CSV/Excel parsing & validation | 400 |
| ImportMapperService | AI/template column mapping | 500 |
| ImportService | Import workflow orchestration | 400 |
| AIAgentService | Chatbot with function calling | 600 |
| PaymentService | Payment CRUD & reconciliation | 300 |
| JournalService | Double-entry accounting | 450 |
Database Models (50+)
- Tenant, User, TenantMembership
- Account, Transaction, Payment
- JournalEntry, JournalLine
- ImportSession, ImportFieldMapping, ImportError, ImportTemplate
- Integration, ExternalTransactionSource
- ExpensePolicy, ExpenseClaim, ExpenseApproval
- AIQueryHistory, Subscription, PitchData
π¨ Frontend App
Tech Stack
- Framework: Next.js 16 (App Router)
- Language: TypeScript
- Styling: Tailwind CSS v4
- State: React Query (@tanstack/react-query)
- API: Connect-RPC (gRPC-Web)
- Forms: React Hook Form + Zod
- Charts: Recharts
Structure
frontend/src/
βββ app/
β βββ (auth)/
β β βββ login/page.tsx
β β βββ register/page.tsx
β βββ dashboard/
β βββ page.tsx (dashboard)
β βββ payments/
β βββ journal-entries/
β βββ accounts/
β βββ data-imports/
β βββ integrations/
β βββ settings/
βββ components/
β βββ ui/ (shadcn components)
β βββ dashboard/
β βββ data-imports/
β βββ pitch-deck/
βββ lib/
β βββ connect-rpc-browser.ts (gRPC client)
β βββ rate-limit-handler.ts
β βββ api/
β βββ imports.ts
βββ hooks/
β βββ use-imports.ts (React hooks)
βββ gen/
βββ accounting_pb.ts (Protobuf messages)
βββ accounting_connect.ts (gRPC service)
ποΈ Database Schema
Core Tables
-- Tenants & Users
tenants (id, name, base_currency, created_at)
users (id, email, password_hash, created_at)
tenant_memberships (tenant_id, user_id, role)
-- Accounting
accounts (id, tenant_id, name, type, code, parent_id)
transactions (id, tenant_id, amount, date, type, status)
payments (id, tenant_id, amount, date, type, payment_method)
journal_entries (id, tenant_id, date, description, status)
journal_lines (entry_id, account_id, debit, credit)
-- Data Imports
import_sessions (id, tenant_id, filename, status, total_rows)
import_field_mappings (id, session_id, source_column, target_field)
import_errors (id, session_id, row_number, error_message)
import_templates (id, name, provider, column_mappings)
mapping_cache (cache_key, mapping_result, expires_at)
-- Integrations
integrations (id, tenant_id, provider, status, credentials)
external_transaction_sources (integration_id, external_id, transaction_id)
-- AI & Analytics
ai_query_history (id, tenant_id, user_id, query, response)
subscriptions (id, tenant_id, plan, mrr, status)
pitch_data (tenant_id, data_type, content)
π Production Deployment
Deployment Script
# Install dependencies
./deploy.sh install
# Deploy to production
./deploy.sh deploy
# Setup SSL certificate
./deploy.sh ssl yourdomain.com
# Status check
./deploy.sh status
Docker Compose Services
| Service | Image | Purpose |
|---|---|---|
| frontend | node:18-alpine | Next.js app |
| backend | golang:1.25-alpine | gRPC server |
| extractor | golang:1.25-alpine | Document AI |
| postgres | postgres:15-alpine | Database |
| redis | redis:7-alpine | Cache & rate limiting |
| nginx | nginx:alpine | Reverse proxy |
Environment Variables
# Backend (.env)
DATABASE_URL=postgres://user:pass@postgres:5432/invoice_pro_db
REDIS_URL=redis://redis:6379
JWT_SECRET=your-secret-key
OPENAI_API_KEY=sk-...
PAYSTACK_SECRET_KEY=sk_...
# Frontend (.env.local)
NEXT_PUBLIC_GRPC_WEB_URL=http://localhost:8080
NEXT_PUBLIC_API_URL=http://localhost:3000/api
Resource Limits (Optimized for $6-24/mo droplets)
| Service | CPU | Memory |
|---|---|---|
| Frontend | 0.5 | 512MB |
| Backend | 1.0 | 1GB |
| Extractor | 0.5 | 512MB |
| PostgreSQL | 1.0 | 1GB |
| Redis | 0.25 | 256MB |
| Nginx | 0.25 | 128MB |
π Monitoring
Health Checks
# Check all services
docker-compose ps
# View logs
docker-compose logs -f backend
docker-compose logs -f frontend
# Database connection
docker-compose exec postgres psql -U postgres -d invoice_pro_db
# Redis status
docker-compose exec redis redis-cli INFO
Key Metrics
- Rate Limiting: Monitor ResourceExhausted errors (code 8)
- Import System: Track import_sessions.status distribution
- API Performance: gRPC response times < 100ms average
- Database: Connection pool usage, slow queries
- Redis: Memory usage, eviction rate
- Always backup PostgreSQL before major updates
- Monitor disk usage (imports create temp files)
- Set up log rotation for Docker containers
- Test SSL renewal process (Let's Encrypt auto-renews)
π‘οΈ Fraud Detection API Standard
The Fraud Detection API follows industry best practices from ISO 31000 (Risk Management), ACFE (Association of Certified Fraud Examiners), and PCI DSS fraud detection guidelines.
Response Structure Overview
All verification responses follow a standardized structure with clear decisions, signals, and recommendations:
message VerificationResult {
// Identifiers
string verification_id = 1;
string document_hash_id = 2;
// Decision (ACCEPT/REVIEW/REJECT)
VerificationDecision decision = 5;
string recommendation = 6;
// Risk Assessment
double risk_score = 7; // 0.0 to 1.0
string risk_level = 8; // low, medium, high, critical
double confidence = 9; // 0.0 to 1.0
// Individual fraud indicators
repeated DetectionSignal signals = 10;
// Duplicate detection results
DuplicateAnalysis duplicate_analysis = 11;
// Visual similarity (recurring vendors)
SimilarityAnalysis similarity_analysis = 12;
// Processing metadata
ProcessingInfo processing_info = 13;
}
Decision Types
| Decision | Risk Level | Action | Use Case |
|---|---|---|---|
| ACCEPT | LOW | Process automatically | Document verified, no risk detected |
| REVIEW | MEDIUM/HIGH | Manual review required | Elevated risk, human verification needed |
| REJECT | CRITICAL | Block/reject document | Confirmed duplicate or critical fraud indicators |
Detection Signals
Each signal represents an individual fraud indicator with its own confidence and risk contribution:
message DetectionSignal {
string signal_type = 1; // duplicate_hash, visual_similarity, etc.
string category = 2; // duplicate_detection, image_forensics, etc.
string severity = 3; // info, low, medium, high, critical
string description = 4; // Human-readable description
double confidence = 5; // 0.0 to 1.0
double risk_contribution = 6; // How much this adds to risk score
map<string, string> metadata = 7;
string phase = 8; // Which phase detected this signal
}
Signal Categories
- duplicate_detection: Exact hash match, invoice number match, metadata-verified duplicate
- image_forensics: EXIF analysis, ELA (Error Level Analysis), copy-move detection
- pdf_forensics: Metadata extraction, incremental updates, structure analysis
- ocr_extraction: Invoice number, amount, merchant name extraction
Common Signal Types
| Signal Type | Severity | Risk | Description |
|---|---|---|---|
duplicate_document |
CRITICAL | 0.6 | Confirmed duplicate (metadata verified) |
duplicate_invoice_number |
CRITICAL | 0.6 | Invoice number already exists in system |
visual_similarity |
INFO | 0.0 | Similar to previous doc (likely recurring vendor) |
copy_move_detected |
HIGH | 0.4 | Cloned regions found (possible manipulation) |
ela_suspicious |
MEDIUM | 0.3 | Error Level Analysis indicates editing |
no_exif_data |
LOW | 0.1 | Image missing EXIF metadata |
Duplicate vs Similarity Analysis
DuplicateAnalysis - True Duplicates
Confirmed duplicates that should be rejected:
message DuplicateAnalysis {
bool is_duplicate = 1;
string detection_method = 2; // "exact_hash", "invoice_number", "metadata_verified"
repeated string duplicate_document_ids = 3;
double match_confidence = 4; // Typically 0.95+ for true duplicates
string explanation = 5;
}
- Exact Hash Match: SHA-256 identical (100% same file)
- Invoice Number Match: Same invoice number in database
- Metadata Verified: Visual similarity + same amount + same date + same reference number
SimilarityAnalysis - Recurring Vendors
Visual similarity that's NOT a duplicate (legitimate recurring transactions):
message SimilarityAnalysis {
bool has_similar_documents = 1;
int32 similar_document_count = 2;
repeated SimilarDocument similar_documents = 3;
string interpretation = 4; // "recurring_vendor", "same_template"
string recommendation = 5;
}
message SimilarDocument {
string document_id = 1;
double visual_similarity_score = 2; // 0.0 to 1.0
int32 hamming_distance = 3; // Perceptual hash distance
DifferentiatingFactors differentiating_factors = 4;
}
Differentiating Factors
Shows WHY visually similar documents are NOT duplicates:
message DifferentiatingFactors {
bool different_invoice_numbers = 1;
bool different_amounts = 2;
bool different_dates = 3;
string amount_difference = 4; // "$52.00 vs $26.00"
string date_difference = 5; // "Oct 2025 vs Dec 2025"
string invoice_numbers = 6; // "#256348 vs #284081"
}
Processing Info
Multi-phase forensics tracking:
message ProcessingInfo {
repeated string phases_completed = 1; // ["hash", "ocr", "image_forensics"]
repeated string phases_pending = 2; // ["deep_analysis"]
int64 processing_duration_ms = 3;
string processed_by = 4; // Service version
map<string, string> phase_timings = 5;
}
Forensics Phases (Multi-Phase Architecture)
- Phase 1 - Hash Analysis: SHA-256 + perceptual hash (exact + visual similarity)
- Phase 2 - OCR Extraction: Invoice number, amount, merchant (docTR/PaddleOCR)
- Phase 3 - Image Forensics: EXIF, ELA, copy-move detection (if image)
- Phase 4 - PDF Forensics: Metadata, structure, timestamps (if PDF)
- Phase 5 - Metadata Verification: Cross-reference to determine true duplicates
- Phase 6 - Deep Analysis (Future): ML-based anomaly detection, behavioral patterns
Example Responses
Scenario 1: Confirmed Duplicate (REJECT)
{
"verification_id": "abc123...",
"decision": "REJECT",
"recommendation": "REJECT: This document is a confirmed duplicate. Do not process to avoid duplicate payments.",
"risk_score": 0.95,
"risk_level": "critical",
"confidence": 0.98,
"signals": [
{
"signal_type": "duplicate_invoice_number",
"category": "duplicate_detection",
"severity": "critical",
"description": "Invoice number #284081 already exists in system",
"confidence": 0.99,
"risk_contribution": 0.6
}
],
"duplicate_analysis": {
"is_duplicate": true,
"detection_method": "invoice_number_match",
"duplicate_document_ids": ["def456..."],
"match_confidence": 0.99,
"explanation": "Duplicate invoice number detected: #284081"
},
"similarity_analysis": {
"has_similar_documents": false
}
}
Scenario 2: Recurring Vendor (ACCEPT)
{
"verification_id": "xyz789...",
"decision": "ACCEPT",
"recommendation": "ACCEPT: Visual similarity detected with previous documents (likely recurring vendor/template), but metadata confirms this is a unique transaction. Safe to process.",
"risk_score": 0.15,
"risk_level": "low",
"confidence": 0.92,
"signals": [],
"duplicate_analysis": {
"is_duplicate": false,
"detection_method": "metadata_verified_unique",
"match_confidence": 0.0,
"explanation": "No duplicates detected. Document is unique."
},
"similarity_analysis": {
"has_similar_documents": true,
"similar_document_count": 2,
"similar_documents": [
{
"document_id": "abc123...",
"visual_similarity_score": 0.92,
"hamming_distance": 3,
"differentiating_factors": {
"different_invoice_numbers": true,
"different_amounts": true,
"different_dates": true,
"amount_difference": "$52.00 vs $26.00",
"date_difference": "Oct 2025 vs Dec 2025",
"invoice_numbers": "#256348 vs #284081"
}
}
],
"interpretation": "recurring_vendor",
"recommendation": "Visual similarity detected with previous documents. This appears to be from the same vendor/template with different transaction details. Document is unique and safe to process."
}
}
Scenario 3: Medium Risk (REVIEW)
{
"verification_id": "review123...",
"decision": "REVIEW",
"recommendation": "REVIEW: Elevated risk detected (medium). Please verify document authenticity before processing.",
"risk_score": 0.55,
"risk_level": "medium",
"confidence": 0.78,
"signals": [
{
"signal_type": "ela_suspicious",
"category": "image_forensics",
"severity": "medium",
"description": "Error Level Analysis indicates possible manipulation",
"confidence": 0.75,
"risk_contribution": 0.3
},
{
"signal_type": "no_exif_data",
"category": "image_forensics",
"severity": "low",
"description": "Image missing EXIF metadata",
"confidence": 0.9,
"risk_contribution": 0.1
}
],
"duplicate_analysis": {
"is_duplicate": false
},
"similarity_analysis": {
"has_similar_documents": false
}
}
Frontend Integration
TypeScript Client Usage
import { FraudDetectionApiClient } from '@/lib/fraud-detection/api-client';
// Upload document for verification
const result = await fraudClient.verifyDocument(file, {
gdprConsent: true,
consentFields: {
purpose: 'fraud_detection',
retention: '90_days'
}
});
// Handle decision
switch (result.decision) {
case 'ACCEPT':
// Process document automatically
console.log('β
', result.recommendation);
await processDocument(result.verificationId);
break;
case 'REVIEW':
// Flag for manual review
console.log('β οΈ', result.recommendation);
await flagForReview(result.verificationId);
break;
case 'REJECT':
// Block document
console.log('π«', result.recommendation);
await rejectDocument(result.verificationId);
break;
}
// Show similarity info if present
if (result.similarityAnalysis?.hasSimilarDocuments) {
console.log('βΉοΈ Recurring vendor detected:',
result.similarityAnalysis.recommendation);
}
UI Display Patterns
- Primary Display: Show decision badge (ACCEPT/REVIEW/REJECT) with color coding
- Recommendation: Display the recommendation text prominently
- Risk Indicator: Show risk level and score as visual gauge
- Signals Section: Expandable list of individual fraud indicators
- Similarity Alert: If visual similarity detected, show info banner explaining recurring vendor
- Differentiating Factors: Show why similar documents are NOT duplicates
Best Practices
- Trust the Decision: Use the decision field as primary guidance (ACCEPT/REVIEW/REJECT)
- Show Context: Always display the recommendation text to explain WHY a decision was made
- Distinguish Duplicates from Similarity: Don't confuse recurring vendors with true duplicates
- Educate Users: Explain that visual similarity (same template) is normal for recurring vendors
- Track Phases: Show which forensics phases have completed for transparency
- Signal Transparency: Display individual signals so users understand risk composition
- Confidence Scores: Show confidence alongside decisions for reliability context
Future Phases (Roadmap)
- Phase 7 - Machine Learning: Behavioral pattern recognition, anomaly detection
- Phase 8 - Network Analysis: Cross-tenant fraud rings, vendor reputation scoring
- Phase 9 - Real-time Monitoring: Live fraud detection dashboard, alert system
- Phase 10 - Blockchain Verification: Immutable audit trails, supplier verification
Logging & Observability
Architecture Overview
Keepfig uses industry-standard logging stack for centralized log management:
| Component | Purpose | Port | Resources |
|---|---|---|---|
| Loki | Log aggregation storage | 3100 | 0.5 CPU, 512MB RAM |
| Promtail | Log collector (Docker) | - | 0.25 CPU, 128MB RAM |
| Grafana | Visualization dashboard | 3000 | 0.5 CPU, 512MB RAM |
Features
- Automatic Log Collection: Auto-discovers all Docker containers via labels
- Centralized Storage: All logs in one place with 30-day retention
- Real-time Search: Query logs across all services instantly
- Pre-built Dashboard: "Keepfig - Application Logs" dashboard included
- Label Extraction: Automatically tags logs with service, container, level
- JSON Parsing: Structured log parsing for better searchability
- Cost-Effective: Label-based indexing (not full-text) saves storage
Quick Start
Development
# Start all services including logging
docker-compose up -d
# Access Grafana
open http://localhost:3000
# Login: admin / admin
Production
# Set secure Grafana password
export GRAFANA_ADMIN_PASSWORD="your-secure-password"
# Deploy with logging
docker-compose -f docker-compose.prod.yml up -d
# Access Grafana
open http://your-server:3000
Using Grafana
Pre-configured Dashboard
Navigate to: Dashboards β Invoice Pro - Application Logs
Dashboard Panels:
- Backend Logs: Fraud detection, currency conversion, verification
- OCR Service Logs: PaddleOCR extraction, currency detection
- Backend Errors: All ERROR/CRITICAL/Failed logs
- Log Rate by Service: Real-time log volume chart
LogQL Query Examples
# All backend logs
{service="backend"}
# Fraud detection logs
{service="backend"} |~ "VerifyDocument|GetVerificationResult"
# Currency conversion logs
{service="backend"} |~ "convertToBaseCurrency|Currency"
# OCR service logs
{service="ocr-service"}
# All errors across all services
{project="invoice-pro"} |~ "ERROR|CRITICAL|Failed"
# Search by verification ID (distributed tracing)
{service="backend"} |~ "VerificationID=abc-123-def-456"
# Count errors per service
sum by (service) (count_over_time({project="invoice-pro"} |~ "ERROR" [5m]))
Log Retention
- Default Retention: 30 days (720 hours)
- Storage Location: Docker volume
loki_data - Auto-cleanup: Enabled (logs older than 30 days deleted automatically)
- Storage Cost: ~50MB/day, ~1.5GB/month
Adjust Retention: Edit loki-config.yaml
limits_config:
retention_period: 720h # Change to desired hours
Security Best Practices
- Change Grafana Password:
# In .env or docker-compose.prod.yml - GF_SECURITY_ADMIN_PASSWORD=your-strong-password - Restrict Grafana Port:
ports: - "127.0.0.1:3000:3000" # Localhost only - Use Reverse Proxy: Setup nginx with SSL for Grafana access
- Enable OAuth: Integrate with existing auth (OAuth, LDAP, SAML)
Troubleshooting
Logs Not Appearing
# Check Promtail is running
docker-compose ps promtail
docker-compose logs promtail
# Verify Loki is accessible
curl http://localhost:3100/ready
# Should return: ready
# Check Docker labels
docker inspect invoice-pro-backend-1 | grep com.docker.compose.service
Grafana Can't Connect to Loki
# Test network connectivity
docker-compose exec grafana ping loki
# Check datasource in Grafana UI
# Configuration β Data Sources β Loki
# URL should be: http://loki:3100
High Disk Usage
# Check Loki storage
docker-compose exec loki du -sh /loki
# Reduce retention in loki-config.yaml
# Then restart
docker-compose restart loki
OCR Service Build Timeout
Models are lazy-loaded at startup (not during build) to avoid GitHub Actions timeout:
- Docker Build: Completes in under 5 minutes (no model download)
- First Startup: Downloads models (~25MB) in 30-60 seconds
- Service Ready: Health check passes after model initialization
- All Requests: Fast (models cached after first startup)
# Check OCR service status
docker compose -f docker-compose.prod.yml ps ocr-service
# Watch model download during first startup
docker compose -f docker-compose.prod.yml logs ocr-service -f
# Look for: "Initializing CPU-optimized docTR OCR predictor (first request)..."
# Look for: "CPU-optimized docTR OCR predictor initialized successfully"
# Test health endpoint (should return after model download)
curl http://localhost:8001/health
# Should return: {"status": "healthy"}
Integration with Application
Backend (Go)
Logs automatically collected via stdout/stderr. Use structured logging:
log.Printf("[VerifyDocument] TenantID=%s, Amount=%.2f %s, Decision=%s",
tenantID, amount, currency, decision)
OCR Service (Python)
import logging
logger = logging.getLogger(__name__)
logger.info(f"Detected currency: {currency} with confidence {confidence}")
Add Correlation IDs
log.Printf("[RequestID=%s] Processing verification...", requestID)
# Search in Grafana:
{service="backend"} |~ "RequestID=abc-123"
Cost Analysis
| Solution | Monthly Cost | Notes |
|---|---|---|
| Loki (Self-hosted) | $0 + hosting | Free, ~$5/month infrastructure |
| Datadog | $31/host | Commercial SaaS |
| New Relic | $25/month | Commercial SaaS |
| Loggly | $79/month | Commercial SaaS |
Annual Savings: $300-900 compared to commercial alternatives
Recent Updates (December 2025)
Universal Currency Detection
Enhancements
- Regex Pattern Matching: Detects ANY ISO 4217 currency code (3-letter format)
- 100+ Priority Currencies: Intelligent matching for common currencies
- Global Symbol Support: African (β¦, β΅), Asian (Β₯, βΉ, β©, ΰΈΏ), Latin American (R$) currencies
- No Configuration Required: Automatically works with invoices from any country
Example Supported Currencies
Europe: EUR β¬, GBP Β£, CHF, SEK kr, NOK kr, DKK kr, PLN zΕ
Africa: NGN β¦, ZAR R, GHS β΅, KES KSh, EGP Β£
Asia: JPY Β₯, CNY Β₯, INR βΉ, KRW β©, THB ΰΈΏ, SGD $, HKD $
Americas: USD $, CAD $, BRL R$, MXN $, ARS $, CLP $
Middle East: AED Ψ―.Ψ₯, SAR Ψ±.Ψ³, ILS βͺ
How It Works
def detect_currency(text: str) -> str:
# 1. Search for any 3-letter ISO code pattern
currency_pattern = r'\b([A-Z]{3})\b'
matches = re.findall(currency_pattern, text_upper)
# 2. Prioritize common currencies if multiple found
# 3. Fallback to symbol detection (β¬, $, Β£, Β₯, βΉ, β½, β¦, etc.)
# 4. Context-aware disambiguation for ambiguous symbols
Files Updated: ocr-service/app/main.py, backend/internal/accountingserver/forensics.go
Enhanced Similar Documents Display
New Information Displayed
When similar documents are detected (not duplicates), the UI now shows:
- Document ID: Full UUID for precise identification
- Document Hash (SHA-256): Cryptographic hash of document
- Perceptual Hash: Visual similarity hash for comparison
- Hamming Distance: Numeric measure of visual similarity (0-64)
- Visual Similarity Score: Percentage match (0-100%)
- Upload Timestamp: When the similar document was uploaded
- Differentiating Factors: Why documents are NOT duplicates
- Different invoice numbers with actual values shown
- Different amounts with comparison
- Different dates with comparison
- View Details Link: Navigate to full verification details
Visual Design
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β Similar Documents Found: β
ββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Document ID: abc-123-def-456... β
β Visual Similarity: 87% β
β β
β SHA-256 Hash: a1b2c3d4e5f6... β
β Perceptual Hash: 1a2b3c4d5e6f... β
β Hamming Distance: 8 (Similar) β
β β
β Uploaded: Dec 3, 2025, 10:30 AM β
β β
β β Why This Is NOT a Duplicate: β
β β Different Invoice Numbers β
β #256348 vs #284081 β
β β Different Amounts β
β $52.00 vs $26.00 β
β β Different Dates β
β Oct 2025 vs Dec 2025 β
β β
β View Full Details β β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
Files Updated: proto/forensics.proto, forensics.go, use-fraud-detection.ts, fraud-check-results.tsx
Deployment Checklist
The deployment workflow automatically handles all services including the new logging stack (Loki, Promtail, Grafana) and OCR service.
Required GitHub Secrets
| Secret Name | Description | Template File |
|---|---|---|
ENV_POSTGRES |
PostgreSQL configuration | .env.postgres.prod |
ENV_GRAFANA |
Grafana admin credentials | .env.grafana.prod |
ENV_BACKEND |
Backend service config | .env.backend.prod |
ENV_EXTRACTOR |
Document extractor config | .env.extractor.prod |
DEPLOY_HOST |
Server IP address | e.g., 123.45.67.89 |
DEPLOY_USER |
SSH username | e.g., root |
DEPLOY_SSH_KEY |
Private SSH key | Full private key content |
Deployment Steps
- Configure GitHub Secrets:
- Go to repository Settings β Secrets and variables β Actions
- Add each secret from the table above
- Use strong passwords for
ENV_POSTGRESandENV_GRAFANA
- Push to Main: Automatic deployment triggers on push to
mainbranch - Verify Services:
ssh root@your-server cd ~/invoice-pro ./deploy.sh monitor # Check all services are running - Access Grafana:
http://your-server:3001(admin credentials fromENV_GRAFANA) - Test Logging: Upload invoice β Check logs in Grafana dashboard
Manual Deployment (Optional)
# SSH into server
ssh root@your-server
# Navigate to project
cd ~/invoice-pro
# Pull latest changes
git pull origin main
# Deploy all services
./deploy.sh deploy
# Monitor health
./deploy.sh monitor
Services Deployed
- β Backend: gRPC API server (port 50052)
- β OCR Service: PaddleOCR document extraction (port 8001)
- β Extractor: Google Document AI integration
- β Loki: Log aggregation (port 3100)
- β Promtail: Log collector
- β Grafana: Log visualization (port 3001)
- β Envoy: gRPC-web proxy (ports 8080/8443)
- β PostgreSQL: Database (port 5432)
- β Redis: Cache (port 6379)
Post-Deployment Verification
- Regenerate Protobuf:
make proto-go - Rebuild Services:
docker-compose build backend ocr-service - Deploy:
docker-compose up -d - Verify Grafana:
open http://localhost:3001 - Test Currency Detection: Upload invoices with various currencies
- Test Similar Documents: Upload recurring vendor invoices