Skip to content

fix(SDK-1231): gate payroll prepare so it cannot wipe a completed calculation - #2656

Open
serikjensen wants to merge 6 commits into
mainfrom
payroll-not-calculated
Open

fix(SDK-1231): gate payroll prepare so it cannot wipe a completed calculation#2656
serikjensen wants to merge 6 commits into
mainfrom
payroll-not-calculated

Conversation

@serikjensen

@serikjensen serikjensen commented Aug 27, 2026

Copy link
Copy Markdown
Member

Problem

Navigating from Payroll Configuration to Payroll Overview sometimes threw "payroll is not calculated".

Root cause

The payroll prepare call reopens/resets a payroll, which clears calculatedAt on the server. Prepare was disabled on the react query call based on the payroll calculation status. Once payroll was calculated, prepare was switched back on. This created a race condition where if you didn't navigate quick enough, the prepare call would execute and clear the payroll calculated at value.

Fix

We update to ensure the prepare call is disabled once calculate has been set on configuration. We also ensure if you come into a payroll mid calculation on configuration that it properly keeps prepare disallowed.

Cleanup

Removes the SDK-1018 fixes that were not actually getting at the root cause

  • Overview refetchOnMount: 'always', gcTime: 0, and the isFetching throw guard.
  • Config post-calculate invalidateQueries (+ now-unused useQueryClient / API_QUERY_NAMESPACE).

Testing

Forcing the behavior

We were able to force the behavior by delaying navigation from the configuration screen. This caused prepare to fire and clear the payroll.

Screen.Recording.2026-08-27.at.4.14.25.PM.mov

Fix

Screen.Recording.2026-08-27.at.4.21.02.PM.mov

🤖 Generated with Claude Code

Posted by Claude on behalf of Steve

…culation

PayrollConfiguration's prepare query was only gated on the transient
`isCalculating` flag. When a calculation completed, `isPolling` cleared in
the same tick that fired RUN_PAYROLL_CALCULATED, so for the render(s) before
unmount `isCalculating` went false and the prepare query re-enabled. A
cancelled-mid-calculation prepare has no cached success, so it refetched,
hit the server, and reset `calculatedAt` to null. PayrollOverview then
mounted, read the nulled payroll, and threw "payroll is not calculated".
The multi-tab case reproduced it every time via a genuinely in-flight
prepare.

Fix: gate prepare on a durable signal, not just `isCalculating`.
- Add `disablePrepare` to usePayrollConfigurationData; `enabled` now also
  requires `!disablePrepare`.
- PayrollConfiguration sets it from a `hasStartedCalculationRef` latch (same
  tab) OR the server payroll status being calculating/calculated (direct
  loads + multi-tab). Prepare is only valid for a fresh, uncalculated draft.

Remove the SDK-1018 band-aids that compensated for this root cause:
- PayrollOverview `refetchOnMount: 'always'`, `gcTime: 0`, and the
  `isFetching` throw guard.
- PayrollConfiguration post-calculate `invalidateQueries` (and now-unused
  useQueryClient / API_QUERY_NAMESPACE imports).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@serikjensen
serikjensen force-pushed the payroll-not-calculated branch from cbbfabd to 5e1ecd8 Compare August 27, 2026 22:35
serikjensen and others added 4 commits August 27, 2026 16:39
…lc mount

The calculatedAt-based gate was too broad: returning to configuration via
Edit reopens an already-calculated payroll, which legitimately needs prepare
to re-fire. Gating on `isCalculatedStatus` left the employee table empty.

Narrow the gate to the two cases that are actually the bug: prepare
re-firing on the SAME mount that kicked off a calculation (the
hasStartedCalculationRef latch), and the server being actively mid-calculate
(isCalculatingStatus, covers multi-tab). A fresh mount on a calculated
payroll now prepares as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…repare gate

Fold every "don't prepare" reason into a single `disablePrepare` the caller
owns, and drop the hook's separate `isCalculating` param so the query no
longer reads `!isCalculating && !disablePrepare`. The hook now exposes one
unambiguous gate used by both the prepare query and the excluded-details
query, and its cancel effect keys off the same flag.

Behavior is unchanged: disablePrepare = isPolling || isCalculatingPayroll ||
hasStartedCalculationRef || server-actively-calculating.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ating

The per-mount latch keyed only on a local Calculate click, so a second tab
that never clicked Calculate had nothing holding prepare off. When the first
tab's calculation finished, the second tab's prepare re-fired and wiped
`calculatedAt`.

Replace `hasStartedCalculationRef` with `hasSeenCalculatingRef`, which trips
the instant this mount observes a calculating payroll from any source (local
click or another tab's calculation seen via the polled status). It stays true
through the calculating -> calculated transition, so the wiping re-fire is
blocked in both the single-tab handoff and the multi-tab case. A fresh mount
that never sees "calculating" (Edit on an already-calculated payroll) leaves
it false, so prepare still reopens the payroll. The guard releases on
calculation failure/timeout so prepare can retry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A second tab suppresses prepare (it saw the payroll calculating), so it has
no prepared rows. When the calculation finished, isPolling cleared and the
full-page calculating loader dropped, exposing an empty table for a few
seconds until navigation to overview completed.

Drive the loader off a derived isCalculatingActive that includes
hasSeenCalculatingRef, so the loader stays up continuously from calc start
through the handoff on both tabs. A fresh Edit mount never sees calculating,
so it still renders the table immediately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@serikjensen
serikjensen marked this pull request as ready for review August 28, 2026 00:34
@serikjensen
serikjensen requested a review from a team as a code owner August 28, 2026 00:34
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant