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

@ -4,7 +4,7 @@ from fastapi import APIRouter
from loguru import logger
from .crud import db
from .views_api import payroll_api_router
from .views_api import payroll_api_router, payroll_employee_router
payroll_static_files = [
{
@ -15,6 +15,7 @@ payroll_static_files = [
payroll_ext: APIRouter = APIRouter(prefix="/payroll", tags=["payroll"])
payroll_ext.include_router(payroll_api_router)
payroll_ext.include_router(payroll_employee_router)
scheduled_tasks: list[asyncio.Task] = []