Skip to content

Instantly share code, notes, and snippets.

@ckozus
Last active April 13, 2026 20:45
Show Gist options
  • Select an option

  • Save ckozus/cfb5b185f9fe479fabe4517403be001d to your computer and use it in GitHub Desktop.

Select an option

Save ckozus/cfb5b185f9fe479fabe4517403be001d to your computer and use it in GitHub Desktop.
IDEA-340 / IDEA-338: Workflow Action Policies — Implementation Plan

Workflow Action Policies — Implementation Plan

Covers IDEA-340 (Workflow Action Policies) and IDEA-338 (Force Complete). The policies system is the foundation; Force Complete is the first action built on top of it.


1. Data Model

active_flow_definition_action_policies table

create_table :active_flow_definition_action_policies do |t|
  t.string   :action_type,             null: false  # "force_complete" | "abandon"
  # Workflow scope — mirrors active_flow_definitions unique index
  t.string   :owner_type                            # "College" | "CollegeSystem" | nil (platform-wide)
  t.integer  :owner_id                              # nil = platform-wide
  t.string   :target_object_type                    # "StudentDeCourse" etc., nil = all
  t.string   :category                              # workflow category, nil = all
  # Conditions (AND within a row — all non-null must be true for policy to fire)
  t.string   :step_class                            # mandatory for step-based conditions
  t.string   :step_name                             # narrows match (e.g. "Approve Application")
  t.string   :step_participant_role                 # further narrows step match
  t.string   :step_state                            # "active" | "executed" | "not_executed"
  t.string   :required_fields_keys,   array: true   # fields keys that must be present
  t.string   :prohibited_fields_keys, array: true   # fields keys that must be absent
  # Display
  t.text     :restriction_reason,     null: false   # shown to user when policy fires
  t.boolean  :active,                 default: true, null: false
  t.timestamps
end

add_index :active_flow_definition_action_policies, [:action_type, :owner_type, :owner_id,
                                      :target_object_type, :category]

Scoping mirrors active_flow_definitions:

  • owner_type / owner_id nil → platform-wide default
  • College-specific policies are additive — they apply alongside platform policies
  • Both are evaluated; any triggered policy blocks the action

Condition evaluation per row (AND):

  • All non-null condition columns must be true simultaneously
  • Step conditions match against current active steps on the workflow
  • Fields conditions match against active_flow.fields / active_flow.fields_jsonb

Multiple rows (OR):

  • Any triggered policy blocks the action and surfaces its restriction_reason

2. Policy Evaluator

New service: ActiveFlowDefinitionActionPolicy.evaluate(action_type, active_flow, owner)

1. Collect applicable policies:
   - WHERE action_type = ? AND active = true
   - AND (owner matches exactly OR owner_type IS NULL)
   - AND (target_object_type matches OR target_object_type IS NULL)
   - AND (category matches OR category IS NULL)

2. For each policy, evaluate conditions against active_flow:
   a. step_class present → find matching active_flow_steps
      - narrow by step_name if set
      - narrow by step_participant_role if set
      - check step_state: active | executed | not_executed
   b. required_fields_keys present → all keys must exist in active_flow.fields
   c. prohibited_fields_keys present → none of the keys may exist in active_flow.fields

3. Return first triggered policy (with restriction_reason), or nil if none fire

Result type: nil (action allowed) or { blocked: true, reason: String }

The evaluator is called when building the three-dot menu options and again as a guard inside the controller action (defense in depth).


3. Force Complete Action (IDEA-338)

Force Complete triggers the workflow's own completion step, bypassing any pending steps.

Step registry

StepModule gains a step_completes_workflow macro (DUAL-18686). Completion steps self-register by target object type. At runtime the evaluator looks up which step class handles completion for the workflow's target object type.

Controller flow

GET  /active_flows/:id/confirm_force_complete  → show confirmation modal
POST /active_flows/:id/force_complete
  1. Authorize: college admin / superadmin only
  2. Policy check: ActiveFlowDefinitionActionPolicy.evaluate(:force_complete, active_flow, owner)
     → blocked? render disabled state with reason
  3. Enqueue ForceCompleteWorkflowJob with active_flow_id, reason, current_user_id

Background job

ForceCompleteWorkflowJob
  1. Re-check policy (guard against race)
  2. Re-check active_flow still active and not already completed
  3. Deactivate all currently active steps
  4. Reactivate the completion step (step_completes_workflow tagged)
  5. Audit log: action=force_complete, user, reason, timestamp

Authorization

ability.rb: :force_complete_workflow on ActiveFlow scoped to coll_admin, coll_super_admin, coll_master_admin. No student or high school roles.


4. UI Changes

Three-dot menu

Current: action link shown or not shown based on can? + abandonable?.

New behaviour (both Abandon and Force Complete):

  • Action always rendered if user has role permission
  • Policy evaluator result determines enabled/disabled state
  • Disabled → grayed out with restriction_reason as tooltip or inline note
  • Never silently hidden

Shared confirmation modal

Single partial parameterized by action type, replacing the existing abandon-specific popup:

- Title: "Abandon Registration" | "Force Complete"
- Warning: "This action cannot be undone."
- Reason textarea: required, max 200 characters
- Submit button: action-specific label and colour

The existing _abandon_popup.html.erb is refactored into this shared component.


5. Abandon — Code Cleanup

Simplify abandonable?

Current StudentTerm#prevent_abandon_based_on_active_active_flow_steps? hard-codes step class names and reads the allow_abandon parameter from step definitions. Both are replaced by policy rows.

After migration:

# StudentDeCourse
def abandonable?
  is_active? && !completed? && !abandoned? && active_flows.exists?
end

# StudentTerm
def abandonable?
  is_active? && student_de_courses.all?(&:abandonable_or_abandoned?)
end

allow_abandon parameter

Currently used only by Bridgeport to allow abandonment on otherwise-restricted steps. Replaced by a college-scoped policy that overrides the platform restriction. The parameter is deprecated and removed from step definitions during migration.

Platform-wide abandon policies (backfill data)

Rows that replace the hard-coded Ruby for the current platform restrictions:

step_class step_name filter condition reason
ApprovalStep includes "Application" active Application review in progress
WaitForApplicationResponseStep active Awaiting application response
CollegeReviewAdmissionApplicationFailureStep active Application failure review in progress

6. Admin Surfaces

Active Admin

New ActiveFlowDefinitionActionPolicy resource:

  • Index: filterable by action_type, owner, target_object_type, category, active
  • Show / Edit / New / Delete
  • Scoped to superadmin role

Insights API

Read endpoint: GET /api/v1/active_flow_definition_action_policies

  • Filterable by action_type, owner_id, owner_type
  • Used by college admin UI and any external tooling

College Admin Section

New section in college settings:

  • Platform policies tab: read-only list of active platform-wide policies that apply to this college
  • College policies tab: CRUD for policies scoped to owner = current_college
  • Cannot create policies for other owners or modify platform defaults

7. Story Breakdown

New stories (under updated DUAL-18685 epic)

Story Scope
Workflow Action Policies: data model and evaluator Migration, model, ActiveFlowDefinitionActionPolicy.evaluate service
Force Complete: backend Controller, routes, background job, authorization (DUAL-18687, updated)
Force Complete + Abandon: shared UI Shared modal, disabled state with reason in three-dot menu (DUAL-18688, updated)
Abandon: migrate hard-coded restrictions Simplify abandonable?, backfill policy rows, remove allow_abandon parameter
Admin surfaces: Active Admin + Insights API Internal CRUD and read endpoints
Admin surfaces: college admin section College-facing policy management UI

Existing stories — no scope change needed

  • DUAL-18686: StepModule step_completes_workflow macro
  • DUAL-18726: Unify completion step classes (nearly done)
  • DUAL-18748: WorkflowVisualizer update

Abandoned

  • DUAL-19008: step_blocks_force_complete macro — replaced by the policies data model

8. Dependency Order

DUAL-18725 step_deprecated macro (Done)
  └─ DUAL-18726 Unify completion steps (Ready for Prod)
       └─ DUAL-18686 step_completes_workflow macro
            └─ Workflow Action Policies: data model + evaluator
                 ├─ Force Complete: backend
                 │    └─ Force Complete + Abandon: shared UI
                 ├─ Abandon: migrate hard-coded restrictions
                 └─ Admin surfaces
                      └─ College admin section
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment