Flows & Tasks Engine

Several business journeys are sequences of modals with branching, back navigation and cancellation. Rather than encoding that in component state, the app uses a small orchestration engine in src/app/core/_base/tasks-flow/tasks-flow.ts.

Core types

Type Role

FlowTask

One step. execute(context?, onDone?). Usually opens a modal or calls an API.

FlowContext

A Map-backed key/value bag shared by every task in the flow. set() returns this so it chains.

FlowTaskResult

{ type: FlowTaskResultType, data?: D } — what a task reports back.

FlowTaskResultType

Done | Cancel | Terminated | Error.

TasksFlow

The runner. Holds the ordered task list, the shared context, and the cursor.

TaskFlowBuilder

Root-provided fluent builder. addTask(), setContextData(), getFlow(). getFlow() hands out the built flow and resets the builder, so one injected builder can construct many flows.

Control flow

Call Behaviour

run()

Executes the task at the current cursor.

runNext()

Advances the cursor, or emits Done to the registered handlers if already at the last task.

runPrevious()

Moves the cursor back, or emits Cancel if already at the first task.

runCurrent()

Re-executes the current task.

registerOnDone(handler)

Registers a completion handler. Handlers receive { type, data: FlowContext } and are cleared after firing once.

The conventional per-task handler maps a result type onto navigation:

private defaultHandler(flow: TasksFlow): FlowTaskDoneHandler {
    return result => {
        switch (result.type) {
            case FlowTaskResultType.Done:       flow.runNext();     break;
            case FlowTaskResultType.Cancel:     flow.runPrevious(); break;
            case FlowTaskResultType.Terminated:                     break;
        }
    };
}

Terminated deliberately does nothing — it ends the flow in place, which is how "user closed the dialog and we should show a banner instead" is expressed.

Task catalog

Tasks live in src/app/core/pyb/tasks/ and are registered by TasksModule.

Task Purpose

Advance Line — tasks/advance-line/

OpenAdvanceLineInputModalTask

Collect the draw amount.

OpenAdvanceLineConfirmationModalTask

Confirm amount, fee and offer type.

OpenAdvanceLineSuccessModalTask

Success confirmation after a completed draw.

OpenAdvanceDetailsModalTask

Show terms of an advance offer.

OpenInstantAdvanceDetailsTask

Show terms of an Instant Advance offer.

OpenAchSetupModalTask

Collect ACH details before a first draw.

AcceptAdvanceLineTask

Accept an offer through the API.

DirectAcceptAdvanceLineTask

Accept without the confirmation step.

StartAdvanceLineAcceptOfferFlowTask

Entry point that launches the accept-offer flow.

WithdrawAdvanceLineOfferTask

Call the withdraw endpoint.

DocuSign — tasks/docusign/

DocusignAdvanceLineTask

Build the redirect URL and leave for DocuSign.

ReSignContractDocusignTask

Same, for the re-assign-contract flow.

General — tasks/general/

UpdateUserStatusTask

Dispatch userStatusUpdateRequested().

DeleteNotificationTask

Remove a dashboard notification once acted on.

Legal — tasks/legal-info-data/

OpenLegalInfoModalTask

Present legal disclosures for acknowledgement. Note the file is open-legal-inf-modal.task.ts — the truncated inf is a typo in the filename, not here.

LTC — tasks/ltc/

LtcOptionsTask

Present available Long Term Contract options.

LtcOptionConfirmationTask

Confirm the chosen option.

LtcDocuSignTask

Redirect to DocuSign for the LTC.

Composed flows

Flows live in src/app/core/pyb/flows/ and are registered by FlowsModule.

Flow Description

AdvanceLineWithdrawFlow

Draw funds from an active Advance Line.

AdvanceLineAcceptOfferFlow

Accept a new Advance Line offer.

AdvanceLinePendingActivationFlow

First draw for an expedited, still-pending supplier.

AdvanceLineLegacyActivationFlow

Activation path for the legacy advance product.

LtcFlowService

Sign a new Long Term Contract.

LtcRenewalFlowService

Renew an existing Long Term Contract.

ReSignContractFlow

Re-sign an updated supplier agreement.

Worked example: AdvanceLineWithdrawFlow

The flow branches on user state while it is being built, which is the main reason this engine exists.

private buildTaskFlow(): TasksFlow {
    const taskFlow = new TaskFlowBuilder();
    const { status }: { status: Status } = this.userService.getCurrentUser();

    taskFlow
        .setContextData('data', this.prepareFlowContextData())
        .addTask(this.openAdvanceLineInputModalTask, this.defaultHandler)
        .addTask(this.openAdvanceLineConfirmationModalTask, this.defaultHandler);

    if (!status.instantAdvanceSigned || this.userService.isAprUserContract()) {
        taskFlow.addTask(this.docusignAdvanceLineTask);          (1)
    } else {
        taskFlow
            .addTask(this.withdrawAdvanceLineTask, this.defaultHandler)
            .addTask(this.updateUserStatusTask, this.defaultHandler)
            .addTask(this.openAdvanceLineSuccessModalTask);      (2)
    }

    return taskFlow.getFlow();
}
1 A supplier who has not yet signed, or who is on an APR contract, must go through DocuSign. The flow ends here because the browser navigates away; it resumes on return via DocusignVerificationService.initializeFlow().
2 An already-signed, non-APR supplier withdraws inline — no redirect, and the success modal closes the loop in-app.

The context is seeded with everything downstream tasks need:

private prepareFlowContextData() {
    const user = this.userService.getCurrentUser();
    const { advanceLine, autodebitPmtCfg, instantAdvanceSigned } = user.status;
    return {
        advanceLine,
        id: user.id,
        autodebitPmtCfg,
        docuSignNoteVisible: !instantAdvanceSigned,
    };
}

Worked example: ReSignContractFlow with a fallback banner

GlobalSubscriptionsService runs the re-sign flow, and if the supplier terminates it, replaces the modal with a persistent banner that can restart it:

private runReSignContractFlow() {
    const banner = reSignContractBannerFactory(() => this.reSignContractFlow.run());
    this.reSignContractFlow.run(({ type }) => {
        if (type === FlowTaskResultType.Terminated) {
            this.bannerService.showBanner(banner);
        }
    });
}

When to use a flow

Use TasksFlow when all of the following hold:

  • there are three or more sequential steps,

  • a step can send the user backwards,

  • the sequence itself depends on user state, or

  • the sequence is interrupted by an external redirect.

For a simple confirm-then-act interaction, open a MatDialog / PybDialogService directly — a flow is overhead.