Skip to content

Commit 60b8e1f

Browse files
Added eventing to case management for flex
- Add EventsManager and connect eventing between BusinessProcess and Case (in the dummy example) - Add/Update dummy application classes for acceptance testing - Update testing, as appropriate - Other minor cleanup
1 parent 0ed2bf7 commit 60b8e1f

20 files changed

Lines changed: 416 additions & 115 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module Flex
2+
class EventsManager
3+
def self.subscribe(event_key, callback)
4+
subscription = ActiveSupport::Notifications.subscribe(event_key) do |name, _started, _finished, _unique_id, payload|
5+
callback.call({
6+
name: name,
7+
payload: payload
8+
})
9+
end
10+
11+
subscription
12+
end
13+
14+
def self.unsubscribe(subscription)
15+
ActiveSupport::Notifications.unsubscribe(subscription)
16+
end
17+
18+
def self.publish(event_key, payload = {})
19+
ActiveSupport::Notifications.instrument(event_key, payload)
20+
end
21+
22+
private
23+
24+
def initialize
25+
# setting initialize to private so that we cannot make new instances of it
26+
end
27+
end
28+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module Flex
2+
module TaskHandlerService
3+
extend ActiveSupport::Concern
4+
5+
def create_task(kase:)
6+
raise NoMethodError, "#{self.class} must implement execute method"
7+
end
8+
end
9+
end

template/{{app_name}}/engines/flex/app/models/flex/application_form.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ class ApplicationForm < ApplicationRecord
1010

1111
def submit_application
1212
self[:status] = :submitted
13-
save
13+
save!
14+
publish_event
15+
end
16+
17+
protected
18+
19+
def event_payload
20+
{ id: id }
1421
end
1522

1623
private
@@ -23,5 +30,9 @@ def prevent_changes_if_submitted
2330
errors.add(:base, "Cannot modify a submitted application")
2431
throw :abort
2532
end
33+
34+
def publish_event
35+
EventsManager.publish("application_submitted", self.event_payload)
36+
end
2637
end
2738
end

template/{{app_name}}/engines/flex/app/models/flex/business_process.rb

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ class BusinessProcess
44

55
attr_accessor :name, :description, :steps, :start, :transitions
66

7-
def initialize(name:, description: "", steps: {}, start: "", transitions: {})
7+
def initialize(name:, type:, description: "", steps: {}, start: "", transitions: {})
8+
@subscriptions = {}
89
@name = name
10+
@type = type # recognizing this is a code smell that we will want to address in the future
911
@description = description
10-
@steps = steps
11-
@start = start
12-
@transitions = transitions
12+
define_start(start)
13+
define_steps(steps)
14+
define_transitions(transitions)
15+
start_listening_for_events
1316
end
1417

1518
def execute(kase)
@@ -25,7 +28,47 @@ def define_steps(steps)
2528
end
2629

2730
def define_transitions(transitions)
31+
if @subscriptions.any?
32+
stop_listening_all_events
33+
end
34+
2835
@transitions = transitions
36+
start_listening_for_events
37+
end
38+
39+
private
40+
41+
def handle_event(event)
42+
kase = @type.find(event[:payload][:case_id])
43+
current_step = kase.business_process_current_step
44+
next_step = @transitions[current_step][event[:name]]
45+
kase.business_process_current_step = next_step
46+
kase.save!
47+
if next_step == "end"
48+
kase.close
49+
else
50+
@steps[next_step].execute(kase)
51+
end
52+
end
53+
54+
def get_event_names_from_transitions
55+
@transitions.values.flat_map(&:keys).uniq
56+
end
57+
58+
def start_listening_for_events
59+
get_event_names_from_transitions.each do |event_name|
60+
new_subscription = EventsManager.subscribe(event_name, ->(event) {
61+
handle_event(event)
62+
})
63+
@subscriptions[event_name] = new_subscription
64+
end
65+
end
66+
67+
def stop_listening_all_events
68+
@subscriptions.each do |event_name, subscription|
69+
EventsManager.unsubscribe(subscription)
70+
end
71+
@subscriptions.clear
2972
end
3073
end
3174
end

template/{{app_name}}/engines/flex/app/models/flex/user_task.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def initialize(name:, task_management_service:)
1010
end
1111

1212
def execute(kase)
13-
@task_management_service.create_task(name, "eventually will contain details about a case instead of static string")
13+
@task_management_service.create_task(kase: kase)
1414
end
1515
end
1616
end

template/{{app_name}}/engines/flex/spec/acceptance/flex/passport_application_case_acceptance_spec.rb

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ module Flex
88
# create new application
99
test_form.save!
1010

11-
# check case created
11+
# check case created and open with correct current step
1212
kase = PassportCase.find(test_form.case_id)
1313
expect(kase).not_to be_nil
14-
expect(kase.business_process_current_step).to eq ("collect application info")
14+
expect(kase.status).to eq ("open")
15+
expect(kase.business_process_current_step).to eq ("collect_application_info")
1516

1617
# submit application
1718
test_form.first_name = "John"
@@ -20,15 +21,24 @@ module Flex
2021
test_form.save!
2122
test_form.submit_application
2223
kase.reload
23-
expect(kase.business_process_current_step).to eq ("verify identity")
24+
expect(kase.business_process_current_step).to eq ("verify_identity")
2425

2526
# verify identity (simulate action that an adjudicator takes)
26-
kase.verify_identity
27-
expect(kase.business_process_current_step).to eq ("review passport photo")
27+
EventsManager.publish("identity_verified", { case_id: kase.id })
28+
kase.reload
29+
expect(kase.business_process_current_step).to eq ("review_passport_photo")
30+
31+
# approve passport photo
32+
EventsManager.publish("passport_photo_approved", { case_id: kase.id })
33+
kase.reload
34+
expect(kase.business_process_current_step).to eq ("notify_user_passport_approved")
2835

29-
# approve application
30-
kase.approve
36+
# notify user
37+
EventsManager.publish("notification_completed", { case_id: kase.id })
38+
kase.reload
3139
expect(kase.business_process_current_step).to eq ("end")
40+
41+
# check case status
3242
expect(kase.status).to eq ("closed")
3343
end
3444
end

template/{{app_name}}/engines/flex/spec/dummy/app/business_processes/flex/passport_application_business_process_manager.rb

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,59 @@ def initialize
1313
def create_passport_application_business_process
1414
business_process = BusinessProcess.new(
1515
name: 'Passport Application Process',
16+
type: PassportCase,
1617
description: 'Process for applying for a passport'
1718
)
18-
business_process.define_steps(self.create_passport_application_business_process_steps)
19+
business_process.define_steps(
20+
{
21+
"collect_application_info" => UserTask.new(
22+
name: "Collect App Info",
23+
task_management_service: UserTaskCreatorService
24+
),
25+
"verify_identity" => SystemProcess.new(name: "Verify Identity", callback: ->(kase) {
26+
IdentityVerifierService.new(kase).verify_identity # IdentityVerifierService would publish an event when verify_identity completes
27+
}),
28+
"manual_adjudicator_review" => UserTask.new(name: "Manual Adjudicator Review", task_management_service: AdjudicatorTaskCreatorService), # create an adjudicator task for manual review
29+
"review_passport_photo" => SystemProcess.new(name: "Review Passport Photo", callback: ->(kase) {
30+
PhotoVerifierService.new(kase).verify_photo # PhotoVerifierService would publish an event when verify_photo completes
31+
}),
32+
"notify_user_passport_approved" => SystemProcess.new(name: "Notify Passport Approval", callback: ->(kase) {
33+
UserNotifierService.new(kase).send_notification("approval") # UserNotifierService would publish an event when send_notification completes
34+
}),
35+
"notify_user_passport_rejected" => SystemProcess.new(name: "Notify Passport Rejection", callback: ->(kase) {
36+
UserNotifierService.new(kase).send_notification("rejection") # UserNotifierService would publish an event when send_notification completes
37+
})
38+
}
39+
)
1940
business_process.define_transitions(
2041
{
21-
"collect application info" => 'verify identity',
22-
"verify identity" => 'review passport photo',
23-
"review passport photo" => 'end'
42+
"collect_application_info" => {
43+
"application_submitted" => 'verify_identity',
44+
"application_cancelled" => 'end'
45+
},
46+
"verify_identity" => {
47+
"identity_verified" => 'review_passport_photo',
48+
"identity_warning" => 'manual_adjudicator_review'
49+
},
50+
"manual_adjudicator_review" => {
51+
"identity_verified" => 'review_passport_photo',
52+
"identity_rejected" => 'application_rejected'
53+
},
54+
"review_passport_photo" => {
55+
"passport_photo_approved" => 'notify_user_passport_approved',
56+
"passport_photo_rejected" => 'review_passport_photo'
57+
},
58+
"notify_user_passport_approved" => {
59+
"notification_completed" => "end"
60+
},
61+
"notify_user_passport_rejected" => {
62+
"notification_completed" => 'end'
63+
}
2464
}
2565
)
26-
business_process.define_start('collect application info')
27-
business_process
28-
end
66+
business_process.define_start('collect_application_info')
2967

30-
def create_passport_application_business_process_steps
31-
{
32-
'collect application info' => SystemProcess.new(name: "Collect Application Info", callback: ->(kase) { kase.mark_application_info_collected }), # simulate collecting application info
33-
'verify identity' => SystemProcess.new(name: "Verify Identity", callback: ->(kase) { kase.verify_identity }), # simulate verifying identity
34-
'review passport photo' => SystemProcess.new(name: "Review Passport Photo", callback: ->(kase) { kase.approve }), # simulate reviewing passport photo
35-
'end' => SystemProcess.new(name: "End", callback: ->(kase) { kase.close }) # close case
36-
}
68+
business_process
3769
end
3870
end
3971
end

template/{{app_name}}/engines/flex/spec/dummy/app/models/flex/passport_application_form.rb

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
require_relative "../../../../../app/models/flex/application_form"
2-
31
module Flex
42
class PassportApplicationForm < ApplicationForm
53
before_create :create_passport_case, unless: -> { has_case_id? }
@@ -18,11 +16,14 @@ def has_all_necessary_fields?
1816
end
1917

2018
def submit_application
21-
if has_all_necessary_fields?
22-
super
23-
# this is temporary and will be changed in another PR when implementing an event-based approach
24-
PassportCase.find(case_id).mark_application_info_collected
25-
end
19+
has_all_necessary_fields? ? super : false
20+
end
21+
22+
protected
23+
24+
def event_payload
25+
parent_payload = super
26+
parent_payload.merge({ case_id: case_id })
2627
end
2728

2829
private

template/{{app_name}}/engines/flex/spec/dummy/app/models/flex/passport_case.rb

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,15 @@ module Flex
22
class PassportCase < Case
33
readonly attribute :passport_id, :string, default: SecureRandom.uuid # always defaults to a new UUID
44

5-
attribute :business_process_current_step, :string, default: "collect application info"
6-
private def business_process_current_step=(value)
7-
self[:business_process_current_step] = value
8-
end
9-
10-
@business_process = PassportApplicationBusinessProcessManager.instance.business_process
5+
attribute :business_process_current_step, :string, default: "collect_application_info"
116

12-
def mark_application_info_collected
13-
self[:business_process_current_step] = "verify identity"
14-
save!
15-
end
7+
after_create :initialize_business_process
168

17-
def verify_identity
18-
self[:business_process_current_step] = "review passport photo"
19-
save!
20-
end
9+
private
2110

22-
def approve
23-
self[:business_process_current_step] = "end"
24-
close
11+
def initialize_business_process
12+
business_process = PassportApplicationBusinessProcessManager.instance.business_process
13+
business_process.execute({ case_id: id })
2514
end
2615
end
2716
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module Flex
2+
class AdjudicatorTaskCreatorService
3+
include TaskHandlerService
4+
5+
def self.create_task(kase:)
6+
end
7+
end
8+
end

0 commit comments

Comments
 (0)