Epic: #58 · Step E (Payroll — flagship)
Rewire the console's Payroll screen off its demo-era model (contractors + rate cards + 4 explicit prove-* steps) onto the real backend's CSV run → start → live progress lifecycle. Builds on auth (#60), onboarding (#63), treasury (#65). This is the "encrypted payroll, live on our L1" demo.
Backend contract (source of truth — services/api/src/routes/payroll.ts)
POST /orgs/:id/payroll { csv, token?:"usdc"|"eurc" }
→ 201 { runId, status:"ready"|"failed", token, tokenId,
summary:{ total, valid, invalid, totalAmount, token, tokenId },
items:[{ rowIndex, recipientInput, resolvedAddress, amount,
status:"pending"|"failed", error }] }
csv rows = "recipient(@handle|0xaddr),amount(decimal ≤6dp)"; optional header; ≤10000 rows.
GET /payroll/:runId → JSON { run:PayrollRun, progress:PayrollProgressCounts, items:[…] }
(Accept: text/event-stream) → SSE `event: progress` `data:{ runId, status, progress }` (immediate + every 2s), closes on complete|failed.
POST /payroll/:runId/start → 202 { runId, status:"running", enqueued, totalPending, progress }
POST /payroll/:runId/pause → 200 { runId, status:"paused", progress }
POST /payroll/:runId/resume → 202 { runId, status:"running", enqueued, totalPending, progress }
PayrollRun = { id, orgId, status:"draft"|"validating"|"ready"|"running"|"paused"|"complete"|"failed", itemCount, totalAmount, token, tokenId, createdBy, error, createdAt, updatedAt }.
PayrollProgressCounts = { total, pending, proving, submitted, confirmed, failed, proved } (proved == confirmed). Proving is server-side + sequential — surfaced only via these counts. /start can 409 treasury_underfunded { availableAmount, requiredAmount, token, tokenId }.
Key constraint
There is no "list payroll runs" endpoint — only GET /payroll/:runId. So the screen is compose → validate → run → watch, not a run history. Persist the current/last run id in localStorage (per org) so a reload re-attaches to the in-flight run; optionally keep a short client-side list of recent run ids and hydrate each via GET /payroll/:runId.
Changes
lib/api.ts — createPayrollRun(orgId, { csv, token? }) → POST /orgs/:id/payroll; getPayrollRun(runId) → GET /payroll/:runId (JSON); subscribePayrollProgress(runId, onProgress, onError) → SSE on /payroll/:runId with Accept: text/event-stream (credentialed EventSource; the URL is the same path — set the header via a fetch-based SSE if EventSource can't send Accept, else rely on the server's dual-mode + a 2s getPayrollRun polling fallback), closing on terminal status; startPayrollRun/pausePayrollRun/resumePayrollRun(runId). REMOVE the demo-era fns/paths: payrolls(GET), createPayroll(POST /payrolls), approvePayroll, proveFunded/proveApproval/proveComputation/provePolicy, payslips.
lib/store.tsx — remove the payrolls batch read (no list endpoint). The Payroll screen owns its run state.
screens/Payroll.tsx — rewrite to the real lifecycle: Compose (a CSV textarea and/or a small row editor "@handle or 0x…, amount", or CSV upload) → Validate (createPayrollRun) showing the summary + per-row resolved/failed items → Run (startPayrollRun) → live progress driven by the SSE progress counts (pending → proving → submitted → confirmed, with a bar + numbers) with Pause/Resume; handle treasury_underfunded with a clear message (amount short + link to Treasury). Replace the old 4-proof UX and the contractor/rate-card model entirely. Persist the active run id in localStorage.
packages/types — PayrollRun, PayrollProgressCounts, create-run response + item types matching the backend; drop the demo-era PayrollBatch/PayrollLine/proof types from the real path.
- Demo — update
demo/api.ts/demo/data.ts so VITE_DEMO_MODE=1 walks a full run: validate a seeded CSV → start → animate the progress counts to complete.
Out of scope
Hiding the no-backend nav (step B — next). Anything payments/invoices/grants.
Definition of done — run ALL, pass:
pnpm --filter @benzo/console typecheck · test · build · VITE_DEMO_MODE=1 … build · pnpm exec biome lint .
Operating rules
No git (can't commit). Match the codebase style. End with (1) a one-line conventional-commit message and (2) the exact git add <paths> list.
Epic: #58 · Step E (Payroll — flagship)
Rewire the console's Payroll screen off its demo-era model (contractors + rate cards + 4 explicit
prove-*steps) onto the real backend's CSV run → start → live progress lifecycle. Builds on auth (#60), onboarding (#63), treasury (#65). This is the "encrypted payroll, live on our L1" demo.Backend contract (source of truth —
services/api/src/routes/payroll.ts)PayrollRun = { id, orgId, status:"draft"|"validating"|"ready"|"running"|"paused"|"complete"|"failed", itemCount, totalAmount, token, tokenId, createdBy, error, createdAt, updatedAt }.PayrollProgressCounts = { total, pending, proving, submitted, confirmed, failed, proved }(proved == confirmed). Proving is server-side + sequential — surfaced only via these counts./startcan 409treasury_underfunded { availableAmount, requiredAmount, token, tokenId }.Key constraint
There is no "list payroll runs" endpoint — only
GET /payroll/:runId. So the screen is compose → validate → run → watch, not a run history. Persist the current/last run id inlocalStorage(per org) so a reload re-attaches to the in-flight run; optionally keep a short client-side list of recent run ids and hydrate each viaGET /payroll/:runId.Changes
lib/api.ts—createPayrollRun(orgId, { csv, token? })→POST /orgs/:id/payroll;getPayrollRun(runId)→GET /payroll/:runId(JSON);subscribePayrollProgress(runId, onProgress, onError)→ SSE on/payroll/:runIdwithAccept: text/event-stream(credentialedEventSource; the URL is the same path — set the header via a fetch-based SSE ifEventSourcecan't send Accept, else rely on the server's dual-mode + a 2sgetPayrollRunpolling fallback), closing on terminal status;startPayrollRun/pausePayrollRun/resumePayrollRun(runId). REMOVE the demo-era fns/paths:payrolls(GET),createPayroll(POST /payrolls),approvePayroll,proveFunded/proveApproval/proveComputation/provePolicy,payslips.lib/store.tsx— remove thepayrollsbatch read (no list endpoint). The Payroll screen owns its run state.screens/Payroll.tsx— rewrite to the real lifecycle: Compose (a CSV textarea and/or a small row editor "@handle or 0x…, amount", or CSV upload) → Validate (createPayrollRun) showing thesummary+ per-row resolved/faileditems → Run (startPayrollRun) → live progress driven by the SSEprogresscounts (pending → proving → submitted → confirmed, with a bar + numbers) with Pause/Resume; handletreasury_underfundedwith a clear message (amount short + link to Treasury). Replace the old 4-proof UX and the contractor/rate-card model entirely. Persist the active run id inlocalStorage.packages/types—PayrollRun,PayrollProgressCounts, create-run response + item types matching the backend; drop the demo-eraPayrollBatch/PayrollLine/proof types from the real path.demo/api.ts/demo/data.tssoVITE_DEMO_MODE=1walks a full run: validate a seeded CSV → start → animate the progress counts to complete.Out of scope
Hiding the no-backend nav (step B — next). Anything payments/invoices/grants.
Definition of done — run ALL, pass:
pnpm --filter @benzo/console typecheck·test·build·VITE_DEMO_MODE=1 … build·pnpm exec biome lint .Operating rules
No git (can't commit). Match the codebase style. End with (1) a one-line conventional-commit message and (2) the exact
git add <paths>list.