Skip to content

Books: monthly journal export and yearly tax report - #18

Merged
lobbystack merged 15 commits into
mainfrom
books
Oct 9, 2026
Merged

lobbystack merged 15 commits into
mainfrom
books

Conversation

@lobbystack

Copy link
Copy Markdown
Owner

OpenProfit can now keep a solo founder's books from the data it already syncs. The Books page exports a month as a journal for QuickBooks, Xero or any accounting software, and lists the year's numbers on the lines of the founder's tax form. The spec is in docs/BOOKS.md; the user docs are at /docs/books.

What's in it

  • Data model (migration 0010): incorporation date, country and region on the workspace; who pays each connection and manual cost, with a start date; one-time and equipment costs; base-currency gross, fees and refunds and the service period on revenue lines; payouts; closed months.
  • Journal engine (src/lib/books.ts): balanced entries per month. Payments and fees go against each provider's balance, revenue is earned over its service period, usage is an expense against the provider's balance, and the payer is owner contributions, owed to founder or the company bank depending on incorporation and card.
  • Month closing (src/server/books.server.ts): a whole-month export closes the month. It exports the same way again, and later provider changes go out as an adjustment in the next export.
  • Export: QuickBooks journal, Xero manual journal and plain journal CSVs at /api/journal.csv, an export dialog with account renaming, and an export_journal MCP tool. The QuickBooks and Xero files leave out money moving through the company bank account, which the bank feed records.
  • Tax report: T2125 and TP-80, GIFI schedules 125 and 100, Schedule C, and Form 1120 with the 1120-S and 1065 equivalents. Depreciation follows each country's rules (class 50; the US de minimis safe harbor with its election statement, or Form 4562). Each line number was checked against the 2025 forms and guides, cited next to the mapping in src/lib/tax.ts.
  • Sync: Stripe payouts and invoice periods, Paddle billing periods, and Polar moved to its orders and refunds APIs.
  • Exchange rates: Bank of Canada daily rates for workspaces in Canadian dollars (migration 0011 clears the cached ECB rates into CAD).
  • Settings: the "This is an incorporated company" checkbox at signup, Books settings, "Paid with" on connections and costs, and one-time and equipment costs.
  • Fixes along the way: Stripe payout holds no longer count as revenue, and the CSV dates one-time costs on their own day.

Checked

  • Four scripts pass: scripts/check-books.ts (journal rules), check-books-close.ts (closing and adjustments), check-books-export.ts (each file balances per journal, bank entries are left out, classes and renames), and check-books-tax.ts (Canada, Quebec, companies, the US, a mid-year incorporation, depreciation). The last three run on a scratch database: DATABASE_URL=/tmp/x SYNC_SCHEDULER=off npx tsx scripts/<name>.ts.
  • pnpm check, tsc --noEmit and pnpm build pass. The production build served /docs/books and the changelog, and /api/journal.csv refuses requests without a session.
  • Browser, on seeded demo data: Books settings, a T2125 and TP-80 for a Quebec business, the export dialog, and a September QuickBooks file whose 11 journals all balance.

Before merging

  • Polar connections need the Orders and Refunds read scopes to get order-level data. Old tokens fall back to the daily totals and keep syncing.
  • Polar payouts aren't in the books: Polar's payouts endpoint only accepts signed-in users, not API tokens.
  • Existing lines keep their stored base amounts until their connection's next full sync, which also fills the new gross, fees and refunds columns.

The foundation for keeping a founder's books from synced data (docs/BOOKS.md): workspace incorporation date, country and region; who pays each connection and manual cost; one-time and equipment costs; base-currency gross, fees and refunds and the service period on revenue lines; payouts; and closed months.

src/lib/books.ts turns a month of lines into balanced journal entries: payments and fees against each provider's balance, revenue earned over its service period, usage against the provider balance, and the payer as owner contributions, owed to founder or the company bank. src/server/books.server.ts loads a month and exports it. Exporting a whole month closes it: it exports the same way again, and later changes go out as adjustments in the next export.

A Books page with stub sections holds the export, the tax report and the settings.
Conversions into CAD use the Bank of Canada daily rate, which the CRA
names in Income Tax Folio S5-F4-C1 ¶1.4. A date without a quote (weekend,
holiday) takes the closest preceding day. One Valet request covers a year
on each side of the date, so a two-year sync makes a few requests.
Currencies the Bank doesn't publish (DKK, CZK and others) still convert
through ECB, and so do conversions out of CAD.

Migration 0011 deletes the cached ECB rates into CAD. Stored lines keep
their base amounts until the next full sync of their connection, or until
the workspace changes its base currency.
The sync writes gross, fees and refunds in base currency at each line's rate, and the period a payment covers where the provider reports one: Stripe invoice line periods and Paddle's billing_period. Lemon Squeezy's subscription invoices carry no period, so its lines stay earned on their date.

Polar revenue comes from orders and refunds instead of daily metrics. Each paid order is one line: the price after discounts and before tax, Polar's fee and the tax, with one subscription interval from the order's date as its period. Each refund is a line dated when it happened. Orders are reread 21 days back, as long as Polar retries a failed renewal. Tokens without orders:read or refunds:read keep the daily metrics lines. A connection that still has daily lines in its history runs a full sync, which replaces them.

Stripe payouts come from payout balance transactions with the payout expanded, so keys need no new permission. Each is stored on its arrival date and deleted once it fails or is canceled. Polar's payouts endpoint accepts only signed-in users, not organization tokens, so Polar reports none.

The Polar docs list the two new scopes.
Workspace creation has an "incorporated company" checkbox that sets incorporated_on to the browser's date. The Books page holds the settings: the incorporation switch and date, the country (Canada, United States, Other) with its province or state, and, for incorporated workspaces, who pays each cost connection and manual cost and since when. Cost connection pages show the same picker.

The cost form adds One time, a category and, for incorporated workspaces, Paid with. A one-time cost lists with its date and no per-month suffix.

MCP: the flat cost tools take and return category, paid_with and paid_with_since. get_books_settings and update_books_settings read and change incorporated_on, country and region.

Adds the shadcn Checkbox, tuned to the tokens.
The Books page's Export dialog downloads one month as the lines CSV, a QuickBooks Online journal entries import, a Xero manual journal import or a plain journal. The product defaults to the switcher's for CSV and to All for journals. A one-product journal says it leaves out shared and unassigned costs. Before a whole month's first export, the dialog says that exporting closes the month and later changes come as adjustments.

QuickBooks and Xero files leave out entries that move money through the company bank account, since the bank feed brings those in. QuickBooks gets one journal number per entry and products as classes. Xero gets the template's headers, signed amounts and products as a tracking category. For those two formats, users can rename accounts (Xero takes codes) and set Xero's tracking category and tax rate. Both are saved to workspaces.book_accounts.

GET /api/journal.csv takes the same session or API token as /api/export.csv, which now also accepts ?product=. The MCP export_journal tool returns the same file. Dialogs scroll when taller than the window.

scripts/check-books-export.ts checks that each journal balances, the bank entries stay out of QuickBooks and Xero, products land in the class and tracking columns, and renamed accounts appear.
# Conflicts:
#	src/server/mcp.server.ts
Stripe's payout_minimum_balance_hold and _release balance transactions counted as revenue; they are money held back from a payout and released later. The CSV export dated a one-time cost on the 1st of its month instead of its own date. A new one-time cost now defaults to today instead of the 1st.
The Books page shows the year's numbers on the lines of the form the workspace files: T2125 (plus TP-80-V in Quebec) for a Canadian sole proprietor, GIFI schedules 125 and 100 for a Canadian company, Schedule C for a US sole proprietor, Form 1120 for a US company (with Schedule L from $250,000), and a plain profit and loss elsewhere. A workspace that incorporated during the year gets two parts, split at the incorporation date.

src/lib/tax.ts holds the rules, with the official source next to each table. In Canada, equipment is class 50: 100% the first year for computers acquired after April 15, 2024 and in use before 2027 (enacted by Bill C-15), 55% declining balance after. In the US, items up to $2,500 fall under the de minimis safe harbor and larger ones get the 100% special allowance for property acquired after January 19, 2025. December's journal now books the year's depreciation.

The report marks lines that no official guidance names, writes the de minimis election statement, and links each provider's invoice page. docs/BOOKS.md now matches the verified lines.
@lobbystack
lobbystack merged commit bdd8d02 into main Oct 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant