feat(PayrollList): disable Run Payroll on regular rows when transition payrolls are pending#1989
feat(PayrollList): disable Run Payroll on regular rows when transition payrolls are pending#1989larchai wants to merge 1 commit into
Conversation
…ns are pending Mirrors the UI guardrail in Gusto-hosted flows: when the company has any unprocessed transition pay periods within the next 90 days, the per-row Run Payroll action on Regular rows is disabled. Off-cycle rows and the Run off-cycle CTA stay enabled (off-cycle is the path actually used to run a transition payroll). Why: today the SDK shows a TransitionPayrollAlert but leaves the Run Payroll button enabled. Running a regular payroll while a transition is pending updates last_paid_date on the backend, drops the transition from the Get Pay Periods response, and a direct call to the transition payroll_uuid 500s — exactly the silent-failure scenario partners (e.g. FreshBooks per EMBJPD-427) want guarded against. Gusto.com/flows already enforce this client-side; the SDK now matches. Changes - New internal hook useUnprocessedTransitionPayPeriods, consumed by both TransitionPayrollAlert and PayrollList (not exported). - PayrollList wires the hook + emits RUN_PAYROLL_BLOCKED_BY_TRANSITION when the disabled state activates. - PayrollListPresentation gates the per-row Run button on Regular rows and surfaces the explanation via aria-label (the design system's Button forwards aria-* but not title). - TransitionPayrollAlert copy extended with "Regular payroll functionality is blocked until you either run or skip transition payrolls." — the alert is now the explanation surface. - docs/workflows-overview/run-payroll.md updated with the coupling note and the new event. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
serikjensen
left a comment
There was a problem hiding this comment.
Just a few changes to pass to claude!
| useEffect(() => { | ||
| if (hasUnprocessedTransitions) { | ||
| onEvent(componentEvents.RUN_PAYROLL_BLOCKED_BY_TRANSITION) | ||
| } | ||
| }, [hasUnprocessedTransitions, onEvent]) | ||
|
|
There was a problem hiding this comment.
I don't think we need to fire an event for this. Events are usually meant for the partner to action on after an action, ex. api response, navigation, etc.
We can revisit if there's partner need for component internals like this.
Longer term, there will be a hook for payroll list and that would just communicate this response as part of the hook response.
For now, i would recommend asking claude to remove the event along with the associated tests and documentation
| const isRegular = !payroll.offCycle | ||
| const isDisabledByTransition = isRegular && hasUnprocessedTransitions | ||
|
|
||
| const ariaLabel = isDisabledByTransition ? t('runPayrollDisabledByTransitionTitle') : undefined |
There was a problem hiding this comment.
Have claude remove this aria label. We already set disabled which will get read by assistive tech
| unprocessedPayPeriods, | ||
| hasUnprocessedTransitions: unprocessedPayPeriods.length > 0, | ||
| isLoading, | ||
| error, |
There was a problem hiding this comment.
Instead of isLoading and error being returned, have Claude update the calls in this file to be suspense based queries. Embedded api has suspense variants of all api calls. The components where this is used are suspense based, so that way we can properly get the loading and error states configured. Otherwise with this configuration they'd need to be wired up separately.
|
also i'd have claude investigate the CI failure for endpoints that's breaking the build for this |
Context
JIRA: EMBJPD-427 — FreshBooks asked for a systematic way to prevent regular payrolls from being run while a transition payroll is pending, instead of relying solely on the
pay_schedule_transitionpartner notification + their own custom UI guardrail. The proper backend fix (returningpay_schedule_transitioninpayroll_blockers) isn't on a near-term roadmap and would still depend on partners consuming the blockers field. In the meantime, this PR closes the related gap in the SDK so it matches the guardrail already shipped in Gusto-hosted flows.Current state vs. this PR
TransitionPayrollAlertsurfaces pending transitions, but the per-row Run button inPayrollListis always enabledPer Jackson Cox's 4/15 testing on the JIRA ticket: if a user runs a regular payroll while a transition is pending, the regular run updates
last_paid_date, drops the transition fromGet Pay Periods, and a direct call to the transitionpayroll_uuid500s. So today partners building on top ofPayrollListhad to add their own front-end guardrail — exactly the case FreshBooks didn't want.What's in the PR
src/components/Payroll/useUnprocessedTransitionPayPeriods.ts. WrapsusePaySchedulesGetPayPeriods({ payrollTypes: Transition, endDate: now+90d })+ the!processedfilter that previously lived inline inTransitionPayrollAlert. Not exported from the package barrel; consumed by bothTransitionPayrollAlertandPayrollList.TransitionPayrollAlert.tsx— refactored to use the hook. Behavior-equivalent except for (3).Payroll.TransitionPayrollAlert.json— appendedRegular payroll functionality is blocked until you either run or skip transition payrolls.to the alert description. The alert is now the explanation surface.PayrollList.tsx— calls the hook, threadshasUnprocessedTransitionsinto the presentation, emitsRUN_PAYROLL_BLOCKED_BY_TRANSITIONonce when the disabled state activates. JSDoc comment documents the coupling.PayrollListPresentation.tsx— gates the per-row Run Payroll button on(payroll.offCycle === false) && hasUnprocessedTransitions. Off-cycle rows and the Run off-cycle CTA untouched. Disabled state communicated to screen readers viaaria-label.runPayrollEvents.RUN_PAYROLL_BLOCKED_BY_TRANSITION.docs/workflows-overview/run-payroll.md— coupling note added to the PayrollList section + new event row.PayrollList.test.tsx(event fires, button disabled when MSW returns a transition row); new assertion inTransitionPayrollAlertPresentation.test.tsxfor the appended copy.Explicitly out of scope
payroll_blockerswork — but the two are additive. If/when the backend starts returning apay_schedule_transitionblocker, the existingblockersprop onPayrollListwould light up the existing Skip-gating logic; gating the Run button on it would be a small follow-up. Defense in depth, same as on Gusto.com.Risks / open questions for the team
PayrollListandTransitionPayrollAlertis now load-bearing. The disabled state fires unconditionally when transitions exist; partners usingPayrollListdirectly outside ofPayrollLandingneed to render an equivalent resolution surface or users see a disabled button without a path forward. Documented in JSDoc + the workflow doc. Open to feedback on whether to formalize this further (e.g. a wrapper context, or makingTransitionPayrollAlertpart of the public API and recommending it explicitly).titleHTML attribute is dropped by react-aria-components. The design system'sButtonacceptstitlein its props type, but RAC'sButtononly forwardsGlobalDOMAttributeswhich excludestitle— so the existingtitle={t('runPayrollTitle')}calls in this file were effectively no-ops on the rendered DOM. Pivoted toaria-labelfor the disabled-state explanation (it IS forwarded by RAC and is the right a11y hook). A hovering sighted user won't see a native HTML tooltip on the disabled button itself — they get the explanation from the alert above. Worth a broader follow-up to either drop the unusedtitleprop fromButtonPropsor fix the forwarding upstream, but out of scope here.A pending transition payroll must be run or skipped before running this payroll.(aria-label only). Mirrors the alert sentence tone. Easy to adjust if the team prefers softer copy.useEffectwatchinghasUnprocessedTransitions. Could instead emit on user attempt (e.g. click on the disabled button), but disabled buttons don't fire onClick, so detection-time is a more reliable signal for telemetry.Test plan
npm run tsccleannpm run i18n:generateregeneratedsrc/types/i18next.d.tsnpx vitest run src/components/Payroll— 576 tests pass, 1 expected fail (pre-existing, unrelated), 1 pre-existing flake onmain(shows hamburger menu when pay period starts today)sdk-app: verified baseline (no transition → button enabled), then exercised the flow on a demo company to confirm disabled state + alert copy + event emission. Off-cycle CTA unaffected.