B-06 · Generate upload file (Xero / QBO / Zoho / Tally)
SOP:
SOP_AI_Bookkeeping_Automation.md§4.4 (Upload File Generation)Actors: Bookkeeper (Action.GENERATE_UPLOAD_FILE). Pre-state:JournalBatchinDRAFTorAPPROVEDwith at least oneAPPROVEDentry. NoDRAFT/FLAGGED/ESCALATEDentries (the route refuses). Post-state: A platform-specific file uploaded to S3 +JournalBatch.uploadFileIdset + batch advances toEXPORTEDif it wasAPPROVED.
The reviewer manually uploads the generated file to the platform-of-record (Xero / QBO / Zoho / Tally). The system never posts via API.
0. Prerequisites
- Every entry in the batch is
APPROVEDorREJECTED(see B-05). - The client's
bookkeepingConfig.platformis set (Xero / QBO / Zoho / Tally).
1. Steps
1.1 Approve the batch (if not already)
When every entry is APPROVED or REJECTED, the batch detail page shows an Approve batch button. Click it. Server transitions DRAFT → APPROVED and writes bookkeeping.journal_batch.approved.
(You can skip this step — generating the upload file from a DRAFT batch is allowed if every entry is approved/rejected, and the route will advance to APPROVED → EXPORTED in one shot.)
1.2 Click Generate upload file
POST /ops/bookkeeping/batches/<batchId>/generate-upload-file
{ "force": false }The route enqueues bookkeeping.generate-upload-file and returns 202 with the job id. (Use force: true to regenerate even if a file already exists; this creates a new versioned File.)
1.3 Worker pipeline
bookkeeping.generate-upload-file handler:
- Loads the batch + every
APPROVEDentry (rejected/escalated are excluded). - Builds
LedgerExportInput:ts{ platform: 'XERO' | 'QUICKBOOKS' | 'ZOHO_BOOKS' | 'TALLY', entries: JournalEntry[], baseCurrency: 'SGD', timestamp: '2026-04-25T10:00:00Z', clientCode: 'ZENITH', period: { start: '2026-04-01', end: '2026-04-30' } } - Dispatches to the per-platform formatter in
packages/ledger-exports/src/:
| Platform | Formatter | Format |
|---|---|---|
XERO | formatXeroCsv | CSV — Manual Journal Import schema (Date, Description, Reference, AccountCode, TaxType, NetAmount, TaxAmount, TrackingName1, TrackingOption1) |
QUICKBOOKS | formatQuickbooksIif (default) or formatQuickbooksCsv | IIF (Trans Type, Date, Account, Name, Amount, Memo, Class) or QBO CSV |
ZOHO_BOOKS | formatZohoCsv | CSV — Manual Journal template (Journal Date, Journal#, Reference#, Notes, Account, Debit, Credit, Tax Name, Tax Amount). Separate debit + credit rows per entry. |
TALLY | formatTallyXml | XML — VOUCHER + ALLLEDGERENTRIES schema. Voucher type derived from documentType ('Purchase' for bill/invoice, 'Payment' for receipt, 'Journal' for adjustments). |
- Renders the file and computes
LedgerExportResult { content, rowCount, checksum }. - Uploads to S3 at
<clientCode>/bookkeeping/<period>/upload-files/v<n>/<platform>-batch.<ext>. - Creates a
Filerow withfileKind = BOOKKEEPING_UPLOAD_FILE, links it viaJournalBatch.uploadFileId. - Transitions
JournalBatch.status = EXPORTED, stampsexportedAt. - Writes
bookkeeping.upload_file.generatedaudit event with{ platform, rowCount, checksum }. - Sends a notification to
bookkeepingConfig.reviewerEmailwith a download link + batch summary (entries, flagged items, total value).
1.4 Download the file
/dashboard/bookkeeping/batches/<batchId> → Download upload file button. Resolves a presigned S3 URL.
1.5 Reviewer manually uploads to the platform
This step is not automated. The reviewer:
- Opens Xero / QBO / Zoho / Tally.
- Imports the downloaded file via the platform's manual journal upload UI.
- Records the upload timestamp + result against the batch on the Posted tab (
POST /ops/bookkeeping/batches/<batchId>/mark-postedwith{ postedAt, externalRef }— optional).
2. Verification
Database
SELECT status, exported_at, upload_file_id FROM journal_batches WHERE id = '<batchId>';
SELECT original_name, storage_key, mime_type, size_bytes
FROM files WHERE id = '<uploadFileId>';S3 / MinIO
<clientCode>/bookkeeping/<period>/upload-files/v1/<platform>-batch.<ext> — the rendered file.
Mailpit
A summary email arrives at bookkeepingConfig.reviewerEmail:
- Subject:
Bookkeeping upload file ready — {clientCode} {period} - Body: entry count, total value, flagged-but-approved count, link to download.
Versioning check
Click Generate upload file again with force: true → a new File row appears with version_no = 2. Both versions stay listed; the latest is the active uploadFileId.
3. Negative & edge cases
- Any entry still
DRAFT/FLAGGED/ESCALATED→ 400 with the open entry ids. The handler refuses to render. - Empty batch (every entry rejected) → 400
"Batch has no approved entries to export." - Platform not set on the client → 400
"Client.bookkeepingConfig.platform is required to generate the upload file." - Render failure (e.g. malformed FX rate on a multi-currency entry) → file not created; batch stays
APPROVED. Handler logs the underlying error, pg-boss retries. - S3 write failure → handler logs and rethrows; pg-boss retries. The DB write is transactional with the S3 upload — no orphan
Filerow on failure. - Rejected entries excluded — confirm by inspecting the file row count vs.
SELECT count(*) FROM journal_entries WHERE batch_id = '<id>' AND status = 'APPROVED'. They should match. - Mark-posted called twice → 409
"Batch already marked as posted."(idempotency byexternalRef).
Next
If you have a bank statement to reconcile, proceed to B-07 · Upload bank statement & parse. Otherwise the batch is closed; future months get fresh batches automatically.