Back to Midday

Accounting Integration Architecture

packages/accounting/ARCHITECTURE.md

latest10.6 KB
Original Source

Accounting Integration Architecture

Deep technical documentation of the accounting sync system architecture.

System Components

Provider Abstraction Layer

mermaid
classDiagram
    class AccountingProvider {
        <<interface>>
        +getConsentUrl(state) string
        +exchangeCodeForTokens(code) TokenResponse
        +refreshTokens(refreshToken) TokenResponse
        +isTokenExpired(expiresAt) boolean
        +getAccounts(tenantId) AccountingAccount[]
        +syncTransactions(params) SyncResult
        +uploadAttachment(params) AttachmentResult
    }

    class XeroProvider {
        -client: XeroClient
        +getConsentUrl(state) string
        +exchangeCodeForTokens(code) TokenResponse
        +refreshTokens(refreshToken) TokenResponse
        +isTokenExpired(expiresAt) boolean
        +getAccounts(tenantId) AccountingAccount[]
        +syncTransactions(params) SyncResult
        +uploadAttachment(params) AttachmentResult
    }

    class QuickBooksProvider {
        <<planned>>
    }

    class FortnoxProvider {
        <<planned>>
    }

    AccountingProvider <|.. XeroProvider
    AccountingProvider <|.. QuickBooksProvider
    AccountingProvider <|.. FortnoxProvider

Worker Pipeline

mermaid
flowchart TB
    subgraph Redis["Redis Queue"]
        Q[accounting queue]
    end

    subgraph Registry["Processor Registry"]
        R[Route by job name]
    end

    subgraph Processors["Processors"]
        P1[SyncTransactionsProcessor]
        P2[SyncAttachmentsProcessor]
        P3[ExportTransactionsProcessor]
        P4[SyncSchedulerProcessor]
    end

    Q --> R
    R -->|sync-accounting-transactions| P1
    R -->|sync-accounting-attachments| P2
    R -->|export-to-accounting| P3
    R -->|accounting-sync-scheduler| P4

    P1 --> |triggers| P2
    P4 --> |triggers| P1

Database Layer

mermaid
erDiagram
    teams ||--o{ transactions : "owns"
    teams ||--o{ apps : "has"
    teams ||--o{ accounting_sync_records : "owns"
    
    transactions ||--o{ transaction_attachments : "has"
    transactions ||--o{ accounting_sync_records : "tracked by"

    transactions {
        uuid id PK
        uuid team_id FK
        date date
        numeric amount
        text name
        text description
        text status
        text category_slug
    }

    transaction_attachments {
        uuid id PK
        uuid transaction_id FK
        uuid team_id FK
        text name
        text_array path
        text type
        int size
    }

    apps {
        uuid id PK
        uuid team_id FK
        text app_id
        jsonb config
        jsonb settings
    }

    accounting_sync_records {
        uuid id PK
        uuid transaction_id FK
        uuid team_id FK
        text provider
        text provider_tenant_id
        text provider_transaction_id
        text_array synced_attachment_ids
        timestamp synced_at
        text sync_type
        text status
        text error_message
    }

Sync Algorithm

Phase 1: Transaction Selection

mermaid
flowchart TD
    A[Start Sync Job] --> B[Get synced transaction IDs]
    B --> C[Query fulfilled transactions]
    C --> D{Has results?}
    D -->|No| E[Return empty result]
    D -->|Yes| F[Map to provider format]
    F --> G[Process in batches]

    subgraph Query["Fulfilled Query"]
        C1[team_id matches]
        C2[status NOT IN excluded, archived]
        C3[NOT already synced]
        C4[date within range]
        C5[has attachments OR status = completed]
        C1 --> C2 --> C3 --> C4 --> C5
    end

Phase 2: Batch Processing

mermaid
flowchart TD
    A[Batch of 50 transactions] --> B[Call provider.syncTransactions]
    B --> C{Success?}
    
    C -->|Yes| D[Record as synced]
    C -->|No| E[Record as failed]
    
    D --> F{Has attachments?}
    F -->|Yes| G[Trigger attachment job]
    F -->|No| H[Continue to next batch]
    
    E --> H
    G --> H
    
    H --> I{More batches?}
    I -->|Yes| A
    I -->|No| J[Check attachment updates]

Phase 3: Attachment Detection

mermaid
flowchart TD
    A[Query synced records] --> B[JOIN with current attachments]
    B --> C[Compare synced_attachment_ids vs current]
    
    C --> D{New attachments found?}
    D -->|No| E[Done]
    D -->|Yes| F[For each transaction with changes]
    
    F --> G[Trigger attachment sync job]
    G --> H{More transactions?}
    H -->|Yes| F
    H -->|No| E

Token Lifecycle

Refresh Flow

mermaid
stateDiagram-v2
    [*] --> CheckExpiry: Job starts
    
    CheckExpiry --> Valid: Token not expired
    CheckExpiry --> Refresh: Token expired
    
    Refresh --> UpdateDB: Get new tokens
    UpdateDB --> Valid: Atomic update complete
    
    Valid --> [*]: Continue with API calls
    
    Refresh --> Error: Refresh failed
    Error --> [*]: Throw error, job retries

Atomic Update

mermaid
sequenceDiagram
    participant Job
    participant Provider
    participant Database

    Job->>Provider: refreshTokens(refreshToken)
    Provider-->>Job: New tokens
    
    Job->>Database: UPDATE apps SET config = config || new_tokens
    Note over Database: JSONB merge preserves other fields
    Database-->>Job: Success
    
    Job->>Job: Update local config reference

Retry Mechanism

BullMQ Configuration

mermaid
flowchart LR
    subgraph Attempt1["Attempt 1"]
        A1[Execute]
    end
    
    subgraph Delay1["Delay"]
        D1[5 minutes]
    end
    
    subgraph Attempt2["Attempt 2"]
        A2[Execute]
    end
    
    subgraph Delay2["Delay"]
        D2[10 minutes]
    end
    
    subgraph Attempt3["Attempt 3"]
        A3[Execute]
    end
    
    subgraph Delay3["Delay"]
        D3[20 minutes]
    end
    
    subgraph Attempt4["Attempt 4"]
        A4[Execute]
    end
    
    subgraph Final["Final"]
        F[Permanent Failure]
    end

    A1 -->|fail| D1 --> A2
    A2 -->|fail| D2 --> A3
    A3 -->|fail| D3 --> A4
    A4 -->|fail| F
    
    A1 -->|success| S1[Done]
    A2 -->|success| S2[Done]
    A3 -->|success| S3[Done]
    A4 -->|success| S4[Done]

Error Classification

mermaid
flowchart TD
    E[Error Occurred] --> T{Error Type}
    
    T -->|Network Timeout| R1[Retry with backoff]
    T -->|Rate Limit 429| R2[Retry with backoff]
    T -->|Auth Error 401| R3[Refresh token, retry]
    T -->|Bad Request 400| F1[Mark failed, no retry]
    T -->|Not Found 404| F2[Mark failed, no retry]
    T -->|Server Error 5xx| R4[Retry with backoff]
    
    R1 --> Q[Back to queue]
    R2 --> Q
    R3 --> Q
    R4 --> Q
    
    F1 --> D[Record in database]
    F2 --> D

Concurrency Model

Queue Worker Settings

typescript
const workerOptions: WorkerOptions = {
  concurrency: 10,              // Max 10 jobs in parallel
  lockDuration: 300000,         // 5 minute lock (API can be slow)
  stalledInterval: 5 * 60 * 1000,
  maxStalledCount: 1,
  limiter: {
    max: 20,                    // Max 20 jobs per second
    duration: 1000,
  },
};

Job Isolation

mermaid
flowchart TB
    subgraph Worker["Worker Process"]
        subgraph Job1["Job 1 (Team A)"]
            DB1[DB Connection]
            TOK1[Token State]
            PROC1[Processing]
        end
        
        subgraph Job2["Job 2 (Team B)"]
            DB2[DB Connection]
            TOK2[Token State]
            PROC2[Processing]
        end
        
        subgraph Job3["Job 3 (Team C)"]
            DB3[DB Connection]
            TOK3[Token State]
            PROC3[Processing]
        end
    end
    
    Redis[(Redis)] --> Worker
    Database[(PostgreSQL)] --> DB1
    Database --> DB2
    Database --> DB3

Data Mapping

Midday to Xero Transaction Mapping

mermaid
flowchart LR
    subgraph Midday["Midday Transaction"]
        M1[id]
        M2[date]
        M3[amount]
        M4[currency]
        M5[name]
        M6[description]
        M7[categorySlug]
    end

    subgraph Xero["Xero BankTransaction"]
        X1[Reference]
        X2[Date]
        X3[LineItems.UnitAmount]
        X4[CurrencyCode]
        X5[Contact.Name]
        X6[LineItems.Description]
        X7[LineItems.AccountCode]
        X8[Type]
    end

    M1 -->|first 8 chars| X1
    M2 --> X2
    M3 -->|abs value| X3
    M3 -->|positive = RECEIVE| X8
    M3 -->|negative = SPEND| X8
    M4 --> X4
    M5 --> X5
    M5 --> X6
    M6 --> X6
    M7 -->|if mapped| X7

Attachment Upload Flow

mermaid
sequenceDiagram
    participant Processor
    participant Database
    participant Storage
    participant Provider

    Processor->>Database: Get attachment metadata
    Database-->>Processor: id, name, path, type, size
    
    Processor->>Storage: Download from vault
    Storage-->>Processor: File blob
    
    Processor->>Processor: Convert to Buffer
    
    Processor->>Provider: Upload attachment
    Note over Provider: POST /BankTransactions/{id}/Attachments
    Provider-->>Processor: Attachment ID
    
    Processor->>Database: Update synced_attachment_ids

Performance Characteristics

Query Complexity

QueryComplexityIndex Used
Get synced IDsO(n)idx_accounting_sync_team_provider
Get transactions for syncO(n log n)transactions PK + team_id
Detect attachment changesO(n)Single JOIN, grouped
Upsert sync recordO(1)Unique constraint

Batch Sizes

OperationBatch SizeRationale
Transaction sync50Balance between API calls and memory
Attachment upload1Sequential for error isolation
Progress updatesPer batchUser feedback without overhead

Rate Limits

ProviderLimitMidday Handling
Xero60 calls/minuteBullMQ limiter (20/sec max)
Xero5000 calls/dayBatch processing reduces calls

Security Model

Data Access

mermaid
flowchart TD
    subgraph RLS["Row Level Security"]
        P1[SELECT: team_id IN user_teams]
        P2[INSERT: team_id IN user_teams]
        P3[UPDATE: team_id IN user_teams]
    end

    subgraph Tables["Protected Tables"]
        T1[accounting_sync_records]
        T2[transactions]
        T3[apps]
    end

    subgraph Worker["Worker Access"]
        W[Service Role Key]
    end

    RLS --> T1
    RLS --> T2
    RLS --> T3
    
    W -->|Bypasses RLS| T1
    W -->|Bypasses RLS| T2
    W -->|Bypasses RLS| T3

Secret Storage

Secret TypeStorageAccess
OAuth Client ID/SecretEnvironment varsWorker process only
Access Tokenapps.config (DB)Encrypted at rest
Refresh Tokenapps.config (DB)Encrypted at rest
OAuth StateEncrypted stringHMAC with server secret