shipped

Spec: F45 — Regional Connector Expansion (SEA + KSA + Latvia)

Status: shipped

1. Problem & CEO Job

Job: See clearly, but Jakarta and HCMC CEOs see different signals. F44 is Vietnam-first (Zalo OA, MISA, Haravan, Base.vn), not suited to manufacturing in Penang, logistics in Riga, or retail in Riyadh.

Each market must expand Lumi’s signal capture to become sellable. This roadmap covers Malaysia, Indonesia, Philippines, Saudi Arabia (KSA), and Latvia (EU); each gets one must-have connector that proves Lumi sees the local business.

Strategic position (strategy v2, 2026-06-14): MY/ID/PH/KSA/LV connectors are parked behind Vietnam vertical proof: scaffolding, not marketing or prioritization. Per competitive-strategy-v2.md and the registry’s Positioning doctrine (v2), do not market or prioritize them until the Vietnam fit-out/renovation/M&E contractor playbook is repeatable. Vietnam connectors (Zalo / MISA / Fast / Haravan) remain first-class.

2. User Stories & Acceptance Criteria

Market-Specific Connectors

Malaysia

  • As a Malaysian SME CEO, I want Lumi to monitor WhatsApp Business for unanswered customer messages, so that my service team’s response gaps become visible before customers complain.

    • Connect WhatsApp Business API (Meta Business Platform) via embedded signup or pre-approved phone number.
    • Message received with no reply within 2h → SourceEvent → SignalInboxItem with proposal_type=risk.
    • Webhook-received messages (real-time); no polling needed.
    • Default rule: event_type=whatsapp_unanswered → severity levels at 2h/6h/24h.
  • As a Malaysian SME CEO, I want Lumi to surface overdue Xero/QB invoices, so that I see cash problems before month-end close.

    • Connect Xero (OAuth2, scoped to accounting.transactions.read). QuickBooks Online (OAuth2, scoped to com.intuit.quickbooks.accounting).
    • Invoice 15+ days overdue → SourceEvent with proposal_type=risk.
    • Invoice 30+ days overdue → critical severity.
    • Multi-currency support (MYR, SGD when cross-border). Store currency in SourceEvent.metadata.

Indonesia

  • As an Indonesian SME CEO, I want Lumi to track Tokopedia/Shopee order fulfillment, so that late shipments don’t destroy my store rating.

    • Connect Tokopedia (seller API — OAuth2, scoped to order reads). Shopee (open platform API for ID market).
    • Order unfulfilled > 24h (ID market expects faster fulfillment than VN) → SourceEvent with proposal_type=task.
    • Shopee order cancellation rate spikes → aggregated risk signal.
    • Daily order summary pushed to Brief: orders, unfulfilled, returns.
  • As an Indonesian SME CEO, I want Lumi to monitor Jurnal.id or Accurate for accounting exceptions, so that I’m not the last to know about overdue receivables.

    • Connect Jurnal.id (API key auth) and Accurate Online (API key).
    • Invoice overdue detection (same rules as MISA/Xero but ID-specific thresholds due to local payment cycles).
    • Daily cash position summary.

Philippines

  • As a Philippine SME CEO, I want Lumi to monitor GCash/PayMaya disbursement accounts and flag payment failures, so that payroll and supplier payments don’t silently fail.

    • Connect GCash (business API — settle to business wallet) and Maya (PayMaya) Business (API for disbursement status).
    • Failed disbursement → SourceEvent with proposal_type=risk (payroll failure = critical).
    • Daily wallet balance summary.
  • As a Philippine SME CEO, I want QuickBooks/Xero integration, so that I get the same accounting signals as other markets.

    • Same Xero/QuickBooks connector as Malaysia (reuses the same connector implementation, just markets PHP locale context).
  • As a Philippine SME CEO, I want Shopee/Lazada integration for PH market, so that e-commerce signal capture matches regional peers.

    • Same Shopee/Lazada connector implementation, configured for PH market endpoints.

Saudi Arabia (KSA)

  • As a Saudi SME CEO, I want Lumi to connect Wafeq (or Zoho Books) for accounting exceptions, so that VAT compliance issues and overdue invoices don’t become audit problems.

    • Connect Wafeq (Saudi/UAE accounting SaaS, dominant in KSA SME market — API key auth) and Zoho Books (OAuth2, scoped to invoices).
    • Invoice overdue detection with KSA-specific thresholds (30/60/90 day buckets — common in KSA B2B payment terms).
    • VAT return deadline approaching → SourceEvent with proposal_type=task.
  • As a Saudi SME CEO, I want WhatsApp Business integration, so that I get the same customer signal quality as APAC regions.

    • Reuses WhatsApp Business connector from Malaysia — same implementation, KSA phone number context.
  • As a Saudi SME CEO, I want Noon/Amazon.sa order monitoring, so that marketplace fulfillment issues hit my brief.

    • Connect Noon seller API (partner API, OAuth-like token). Amazon SP-API for .sa marketplace.
    • Unfulfilled orders > 48h → SourceEvent with proposal_type=task.
    • Return/refund received → SourceEvent with proposal_type=risk.

Latvia (EU)

  • As a Latvian SME CEO, I want Lumi to connect my bank (Swedbank/SEB/Luminor) for transaction alerts, so that I see unusual cash movements and overdue receivables without logging into internet banking.

    • Connect via PSD2/Open Banking API (Nordic API Gateway / Aiia / Tink — aggregator that covers Baltic banks). OAuth2 with bank-level consent.
    • Transaction above configurable threshold → SourceEvent with proposal_type=decision.
    • Incoming payment matched to invoice → clears the overdue signal (reconciliation awareness).
    • Daily account balance summary.
  • As a Latvian SME CEO, I want Jumis/Tildes Birojs integration, so that local accounting data feeds Lumi instead of requiring double entry.

    • Connect Jumis (API via Jumis Cloud or scheduled export) and Tildes Birojs (API or scheduled export).
    • Overdue invoice detection with EUR currency context.
    • If API is not available, support CSV upload via Lumi’s upload system (F34) with a configured import template.
  • As a Latvian SME CEO, I want Xero integration as a fallback, so that international-standard accounting works on day one even without local software support.

    • Same Xero connector as other markets. EU VAT handling in signal context.

Cross-Market Framework

  • As a product team, I want the connector catalog to be market-aware, so that a Malaysian CEO only sees connectors relevant to Malaysia.

    • Each connector has a markets array: ["vn", "my", "id", "ph", "sa", "lv", "global"].
    • The catalog endpoint GET /api/source-connections/catalog/ accepts ?market=my and returns only relevant connectors.
    • Global connectors (Shopify, Google Workspace, Xero, QuickBooks, MS 365) appear in all markets.
  • As a CEO in any market, I want the same China/Vietnam cross-border awareness, so that Lumi signals don’t assume single-market operations.

    • Connectors support multi-currency context (IDR, MYR, PHP, SAR, EUR, SGD, VND, USD).
    • SourceEvents carry currency and amount in metadata where applicable.
    • Brief generation is currency-aware — doesn’t compare PHP amounts to MYR amounts on the same brief panel.

3. Scope Boundaries

  • In:
    • Native connectors for 5 target markets: MY, ID, PH, KSA, LV.
    • Market-aware connector catalog (filtered by country code).
    • Multi-currency awareness in SourceEvent metadata and Brief display.
    • Eastern EU market entry via Latvia (PSD2 banking, Eurozone, GDPR-ready).
    • WhatsApp Business as the common-thread connector (dominant messaging in MY, ID, PH, KSA).
    • Reuse of F44’s BaseConnector framework — no new infrastructure.
  • Out:
    • Full Open Banking implementation for every EU country — Latvia is the wedge, others follow.
    • In-country payroll processing — Lumi signals payroll failures, never runs payroll.
    • Zalo in non-Vietnam markets (Zalo has no meaningful presence outside Vietnam).
    • WhatsApp consumer app integration — only WhatsApp Business API (legal, contract-bound, metered).
    • Building any source system’s UI inside Lumi (same rule as F44).
    • Japanese, Korean, Chinese, Indian, African, LATAM market connectors (future specs).

4. Data Model

4.1 BaseConnector Extension

New optional method on BaseConnector:

class BaseConnector:
    markets: list[str] = ["global"]     # Country codes this connector supports
    currency_context: str | None = None  # Default currency (ISO 4217), or None for multi

    def normalize(self, raw_event: dict) -> NormalizedEvent:
        """Subclasses must include currency + amount in metadata when applicable."""

4.2 SourceEvent Metadata Extensions

Financial events carry currency data in payload_json:

{
    "event_type": "invoice_overdue",
    "title": "Invoice #1042 overdue — 32 days",
    "body_text": "Client: ABC Corp. Amount: 14,500 MYR. Due: 10 May 2026.",
    "metadata": {
        "currency": "MYR",
        "amount": 14500.00,
        "amount_usd_approx": 3100.00,
        "invoice_id": "INV-1042",
        "customer_name": "ABC Corp",
        "due_date": "2026-05-10",
        "days_overdue": 32,
        "connector_market": "my"
    }
}

4.3 Connector Catalog Schema Addition

# SourceConnection.catalog response adds:
{
    "provider_slug": "xero",
    "markets": ["global"],
    "currency_context": "multi",
    ...
},
{
    "provider_slug": "wafeq",
    "markets": ["sa", "ae"],
    "currency_context": "SAR",
    ...
},
{
    "provider_slug": "jurnal",
    "markets": ["id"],
    "currency_context": "IDR",
    ...
}

4.4 No New Models

All market-specific behavior is configuration on existing models. SourceConnection.config_json stores market overrides (e.g., payment term thresholds per country).

5. API Contract

No new endpoint shapes beyond what F44 defines. Extensions:

ChangeDetail
GET /api/source-connections/catalog/Now accepts ?market=my query param. Default shows global + market-specific connectors.
POST /api/source-connections/config_json accepts market field. Default to org’s country if not specified.
SourceEvent responseIncludes currency and amount fields when present in metadata.

The WhatsApp Business connector uses Meta’s webhook system (real-time push), so no polling is needed — same pattern as the F33 generic webhook endpoint, but with Meta’s signature verification (HMAC-SHA256 of payload with app secret).

6. UI / UX

6.1 Market-Aware Connector Catalog

The /settings/sources page shows connectors relevant to the org’s market:

┌─ Connect a Source ─────────────────────────────────────┐
│  Your market: Malaysia                                  │
│                                                         │
│  [Messaging]                                            │
│  ┌─ WhatsApp Business ──────────────────────────────┐  │
│  │ Monitor customer messages. Flag unanswered.       │  │
│  │ [Connect]                                         │  │
│  └──────────────────────────────────────────────────┘  │
│                                                         │
│  [Accounting]                                           │
│  ┌─ Xero ──────────────────────────────────────────┐  │
│  │ Overdue invoices, cash position, VAT reminders.  │  │
│  │ [Connect]                                         │  │
│  └──────────────────────────────────────────────────┘  │
│  ┌─ QuickBooks Online ─────────────────────────────┐  │
│  │ Same signals. Alternative for QBO users.         │  │
│  │ [Connect]                                         │  │
│  └──────────────────────────────────────────────────┘  │
│                                                         │
│  [E-commerce]                                           │
│  ┌─ Shopify ───────────────────────────────────────┐  │
│  │ Order fulfillment, refunds, abandoned checkouts. │  │
│  │ [Connect]                                         │  │
│  └──────────────────────────────────────────────────┘  │
│                                                         │
│  [Take Payments]                                        │
│  ┌─ GrabPay / Touch 'n Go ─────────────────────────┐  │
│  │ Failed settlement notifications. (Phase 2)       │  │
│  │ [Coming soon]                                    │  │
│  └──────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────┘

6.2 Market Selector for Global Orgs

Organizations operating in multiple markets can toggle the catalog filter. The org’s primary market auto-selects, but admins can view “All markets” to see every connector.

6.3 Signal Display with Currency

In the Signal Inbox and Brief, financial signals show currency badges:

┌─ Risk ──────────────────────────────────────────────┐
│ [MYR] Invoice #1042 — 32 days overdue               │
│ ABC Corp · MYR 14,500 · Due 10 May                  │
│ Source: Xero · 2 hours ago                           │
└──────────────────────────────────────────────────────┘

6.4 i18n

No new namespace. Extend sources namespace with market-specific connector descriptions:

  • sources.connector_desc_whatsapp, sources.connector_desc_xero, sources.connector_desc_tokopedia
  • sources.connector_desc_wafeq, sources.connector_desc_jurnal, sources.connector_desc_jumis
  • sources.connector_desc_gcash, sources.connector_desc_noon
  • sources.market_label_my, sources.market_label_id, sources.market_label_ph, sources.market_label_sa, sources.market_label_lv
  • sources.all_markets, sources.coming_soon

All 8 active locales updated (en, en-AU, en-CA, en-GB, en-IN, en-SG, en-ZA, vi).

7. Test Plan

Coverage note (2026-06-14): Regional connector tests live in backend/tests/test_connectors.py. Real classes include TestWhatsAppConnector, TestXeroMultiCurrency, TestTokopediaConnector, TestWafeqConnector, TestGCashConnector, TestJumisFallback, TestPSD2BankConnector, TestRegionalConnectorTenantIsolation, TestSourceEventCurrencyMetadata, TestCatalogMarketFilter. Market-filter scoping is in test_multitenancy.py. Rows below use original per-case naming; the file is authoritative.

#CaseTypeWhere
1Connector catalog filters by market=mybackendtests/test_connectors.py::TestCatalogMarketFilter
2Connector catalog with no market param returns global + all market connectorsbackendtests/test_connectors.py
3Connector catalog for unsupported market returns global connectors onlybackendtests/test_connectors.py
4WhatsApp connector webhook receives message → creates eventbackendtests/test_connectors.py::TestWhatsAppConnector
5WhatsApp connector webhook signature mismatch returns 401, connector stays activebackendtests/test_connectors.py
6WhatsApp connector — unanswered messages create SourceEvents at 2h/6h/24h severity tiersbackendtests/test_connectors.py
7Xero connector detects MYR overdue invoice with correct currencybackendtests/test_connectors.py::TestXeroMultiCurrency
8Xero connector — multi-currency conversion rate unavailable → amount_usd_approx = nullbackendtests/test_connectors.py
9Tokopedia connector detects unfulfilled order (ID market, 24h threshold)backendtests/test_connectors.py::TestTokopediaConnector
10Shopee regional connector configured for ID vs PH returns correct market endpointsbackendtests/test_connectors.py
11Wafeq connector detects overdue invoice (SAR, KSA 30/60/90 day thresholds)backendtests/test_connectors.py::TestWafeqConnector
12Wafeq connector — Arabic error message normalized with raw stored in metadatabackendtests/test_connectors.py
13Jumis connector handles CSV fallback when API unavailablebackendtests/test_connectors.py::TestJumisFallback
14Jumis CSV import — unknown columns logged as warning, not errorbackendtests/test_connectors.py
15GCash disbursement failure → risk signal (PHP)backendtests/test_connectors.py::TestGCashConnector
16GCash settlement API 503 → retry ×3; after 3 consecutive failures → degradedbackendtests/test_connectors.py
17PSD2 bank connector via Tink aggregator (EUR, Latvia)backendtests/test_connectors.py::TestPSD2BankConnector
18PSD2 bank consent expires (90-day SCA) → degraded, “Bank consent expired” warningbackendtests/test_connectors.py
19SourceEvent carries correct currency + amount in metadata for all financial connectorsbackendtests/test_sources.py (extend existing)
20Cross-tenant isolation — MY org’s WhatsApp never leaks to ID orgbackendtests/test_multitenancy.py
21Market shown to wrong market (UI bug) — catalog API enforces filter server-sidebackendtests/test_multitenancy.py
22Org country determined at signup auto-filters connector catalogbackendtests/test_connectors.py
23Admin overrides org market in settings → catalog respects overridebackendtests/test_connectors.py
24Noon seller API connector — unfulfilled orders > 48h create task signalsbackendtests/test_connectors.py
25Amazon SP-API for .sa — return/refund received creates risk signalbackendtests/test_connectors.py
261Office staffing alert — leave within 7d of project deadline → commitment signalbackendtests/test_connectors.py
27Admin connects WhatsApp Business, sees events in inboxe2edescribe("Regional Connectors")
28Brief shows currency badge on financial signalse2esame block
29Connector config_json stores market override correctlybackendtests/test_connectors.py

8. Current Behavior

  • The market-aware connector catalog does exist and is served from the runtime connector registry via GET /api/source-connections/catalog/.
  • Regional connector code exists today for WhatsApp Business, Xero, Tokopedia, Shopee, GCash, Maya, Wafeq, Zoho Books, Noon, Amazon.sa, PSD2 bank, Jumis, Tildes Birojs, Jurnal.id, and Accurate Online.
  • WhatsApp Business is implemented as a webhook-only source connector, not as a conversational bot or in-app messaging client.
  • Regional connector credentials inherit F44’s required production Fernet boundary; WhatsApp rejects missing app-secret configuration and invalid signatures.
  • Multi-currency metadata exists for financial connectors in the current connector framework, but brief-display polish and broader UX claims still depend on the consuming surfaces.
  • The feature is strategically deprioritized behind Vietnam proof, but it is no longer “spec only.”

9. Error States

#ScenarioUser-visibleInternalLogRetry
1WhatsApp webhook signature HMAC mismatch (single)No visible change — transientReject payloadWARNING “whatsapp_hmac_mismatch”Next webhook delivery
2WhatsApp webhook signature HMAC mismatch (3× consecutive)No visible change — Sentry onlyFire Sentry alertERROR “whatsapp_hmac_mismatch_3x”Next webhook delivery
3PSD2 bank consent expires (90-day SCA)Connector badge: “Degraded — bank consent expired”status=degraded, last_error_text setWARNING “psd2_consent_expired”Re-authorization flow only
4Wafeq Arabic error, known error codeConnector badge: “Degraded” with English errorNormalize to English, raw in metadataWARNING “wafeq_error_“Next poll cycle
5Wafeq Arabic error, unknown error codeConnector badge: “Degraded — unexpected error”Raw Arabic in last_raw_errorERROR “wafeq_unknown_error”Next poll cycle
6GCash 503Transient — no visible changeBackoff ×3 (60/120/240s)INFO “gcash_503_retry_N”Next poll cycle
7GCash 503 ×3 consecutiveConnector badge: “Degraded”status=degradedWARNING “gcash_3x_503”Next poll auto-recovers
8Jumis CSV header mismatch (unknown columns)Import succeeds with partial dataUnknown columns skippedWARNING “jumis_csv_unknown_columns”n/a
9Jumis CSV missing required columnsImport fails, validation error shownRaw payload preview storedERROR “jumis_csv_missing_columns”Re-upload with correct format
10Multi-currency conversion unavailableBrief shows original currency onlyamount_usd_approx = nullWARNING “forex_rate_unavailable”Retry on next brief gen
11Noon API returns 401Connector badge: “Degraded — reconnect”Token refresh fails → degradedWARNING “noon_auth_expired”Manual reconnect
12Amazon SP-API throttling token exhaustedTransient — no visible changeWait for throttling reset (documented per API)INFO “amazon_throttled”Next poll cycle
131Office API downTransient — no visible changeSkip poll cycleWARNING “1office_unreachable”Next poll cycle
14Xero webhook intent-to-receive failsWebhook registration fails on connectstatus=active (poll fallback), last_error_text setWARNING “xero_webhook_registration_failed”3× retry at 1-hour intervals

10. Diagrams & Docs Touched

  • F44 — API Integrations: update §11 to note that F45 extends F44 into SEA + KSA + EU markets.
  • F24 — Billing: multi-currency billing awareness (prices in MYR/IDR/PHP/SAR/EUR, not just VND/USD). Separate spec if needed.
  • backend/DIAGRAMS.md: add market-aware connector registry and currency pipeline.
  • backend/apps/connectors/ — 10+ new connector implementations.

11. Implementation Audit

Edge Cases Catalog (formerly §9, 2026-06-14; 12 items): same-event/different-legal-meaning across markets, metadata-only/consent-gated capture (GDPR/PDPL), currency/timezone/business-calendar differences, WhatsApp HMAC mismatch replay protection, PSD2 90-day SCA consent expiry, Wafeq Arabic error normalization, GCash maintenance-window 503 backoff, Jumis CSV format drift, multi-currency conversion unavailable, market-connector displayed-to-wrong-market server-side protection, org country change mid-lifecycle, multi-national multi-market org override. Full catalog in git history.

#ClaimStatusEvidence
1Market-aware connector catalog (?market= filter on /api/source-connections/catalog/)✅ matchbackend/apps/sources/views.py, backend/tests/test_connectors.py
2WhatsApp Business connector — Meta webhook verification and unanswered-message ingestion✅ matchbackend/apps/connectors/whatsapp.py, F76 (planned spec)
3Xero connector — OAuth2, overdue invoice detection, multi-currency (MYR/PHP/EUR)✅ matchbackend/apps/connectors/xero.py
4QuickBooks Online connector — OAuth2, overdue invoice detection (MY/PH)✅ matchbackend/apps/connectors/qbo.py
5Tokopedia connector — OAuth2, unfulfilled orders > 24h, IDR currency✅ matchbackend/apps/connectors/tokopedia.py
6Shopee regional connector — market-scoped endpoints (ID/PH/MY)✅ matchbackend/apps/connectors/shopee.py
7GCash Business connector — disbursement failure → critical risk, 503 backoff ×3 → degraded after 3 consecutive 503s (raises RuntimeError caught by sync_single_connection → mark_connection_degraded; cursor_json.retry_count persisted to DB between cycles; resets to 0 on success)✅ matchbackend/apps/connectors/gcash.py, backend/tests/test_connectors.py::TestGCashConnector::test_gcash_settlement_api_503_retries_then_degraded
8Wafeq connector — Arabic error normalization, KSA 30/60/90-day severity buckets✅ matchbackend/apps/connectors/wafeq.py
9PSD2 bank connector — consent expiry handling, EUR, LV✅ matchbackend/apps/connectors/psd2_bank.py
10Jumis CSV import connector — header-name matching and validation path✅ matchbackend/apps/connectors/jumis.py
11Multi-currency connector metadata path exists in shipped regional connectors✅ matchbackend/tests/test_connectors.py, docs/specs/F44-api-integrations.md
12/settings/sources frontend is the shared connection surface for regional connectors⚠️ partialBackend/API contract shipped; dedicated market-polish claims in this spec overstate the current frontend surface

12. Open Questions / Decisions

  1. WhatsApp Business pricing: Meta charges per conversation (marketing/service/utility tiers). Who pays — Lumi or the customer? Decision: Lumi absorbs cost and bundles into the subscription. Per-conversation pricing is a billing nightmare for SME CEOs. Lumi’s margin covers the WhatsApp API cost.
  2. PSD2 aggregator selection: Tink (Visa-owned, covers Nordics+Baltics), Aiia (Mastercard-owned, covers Baltics), or Nordigen (free tier, GoCardless-owned, covers Baltics). Decision: Tink — strongest Baltic coverage, Visa backing means longevity, and Lumi’s EU expansion path goes through the same aggregator.
  3. Jumis/Tildes Birojs API availability: Both are legacy desktop accounting tools with limited or no REST APIs. The Jumis Cloud edition has a nascent API. Decision: ship CSV import first (via F34 upload system), then add API when Jumis Cloud API stabilizes. The import template is a documented CSV format the customer exports from Jumis and drops into Lumi.
  4. Market determination: How does Lumi know an org’s market? Decision: org signup captures country (already in Organization model). The connector catalog uses this. Admins can override in org settings for multi-market operations.
  5. Philippines e-wallet vs bank: GCash and Maya dominate but are consumer-first. Their business APIs are less mature than, say, Stripe. Do we wait or ship what exists? Decision: ship what exists. Even a partial GCash business API integration (disbursement status, balance) is better than nothing. The alternative is bank-specific APIs (BDO, BPI, Metrobank) — each would require a separate connector. GCash aggregates the e-wallet layer which is where most PH SME money actually moves.
  6. KSA Arabic-first UX: Wafeq and Noon are Arabic-first products. Error messages, invoice descriptions, and customer names arrive in Arabic. Decision: store raw Arabic in SourceEvent.body_text and metadata. Brief generation attempts Arabic→English summarization via LLM when the CEO’s language is English. When the CEO’s language is Arabic (future RTL support), display original Arabic.

13. Bug / Change Log

2026-07-14 — Inherit fail-closed connector trust boundary

Type: Security hardening

  • Required encrypted production connector storage and signed webhook ingestion.
  • Added proof for safe/unsafe regional provider host and role/state paths through F45’s Contract Graph declaration.

2026-07-02 — Implement GCash 3×503 degradation; harden hollow test

Type: Bug Fix + Test

Summary:

  • gcash.py poll() now persists cursor_json.retry_count to the DB on every 503, so the counter survives across separate sync_single_connection task invocations (previously the in-memory mutation was never written back and reset to 0 on each call).
  • After the 3rd consecutive 503, poll() raises RuntimeError("GCash API unavailable — N consecutive 503s"). sync_single_connection catches any exception from poll() and calls mark_connection_degraded, setting connection.status = "degraded". This matches the existing degradation mechanism used by all other connectors — no new code path introduced.
  • On a successful (non-503) poll, retry_count is reset to 0 and cursor_json is saved, preventing a false degradation after recovery.
  • test_gcash_settlement_api_503_retries_then_degraded was hollow (pre-set status="degraded" and asserted it). Rewritten to drive three real sync cycles through sync_single_connection with HTTP mocked to 503, asserting: (1) after 1st 503 — status still active; (2) after 2nd 503 — still active; (3) after 3rd 503 — status degraded.

Root cause of the gap:

  • The original poll() mutated cursor["retry_count"] in-memory but never called connection.save(update_fields=["cursor_json"]). Because sync_single_connection fetches a fresh DB object on each invocation, the count was always 0. The logger.warning("gcash_3x_503 — marking for degradation") logged the intent but no degradation ever happened.

Prior spec claim:

  • §11 row 7 marked ✅ but described only the backoff log lines, not actual degradation. Corrected above.

2026-07-02 — Spec drift fix: checked shipped ACs in §2

Type: Documentation

Summary:

  • 21 of 41 [ ] ACs in §2 flipped to [x] — confirmed shipped against §11 audit rows 1–11.
  • Confirmed: WhatsApp Business MY (×4, row 2), Xero/QB MY (×4, rows 3–4+11), Tokopedia+Shopee connection and 24h unfulfilled (×2, rows 5–6), Xero/QB PH reuse (×1, rows 3–4), Wafeq KSA 30/60/90 day thresholds (×1, row 8), WhatsApp KSA reuse (×1, row 2), PSD2 connect/consent (×1, row 9), Jumis CSV upload (×1, row 10), Xero LV (×1, row 3), market-aware catalog ×3 (row 1), multi-currency metadata ×2 (row 11).
  • Remaining [ ]: Jurnal.id/Accurate (not in §11), GCash+Maya combined AC (Maya not in §11), Shopee/Lazada PH (Lazada not in §11), Wafeq+Zoho combined AC (Zoho not in §11), VAT deadline (not in §11 row 8), Noon/Amazon.sa (not in §11), Shopee cancellation spike (not specifically in §11), PSD2 threshold/reconciliation/balance (not specifically in §11 row 9), Jumis+Tildes combined AC (Tildes not in §11), Jumis EUR overdue (not in §11 row 10), brief currency-aware display (§11 row 12 ⚠️ partial).
  • No code changes.

2026-06-15 — Frontend route namespaced

Frontend route namespaced: /sources -> /settings/sources (app-wide IA restructure; API paths unchanged).

2026-06-14 — Strategy v2 realignment (re-sequenced/deferred)

Type: Documentation Change

Summary:

  • Added a “Strategic position (strategy v2, 2026-06-14)” note to §1 parking the MY/ID/PH/KSA/LV connectors behind Vietnam vertical proof; Vietnam (Zalo/MISA/Fast/Haravan) connectors stay first-class.

Reason:

  • Align with competitive-strategy-v2.md and the registry’s Positioning doctrine (v2): country selection is downstream of vertical proof (“validation before velocity”).

Impact:

  • Priority/positioning only. Feature, status, API, data model, and tests are unchanged.

Tests:

  • None (documentation-only change).

Notes:

  • Code kept as scaffolding; do not market or prioritize non-Vietnam connectors until the Vietnam fit-out/renovation/M&E vertical playbook is repeatable.

2026-06-14 — Spec restructured to TEMPLATE.md canonical order (§1–§13)

Type: Documentation Change

Summary:

  • §9 Edge Cases (12 items, 9.1–9.12) absorbed into §11 Implementation Audit note.
  • §10 Error States → §9, §11 Diagrams & Docs Touched → §10, §12 Implementation Audit (narrative + Implementation Order) → §11 4-column table (12 rows, all ✅).
  • §13 Open Questions / Decisions → §12.
  • §15 Per-Region Connector Mini-Specs renumbered to §14 (non-canonical appendix, kept for reference; originally misplaced between §13 and §14 BL).
  • §14 Bug / Change Log → §13 (moved before Per-Region appendix).

Reason:

  • Align with TEMPLATE.md canonical section order per SP fleet restructuring pass.

Impact:

  • Spec authors and future implementation agents.

2026-06-13 — Spec refinement: numbered edge cases, detailed error matrix, expanded test plan

Type: Documentation Change

Summary:

  • Replaced §9 loose paragraph with 12 numbered edge cases (9.1–9.12) covering market-switch, PSD2 consent expiry, Arabic error handling, Jumis CSV drift, multi-currency conversion failures, and multi-national org operations.
  • Replaced §10 single-column error table with 14-row detailed matrix (user-visible/internal/log/retry) matching F44 quality standard.
  • Expanded §7 test plan from 12 to 29 test cases, adding coverage for market filtering variants, signature validation, Arabic normalization, CSV schema drift, backoff logic, consent expiry, and multi-currency fallback.
  • Updated spec status from draft to agreed.

Reason:

  • Spec needed hardening per implementation audit gap analysis. Edge cases were loose prose, test plan was thin, error states lacked structured detail.

Impact:

  • Spec authors and future implementation agents.

Tests:

  • Spec-only change — no code tests affected.

14. Per-Region Connector Mini-Specs (Appendix)

Each region section below covers the connector set for that market: auth flow, API contract table, error states, and test plan additions. Connectors shared across markets (WhatsApp Business, Xero, QuickBooks, Shopee) are documented once and cross-referenced.


15.1 Malaysia (MY) — Connectors

Connectors: WhatsApp Business, Xero, QuickBooks Online, Touch ‘n Go e-Wallet, GrabPay

15.1.1 WhatsApp Business (MY)

Shared implementation across MY, ID, PH, KSA (documented here once).

Auth flow:

  • Meta Business Platform embedded signup (OAuth2) or pre-approved phone number.
  • Meta app secret stored in SourceConnection.config_json.meta_app_secret.
  • Webhook verification: Meta sends hub.verify_token GET request; connector responds with challenge string from config.
  • HMAC-SHA256 signature verification on all incoming webhook payloads (X-Hub-Signature-256 header vs SHA256(app_secret + payload_body)).
  • Webhook delivery: Meta pushes messages and message_template_status_update webhook events in real-time.
  • Default rule on connect: event_type=whatsapp_unanswered → proposal_type=risk with severity tiers at 2h / 6h / 24h.

API contract — Webhook ingest endpoint:

MethodPathAuthRequestResponseErrors
GET/api/source-connections/webhook/<connection_id>/hub.verify_token matchQuery: hub.mode, hub.verify_token, hub.challenge200 — challenge string401 — verify token mismatch
POST/api/source-connections/webhook/<connection_id>/HMAC-SHA256 signatureMeta webhook JSON (message, status update, etc.)202 — {"event_id": "..."}401 — signature mismatch; 400 — invalid payload → degraded; 500 — ingest error → degraded

Error states:

ScenarioUser-visibleInternalLogRetry
HMAC mismatch (single)None — transientReject payloadWARNING whatsapp_hmac_mismatchNext webhook delivery (automatic)
HMAC mismatch (3× consecutive)None — Sentry onlyFire Sentry alertERROR whatsapp_hmac_mismatch_3xNext webhook delivery
Meta webhook disabled on their endConnector badge: “Degraded”Poll health check detects missing webhookERROR whatsapp_webhook_not_foundManual re-connect
Rate-limited by Meta APIVisible on connector: “Rate limited — retrying”Backoff 60s, then resumeWARNING whatsapp_rate_limitedNext poll cycle

Test plan additions (extends §7):

#CaseTypeWhere
30WhatsApp webhook verification (GET challenge)backendtest_connectors.py::TestWhatsAppWebhookVerify
31WhatsApp webhook receives text message → creates SourceEventbackendtest_connectors.py::TestWhatsAppInboundMessage
32WhatsApp unanswered > 2h → risk at medium severitybackendtest_connectors.py::TestWhatsAppSeverity2h
33WhatsApp unanswered > 6h → risk at high severitybackendtest_connectors.py::TestWhatsAppSeverity6h
34WhatsApp unanswered > 24h → risk at critical severitybackendtest_connectors.py::TestWhatsAppSeverity24h
35WhatsApp signature mismatch (single) → connector stays activebackendtest_connectors.py::TestWhatsAppSingleSignatureFail
36WhatsApp signature mismatch (3 consecutive) → Sentry firedbackendtest_connectors.py::TestWhatsAppThreeSignatureFail
37WhatsApp webhook for disabled connection → 401backendtest_connectors.py::TestWhatsAppDisabledConnection

15.1.2 Xero (MY, PH, LV)

Shared connector for all markets (documented once here). Xero supports multi-currency (MYR, SGD, PHP, EUR, USD).

Auth flow:

  • OAuth2 authorization code flow. Scopes: accounting.transactions.read, accounting.settings.read, offline_access.
  • tenant_id stored in config_json.xero_tenant_id (Xero supports multi-tenant orgs).
  • Token refresh handled by the connector’s health_check (401 → refresh, if refresh fails → degraded).
  • Webhook intent-to-receive flow: Xero sends intent_to_receive POST with a key; connector responds with the key. If webhook registration fails, connector falls back to polling.

API contract:

MethodPathAuth/RoleRequestResponseErrors
POST/api/source-connections/Ownerprovider_slug=xero, config_json with OAuth token201 — connection400 — missing token; 400 — duplicate provider
POST/api/source-connections/<id>/sync/Owner—200 — {"events_created": N}409 — connection disabled
(internal)Poll Xero Invoices API—Config: tenant_id, access_tokenlist[dict] invoices401 — token expired → refresh; 429 — throttled → backoff

Polling behavior:

  • Endpoint: GET https://api.xero.com/api.xro/2.0/Invoices
  • Filter: where=Status=="AUTHORISED"&&DueDate<DateTime.Now (overdue invoices).
  • Cursor pagination via cursor_json.last_invoice_id.
  • Interval: 15 minutes (configurable per connection).

Normalized event shape:

{
  "event_type": "invoice_overdue",
  "currency": "MYR",
  "amount": 14500.00,
  "market": "my"
}

Error states:

ScenarioUser-visibleInternalLogRetry
Token expiredConnector badge: “Degraded — re-authorize”Attempt refresh; if fails → degradedWARNING xero_token_expiredManual re-connect
Webhook registration failsstatus=active (poll fallback), tooltip in UIPoll fallbackWARNING xero_webhook_registration_failed3× retry at 1-hour intervals
Rate limited (429)Transient — no visible changeExponential backoff 60/120/240sINFO xero_rate_limitedNext poll cycle
Xero API down (5xx)Transient — skipped pollLog warningWARNING xero_api_errorNext poll cycle

Test plan additions:

#CaseTypeWhere
38Xero polls overdue invoices → creates eventsbackendtest_connectors.py::TestXeroPollOverdue
39Xero multi-currency: MYR invoice with correct currency fieldbackendtest_connectors.py::TestXeroMultiCurrencyMYR
40Xero multi-currency: PHP invoice in PH market contextbackendtest_connectors.py::TestXeroMultiCurrencyPHP
41Xero webhook intent-to-receive succeedsbackendtest_connectors.py::TestXeroWebhookIntent
42Xero webhook registration fails → poll fallbackbackendtest_connectors.py::TestXeroWebhookFallback
43Xero token refresh succeeds on 401backendtest_connectors.py::TestXeroTokenRefresh
44Xero token refresh fails → degradedbackendtest_connectors.py::TestXeroTokenRefreshFailed

15.1.3 QuickBooks Online (MY, PH)

Shared connector for MY and PH markets.

Auth flow:

  • OAuth2 authorization code flow via Intuit’s OAuth playground.
  • Scopes: com.intuit.quickbooks.accounting.
  • realm_id (company ID) stored in config_json.qbo_realm_id.
  • Token refresh via refresh_token (Intuit tokens expire in 101 days, refresh tokens in 101 days).

API contract:

MethodPathAuth/RoleRequestResponseErrors
POST/api/source-connections/Ownerprovider_slug=quickbooks_online201 — connection400 — missing token
(internal)Poll QBO Invoice API—realm_id, access_tokenlist[dict] invoices401 → refresh; 403 → re-auth

Polling:

  • Endpoint: GET https://quickbooks.api.intuit.com/v3/company/<realm_id>/query?query=select * from Invoice where DueDate < 'now' and Balance > 0
  • Uses QBO’s query language (SQL-like via POST or GET).
  • Cursor via cursor_json.last_updated (QBO uses MetaData.LastUpdatedTime for change tracking).

Test plan additions:

#CaseTypeWhere
45QBO polls overdue invoices → creates eventsbackendtest_connectors.py::TestQBOOverdueInvoices
46QBO multi-currency: MYR invoice billingbackendtest_connectors.py::TestQBOMultiCurrencyMYR
47QBO token refresh succeeds on 401backendtest_connectors.py::TestQBOTokenRefresh

15.1.4 Touch ‘n Go e-Wallet (MY)

Auth flow:

  • API key (tn_g_api_key) + merchant ID stored in config_json.
  • All requests signed with HMAC-SHA256 using the API secret.
  • No OAuth — server-to-server with static API credentials.

API contract:

MethodPathAuth/RoleRequestResponseErrors
POST/api/source-connections/Ownerprovider_slug=touch_n_go201 — connection400 — missing api_key
(internal)Poll TnG transaction API—HMAC-signed requestlist[dict] transactions401 → degraded

Signals captured:

  • Failed/disputed transaction → SourceEvent with proposal_type=risk.
  • Daily balance summary (balance below configurable threshold → risk).
  • Settlement delayed > 24h → risk.

Error states:

ScenarioUser-visibleInternalLogRetry
API key invalidConnector badge: “Degraded”401 on first pollERROR tn_g_auth_failedManual re-connect
TnG system maintenanceTransient (PH bank maintenance window 2-4 AM)Skip pollINFO tn_g_maintenanceNext poll cycle

Test plan additions:

#CaseTypeWhere
48TnG polls transactions → creates eventsbackendtest_connectors.py::TestTnGPollTransactions
49TnG dispute detected → risk signalbackendtest_connectors.py::TestTnGDisputeEvent
50TnG invalid API key → degraded on first pollbackendtest_connectors.py::TestTnGAuthFailure

15.1.5 GrabPay (MY)

Auth flow: API key exchange (similar to Touch ‘n Go). Partner application with mPOS or GrabPay Business enrollment.

Signals captured:

  • Settlement failed notification → proposal_type=risk.
  • Daily settlement summary.

Test plan additions:

#CaseTypeWhere
51GrabPay settlement failure → risk signalbackendtest_connectors.py::TestGrabPaySettlementFailure
52GrabPay daily settlement summary pollbackendtest_connectors.py::TestGrabPayDailySummary

15.2 Indonesia (ID) — Connectors

Connectors: Tokopedia, Shopee (regional), Jurnal.id, Accurate Online, WhatsApp Business (shared with §15.1.1)

15.2.1 Tokopedia (ID)

Auth flow:

  • OAuth2 authorization code flow via Tokopedia Open Platform.
  • Scopes: seller.order.read, seller.finance.read.
  • shop_id and access_token stored in config_json.
  • Token refresh via refresh_token (expires in 24h — Tokopedia uses short-lived tokens + refresh).

API contract:

MethodPathAuth/RoleRequestResponseErrors
POST/api/source-connections/Ownerprovider_slug=tokopedia201 — connection400 — missing token or shop_id
(internal)Poll Tokopedia Order API—shop_id, access_tokenlist[dict] orders401 → refresh; 401 after refresh failure → degraded

Polling behavior:

  • Endpoint: GET https://api.tokopedia.com/v2/orders/latest?shop_id=<id>&status=1 (status=1 = unfulfilled).
  • Cursor via cursor_json.last_order_id.
  • Interval: 10 minutes (ID market expects faster fulfillment tracking).
  • Threshold: ID market expects faster fulfillment → order unfulfilled > 24h → proposal_type=task (not risk — CEO can act).
  • Cancellation rate spike: aggregated over 7-day rolling window. Rate > 5% → risk signal.

Normalized event:

{
  "event_type": "order_unfulfilled",
  "currency": "IDR",
  "amount": 250000.00,
  "market": "id",
  "proposal_type": "task"
}

Error states:

ScenarioUser-visibleInternalLogRetry
Token expiredConnector badge: “Degraded — re-authorize”Refresh token; if fail → degradedWARNING tokopedia_token_expiredManual re-connect
Tokopedia API rate limit (429)TransientBackoff 60sINFO tokopedia_rate_limitedNext poll cycle
Shop deleted/disabledConnector badge: “Degraded — reconnect”Poll returns 404WARNING tokopedia_shop_not_foundManual re-connect

Test plan additions:

#CaseTypeWhere
53Tokopedia polls unfulfilled orders → creates task eventsbackendtest_connectors.py::TestTokopediaUnfulfilledOrders
54Tokopedia unfulfilled > 24h with IDR currency in metadatabackendtest_connectors.py::TestTokopedia24hThreshold
55Tokopedia cancellation rate spike > 5% → aggregated riskbackendtest_connectors.py::TestTokopediaCancellationRate
56Tokopedia token refresh succeeds on 401backendtest_connectors.py::TestTokopediaTokenRefresh

15.2.2 Shopee Regional Connector (ID, PH, MY)

Shared connector for ID and PH markets (with future MY expansion). Same implementation, different API endpoints per country.

Auth flow:

  • Open Platform API for Shopee — OAuth-like partner token.
  • partner_id, partner_key from Shopee Partner application.
  • Each API call signed with HMAC-SHA256 using partner_key.
  • shop_id scoped per market (different shop ID for ID vs PH).
  • market field in config_json determines which API base URL to use:
    • ID: https://partner.shopeemobile.com/api/v2/
    • PH: https://partner.shopee.ph/api/v2/
    • MY: https://partner.shopee.com.my/api/v2/

API contract:

MethodPathAuth/RoleRequestResponseErrors
POST/api/source-connections/Ownerprovider_slug=shopee, config_json.market="id" or "ph"201 — connection400 — missing market or shop_id
(internal)Poll Shopee Order API—Signed request with shop_id, timestamplist[dict] orders401 → degraded

Signals captured:

  • Order unfulfilled > 24h → proposal_type=task (task: fulfill the order).
  • Order cancellation spike (7-day rolling average > 5% cancellation rate) → aggregated risk.
  • Return/refund initiated → proposal_type=risk.

Test plan additions:

#CaseTypeWhere
57Shopee ID: polls unfulfilled orders using ID endpointbackendtest_connectors.py::TestShopeeIDOrders
58Shopee PH: polls orders using PH endpointbackendtest_connectors.py::TestShopeePHOrders
59Shopee: market config mismatch returns validation errorbackendtest_connectors.py::TestShopeeMarketMismatch
60Shopee: order cancellation spike triggerbackendtest_connectors.py::TestShopeeCancellationSpike

15.2.3 Jurnal.id (ID)

Auth flow:

  • API key (jurnal_api_key) stored in config_json.
  • No OAuth — simple API key authentication.
  • Base URL: https://api.jurnal.id/core/api/v1/.

Signals captured:

  • Overdue invoice detection (same pattern as MISA/Xero but ID-specific thresholds due to local payment cycles: 30/60/90 day buckets).
  • Daily cash position summary.

Test plan additions:

#CaseTypeWhere
61Jurnal.id polls overdue invoices → eventsbackendtest_connectors.py::TestJurnalOverdueInvoices
62Jurnal.id 30/60/90 day severity tiersbackendtest_connectors.py::TestJurnalSeverityTiers

15.2.4 Accurate Online (ID)

Auth flow:

  • API key (accurate_api_key) stored in config_json.
  • Base URL: https://api.accurate.id/accurate/api/.
  • HMAC signing with API secret for all requests.

Signals captured:

  • Overdue receivables (same as Jurnal.id pattern).
  • Cash position changes.

Test plan additions:

#CaseTypeWhere
63Accurate polls overdue invoices → eventsbackendtest_connectors.py::TestAccurateOverdueInvoices
64Accurate API key invalid → degradedbackendtest_connectors.py::TestAccurateAuthFailure

15.3 Philippines (PH) — Connectors

Connectors: GCash Business, Maya Business, Shopee (regional, shared §15.2.2), WhatsApp Business (shared §15.1.1), Xero (shared §15.1.2), QuickBooks Online (shared §15.1.3)

15.3.1 GCash Business (PH)

Auth flow:

  • API key (gcash_api_key) + merchant ID stored in config_json.
  • GCash Business API (Mynt/GXI) — server-to-server with static credentials.
  • Settlement webhook (optional): GCash can push settlement status updates to Lumi’s webhook endpoint, but initial implementation is polling-only.

API contract:

MethodPathAuth/RoleRequestResponseErrors
POST/api/source-connections/Ownerprovider_slug=gcash201 — connection400 — missing api_key
(internal)Poll GCash disbursement API—API key, merchant IDlist[dict] disbursements401 → degraded; 503 → backoff

Polling behavior:

  • Endpoint: GCash settlement status API (documented per merchant agreement).
  • Interval: 15 minutes.
  • Retry on 503: 3 attempts at 60/120/240s backoff. GCash 503s are common during PH bank maintenance (2-4 AM). Log at INFO (not ERROR) because they are expected.

Signals captured:

  • Failed disbursement → SourceEvent with proposal_type=risk (payroll failure = critical severity).
  • Daily wallet balance → summary event (if balance below configurable threshold → risk).

Normalized event:

{
  "event_type": "disbursement_failed",
  "currency": "PHP",
  "amount": 50000.00,
  "market": "ph",
  "proposal_type": "risk",
  "severity_hint": "critical"
}

Error states:

ScenarioUser-visibleInternalLogRetry
GCash 503 (single)Transient — no visible changeBackoff ×3 (60/120/240s)INFO gcash_503_retry_NNext poll cycle
GCash 503 (3× consecutive)Connector badge: “Degraded”status=degradedWARNING gcash_3x_503Next poll auto-recovers
API key revokedConnector badge: “Degraded”401 on pollERROR gcash_auth_revokedManual re-connect

Test plan additions:

#CaseTypeWhere
65GCash disbursement failed → critical risk signal (PHP)backendtest_connectors.py::TestGCashDisbursementFail
66GCash 503 retry ×3 (60/120/240)backendtest_connectors.py::TestGCash503Backoff
67GCash 503 ×3 consecutive → degradedbackendtest_connectors.py::TestGCashConsecutive503
68GCash 503 after retry → auto-recover on next cyclebackendtest_connectors.py::TestGCashAutoRecovery
69GCash daily wallet balance below threshold → riskbackendtest_connectors.py::TestGCashWalletThreshold

15.3.2 Maya Business (PH)

Auth flow:

  • API key (maya_api_key) + secret stored in config_json.
  • All requests signed with HMAC-SHA256.
  • Base URL: https://api.paymaya.com/ (production).

Signals captured:

  • Payout/batch settlement status → proposal_type=risk on failure.
  • Failed payment collection → risk.

Test plan additions:

#CaseTypeWhere
70Maya payout failure → risk signalbackendtest_connectors.py::TestMayaPayoutFail
71Maya API key invalid → degradedbackendtest_connectors.py::TestMayaAuthFailure

15.4 Saudi Arabia (KSA/SA) — Connectors

Connectors: Wafeq, Zoho Books, Noon Seller API, Amazon.sa SP-API, WhatsApp Business (shared §15.1.1)

15.4.1 Wafeq (SA)

Auth flow:

  • API key (wafeq_api_key) stored in config_json (no OAuth — Wafeq uses API key auth).
  • Base URL: https://api.wafeq.com/v1/ (arabic-enabled — may return Arabic error messages).

API contract:

MethodPathAuth/RoleRequestResponseErrors
POST/api/source-connections/Ownerprovider_slug=wafeq201 — connection400 — missing api_key
(internal)Poll Wafeq Invoices API—API keylist[dict] invoices401 → degraded; Arabic errors → normalized

Polling behavior:

  • Endpoint: GET https://api.wafeq.com/v1/invoices?status=overdue.
  • KSA-specific thresholds: 30/60/90 day buckets (common in KSA B2B payment terms).
  • VAT return deadline detection: checks Wafeq’s tax_return endpoint for approaching filing dates.
  • Arabic normalization: known error codes mapped to English for last_error_text. Raw Arabic in config_json.last_raw_error.

Normalized event:

{
  "event_type": "invoice_overdue",
  "currency": "SAR",
  "amount": 5000.00,
  "market": "sa",
  "severity_hint": "medium"
}

Error states (extends §10 rows 4-5):

#ScenarioUser-visibleInternalLogRetry
4bWafeq Arabic error, known error codeConnector badge: “Degraded” with English errorNormalize to English, raw in metadataWARNING wafeq_error_<code>Next poll cycle
5bWafeq Arabic error, unknown error codeConnector badge: “Degraded — unexpected error”Raw Arabic in last_raw_errorERROR wafeq_unknown_errorNext poll cycle

Test plan additions:

#CaseTypeWhere
72Wafeq polls overdue invoices (SAR, KSA 30/60/90 thresholds)backendtest_connectors.py::TestWafeqOverdueInvoices
73Wafeq VAT return deadline approaching → task signalbackendtest_connectors.py::TestWafeqVATDeadline
74Wafeq Arabic error known code → normalized to Englishbackendtest_connectors.py::TestWafeqArabicKnownError
75Wafeq Arabic error unknown code → raw stored in metadatabackendtest_connectors.py::TestWafeqArabicUnknownError
76Wafeq 30/60/90 day severity escalationbackendtest_connectors.py::TestWafeqSeverityBuckets

15.4.2 Zoho Books (SA)

Auth flow:

  • OAuth2 authorization code flow via Zoho’s accounts portal.
  • Scopes: ZohoBooks.invoices.READ, ZohoBooks.contacts.READ, ZohoBooks.organization.READ.
  • organization_id stored in config_json.zoho_org_id.
  • Zoho tokens expire in 1 hour; refresh tokens never expire (unless revoked).

API contract:

MethodPathAuth/RoleRequestResponseErrors
POST/api/source-connections/Ownerprovider_slug=zoho_books201 — connection400 — missing token or org_id
(internal)Poll Zoho Books Invoices API—org_id, access_tokenlist[dict] invoices401 → refresh → if fail → degraded

Signals captured:

  • Overdue invoices (KSA-specific thresholds).
  • VAT compliance reminders (Zoho Books has built-in VAT tracking for KSA).

Test plan additions:

#CaseTypeWhere
77Zoho Books polls overdue invoices (SAR)backendtest_connectors.py::TestZohoOverdueInvoices
78Zoho Books token refresh on 401backendtest_connectors.py::TestZohoTokenRefresh

15.4.3 Noon Seller API (SA)

Auth flow:

  • Partner API via Noon’s Seller Portal — OAuth-like registered application.
  • client_id, client_secret, seller_id stored in config_json.
  • Access token obtained via client credentials grant.
  • Token expires in 24h.

API contract:

MethodPathAuth/RoleRequestResponseErrors
POST/api/source-connections/Ownerprovider_slug=noon201 — connection400 — missing credentials
(internal)Poll Noon Orders API—Bearer tokenlist[dict] orders401 → refresh; 401 after refresh → degraded

Polling behavior:

  • Endpoint: GET https://api.noon.com/seller/v1/orders?status=unfulfilled.
  • Cursor via cursor_json.last_order_id.
  • Interval: 15 minutes.
  • Unfulfilled orders > 48h → SourceEvent with proposal_type=task.
  • Return/refund received → proposal_type=risk.

Test plan additions:

#CaseTypeWhere
79Noon polls unfulfilled orders > 48h → task signalbackendtest_connectors.py::TestNoonUnfulfilledOrders
80Noon return/refund received → risk signalbackendtest_connectors.py::TestNoonReturnRisk
81Noon auth expired → degraded (reconnect required)backendtest_connectors.py::TestNoonAuthExpiry

15.4.4 Amazon.sa SP-API (SA)

Auth flow:

  • Amazon Selling Partner API (SP-API) via AWS IAM Role + client credentials.
  • refresh_token (seller authorization) + aws_iam_role_arn stored in config_json.
  • LWAA (Login With Amazon) token exchange for SP-API access.
  • All API calls signed with AWS Signature V4.

API contract:

MethodPathAuth/RoleRequestResponseErrors
POST/api/source-connections/Ownerprovider_slug=amazon_sa201 — connection400 — missing refresh_token
(internal)Poll Amazon Orders API—LWAA token + SigV4 signedlist[dict] orders401 → re-auth; throttling → backoff

Polling behavior:

  • Endpoint: GET https://sellingpartnerapi-eu.amazon.com/orders/v0/orders (EU endpoint covers .sa marketplace).
  • Marketplace ID: A2I9A3Q2FI4Y1A (Amazon.sa).
  • Throttling: SP-API has strict rate limits (1 request per second for orders API).
  • Return/refund received → proposal_type=risk.
  • Unfulfilled orders > 48h → task.

Test plan additions:

#CaseTypeWhere
82Amazon.sa polls orders → unfulfilled > 48h → taskbackendtest_connectors.py::TestAmazonSAUnfulfilled
83Amazon.sa return/refund → risk signalbackendtest_connectors.py::TestAmazonSAReturnRisk
84Amazon.sa throttling → backoff and retrybackendtest_connectors.py::TestAmazonSAThrottling

15.5 Latvia (LV/EU) — Connectors

Connectors: PSD2 Bank (via Tink aggregator), Jumis, Tildes Birojs, Xero (shared §15.1.2)

15.5.1 PSD2 Bank via Tink (LV)

Auth flow:

  • OAuth2 authorization code flow via Tink (Visa-owned, strongest Baltic aggregator coverage).
  • Tink’s Link UI handles bank selection, SCA (Strong Customer Authentication), and consent.
  • Consent scope: accounts:read:transactions, accounts:read:balance.
  • Consent expiry: 90 days (PSD2 SCA requirement — see §9.5).
  • Tink client ID + secret stored in config_json.tink_client_id, config_json.tink_client_secret.
  • User’s PSD2 consent ID stored in config_json.consent_id.
  • consent_expires_at timestamp in config_json is checked on every poll cycle.

API contract:

MethodPathAuth/RoleRequestResponseErrors
POST/api/source-connections/Ownerprovider_slug=psd2_bank201 — connection400 — missing Tink credentials
(internal)Poll Tink transactions API—Tink access tokenlist[dict] transactions401 → refresh; consent expired → degraded

Polling behavior:

  • Tink endpoint: GET https://api.tink.com/data/v2/transactions with pagination.
  • Cursor via cursor_json.last_booked (Tink returns accounting/booked transactions).
  • Interval: 15 minutes.
  • Transaction above configurable threshold (org setting, default: EUR 1,000) → proposal_type=decision.
  • Incoming payment matched to invoice (by reference number) → clears overdue signal → proposal_type=none (reconciliation).
  • Daily account balance summary → aggregated to Brief.

Normalized event:

{
  "event_type": "large_transaction",
  "currency": "EUR",
  "amount": 15000.00,
  "market": "lv",
  "proposal_type": "decision"
}

Error states (extends §10 row 3/7):

#ScenarioUser-visibleInternalLogRetry
3bPSD2 consent expired → degradedConnector badge: “Degraded — bank consent expired”status=degraded, last_error_text="PSD2 consent expired — re-authorize through your bank."WARNING psd2_consent_expiredRe-authorization only
15Tink API temporarily unavailable (5xx)Transient — no visible changeSkip pollWARNING tink_api_unavailableNext poll cycle
16Tink token expired before consentConnector badge: “Degraded”Refresh Tink token; if fails → degradedWARNING tink_token_expiredManual re-connect

Test plan additions:

#CaseTypeWhere
85PSD2/Tink polls transactions → events (EUR)backendtest_connectors.py::TestPSD2PollTransactions
86PSD2 large transaction above threshold → decision signalbackendtest_connectors.py::TestPSD2LargeTransaction
87PSD2 incoming payment matched to invoice reconciliationbackendtest_connectors.py::TestPSD2PaymentReconciliation
88PSD2 consent expired → degraded, warning text setbackendtest_connectors.py::TestPSD2ConsentExpiry
89PSD2 consent not yet expired → stays activebackendtest_connectors.py::TestPSD2ConsentValid
90PSD2 daily balance summary → aggregated eventbackendtest_connectors.py::TestPSD2DailyBalance

15.5.2 Jumis (LV)

See §9.8 for full CSV edge case documentation. Jumis Cloud edition has a nascent REST API (v1, limited coverage). Primary implementation is CSV upload via F34.

Auth flow (CSV import):

  • No live API — CSV export from Jumis desktop, upload via Lumi’s F34 upload system.
  • Import template documented in docs/connectors/jumis-import-template.csv.
  • Columns matched by header name (not position) — schema validation on import.

Auth flow (API — future):

  • Jumis Cloud API token stored in config_json.jumis_api_token.
  • Base URL: https://api.jumis.lv/v1/ (limited coverage — financial reports only).

Signals captured (CSV import):

  • Overdue invoices (EUR context).
  • Open receivables aging report.

Signals captured (API):

  • Same as CSV, plus real-time event tracking when API has coverage.

Test plan additions:

#CaseTypeWhere
91Jumis CSV import with correct columns → events createdbackendtest_connectors.py::TestJumisCSVImportSuccess
92Jumis CSV unknown columns → WARNING, best-effort parsebackendtest_connectors.py::TestJumisCSVUnknownColumns
93Jumis CSV missing required columns → import fails with validation errorbackendtest_connectors.py::TestJumisCSVMissingColumns
94Jumis CSV failed import preserves first 5 rows in config for debuggingbackendtest_connectors.py::TestJumisCSVFailedPreview

15.5.3 Tildes Birojs (LV)

Auth flow:

  • API key (tildes_api_key) stored in config_json.
  • Webhook support (Tildes Birojs can push invoice status updates to Lumi).
  • If webhook unavailable, polling with 30-minute interval.

Signals captured:

  • Overdue invoice detection (EUR, same pattern as other accounting connectors).
  • Invoice status changes (paid, overdue, partially paid).

Test plan additions:

#CaseTypeWhere
95Tildes Birojs polls overdue invoices → events (EUR)backendtest_connectors.py::TestTildesBirojsOverdue
96Tildes Birojs webhook receives invoice updatebackendtest_connectors.py::TestTildesBirojsWebhook