e038dd94

Add centjes-stripe: book Stripe sales, fees and payouts one month at a time

Implements `centjes-stripe-handoff.md`: a new package that turns Stripe API data
into `.cent` transactions, so Stripe sales and fees can be booked once a month
instead of one charge at a time.

`nix flake check` passes.

## Commits

1. **Generalize `Centjes.Merge`** — `mergeTransactionDeclarations` alongside the
   existing price merge, both on one `mergeDeclarations` helper. Transactions
   sort by day only, and same-day insertion preserves emission order, which is
   what lets the per-month balance assertion sit on the last transaction of its
   month. Five golden scenarios in `centjes-gen` plus a round-trip test.
2. **`centjes-stripe`** — `Types` / `API` / `Aggregate` / `Declarations` /
   `Command.Import`, wired into `stack.yaml`, `nix/overlay.nix`, the
   `coverage-report` list and `CLAUDE.md`.
3. **Track the golden output** — a global `result*` gitignore was swallowing
   `result.cent`; `.gitignore` now has `!result.cent`.
4. **Drop unused instances** that `weeder-check` found.
5. **Source of truth, unfinished months, conduit** — see below.

## The output file is the source of truth

The importer only ever *adds* months the file has no transaction dated in. It
never rewrites or corrects a month that is already there, so a hand edit sticks.
The rule lives in one place, `transactionsToAdd`, and is what the
`Command.ImportSpec` tests are mostly about — including one that hand-edits July
the way the emitter's own `[check]` note asks for and asserts the text comes back
verbatim.

Filtering *after* emitting rather than before also fixes a real bug: the opening
balance transaction was outside the month filter, so every run that added a month
re-emitted it with a new date.

The consequence to accept is the other side of the same coin: Stripe activity
landing in a month already in the file is not picked up, and the next month's
balance assertion is what notices. Splitting a month beats silently rewriting the
user's work.

## Never books a month Stripe is still collecting

A month that has not ended produces no transactions at all. Given never-overwrite,
half a month written now would stay half a month forever. `aggregateMonths` takes
today as an argument, reports the held-back months as
`stripeAggregateUnfinishedMonths`, and the command logs each one so a gap in the
output is something you were told about. An unfinished month still counts towards
the closing balance that gets checked against Stripe, so holding it back does not
weaken that check.

## Lazy fetches

`sourceBalanceTransactions` and `sourceInvoices` are conduit sources following
`centjes-stocks` and `centjes-cryptocurrencies`. Pagination yields a page before
recursing, so the next request waits until downstream asks for something past the
previous page: one page in memory rather than every page. Invoices are folded
into their index as the pages arrive.

## What the emitter produces

```
2026-07-31
  | Stripe sales 2026-07
  * income:domestic -370.00 CHF
  * income:VAT       -29.97 CHF ~e 8.1%
  * income:foreign  -520.00 CHF
  * assets:stripe   +919.97 CHF
  + attach documents/work/cs-kerckhove/stripe/sales-2026-07.pdf

2026-07-31
  | Stripe fees 2026-07
  -- Card payments - Stripe fee
  * expenses:banking:stripe  +9.86 CHF
  -- Stripe Tax - integration fee
  * expenses:banking:stripe  +0.46 CHF
  * assets:stripe           -10.32 CHF
  -- [check] Split these fees by VAT rate off this invoice if the input tax is worth reclaiming.
  + attach documents/work/cs-kerckhove/stripe/tax-invoice-2026-07.pdf
  + tag tax-deductible
  + tag not-vat-deductible
  + assert assets:stripe = +709.65 CHF
```

Posting order is load-bearing and nothing in `centjes check` enforces it, so
`DeclarationsSpec` asserts the invariant `produceVATReport` actually depends on:
every domestic income posting immediately followed by its VAT posting, carrying a
percentage ratio. Two rates in one month come out as two adjacent pairs.

## Decisions worth reviewing

- **Dedup by month, not by `(day, description)`** as the handoff suggested. No
  description parsing, and renaming a transaction does not make the importer add
  its month again. Cost: `stripe.cent` cannot hold a hand-written transaction
  dated in a month that is also being imported.
- **The VAT rate is derived from the amounts**, not read off the response:
  `tax/net` snapped to whichever Swiss rate reproduces the tax within one minor
  unit, hard error if none does. The field carrying the rate moved between Stripe
  API versions; the amounts did not.
- **One transaction per payout, not per month.** Neon books each payout as one
  statement line, so `assets:self-transfer:stripe-neon` only nets to zero per
  payout if this side matches. Sales and fees stay monthly.
- **Refunds and unrecognised balance-transaction types are hard errors.** A
  refund reduces revenue but does not say which VAT bucket; an unknown type fails
  at the codec naming what it saw.
- **Fee VAT split**: not available from the API, so the fee transaction is gross
  and `not-vat-deductible` with a `[check]` note. Never-overwrite is what makes
  splitting it by hand off the PDF a workable answer.

## Not exercised against the real API

No live request was made while writing this. `test_resources/responses/` is
written from Stripe's API reference and `test_resources/RESPONSES.md` says so.
So pagination, the `net = amount - fee` assertion, invoice-to-charge matching and
the `/v1/balance` cross-check are all unverified against reality, and the first
real run is the real test.

One thing that run will settle: Stripe posts the monthly fee invoice at or after
month end, so July's fees may carry an August `created` and land in the August fee
transaction, whose attachment is named for August. If so the attachment naming
needs revisiting; `centjes check` will say which file is missing.

Suite timing

Time to Start Worker time Duration Time to finish Idle
Config 0s 1s 1s 2s 0s
Eval 2s 14s 14s 16s 0s
Build 15s 0s 0s 15s 0s
Suite 0s 16s 15s 16s 0s

Timeline

0s10s