Skip to content

B-02 · Configure ingestion channels

SOP: SOP_AI_Bookkeeping_Automation.md §3 (Email / WhatsApp / Folder)Actors: Platform Admin (admin@spade.local). Pre-state: A bookkeeping-enabled client exists (B-01 or seeded ZENITH). Post-state: One or more ClientIngestionChannel rows enabled for the client. The folder-sync worker picks up enabled cloud-folder channels on its next tick.

The seeded ZENITH client already has two channels (WHATSAPP + GOOGLE_DRIVE) — use this flow to add, edit, or disable channels.

0. Prerequisites

  • A bookkeeping-enabled client.
  • API logged in as admin@spade.local.

1. Steps

1.1 List existing channels

Web: /dashboard/clients/<clientId>/ingestion-channels. The seed prints the URL.

Or via API:

http
GET /admin/clients/<clientId>/ingestion-channels

ZENITH should return two channels:

  • WHATSAPPZenith Ops WhatsApp+6591234567
  • GOOGLE_DRIVEZenith Shared Drive — Bookkeeping/Shared drives/Zenith/Bookkeeping/Inbox

Both enabled: true. lastPolledAt and lastError columns are empty (the seed does not run the poller).

1.2 Add a new channel

http
POST /admin/clients/<clientId>/ingestion-channels

{
  "channelType": "SHAREPOINT",
  "label": "Testbook SharePoint Inbox",
  "enabled": true,
  "configJson": {
    "siteId": "<sharepoint-site-id>",
    "driveId": "<drive-id>",
    "folderPath": "/Documents/Bookkeeping/Inbox"
  },
  "credentialsRef": "secretref:testbk/sharepoint"
}

Channel types currently supported: WHATSAPP, GOOGLE_DRIVE, SHAREPOINT, DROPBOX. The Add channel dialog shows the full enum.

configJson is per-type:

TypeRequired keys
WHATSAPPphoneNumberId, displayNumber
GOOGLE_DRIVEdriveId, folderPath
SHAREPOINTsiteId, driveId, folderPath
DROPBOXaccountId, folderPath

credentialsRef is a placeholder string the worker resolves against the secret manager. In dev with a mock secret manager this is informational.

1.3 Edit a channel

http
PUT /admin/clients/<clientId>/ingestion-channels/<channelId>

{
  "label": "Updated label",
  "enabled": false,
  "configJson": { ... }
}

Toggling enabled: false is the soft-delete: the worker skips polling for this channel. The row stays in the table for audit.

1.4 Hard-delete a channel

http
DELETE /admin/clients/<clientId>/ingestion-channels/<channelId>

Only allowed if the channel has not produced any BookkeepingDocument rows yet. Otherwise the row stays and the API returns 409 with the document count.

1.5 Verify the worker picks it up

bash
pnpm --filter @breezycorp/worker dev

bookkeeping.folder-sync runs on a configurable interval (default 5 min). On each tick it iterates enabled GOOGLE_DRIVE / SHAREPOINT / DROPBOX channels (WhatsApp is webhook-only, no polling) and:

  • Fetches new docs since lastCursor.
  • For each new file, enqueues bookkeeping.ingest-document.
  • Updates lastCursor and lastPolledAt.

Watch worker logs for bookkeeping.folder-sync.poll channelId=<id>.

2. Verification

Database

sql
SELECT id, channel_type, label, enabled, last_polled_at, last_error
  FROM client_ingestion_channels WHERE client_id = '<clientId>';

Web UI

/dashboard/clients/<clientId>/ingestion-channels lists every channel with toggle states and last-polled stamp.

Audit log

client.ingestion_channel.created  channelType=SHAREPOINT
client.ingestion_channel.updated  field=enabled
client.ingestion_channel.deleted  channelId=<id>

3. Negative & edge cases

  • Empty configJson → 400 Bad Request "configJson must not be empty".
  • Wrong shape for configJson (e.g. WHATSAPP missing phoneNumberId) → 400 with the missing keys.
  • Hard-delete with documents present → 409 with documentCount.
  • Multiple WHATSAPP channels for the same client — allowed, but the inbound webhook (POST /hooks/whatsapp) routes by phoneNumberId, so each must have a distinct ID.
  • Disabled channel + new file — folder-sync skips it. The webhook for WHATSAPP still accepts inbound but the handler short-circuits when the channel is disabled.

Next

Proceed to B-03 · Ingest a document — manual upload for the simplest happy-path or B-04 · Ingest via channels for the WhatsApp / Drive paths.

Internal use only — BreezyCorp