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 moreClientIngestionChannelrows 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:
GET /admin/clients/<clientId>/ingestion-channelsZENITH should return two channels:
- WHATSAPP — Zenith Ops WhatsApp —
+6591234567 - GOOGLE_DRIVE — Zenith 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
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:
| Type | Required keys |
|---|---|
WHATSAPP | phoneNumberId, displayNumber |
GOOGLE_DRIVE | driveId, folderPath |
SHAREPOINT | siteId, driveId, folderPath |
DROPBOX | accountId, 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
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
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
pnpm --filter @breezycorp/worker devbookkeeping.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
lastCursorandlastPolledAt.
Watch worker logs for bookkeeping.folder-sync.poll channelId=<id>.
2. Verification
Database
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 missingphoneNumberId) → 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 byphoneNumberId, 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.