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.
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_idnil → 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
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).
Force Complete triggers the workflow's own completion step, bypassing any pending steps.
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.
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
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
ability.rb: :force_complete_workflow on ActiveFlow scoped to coll_admin, coll_super_admin, coll_master_admin. No student or high school roles.
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_reasonas tooltip or inline note - Never silently hidden
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.
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?)
endCurrently 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.
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 |
New ActiveFlowDefinitionActionPolicy resource:
- Index: filterable by action_type, owner, target_object_type, category, active
- Show / Edit / New / Delete
- Scoped to superadmin role
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
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
| 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 |
- DUAL-18686: StepModule
step_completes_workflowmacro - DUAL-18726: Unify completion step classes (nearly done)
- DUAL-18748: WorkflowVisualizer update
- DUAL-19008:
step_blocks_force_completemacro — replaced by the policies data model
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