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→SignalInboxItemwithproposal_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 tocom.intuit.quickbooks.accounting). - Invoice 15+ days overdue →
SourceEventwithproposal_type=risk. - Invoice 30+ days overdue → critical severity.
- Multi-currency support (MYR, SGD when cross-border). Store currency in
SourceEvent.metadata.
- Connect Xero (OAuth2, scoped to
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) →SourceEventwithproposal_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 →
SourceEventwithproposal_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 →
SourceEventwithproposal_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 →
SourceEventwithproposal_type=task. - Return/refund received →
SourceEventwithproposal_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 →
SourceEventwithproposal_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
marketsarray:["vn", "my", "id", "ph", "sa", "lv", "global"]. - The catalog endpoint
GET /api/source-connections/catalog/accepts?market=myand returns only relevant connectors. - Global connectors (Shopify, Google Workspace, Xero, QuickBooks, MS 365) appear in all markets.
- Each connector has a
-
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
currencyandamountin 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:
| Change | Detail |
|---|---|
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 response | Includes 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_tokopediasources.connector_desc_wafeq,sources.connector_desc_jurnal,sources.connector_desc_jumissources.connector_desc_gcash,sources.connector_desc_noonsources.market_label_my,sources.market_label_id,sources.market_label_ph,sources.market_label_sa,sources.market_label_lvsources.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 includeTestWhatsAppConnector,TestXeroMultiCurrency,TestTokopediaConnector,TestWafeqConnector,TestGCashConnector,TestJumisFallback,TestPSD2BankConnector,TestRegionalConnectorTenantIsolation,TestSourceEventCurrencyMetadata,TestCatalogMarketFilter. Market-filter scoping is intest_multitenancy.py. Rows below use original per-case naming; the file is authoritative.
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Connector catalog filters by market=my | backend | tests/test_connectors.py::TestCatalogMarketFilter |
| 2 | Connector catalog with no market param returns global + all market connectors | backend | tests/test_connectors.py |
| 3 | Connector catalog for unsupported market returns global connectors only | backend | tests/test_connectors.py |
| 4 | WhatsApp connector webhook receives message → creates event | backend | tests/test_connectors.py::TestWhatsAppConnector |
| 5 | WhatsApp connector webhook signature mismatch returns 401, connector stays active | backend | tests/test_connectors.py |
| 6 | WhatsApp connector — unanswered messages create SourceEvents at 2h/6h/24h severity tiers | backend | tests/test_connectors.py |
| 7 | Xero connector detects MYR overdue invoice with correct currency | backend | tests/test_connectors.py::TestXeroMultiCurrency |
| 8 | Xero connector — multi-currency conversion rate unavailable → amount_usd_approx = null | backend | tests/test_connectors.py |
| 9 | Tokopedia connector detects unfulfilled order (ID market, 24h threshold) | backend | tests/test_connectors.py::TestTokopediaConnector |
| 10 | Shopee regional connector configured for ID vs PH returns correct market endpoints | backend | tests/test_connectors.py |
| 11 | Wafeq connector detects overdue invoice (SAR, KSA 30/60/90 day thresholds) | backend | tests/test_connectors.py::TestWafeqConnector |
| 12 | Wafeq connector — Arabic error message normalized with raw stored in metadata | backend | tests/test_connectors.py |
| 13 | Jumis connector handles CSV fallback when API unavailable | backend | tests/test_connectors.py::TestJumisFallback |
| 14 | Jumis CSV import — unknown columns logged as warning, not error | backend | tests/test_connectors.py |
| 15 | GCash disbursement failure → risk signal (PHP) | backend | tests/test_connectors.py::TestGCashConnector |
| 16 | GCash settlement API 503 → retry ×3; after 3 consecutive failures → degraded | backend | tests/test_connectors.py |
| 17 | PSD2 bank connector via Tink aggregator (EUR, Latvia) | backend | tests/test_connectors.py::TestPSD2BankConnector |
| 18 | PSD2 bank consent expires (90-day SCA) → degraded, “Bank consent expired” warning | backend | tests/test_connectors.py |
| 19 | SourceEvent carries correct currency + amount in metadata for all financial connectors | backend | tests/test_sources.py (extend existing) |
| 20 | Cross-tenant isolation — MY org’s WhatsApp never leaks to ID org | backend | tests/test_multitenancy.py |
| 21 | Market shown to wrong market (UI bug) — catalog API enforces filter server-side | backend | tests/test_multitenancy.py |
| 22 | Org country determined at signup auto-filters connector catalog | backend | tests/test_connectors.py |
| 23 | Admin overrides org market in settings → catalog respects override | backend | tests/test_connectors.py |
| 24 | Noon seller API connector — unfulfilled orders > 48h create task signals | backend | tests/test_connectors.py |
| 25 | Amazon SP-API for .sa — return/refund received creates risk signal | backend | tests/test_connectors.py |
| 26 | 1Office staffing alert — leave within 7d of project deadline → commitment signal | backend | tests/test_connectors.py |
| 27 | Admin connects WhatsApp Business, sees events in inbox | e2e | describe("Regional Connectors") |
| 28 | Brief shows currency badge on financial signals | e2e | same block |
| 29 | Connector config_json stores market override correctly | backend | tests/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
| # | Scenario | User-visible | Internal | Log | Retry |
|---|---|---|---|---|---|
| 1 | WhatsApp webhook signature HMAC mismatch (single) | No visible change — transient | Reject payload | WARNING “whatsapp_hmac_mismatch” | Next webhook delivery |
| 2 | WhatsApp webhook signature HMAC mismatch (3× consecutive) | No visible change — Sentry only | Fire Sentry alert | ERROR “whatsapp_hmac_mismatch_3x” | Next webhook delivery |
| 3 | PSD2 bank consent expires (90-day SCA) | Connector badge: “Degraded — bank consent expired” | status=degraded, last_error_text set | WARNING “psd2_consent_expired” | Re-authorization flow only |
| 4 | Wafeq Arabic error, known error code | Connector badge: “Degraded” with English error | Normalize to English, raw in metadata | WARNING “wafeq_error_“ | Next poll cycle |
| 5 | Wafeq Arabic error, unknown error code | Connector badge: “Degraded — unexpected error” | Raw Arabic in last_raw_error | ERROR “wafeq_unknown_error” | Next poll cycle |
| 6 | GCash 503 | Transient — no visible change | Backoff ×3 (60/120/240s) | INFO “gcash_503_retry_N” | Next poll cycle |
| 7 | GCash 503 ×3 consecutive | Connector badge: “Degraded” | status=degraded | WARNING “gcash_3x_503” | Next poll auto-recovers |
| 8 | Jumis CSV header mismatch (unknown columns) | Import succeeds with partial data | Unknown columns skipped | WARNING “jumis_csv_unknown_columns” | n/a |
| 9 | Jumis CSV missing required columns | Import fails, validation error shown | Raw payload preview stored | ERROR “jumis_csv_missing_columns” | Re-upload with correct format |
| 10 | Multi-currency conversion unavailable | Brief shows original currency only | amount_usd_approx = null | WARNING “forex_rate_unavailable” | Retry on next brief gen |
| 11 | Noon API returns 401 | Connector badge: “Degraded — reconnect” | Token refresh fails → degraded | WARNING “noon_auth_expired” | Manual reconnect |
| 12 | Amazon SP-API throttling token exhausted | Transient — no visible change | Wait for throttling reset (documented per API) | INFO “amazon_throttled” | Next poll cycle |
| 13 | 1Office API down | Transient — no visible change | Skip poll cycle | WARNING “1office_unreachable” | Next poll cycle |
| 14 | Xero webhook intent-to-receive fails | Webhook registration fails on connect | status=active (poll fallback), last_error_text set | WARNING “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.
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | Market-aware connector catalog (?market= filter on /api/source-connections/catalog/) | ✅ match | backend/apps/sources/views.py, backend/tests/test_connectors.py |
| 2 | WhatsApp Business connector — Meta webhook verification and unanswered-message ingestion | ✅ match | backend/apps/connectors/whatsapp.py, F76 (planned spec) |
| 3 | Xero connector — OAuth2, overdue invoice detection, multi-currency (MYR/PHP/EUR) | ✅ match | backend/apps/connectors/xero.py |
| 4 | QuickBooks Online connector — OAuth2, overdue invoice detection (MY/PH) | ✅ match | backend/apps/connectors/qbo.py |
| 5 | Tokopedia connector — OAuth2, unfulfilled orders > 24h, IDR currency | ✅ match | backend/apps/connectors/tokopedia.py |
| 6 | Shopee regional connector — market-scoped endpoints (ID/PH/MY) | ✅ match | backend/apps/connectors/shopee.py |
| 7 | GCash 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) | ✅ match | backend/apps/connectors/gcash.py, backend/tests/test_connectors.py::TestGCashConnector::test_gcash_settlement_api_503_retries_then_degraded |
| 8 | Wafeq connector — Arabic error normalization, KSA 30/60/90-day severity buckets | ✅ match | backend/apps/connectors/wafeq.py |
| 9 | PSD2 bank connector — consent expiry handling, EUR, LV | ✅ match | backend/apps/connectors/psd2_bank.py |
| 10 | Jumis CSV import connector — header-name matching and validation path | ✅ match | backend/apps/connectors/jumis.py |
| 11 | Multi-currency connector metadata path exists in shipped regional connectors | ✅ match | backend/tests/test_connectors.py, docs/specs/F44-api-integrations.md |
| 12 | /settings/sources frontend is the shared connection surface for regional connectors | ⚠️ partial | Backend/API contract shipped; dedicated market-polish claims in this spec overstate the current frontend surface |
12. Open Questions / Decisions
- 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.
- 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.
- 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.
- Market determination: How does Lumi know an org’s market? Decision: org signup captures country (already in
Organizationmodel). The connector catalog uses this. Admins can override in org settings for multi-market operations. - 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.
- 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_textandmetadata. 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.pypoll()now persistscursor_json.retry_countto the DB on every 503, so the counter survives across separatesync_single_connectiontask invocations (previously the in-memory mutation was never written back and reset to 0 on each call).- After the 3rd consecutive 503,
poll()raisesRuntimeError("GCash API unavailable — N consecutive 503s").sync_single_connectioncatches any exception frompoll()and callsmark_connection_degraded, settingconnection.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_countis reset to 0 andcursor_jsonis saved, preventing a false degradation after recovery. test_gcash_settlement_api_503_retries_then_degradedwas hollow (pre-setstatus="degraded"and asserted it). Rewritten to drive three real sync cycles throughsync_single_connectionwith HTTP mocked to 503, asserting: (1) after 1st 503 — status stillactive; (2) after 2nd 503 — stillactive; (3) after 3rd 503 — statusdegraded.
Root cause of the gap:
- The original
poll()mutatedcursor["retry_count"]in-memory but never calledconnection.save(update_fields=["cursor_json"]). Becausesync_single_connectionfetches a fresh DB object on each invocation, the count was always 0. Thelogger.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_tokenGET request; connector responds with challenge string from config. - HMAC-SHA256 signature verification on all incoming webhook payloads (
X-Hub-Signature-256header vs SHA256(app_secret + payload_body)). - Webhook delivery: Meta pushes
messagesandmessage_template_status_updatewebhook events in real-time. - Default rule on connect:
event_type=whatsapp_unanswered→proposal_type=riskwith severity tiers at 2h / 6h / 24h.
API contract — Webhook ingest endpoint:
| Method | Path | Auth | Request | Response | Errors |
|---|---|---|---|---|---|
| GET | /api/source-connections/webhook/<connection_id>/ | hub.verify_token match | Query: hub.mode, hub.verify_token, hub.challenge | 200 — challenge string | 401 — verify token mismatch |
| POST | /api/source-connections/webhook/<connection_id>/ | HMAC-SHA256 signature | Meta webhook JSON (message, status update, etc.) | 202 — {"event_id": "..."} | 401 — signature mismatch; 400 — invalid payload → degraded; 500 — ingest error → degraded |
Error states:
| Scenario | User-visible | Internal | Log | Retry |
|---|---|---|---|---|
| HMAC mismatch (single) | None — transient | Reject payload | WARNING whatsapp_hmac_mismatch | Next webhook delivery (automatic) |
| HMAC mismatch (3× consecutive) | None — Sentry only | Fire Sentry alert | ERROR whatsapp_hmac_mismatch_3x | Next webhook delivery |
| Meta webhook disabled on their end | Connector badge: “Degraded” | Poll health check detects missing webhook | ERROR whatsapp_webhook_not_found | Manual re-connect |
| Rate-limited by Meta API | Visible on connector: “Rate limited — retrying” | Backoff 60s, then resume | WARNING whatsapp_rate_limited | Next poll cycle |
Test plan additions (extends §7):
| # | Case | Type | Where |
|---|---|---|---|
| 30 | WhatsApp webhook verification (GET challenge) | backend | test_connectors.py::TestWhatsAppWebhookVerify |
| 31 | WhatsApp webhook receives text message → creates SourceEvent | backend | test_connectors.py::TestWhatsAppInboundMessage |
| 32 | WhatsApp unanswered > 2h → risk at medium severity | backend | test_connectors.py::TestWhatsAppSeverity2h |
| 33 | WhatsApp unanswered > 6h → risk at high severity | backend | test_connectors.py::TestWhatsAppSeverity6h |
| 34 | WhatsApp unanswered > 24h → risk at critical severity | backend | test_connectors.py::TestWhatsAppSeverity24h |
| 35 | WhatsApp signature mismatch (single) → connector stays active | backend | test_connectors.py::TestWhatsAppSingleSignatureFail |
| 36 | WhatsApp signature mismatch (3 consecutive) → Sentry fired | backend | test_connectors.py::TestWhatsAppThreeSignatureFail |
| 37 | WhatsApp webhook for disabled connection → 401 | backend | test_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_idstored inconfig_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_receivePOST with a key; connector responds with the key. If webhook registration fails, connector falls back to polling.
API contract:
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| POST | /api/source-connections/ | Owner | provider_slug=xero, config_json with OAuth token | 201 — connection | 400 — 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_token | list[dict] invoices | 401 — 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:
| Scenario | User-visible | Internal | Log | Retry |
|---|---|---|---|---|
| Token expired | Connector badge: “Degraded — re-authorize” | Attempt refresh; if fails → degraded | WARNING xero_token_expired | Manual re-connect |
| Webhook registration fails | status=active (poll fallback), tooltip in UI | Poll fallback | WARNING xero_webhook_registration_failed | 3× retry at 1-hour intervals |
| Rate limited (429) | Transient — no visible change | Exponential backoff 60/120/240s | INFO xero_rate_limited | Next poll cycle |
| Xero API down (5xx) | Transient — skipped poll | Log warning | WARNING xero_api_error | Next poll cycle |
Test plan additions:
| # | Case | Type | Where |
|---|---|---|---|
| 38 | Xero polls overdue invoices → creates events | backend | test_connectors.py::TestXeroPollOverdue |
| 39 | Xero multi-currency: MYR invoice with correct currency field | backend | test_connectors.py::TestXeroMultiCurrencyMYR |
| 40 | Xero multi-currency: PHP invoice in PH market context | backend | test_connectors.py::TestXeroMultiCurrencyPHP |
| 41 | Xero webhook intent-to-receive succeeds | backend | test_connectors.py::TestXeroWebhookIntent |
| 42 | Xero webhook registration fails → poll fallback | backend | test_connectors.py::TestXeroWebhookFallback |
| 43 | Xero token refresh succeeds on 401 | backend | test_connectors.py::TestXeroTokenRefresh |
| 44 | Xero token refresh fails → degraded | backend | test_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 inconfig_json.qbo_realm_id.- Token refresh via
refresh_token(Intuit tokens expire in 101 days, refresh tokens in 101 days).
API contract:
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| POST | /api/source-connections/ | Owner | provider_slug=quickbooks_online | 201 — connection | 400 — missing token |
| (internal) | Poll QBO Invoice API | — | realm_id, access_token | list[dict] invoices | 401 → 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 usesMetaData.LastUpdatedTimefor change tracking).
Test plan additions:
| # | Case | Type | Where |
|---|---|---|---|
| 45 | QBO polls overdue invoices → creates events | backend | test_connectors.py::TestQBOOverdueInvoices |
| 46 | QBO multi-currency: MYR invoice billing | backend | test_connectors.py::TestQBOMultiCurrencyMYR |
| 47 | QBO token refresh succeeds on 401 | backend | test_connectors.py::TestQBOTokenRefresh |
15.1.4 Touch ‘n Go e-Wallet (MY)
Auth flow:
- API key (
tn_g_api_key) + merchant ID stored inconfig_json. - All requests signed with HMAC-SHA256 using the API secret.
- No OAuth — server-to-server with static API credentials.
API contract:
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| POST | /api/source-connections/ | Owner | provider_slug=touch_n_go | 201 — connection | 400 — missing api_key |
| (internal) | Poll TnG transaction API | — | HMAC-signed request | list[dict] transactions | 401 → degraded |
Signals captured:
- Failed/disputed transaction →
SourceEventwithproposal_type=risk. - Daily balance summary (balance below configurable threshold → risk).
- Settlement delayed > 24h → risk.
Error states:
| Scenario | User-visible | Internal | Log | Retry |
|---|---|---|---|---|
| API key invalid | Connector badge: “Degraded” | 401 on first poll | ERROR tn_g_auth_failed | Manual re-connect |
| TnG system maintenance | Transient (PH bank maintenance window 2-4 AM) | Skip poll | INFO tn_g_maintenance | Next poll cycle |
Test plan additions:
| # | Case | Type | Where |
|---|---|---|---|
| 48 | TnG polls transactions → creates events | backend | test_connectors.py::TestTnGPollTransactions |
| 49 | TnG dispute detected → risk signal | backend | test_connectors.py::TestTnGDisputeEvent |
| 50 | TnG invalid API key → degraded on first poll | backend | test_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:
| # | Case | Type | Where |
|---|---|---|---|
| 51 | GrabPay settlement failure → risk signal | backend | test_connectors.py::TestGrabPaySettlementFailure |
| 52 | GrabPay daily settlement summary poll | backend | test_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_idandaccess_tokenstored inconfig_json.- Token refresh via
refresh_token(expires in 24h — Tokopedia uses short-lived tokens + refresh).
API contract:
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| POST | /api/source-connections/ | Owner | provider_slug=tokopedia | 201 — connection | 400 — missing token or shop_id |
| (internal) | Poll Tokopedia Order API | — | shop_id, access_token | list[dict] orders | 401 → 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:
| Scenario | User-visible | Internal | Log | Retry |
|---|---|---|---|---|
| Token expired | Connector badge: “Degraded — re-authorize” | Refresh token; if fail → degraded | WARNING tokopedia_token_expired | Manual re-connect |
| Tokopedia API rate limit (429) | Transient | Backoff 60s | INFO tokopedia_rate_limited | Next poll cycle |
| Shop deleted/disabled | Connector badge: “Degraded — reconnect” | Poll returns 404 | WARNING tokopedia_shop_not_found | Manual re-connect |
Test plan additions:
| # | Case | Type | Where |
|---|---|---|---|
| 53 | Tokopedia polls unfulfilled orders → creates task events | backend | test_connectors.py::TestTokopediaUnfulfilledOrders |
| 54 | Tokopedia unfulfilled > 24h with IDR currency in metadata | backend | test_connectors.py::TestTokopedia24hThreshold |
| 55 | Tokopedia cancellation rate spike > 5% → aggregated risk | backend | test_connectors.py::TestTokopediaCancellationRate |
| 56 | Tokopedia token refresh succeeds on 401 | backend | test_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_keyfrom Shopee Partner application.- Each API call signed with HMAC-SHA256 using
partner_key. shop_idscoped per market (different shop ID for ID vs PH).marketfield inconfig_jsondetermines 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/
- ID:
API contract:
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| POST | /api/source-connections/ | Owner | provider_slug=shopee, config_json.market="id" or "ph" | 201 — connection | 400 — missing market or shop_id |
| (internal) | Poll Shopee Order API | — | Signed request with shop_id, timestamp | list[dict] orders | 401 → 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:
| # | Case | Type | Where |
|---|---|---|---|
| 57 | Shopee ID: polls unfulfilled orders using ID endpoint | backend | test_connectors.py::TestShopeeIDOrders |
| 58 | Shopee PH: polls orders using PH endpoint | backend | test_connectors.py::TestShopeePHOrders |
| 59 | Shopee: market config mismatch returns validation error | backend | test_connectors.py::TestShopeeMarketMismatch |
| 60 | Shopee: order cancellation spike trigger | backend | test_connectors.py::TestShopeeCancellationSpike |
15.2.3 Jurnal.id (ID)
Auth flow:
- API key (
jurnal_api_key) stored inconfig_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:
| # | Case | Type | Where |
|---|---|---|---|
| 61 | Jurnal.id polls overdue invoices → events | backend | test_connectors.py::TestJurnalOverdueInvoices |
| 62 | Jurnal.id 30/60/90 day severity tiers | backend | test_connectors.py::TestJurnalSeverityTiers |
15.2.4 Accurate Online (ID)
Auth flow:
- API key (
accurate_api_key) stored inconfig_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:
| # | Case | Type | Where |
|---|---|---|---|
| 63 | Accurate polls overdue invoices → events | backend | test_connectors.py::TestAccurateOverdueInvoices |
| 64 | Accurate API key invalid → degraded | backend | test_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 inconfig_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:
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| POST | /api/source-connections/ | Owner | provider_slug=gcash | 201 — connection | 400 — missing api_key |
| (internal) | Poll GCash disbursement API | — | API key, merchant ID | list[dict] disbursements | 401 → 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 →
SourceEventwithproposal_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:
| Scenario | User-visible | Internal | Log | Retry |
|---|---|---|---|---|
| GCash 503 (single) | Transient — no visible change | Backoff ×3 (60/120/240s) | INFO gcash_503_retry_N | Next poll cycle |
| GCash 503 (3× consecutive) | Connector badge: “Degraded” | status=degraded | WARNING gcash_3x_503 | Next poll auto-recovers |
| API key revoked | Connector badge: “Degraded” | 401 on poll | ERROR gcash_auth_revoked | Manual re-connect |
Test plan additions:
| # | Case | Type | Where |
|---|---|---|---|
| 65 | GCash disbursement failed → critical risk signal (PHP) | backend | test_connectors.py::TestGCashDisbursementFail |
| 66 | GCash 503 retry ×3 (60/120/240) | backend | test_connectors.py::TestGCash503Backoff |
| 67 | GCash 503 ×3 consecutive → degraded | backend | test_connectors.py::TestGCashConsecutive503 |
| 68 | GCash 503 after retry → auto-recover on next cycle | backend | test_connectors.py::TestGCashAutoRecovery |
| 69 | GCash daily wallet balance below threshold → risk | backend | test_connectors.py::TestGCashWalletThreshold |
15.3.2 Maya Business (PH)
Auth flow:
- API key (
maya_api_key) + secret stored inconfig_json. - All requests signed with HMAC-SHA256.
- Base URL:
https://api.paymaya.com/(production).
Signals captured:
- Payout/batch settlement status →
proposal_type=riskon failure. - Failed payment collection → risk.
Test plan additions:
| # | Case | Type | Where |
|---|---|---|---|
| 70 | Maya payout failure → risk signal | backend | test_connectors.py::TestMayaPayoutFail |
| 71 | Maya API key invalid → degraded | backend | test_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 inconfig_json(no OAuth — Wafeq uses API key auth). - Base URL:
https://api.wafeq.com/v1/(arabic-enabled — may return Arabic error messages).
API contract:
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| POST | /api/source-connections/ | Owner | provider_slug=wafeq | 201 — connection | 400 — missing api_key |
| (internal) | Poll Wafeq Invoices API | — | API key | list[dict] invoices | 401 → 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_returnendpoint for approaching filing dates. - Arabic normalization: known error codes mapped to English for
last_error_text. Raw Arabic inconfig_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):
| # | Scenario | User-visible | Internal | Log | Retry |
|---|---|---|---|---|---|
| 4b | Wafeq Arabic error, known error code | Connector badge: “Degraded” with English error | Normalize to English, raw in metadata | WARNING wafeq_error_<code> | Next poll cycle |
| 5b | Wafeq Arabic error, unknown error code | Connector badge: “Degraded — unexpected error” | Raw Arabic in last_raw_error | ERROR wafeq_unknown_error | Next poll cycle |
Test plan additions:
| # | Case | Type | Where |
|---|---|---|---|
| 72 | Wafeq polls overdue invoices (SAR, KSA 30/60/90 thresholds) | backend | test_connectors.py::TestWafeqOverdueInvoices |
| 73 | Wafeq VAT return deadline approaching → task signal | backend | test_connectors.py::TestWafeqVATDeadline |
| 74 | Wafeq Arabic error known code → normalized to English | backend | test_connectors.py::TestWafeqArabicKnownError |
| 75 | Wafeq Arabic error unknown code → raw stored in metadata | backend | test_connectors.py::TestWafeqArabicUnknownError |
| 76 | Wafeq 30/60/90 day severity escalation | backend | test_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_idstored inconfig_json.zoho_org_id.- Zoho tokens expire in 1 hour; refresh tokens never expire (unless revoked).
API contract:
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| POST | /api/source-connections/ | Owner | provider_slug=zoho_books | 201 — connection | 400 — missing token or org_id |
| (internal) | Poll Zoho Books Invoices API | — | org_id, access_token | list[dict] invoices | 401 → 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:
| # | Case | Type | Where |
|---|---|---|---|
| 77 | Zoho Books polls overdue invoices (SAR) | backend | test_connectors.py::TestZohoOverdueInvoices |
| 78 | Zoho Books token refresh on 401 | backend | test_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_idstored inconfig_json.- Access token obtained via client credentials grant.
- Token expires in 24h.
API contract:
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| POST | /api/source-connections/ | Owner | provider_slug=noon | 201 — connection | 400 — missing credentials |
| (internal) | Poll Noon Orders API | — | Bearer token | list[dict] orders | 401 → 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 →
SourceEventwithproposal_type=task. - Return/refund received →
proposal_type=risk.
Test plan additions:
| # | Case | Type | Where |
|---|---|---|---|
| 79 | Noon polls unfulfilled orders > 48h → task signal | backend | test_connectors.py::TestNoonUnfulfilledOrders |
| 80 | Noon return/refund received → risk signal | backend | test_connectors.py::TestNoonReturnRisk |
| 81 | Noon auth expired → degraded (reconnect required) | backend | test_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_arnstored inconfig_json.- LWAA (Login With Amazon) token exchange for SP-API access.
- All API calls signed with AWS Signature V4.
API contract:
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| POST | /api/source-connections/ | Owner | provider_slug=amazon_sa | 201 — connection | 400 — missing refresh_token |
| (internal) | Poll Amazon Orders API | — | LWAA token + SigV4 signed | list[dict] orders | 401 → 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:
| # | Case | Type | Where |
|---|---|---|---|
| 82 | Amazon.sa polls orders → unfulfilled > 48h → task | backend | test_connectors.py::TestAmazonSAUnfulfilled |
| 83 | Amazon.sa return/refund → risk signal | backend | test_connectors.py::TestAmazonSAReturnRisk |
| 84 | Amazon.sa throttling → backoff and retry | backend | test_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_attimestamp inconfig_jsonis checked on every poll cycle.
API contract:
| Method | Path | Auth/Role | Request | Response | Errors |
|---|---|---|---|---|---|
| POST | /api/source-connections/ | Owner | provider_slug=psd2_bank | 201 — connection | 400 — missing Tink credentials |
| (internal) | Poll Tink transactions API | — | Tink access token | list[dict] transactions | 401 → refresh; consent expired → degraded |
Polling behavior:
- Tink endpoint:
GET https://api.tink.com/data/v2/transactionswith 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):
| # | Scenario | User-visible | Internal | Log | Retry |
|---|---|---|---|---|---|
| 3b | PSD2 consent expired → degraded | Connector badge: “Degraded — bank consent expired” | status=degraded, last_error_text="PSD2 consent expired — re-authorize through your bank." | WARNING psd2_consent_expired | Re-authorization only |
| 15 | Tink API temporarily unavailable (5xx) | Transient — no visible change | Skip poll | WARNING tink_api_unavailable | Next poll cycle |
| 16 | Tink token expired before consent | Connector badge: “Degraded” | Refresh Tink token; if fails → degraded | WARNING tink_token_expired | Manual re-connect |
Test plan additions:
| # | Case | Type | Where |
|---|---|---|---|
| 85 | PSD2/Tink polls transactions → events (EUR) | backend | test_connectors.py::TestPSD2PollTransactions |
| 86 | PSD2 large transaction above threshold → decision signal | backend | test_connectors.py::TestPSD2LargeTransaction |
| 87 | PSD2 incoming payment matched to invoice reconciliation | backend | test_connectors.py::TestPSD2PaymentReconciliation |
| 88 | PSD2 consent expired → degraded, warning text set | backend | test_connectors.py::TestPSD2ConsentExpiry |
| 89 | PSD2 consent not yet expired → stays active | backend | test_connectors.py::TestPSD2ConsentValid |
| 90 | PSD2 daily balance summary → aggregated event | backend | test_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:
| # | Case | Type | Where |
|---|---|---|---|
| 91 | Jumis CSV import with correct columns → events created | backend | test_connectors.py::TestJumisCSVImportSuccess |
| 92 | Jumis CSV unknown columns → WARNING, best-effort parse | backend | test_connectors.py::TestJumisCSVUnknownColumns |
| 93 | Jumis CSV missing required columns → import fails with validation error | backend | test_connectors.py::TestJumisCSVMissingColumns |
| 94 | Jumis CSV failed import preserves first 5 rows in config for debugging | backend | test_connectors.py::TestJumisCSVFailedPreview |
15.5.3 Tildes Birojs (LV)
Auth flow:
- API key (
tildes_api_key) stored inconfig_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:
| # | Case | Type | Where |
|---|---|---|---|
| 95 | Tildes Birojs polls overdue invoices → events (EUR) | backend | test_connectors.py::TestTildesBirojsOverdue |
| 96 | Tildes Birojs webhook receives invoice update | backend | test_connectors.py::TestTildesBirojsWebhook |