feat: CSV export and employee payslip view

Two audiences the super-user API did not serve.

Accounting gets `GET /api/v1/payouts.csv`, filterable by contract, status
and date range. The range bounds the *payday* rather than the row
timestamp, so a period contains the paydays that belong to it even when one
of them took three days of retries to settle — otherwise a late retry lands
in the wrong month's export.

Every exported field is neutralised against spreadsheet formula injection.
`detail` carries exception text and the memo carries operator input, and a
cell beginning `=`, `+`, `-` or `@` executes when the file is opened. Worth
the eight lines: this file is written specifically to be opened in somebody
else's spreadsheet.

Employees get `/api/v1/my/payouts`, `/my/payouts.csv` and `/my/contracts`
on a **separate router** gated by a wallet invoice key rather than by
super-user rights. Separate router so it cannot inherit — or accidentally
shed — the wrong gate. Scoped to the wallet rather than the account because
an invoice key names exactly one wallet, leaving no lookup that could widen
the result to a sibling wallet the key does not cover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jy52j9GRZ6XKa1Zt21LLj
This commit is contained in:
Padreug 2026-08-31 13:53:12 +02:00
commit 4b6d647d23
7 changed files with 294 additions and 3 deletions

View file

@ -134,3 +134,33 @@ It is recorded in the ledger like any other payout, with the early payment
noted in `detail`, and the ledger row is returned even when the payout
failed — a manual payout that did not land is exactly when you want to know
why.
## Accounting export
`GET /payroll/api/v1/payouts.csv` (super user) exports the ledger, with
optional `contract_id`, `status`, `since` and `until`. `since`/`until` bound
the **payday**, not the row timestamp, so an accounting period contains the
paydays that belong to it even when one took three days of retries to
settle.
Every exported field is neutralised against spreadsheet formula injection —
a cell beginning `=`, `+`, `-` or `@` is prefixed with an apostrophe.
`detail` carries exception text and the memo carries operator input, and
neither is worth trusting to a colleague's Excel.
## What an employee can see
`/payroll/api/v1/my/payouts`, `/my/payouts.csv` and `/my/contracts` are
gated on a **wallet invoice key**, not on admin rights: whoever holds the
read key for a wallet may see what payroll has paid into that wallet, and
nothing else. They live on a separate router from the super-user API so
they cannot inherit — or accidentally shed — the wrong gate.
Scoped to the wallet rather than to the account on purpose: an invoice key
names exactly one wallet, so there is no lookup that could widen the result
to a sibling wallet the key does not cover.
Note this stays reachable even with `payroll` in `LNBITS_ADMIN_EXTENSIONS`.
That flag governs who sees the extension in the UI; the API routes are
mounted instance-wide, and these three are gated by the wallet key they
require rather than by UI visibility.