Skip to content

feat: extract PayrollExecutionFlow from PayrollFlow #1051

Merged
jeffredodd merged 9 commits intomainfrom
jdj/SDK-345-extract-payroll-execution-flow
Feb 19, 2026
Merged

feat: extract PayrollExecutionFlow from PayrollFlow #1051
jeffredodd merged 9 commits intomainfrom
jdj/SDK-345-extract-payroll-execution-flow

Conversation

@jeffredodd
Copy link
Copy Markdown
Contributor

@jeffredodd jeffredodd commented Feb 8, 2026

Summary

  • Extract the execution portion of the payroll state machine (configuration -> overview -> receipts) into a standalone PayrollExecutionFlow component
  • Slim down payrollStateMachine.ts to landing-only breadcrumbs
  • Refactor PayrollFlow into a lightweight orchestrator that delegates to PayrollExecutionFlow
  • Add useEmitOnDataReady hook for flow data communication
  • Add RUN_PAYROLL_DATA_LOADED event constant

This is a pure refactor to make the execution flow reusable -- no new features. Enables off-cycle payroll support in follow-up PRs.

Test plan

  • Existing payroll flow stories render and behave identically
  • Existing payroll tests pass without changes
  • PayrollFlow correctly transitions between landing and execution states
  • Verify no regressions in breadcrumb navigation

Made with Cursor

Copilot AI review requested due to automatic review settings February 8, 2026 23:48
@jeffredodd jeffredodd changed the title feat: extract PayrollExecutionFlow from PayrollFlow [1/4] feat: extract PayrollExecutionFlow from PayrollFlow Feb 8, 2026
@jeffredodd jeffredodd marked this pull request as draft February 8, 2026 23:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the payroll “execution” portion (configuration → overview → receipts) into a reusable PayrollExecutionFlow, leaving PayrollFlow as a landing-orchestrator for the upcoming off-cycle payroll work.

Changes:

  • Added PayrollExecutionFlow + payrollExecutionMachine and updated PayrollFlow to switch between landing/execution flows.
  • Added RUN_PAYROLL_DATA_LOADED event and a useEmitOnDataReady hook to emit it when pay period data becomes available.
  • Slimmed payrollStateMachine.ts down to landing-only breadcrumb nodes.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/shared/constants.ts Adds RUN_PAYROLL_DATA_LOADED event constant.
src/hooks/useEmitOnDataReady.ts New hook to emit an event once when data becomes available.
src/components/Payroll/PayrollFlow/payrollStateMachine.ts Removes execution-machine logic; keeps landing breadcrumb node.
src/components/Payroll/PayrollFlow/PayrollFlow.tsx Orchestrates between landing flow and PayrollExecutionFlow.
src/components/Payroll/PayrollExecutionFlow/payrollExecutionMachine.ts New extracted execution state machine + breadcrumb nodes.
src/components/Payroll/PayrollExecutionFlow/index.ts Barrel exports for the new execution flow/machine.
src/components/Payroll/PayrollExecutionFlow/PayrollExecutionFlow.tsx New flow component that instantiates the execution machine.
src/components/Payroll/PayrollConfiguration/PayrollConfiguration.tsx Emits RUN_PAYROLL_DATA_LOADED once pay period data is ready.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/Payroll/PayrollExecutionFlow/payrollExecutionMachine.ts Outdated
Comment thread src/components/Payroll/PayrollExecutionFlow/payrollExecutionMachine.ts Outdated
Comment thread src/components/Payroll/PayrollExecutionFlow/payrollExecutionMachine.ts Outdated
Comment thread src/components/Payroll/PayrollFlow/PayrollFlow.tsx Outdated
Comment thread src/components/Payroll/PayrollExecutionFlow/PayrollExecutionFlow.tsx Outdated
@jeffredodd jeffredodd force-pushed the jdj/SDK-345-extract-payroll-execution-flow branch from f094b0c to 7767855 Compare February 9, 2026 00:13
@jeffredodd jeffredodd marked this pull request as ready for review February 9, 2026 19:46
@jeffredodd jeffredodd force-pushed the jdj/SDK-345-extract-payroll-execution-flow branch from 659d275 to 9d4c85d Compare February 9, 2026 19:46
Copy link
Copy Markdown
Member

@serikjensen serikjensen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chatted with @jeffredodd about this one. Biggest feedback items

  • move the PayrollExecutionFlow into a dedicated contextual component
  • add the PayrollExecutionFlowContextual into the PayrollFlow landing transitions and pass through the employee id and company id via the flow context
  • update to remove the useEmitOnDataReady operations and just fetch the data when needed within the PayrollExecutionFlow

@jeffredodd jeffredodd force-pushed the jdj/SDK-345-extract-payroll-execution-flow branch from 6b6b028 to e2d6362 Compare February 10, 2026 01:48
@jeffredodd
Copy link
Copy Markdown
Contributor Author

Latest changes: make PayrollExecutionFlow composable

The latest commit refactors PayrollExecutionFlow into a parent-agnostic, composable module that any parent flow can host. This is in preparation for off-cycle payroll support, where OffCyclePayrollFlow will also render PayrollExecutionFlow with its own event handling and breadcrumb trail.

Key changes

Execution machine is now parent-agnostic:

  • Removed PAYROLL_EXIT_FLOW, RUN_PAYROLL_CANCELLED, and RUN_PAYROLL_PROCESSED transitions from the execution machine. These "leave execution" events now pass through unhandled and bubble to the parent via the onEvent prop chain. Each parent decides how to respond.
  • Removed submittedOverview and submittedReceipts states from the execution machine. These are post-execution viewing states that belong at the parent level (original machine had parent: 'landing' breadcrumbs and Payroll.PayrollLanding namespace).
  • Removed dead RUN_PAYROLL_SELECTED/REVIEW_PAYROLL payload types that could never reach the execution machine.

Bug fixes:

  • REVIEW_PAYROLL regression: Added initialState prop ('configuration' | 'overview') to PayrollExecutionFlow. REVIEW_PAYROLL now correctly starts the flow at overview instead of always starting at configuration.
  • Blockers from landing: The no-op transition(RUN_PAYROLL_BLOCKERS_VIEW_ALL, 'landing') is replaced with a real blockers state in the parent machine.
  • Breadcrumb navigation from execution: Added a guarded BREADCRUMB_NAVIGATE handler in the parent's execution state so clicking the "Landing" breadcrumb from within execution actually navigates back.
  • prefixBreadcrumbs referential instability: Fixed prefixBreadcrumbs = [] creating a new array on every render by using a module-level constant.

Submitted states restored to parent:

  • submittedOverview and submittedReceipts are now states in PayrollFlow's landing machine with parent: 'landing' breadcrumb hierarchy (matching the original machine).
  • RUN_PAYROLL_PROCESSED is handled by the parent's execution state, transitioning to submittedOverview.

Composability for future OffCyclePayrollFlow

With these changes, any parent flow can:

  1. Render PayrollExecutionFlow with its own props (companyId, payrollId, initialState, prefixBreadcrumbs)
  2. Handle bubbled events (PAYROLL_EXIT_FLOW, RUN_PAYROLL_CANCELLED, RUN_PAYROLL_PROCESSED) however it needs
  3. Own its own post-execution states
  4. Prefix its own breadcrumb trail

namespace: 'Payroll.PayrollBlocker',
},
},
submittedOverview: {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@serikjensen I realized we probably need these states and this machine at the flow level since Execution shouldn't be responsible for viewing static payroll stuff.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

},
}),
),
),
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RUN_PAYROLL_CANCELLED_ALERT_DISMISSED is emitted by PayrollLanding when the cancelled alert is dismissed, but this state machine no longer handles that event. As a result, showPayrollCancelledAlert will stay true and the alert won't dismiss. Add a landing-state transition for componentEvents.RUN_PAYROLL_CANCELLED_ALERT_DISMISSED that reduces showPayrollCancelledAlert back to false (similar to the previous machine behavior).

Suggested change
),
),
transition(
componentEvents.RUN_PAYROLL_CANCELLED_ALERT_DISMISSED,
'landing',
reduce(
(ctx: PayrollFlowContextInterface): PayrollFlowContextInterface => ({
...ctx,
showPayrollCancelledAlert: false,
}),
),
),

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +92
type LandingEventPayloads = {
[componentEvents.RUN_PAYROLL_SELECTED]: {
payrollUuid: string
}
[componentEvents.REVIEW_PAYROLL]: {
payrollUuid: string
}
}

const breadcrumbNavigateTransition =
createBreadcrumbNavigateTransition<PayrollFlowContextInterface>()

const landingBreadcrumbNavigateTransition = transition(
componentEvents.BREADCRUMB_NAVIGATE,
'landing',
guard(
(_ctx: PayrollFlowContextInterface, ev: { payload: { key: string } }) =>
ev.payload.key === 'landing',
),
reduce(toLandingReducer),
)

function toLandingReducer(ctx: PayrollFlowContextInterface): PayrollFlowContextInterface {
return {
...ctx,
component: PayrollLandingContextual,
payrollUuid: undefined,
progressBarType: null,
currentBreadcrumbId: 'landing',
executionInitialState: undefined,
}
}

const toExecutionReducer = (
ctx: PayrollFlowContextInterface,
ev: { payload: { payrollUuid: string } },
executionInitialState: PayrollExecutionInitialState,
): PayrollFlowContextInterface => ({
...ctx,
component: PayrollExecutionFlowContextual,
payrollUuid: ev.payload.payrollUuid,
showPayrollCancelledAlert: false,
executionInitialState,
})
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The landing->execution transitions ignore the payPeriod that is currently emitted by PayrollList for RUN_PAYROLL_SELECTED/REVIEW_PAYROLL. The execution flow breadcrumbs (e.g. Payroll.PayrollConfiguration.breadcrumbLabel) interpolate startDate/endDate, so without carrying payPeriod (or otherwise populating it in the execution machine context) the breadcrumb label will render with missing dates. Preserve payPeriod in the event payload and/or populate the execution flow context with payPeriod (or start/end dates) on entry so breadcrumb interpolation works from the first render.

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +65
const executionFlowMachine = useMemo(() => {
const baseBreadcrumbs = buildBreadcrumbs(payrollExecutionBreadcrumbsNodes)
const breadcrumbs = Object.fromEntries(
Object.entries(baseBreadcrumbs).map(([stateKey, trail]) => [
stateKey,
[...prefixBreadcrumbs, ...trail],
]),
)

const initialBreadcrumbContext = updateBreadcrumbs(initialState, {
breadcrumbs,
} as PayrollFlowContextInterface)

Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial breadcrumb context is built via updateBreadcrumbs(initialState, { breadcrumbs }) without any variables (start/end dates), but the configuration breadcrumb label (Payroll.PayrollConfiguration.breadcrumbLabel) requires startDate/endDate interpolation. Unless payPeriod (or variables) are set into the execution machine context before the first render, users will see an incomplete breadcrumb label. Consider accepting initial pay period / start+end date props (or emitting a data-loaded event) and calling updateBreadcrumbs('configuration' | 'overview', ctx, { startDate, endDate }) during initialization.

Copilot uses AI. Check for mistakes.
Comment on lines +166 to +206
blockers: state<MachineTransition>(
breadcrumbNavigateTransition('landing'),
transition(componentEvents.PAYROLL_EXIT_FLOW, 'landing', reduce(toLandingReducer)),
),
submittedOverview: state<MachineTransition>(
breadcrumbNavigateTransition('landing'),
transition(
componentEvents.RUN_PAYROLL_RECEIPT_GET,
'submittedReceipts',
reduce(
(ctx: PayrollFlowContextInterface): PayrollFlowContextInterface => ({
...updateBreadcrumbs('submittedReceipts', ctx, {
startDate: ctx.payPeriod?.startDate ?? '',
endDate: ctx.payPeriod?.endDate ?? '',
}),
component: PayrollReceiptsContextual,
progressBarType: 'breadcrumbs',
alerts: undefined,
ctaConfig: {
labelKey: 'exitFlowCta',
namespace: 'Payroll.PayrollReceipts',
},
}),
),
),
transition(
componentEvents.RUN_PAYROLL_CANCELLED,
'landing',
reduce(
(ctx: PayrollFlowContextInterface): PayrollFlowContextInterface => ({
...toLandingReducer(ctx),
showPayrollCancelledAlert: true,
}),
),
),
transition(componentEvents.PAYROLL_EXIT_FLOW, 'landing', reduce(toLandingReducer)),
),
submittedReceipts: state<MachineTransition>(
breadcrumbNavigateTransition('landing'),
transition(componentEvents.PAYROLL_EXIT_FLOW, 'landing', reduce(toLandingReducer)),
),
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breadcrumb navigation back to landing currently uses the generic createBreadcrumbNavigateTransition('landing'), which applies the breadcrumb node’s onNavigate. That onNavigate only swaps component/progressBarType/currentBreadcrumbId, while toLandingReducer also clears payrollUuid and executionInitialState. To avoid divergent landing resets depending on whether the user clicks the breadcrumb vs. exit CTA, consider reusing toLandingReducer for the landing breadcrumb transition in these states (or update the landing breadcrumb onNavigate to mirror toLandingReducer).

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +21
import { createMachine, state, transition, reduce, guard } from 'robot3'
import { useMemo } from 'react'
import { payrollFlowBreadcrumbsNodes, payrollMachine } from './payrollStateMachine'
import { PayrollExecutionFlow, type PayrollExecutionInitialState } from '../PayrollExecutionFlow'
import { payrollFlowBreadcrumbsNodes } from './payrollStateMachine'
import type { PayrollFlowProps } from './PayrollFlowComponents'
import {
SaveAndExitCta,
PayrollLandingContextual,
PayrollBlockerContextual,
PayrollOverviewContextual,
PayrollReceiptsContextual,
type PayrollFlowContextInterface,
} from './PayrollFlowComponents'
import { Flow } from '@/components/Flow/Flow'
import { buildBreadcrumbs } from '@/helpers/breadcrumbHelpers'
import { useFlow } from '@/components/Flow/useFlow'
import { buildBreadcrumbs, updateBreadcrumbs } from '@/helpers/breadcrumbHelpers'
import { ensureRequired } from '@/helpers/ensureRequired'
import { componentEvents } from '@/shared/constants'
import type { MachineEventType, MachineTransition } from '@/types/Helpers'
import { createBreadcrumbNavigateTransition } from '@/components/Common/FlowBreadcrumbs/breadcrumbTransitionHelpers'

Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions adding a useEmitOnDataReady hook and a RUN_PAYROLL_DATA_LOADED event constant, but those identifiers do not exist in the updated codebase (search across src returns no matches). Either include the missing hook/constant changes, or update the PR description to reflect what actually landed in this refactor.

Copilot uses AI. Check for mistakes.
@jeffredodd
Copy link
Copy Markdown
Contributor Author

jeffredodd commented Feb 13, 2026

Ok, I think i've made a lot of updates here that should hopefully make a visual difference. I've also been working ahead to look at how this will interact with Off Cycle and generated this diagram of what I have working between this PR and the other off cycle PR #1055.

Untitled diagram-2026-02-13-161352

@jeffredodd jeffredodd force-pushed the jdj/SDK-345-extract-payroll-execution-flow branch from d46c7a9 to 72758f3 Compare February 17, 2026 17:39
Copy link
Copy Markdown
Member

@serikjensen serikjensen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all the updates here!

if there's some way to just make the api call to get the pay period in the parent directly instead of having to use the onPayPeriodReady prop that would be preferable, but going to unblock

@jeffredodd jeffredodd force-pushed the jdj/SDK-345-extract-payroll-execution-flow branch from 05c1edc to 6b6dfcd Compare February 19, 2026 01:44
jeffredodd and others added 7 commits February 18, 2026 17:45
Refactor the payroll flow to separate landing and execution concerns.
The execution portion (configuration -> overview -> receipts) is now a
standalone PayrollExecutionFlow component that can be composed into
any parent flow. PayrollFlow becomes a lightweight orchestrator.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add missing progressBarCta (SaveAndExitCta) to execution flow context
- Fix i18n namespaces for submittedOverview/submittedReceipts breadcrumbs
- Handle RUN_PAYROLL_CANCELLED to return to landing with cancelled alert
- Implement prefixBreadcrumbs prop by prepending to each breadcrumb trail

Co-authored-by: Cursor <cursoragent@cursor.com>
Wire PayrollExecutionFlow into PayrollFlow's state machine as a
contextual component instead of rendering it directly with props.
Remove useEmitOnDataReady data-passing pattern and dataLoadedTransition
in favor of fetching pay period data on demand.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…outing

Make PayrollExecutionFlow a parent-agnostic module that any parent flow
(PayrollFlow today, OffCyclePayrollFlow in the future) can host by
rendering it with props and handling bubbled events.

Execution machine changes:
- Remove exit/cancel/processed transitions from execution machine. These
  events now pass through unhandled and bubble to the parent, which
  decides how to handle them. This makes the module composable -- each
  parent can respond differently to these events.
- Remove submittedOverview/submittedReceipts states and breadcrumb nodes
  from execution machine. These are post-execution viewing states that
  belong at the parent level (original machine had parent: 'landing').
- Remove dead RUN_PAYROLL_SELECTED/REVIEW_PAYROLL payload types that
  could never reach the execution machine.

PayrollExecutionFlow component changes:
- Add initialState prop ('configuration' | 'overview') so REVIEW_PAYROLL
  can start the execution flow at overview instead of always starting at
  configuration. This fixes a behavioral regression where reviewing an
  already-calculated payroll would show the configuration screen.
- Fix prefixBreadcrumbs referential instability (new [] on every render)
  with a module-level EMPTY_BREADCRUMBS constant.

PayrollFlow parent orchestrator changes:
- Add executionInitialState to context, set by RUN_PAYROLL_SELECTED
  ('configuration') and REVIEW_PAYROLL ('overview').
- Add BREADCRUMB_NAVIGATE handler in execution state so clicking the
  Landing breadcrumb from within execution navigates back to landing.
- Add RUN_PAYROLL_PROCESSED handler in execution state -> submittedOverview.
- Add blockers state to landing machine (was a no-op transition before).
- Add submittedOverview and submittedReceipts states with correct
  parent: 'landing' breadcrumb hierarchy.

Co-authored-by: Cursor <cursoragent@cursor.com>
Move parent machine definition from PayrollFlow.tsx into
payrollStateMachine.ts and extract shared transitions to reduce
duplication. Extract PayrollExecutionFlowContextual into its own file
to avoid a circular dependency between PayrollFlowComponents and
PayrollExecutionFlow.

Bug fixes:
- Include payPeriod in RUN_PAYROLL_PROCESSED event from PayrollOverview
  so parent machine has dates for submitted breadcrumbs
- Add RUN_PAYROLL_CANCELLED_ALERT_DISMISSED handler in parent landing
  state to clear stale alert flag
- Align landing breadcrumb onNavigate with toLandingReducer to clear
  payrollUuid and executionInitialState on navigation
- Add missing updateBreadcrumbs call in receiptGetTransition
- Add RUN_PAYROLL_DATA_READY event so PayrollConfiguration can update
  breadcrumb date labels once pay period data loads

Add state machine transition tests for both payrollFlowMachine (14
tests) and payrollExecutionMachine (15 tests).

Co-authored-by: Cursor <cursoragent@cursor.com>
jeffredodd and others added 2 commits February 18, 2026 17:45
… ready event

Move the RUN_PAYROLL_DATA_READY event emission from a useRef+useEffect
pattern in PayrollConfiguration into an onPayPeriodReady callback in
usePayrollConfigurationData's queryFn. This co-locates the notification
with data fetching, matching the existing onDataReady pattern in
usePreparedPayrollData.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@jeffredodd jeffredodd force-pushed the jdj/SDK-345-extract-payroll-execution-flow branch from 6b6dfcd to cd2fd72 Compare February 19, 2026 01:45
@jeffredodd jeffredodd merged commit a963c94 into main Feb 19, 2026
14 checks passed
@jeffredodd jeffredodd deleted the jdj/SDK-345-extract-payroll-execution-flow branch February 19, 2026 01:48
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.

3 participants