Skip to content

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: JournalBatch in DRAFT or APPROVED with at least one APPROVED entry. No DRAFT / FLAGGED / ESCALATED entries (the route refuses). Post-state: A platform-specific file uploaded to S3 + JournalBatch.uploadFileId set + batch advances to EXPORTED if it was APPROVED.

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 APPROVED or REJECTED (see B-05).
  • The client's bookkeepingConfig.platform is 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

http
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:

  1. Loads the batch + every APPROVED entry (rejected/escalated are excluded).
  2. 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' }
    }
  3. Dispatches to the per-platform formatter in packages/ledger-exports/src/:
PlatformFormatterFormat
XEROformatXeroCsvCSV — Manual Journal Import schema (Date, Description, Reference, AccountCode, TaxType, NetAmount, TaxAmount, TrackingName1, TrackingOption1)
QUICKBOOKSformatQuickbooksIif (default) or formatQuickbooksCsvIIF (Trans Type, Date, Account, Name, Amount, Memo, Class) or QBO CSV
ZOHO_BOOKSformatZohoCsvCSV — Manual Journal template (Journal Date, Journal#, Reference#, Notes, Account, Debit, Credit, Tax Name, Tax Amount). Separate debit + credit rows per entry.
TALLYformatTallyXmlXML — VOUCHER + ALLLEDGERENTRIES schema. Voucher type derived from documentType ('Purchase' for bill/invoice, 'Payment' for receipt, 'Journal' for adjustments).
  1. Renders the file and computes LedgerExportResult { content, rowCount, checksum }.
  2. Uploads to S3 at <clientCode>/bookkeeping/<period>/upload-files/v<n>/<platform>-batch.<ext>.
  3. Creates a File row with fileKind = BOOKKEEPING_UPLOAD_FILE, links it via JournalBatch.uploadFileId.
  4. Transitions JournalBatch.status = EXPORTED, stamps exportedAt.
  5. Writes bookkeeping.upload_file.generated audit event with { platform, rowCount, checksum }.
  6. Sends a notification to bookkeepingConfig.reviewerEmail with 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:

  1. Opens Xero / QBO / Zoho / Tally.
  2. Imports the downloaded file via the platform's manual journal upload UI.
  3. Records the upload timestamp + result against the batch on the Posted tab (POST /ops/bookkeeping/batches/<batchId>/mark-posted with { postedAt, externalRef } — optional).

2. Verification

Database

sql
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 File row 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 by externalRef).

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.

Internal use only — BreezyCorp