P-03 · Client intake — Zero-change fast path
SOP:
Payroll_Processing.md§6 / Step 2.0 (S1 → S4) — one-click variation per 2026-04-22 client requirementActors: Client Submitter (CR) — magic-link authenticated. Pre-state: Cycle inREQUESTED. Submitter has the data-request email. Post-state: Cycle transitionsREQUESTED → INTAKE_IN_PROGRESS → SUBMITTEDwith a zero-item submission and a server-synthesized declaration marker.
This is the happy-path shortcut for clients with no payroll deltas this month. The magic-link click is treated as consent — no monthly declaration form is required.
0. Prerequisites
- P-01 has run for the target client + month.
- Submitter has the data-request email open in mailpit.
1. Steps
There are two entry points; both funnel into the same zero-touch submit.
Entry A — one-click from email
Click No changes this month in the mailpit email. Browser lands on http://localhost:3000/portal/<jwt>?choice=no.
Entry B — in-portal decision card
If the client opened the portal first (or clicked Yes then reconsidered), the in-portal decision card offers a No changes this month button that auto-submits without entering the wizard.
Server behaviour (both entries)
Portal auto-POSTs to
/portal/cycles/<token>/submitwith nomonthlyDeclarationfield:json{ "attestationText": "Zero-change confirmation via one-click \"No changes\" flow." }Server detects the fast-path (declaration absent and zero items in draft) and:
Skips
validateDeclaration(declaration is not required for zero-touch confirmations).Skips the synchronous BLOCKING validation gate (
ATTENDANCE_REQUIREDet al. are no-ops with no items to validate).Skips the completeness gauge (0% is the correct state for a no-changes cycle).
Auto-creates an empty
DRAFTsubmission and transitions it toSUBMITTED.Persists a server-synthesized marker in
submissions.monthly_declaration:json{ "noChangesFromPriorMonth": true, "signedAt": "<iso timestamp>", "source": "NO_CHANGES_FAST_PATH" }
Cycle transitions
REQUESTED → INTAKE_IN_PROGRESS → SUBMITTED. The intermediateINTAKE_IN_PROGRESSis logged but the user never sees it.Portal renders a dedicated zero-change success card — green tick, "You're all set — we've recorded zero payroll changes for {month}", plus a single secondary "Changed my mind — open the portal" button (visible only while the cycle is still
SUBMITTED).
2. Verification
Database
SELECT
s.version_no,
s.submitted_at,
s.monthly_declaration ->> 'source' AS source,
s.monthly_declaration ->> 'noChangesFromPriorMonth' AS no_changes,
(SELECT COUNT(*) FROM submission_items WHERE submission_id = s.id) AS item_count
FROM submissions s
WHERE s.cycle_id = '<cycleId>'
ORDER BY s.version_no DESC LIMIT 1;
-- source = 'NO_CHANGES_FAST_PATH'
-- no_changes = 'true'
-- item_count = 0Audit log
Exactly one submission.submitted event — not two — even if the user reloads ?choice=no. The backend idempotency guard in apps/api/src/routes/portal/index.ts short-circuits when the cycle is already past intake.
Mailpit
No outbound email is sent at this step.
3. Negative & edge cases
- Reload
?choice=noafter success → portal renders the same success card. API returns200 OKwithnewStatus = SUBMITTED. No second submit happens. - Items already drafted (item count > 0) + missing declaration → backend rejects with
DECLARATION_INCOMPLETE. The fast-path is strictly scoped to zero-item submissions. - Legacy compatibility — clients / integrations that still send a full
monthlyDeclarationwithnoChangesFromPriorMonth: truecontinue to work unchanged. The declaration is validated and persisted verbatim. The fast-path kicks in only when the body omits the declaration altogether. - "Changed my mind" — clicking the secondary button reopens the wizard, but the cycle stays at
SUBMITTEDuntil the wizard re-submits. To convert a zero-change submission into a real one, the client withdraws via the in-portalWithdraw submissionaction; that transitions back toINTAKE_IN_PROGRESSand clears theNO_CHANGES_FAST_PATHmarker.
Next
The cycle is in SUBMITTED. Validation runs automatically as the first step of P-07 · Generate export. Auto-approval requests fire at the same step (per the 2026-04-22 client requirement).