call_center 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -9,58 +9,64 @@ Call Center streamlines the process of defining multi-party call workflows in yo
9
9
 
10
10
  [Twilio](http://www.twilio.com/docs) provides a two-part API for managing phone calls, and is mostly driven by callbacks. Call Center DRYs up the application logic dealing with a callback driven API so you can focus on the business logic of your call center.
11
11
 
12
- ### Not DRY
13
- Twilio requests your application to return [TwiML](http://www.twilio.com/docs/api/twiml/) that describes the call workflow. TwiML contains commands which Twilio then executes. It is essentially an application-to-application API, synonymous to making a REST call.
14
-
15
- In the context of "[Skinny Controller, Fat Model](http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model)", outgoing REST calls for the function of business logic are not a view concern but a model concern. Therefore, so is TwiML.
16
-
17
- Twilio supports callbacks URLs and redirects to URLs that also render TwiML as a method of modifying live calls. Incoming callbacks are handled by the controller first, but the response is still a model concern.
18
-
19
- Terminology
20
- -----------
21
-
22
- * **Call** - An application resource of yours that encapsulates a phone call. Phone calls are then acted on: answered, transferred, declined, etc.
23
- * **Event** - Is something that happens outside or inside your application in relation to a **Call**. Someone picks up, hangs up, presses a button, etc. But overall, it's anything that can be triggered by Twilio callbacks.
24
- * **State** - Is the status a **Call** is in which is descriptive of what's happened so far and what are the next things that should happen. (e.g. a call on hold is waiting for the agent to return)
25
- * **CallFlow** - Is a definition of the process a **Call** goes through. **Events** drive the flow between **States**. (e.g. a simple workflow is when noone answers the call, send the call to voicemail)
26
- * **Render** - Is the ability of the **CallFlow** to return TwiML to bring the call into the **State** or modify the live call through a **Redirect**.
27
- * **Redirect** - Is a way of modifying a live call outside of a TwiML response (e.g. background jobs)
28
-
29
12
  Usage
30
13
  -----
31
14
 
32
15
  class Call
33
16
  include CallCenter
34
17
 
35
- call_flow :state, :intial => :answered do
36
- state :answered do
37
- event :incoming_call, :to => :voicemail, :if => :not_during_business_hours?
38
- event :incoming_call, :to => :sales
18
+ call_flow :state, :intial => :incoming do
19
+ actor :customer do |call, event|
20
+ "/voice/calls/flow?event=#{event}&actor=customer&call_id=#{call.id}"
39
21
  end
40
22
 
41
- state :voicemail do
42
- event :customer_hangs_up, :to => :voicemail_completed
23
+ state :incoming do
24
+ response do |x|
25
+ x.Gather :numDigits => '1', :action => customer(:wants_voicemail) do
26
+ x.Say "Hello World"
27
+ x.Play some_nice_music, :loop => 100
28
+ end
29
+ # <?xml version="1.0" encoding="UTF-8" ?>
30
+ # <Response>
31
+ # <Gather numDigits="1" action="/voice/calls/flow?event=wants_voicemail&actor=customer&call_id=5000">
32
+ # <Say>Hello World</Say>
33
+ # <Play loop="100">http://some.nice.music.com/1.mp3</Play>
34
+ # </Gather>
35
+ # </Response>
36
+ end
37
+
38
+ event :called, :to => :routing, :if => :agents_available?
39
+ event :called, :to => :voicemail
40
+ event :wants_voicemail, :to => :voicemail
41
+ event :customer_hangs_up, :to => :cancelled
43
42
  end
44
43
 
45
- on_render(:sales) do |call, x|
46
- x.Say "This is Sales!"
44
+ state :voicemail do
45
+ response do |x|
46
+ x.Say "Please leave a message"
47
+ x.Record(:action => customer(:voicemail_complete))
48
+ # <?xml version="1.0" encoding="UTF-8" ?>
49
+ # <Response>
50
+ # <Say>Please leave a message</Say>
51
+ # <Record action="/voice/calls/flow?event=voicemail_complete&actor=customer&call_id=5000"/>
52
+ # </Response>
53
+ end
54
+
55
+ event :voicemail_complete, :to => :voicemail_completed
56
+ event :customer_hangs_up, :to => :cancelled
47
57
  end
48
58
 
49
- on_render(:voicemail) do |call, x|
50
- x.Say "Leave a voicemail!"
51
- end
59
+ state :routing do
52
60
 
53
- on_flow_to(:voicemail) do |call, transition|
54
- call.notify(:voicemail)
55
61
  end
56
62
  end
57
63
  end
58
64
 
59
65
  Benefits of **CallCenter** is that it's backed by [state_machine](https://github.com/pluginaweek/state_machine). Which means you can interact with events the same you do in `state_machine`.
60
66
 
61
- @call.incoming_call!
62
- @call.voicemail?
63
- @call.sales?
67
+ @call.called!
68
+ @call.wants_voicemail!
69
+ @call.routing?
64
70
  @call.render # See Rendering
65
71
 
66
72
  Flow
@@ -96,63 +102,69 @@ Rendering
96
102
 
97
103
  Rendering is your way of interacting with Twilio. Thus, it provides two facilities: access to an XML builder and access to your call.
98
104
 
99
- on_render(:sales) do |the_call, xml_builder|
100
- xml_builder.Say "This is Sales!"
101
-
102
- the_call.flag! # You can access the call explicitly
103
- flag! # Or access it implicitly
105
+ state :sales do
106
+ response do |xml_builder, the_call|
107
+ xml_builder.Say "This is #{the_call.agent.name}!" # Or agent.name, you can access he call implicitly
108
+ end
104
109
  end
105
110
 
106
111
  Renders with `@call.render` if the current state is :sales:
107
112
 
108
113
  <?xml version="1.0" encoding="UTF-8"?>
109
114
  <Response>
110
- <Say>This is Sales!</Say>
115
+ <Say>This is Henry!</Say>
111
116
  </Response>
112
117
 
113
118
  Callbacks
114
119
  ---------
115
120
 
116
- If you ever want to do something special after entering a state, but only if it's a new transition (e.g. NOT from :voicemail => :voicemail), you can do this:
121
+ You have control over what you want to happen before/after state transitions:
117
122
 
118
- on_flow_to(:voicemail) do |the_call, the_transition|
119
- the_call.notify(transition.event) # Explicitly
120
- notify(transition.event) # Implicitly
121
- end
123
+ state :voicemail do
124
+ before(:always) { # Invokes before any transition }
125
+ before(:always, :uniq => true) { # Invokes before transitions to a different state }
122
126
 
123
- Redirects
124
- ---------
127
+ after(:always) { # Invokes after any transition }
128
+ after(:success) { # Invokes after any successful transition }
129
+ after(:failure) { # Invokes after any failed transition (those not covered in your call flow) }
125
130
 
126
- Redirects are a request made to the [Twilio REST API](http://www.twilio.com/docs/api/rest/) that points to a callback URL which returns TwiML to be executed on a call. It is up to you how you want to perform this (e.g. with your favority http libraries, or with [Twilio Libraries](http://www.twilio.com/docs/libraries/)).
131
+ after(:always, :uniq => true) { # Invokes after any transition to a different state }
132
+ after(:success, :uniq => true) { # Successful unique transitions }
133
+ after(:failure, :uniq => true) { # Failed unique transitions }
134
+ end
127
135
 
128
- Redirect to events look like this:
136
+ For example,
129
137
 
130
- ...
131
- call_flow :state, :intial => :answered do
132
- state :answered do
133
- ...
134
- end
138
+ state :voicemail do
139
+ before(:always) { log_start_event }
135
140
 
136
- state :ending_call do
137
- event :end_call, :to => :ended_call
138
- end
141
+ after(:always) { log_end_event }
142
+ after(:failure) { notify_airbrake }
139
143
 
140
- on_render(:ending_call) do
141
- redirect_and_end_call!(:status => 'completed')
142
- end
144
+ after(:success, :uniq => true) { notify_browser }
145
+ after(:failure, :uniq => true) { notify_cleanup_browser }
143
146
  end
144
- ...
145
147
 
146
- For your **Call** to support this syntax, it must adhere to the following API:
148
+ Motivation
149
+ ----------
147
150
 
148
- class Call
149
- def redirect_to(event, *args)
150
- # where:
151
- # event #=> :end_call
152
- # args #=> [:status => 'completed]
153
- @account.calls.get(self.sid).update({:url => "http://myapp.com/call_flow?event=#{event}"})
154
- end
155
- end
151
+ ### Not DRY
152
+ Twilio requests your application to return [TwiML](http://www.twilio.com/docs/api/twiml/) that describes the call workflow. TwiML contains commands which Twilio then executes. It is essentially an application-to-application API, synonymous to making a REST call.
153
+
154
+ In the context of "[Skinny Controller, Fat Model](http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model)", outgoing REST calls for the function of business logic are not a view concern but a model concern. Therefore, so is TwiML.
155
+
156
+ Twilio supports callbacks URLs and redirects to URLs that also render TwiML as a method of modifying live calls. Incoming callbacks are handled by the controller first, but the response is still a model concern.
157
+
158
+
159
+ Terminology
160
+ -----------
161
+
162
+ * **Call** - An application resource of yours that encapsulates a phone call. Phone calls are then acted on: answered, transferred, declined, etc.
163
+ * **Event** - Is something that happens outside or inside your application in relation to a **Call**. Someone picks up, hangs up, presses a button, etc. But overall, it's anything that can be triggered by Twilio callbacks.
164
+ * **State** - Is the status a **Call** is in which is descriptive of what's happened so far and what are the next things that should happen. (e.g. a call on hold is waiting for the agent to return)
165
+ * **CallFlow** - Is a definition of the process a **Call** goes through. **Events** drive the flow between **States**. (e.g. a simple workflow is when noone answers the call, send the call to voicemail)
166
+ * **Render** - Is the ability of the **CallFlow** to return TwiML to bring the call into the **State** or modify the live call through a **Redirect**.
167
+ * **Redirect** - Is a way of modifying a live call outside of a TwiML response (e.g. background jobs)
156
168
 
157
169
  Tools
158
170
  -----
data/Rakefile CHANGED
@@ -37,7 +37,7 @@ Rcov::RcovTask.new do |test|
37
37
  test.libs << 'test'
38
38
  test.pattern = 'test/**/*_test.rb'
39
39
  test.verbose = true
40
- test.rcov_opts << '--exclude "gems/*"'
40
+ test.rcov_opts << '--exclude "gems/*,lib/call_center/core_ext/object_instance_exec.rb"'
41
41
  end
42
42
 
43
43
  task :default => :test
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.3
1
+ 0.0.4
data/call_center.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{call_center}
8
- s.version = "0.0.3"
8
+ s.version = "0.0.4"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Henry Hsu"]
12
- s.date = %q{2011-09-29}
12
+ s.date = %q{2011-10-23}
13
13
  s.description = %q{Support for describing call center workflows}
14
14
  s.email = %q{hhsu@zendesk.com}
15
15
  s.extra_rdoc_files = [
@@ -30,6 +30,7 @@ Gem::Specification.new do |s|
30
30
  "init.rb",
31
31
  "lib/call_center.rb",
32
32
  "lib/call_center/core_ext/object_instance_exec.rb",
33
+ "lib/call_center/flow_callback.rb",
33
34
  "lib/call_center/state_machine_ext.rb",
34
35
  "lib/call_center/test/dsl.rb",
35
36
  "test/call_center_test.rb",
data/lib/call_center.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'call_center/core_ext/object_instance_exec'
2
2
  require 'state_machine'
3
3
  require 'call_center/state_machine_ext'
4
+ require 'call_center/flow_callback'
4
5
 
5
6
  module CallCenter
6
7
  def self.included(base)
@@ -21,6 +22,15 @@ module CallCenter
21
22
  self.cached_state_machines["#{klass.name}_#{state_machine_name}"]
22
23
  end
23
24
 
25
+ def self.render_twiml
26
+ xml = Builder::XmlMarkup.new
27
+ xml.instruct!
28
+ xml.Response do
29
+ yield(xml)
30
+ end
31
+ xml.target!
32
+ end
33
+
24
34
  module ClassMethods
25
35
  attr_accessor :call_flow_state_machine_name
26
36
 
@@ -32,12 +42,7 @@ module CallCenter
32
42
  if state_machine = CallCenter.cached(self, state_machine_name)
33
43
  state_machine = state_machine.duplicate_to(self)
34
44
  else
35
- state_machine = state_machine(*args, &blk)
36
- state_machine.instance_eval do
37
- after_transition any => any do |call, transition|
38
- call.flow_to(transition) if transition.from_name != transition.to_name
39
- end
40
- end
45
+ state_machine = state_machine(*args, &blk).setup_call_flow(self)
41
46
  CallCenter.cache(self, state_machine)
42
47
  end
43
48
  self.call_flow_state_machine_name ||= state_machine.name
@@ -50,40 +55,27 @@ module CallCenter
50
55
  end
51
56
 
52
57
  module InstanceMethods
53
- def render(state_machine_name = self.class.call_flow_state_machine_name)
54
- xml = Builder::XmlMarkup.new
55
- render_block = current_block_accessor(:render_blocks, state_machine_name)
56
-
57
- xml.instruct!
58
- xml.Response do
59
- self.instance_exec(self, xml, &render_block) if render_block
58
+ def render(name = nil)
59
+ name ||= self.class.call_flow_state_machine_name
60
+ return unless name
61
+ CallCenter.render_twiml do |xml|
62
+ if render_block = state_machine_for_name(name).block_accessor(:response_blocks, current_state(name))
63
+ render_block.arity == 2 ? self.instance_exec(xml, self, &render_block) : self.instance_exec(xml, &render_block)
64
+ end
60
65
  end
61
- xml.target!
62
- end
63
-
64
- def flow_to(transition, state_machine_name = self.class.call_flow_state_machine_name)
65
- block = current_block_accessor(:flow_to_blocks, state_machine_name)
66
- self.instance_exec(self, transition, &block) if block
67
66
  end
68
67
 
69
68
  def draw_call_flow(*args)
70
- current_state_machine.draw(*args)
69
+ self.class.current_state_machine.draw(*args)
71
70
  end
72
71
 
73
72
  private
74
73
 
75
- def current_block_accessor(accessor, state_machine_name)
76
- csm = self.class.state_machines[state_machine_name]
77
- return unless csm.respond_to?(accessor)
78
- blocks, name = csm.send(accessor), csm.name
79
- blocks[current_flow_state(state_machine_name)] if blocks
80
- end
81
-
82
- def current_state_machine
83
- self.class.current_state_machine
74
+ def state_machine_for_name(state_machine_name)
75
+ self.class.state_machines[state_machine_name]
84
76
  end
85
77
 
86
- def current_flow_state(state_machine_name)
78
+ def current_state(state_machine_name)
87
79
  send(state_machine_name).to_sym
88
80
  end
89
81
 
@@ -0,0 +1,69 @@
1
+ module CallCenter
2
+ class FlowCallback
3
+ attr_reader :state_name, :scope, :block
4
+
5
+ def self.create(state_name, scope, options, block)
6
+ case scope
7
+ when :success
8
+ FlowCallback.new(state_name, scope, options, block).extend(SuccessFlowCallback)
9
+ when :failure
10
+ FlowCallback.new(state_name, scope, options, block).extend(FailureFlowCallback)
11
+ else
12
+ FlowCallback.new(state_name, scope, options, block).extend(AlwaysFlowCallback)
13
+ end
14
+ end
15
+
16
+ def initialize(state_name, scope, options, block)
17
+ raise "Invalid scope: #{scope} for flow callback" unless [:always, :success, :failure].include?(scope)
18
+ @state_name, @scope, @block = state_name, scope, block
19
+ extend(UniqueFlowCallback) if options[:uniq]
20
+ end
21
+
22
+ def run(flow, transition)
23
+ @transition = transition
24
+ flow.instance_exec(transition, &block) if should_run?
25
+ end
26
+
27
+ def setup(context)
28
+ callback = self
29
+ context.before_transition(transition_parameters(context)) do |call, transition|
30
+ callback.run(call, transition)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def transition_parameters(context)
37
+ { context.any => @state_name }
38
+ end
39
+
40
+ def should_run?
41
+ true
42
+ end
43
+ end
44
+
45
+ module AlwaysFlowCallback
46
+ def success; true; end
47
+ def failure; true; end
48
+ end
49
+
50
+ module SuccessFlowCallback
51
+ def success; true; end
52
+ def failure; false; end
53
+ end
54
+
55
+ module FailureFlowCallback
56
+ def success; false; end
57
+ def failure; true; end
58
+ end
59
+
60
+ module UniqueFlowCallback
61
+ def should_run?
62
+ !@transition.loopback?
63
+ end
64
+
65
+ def transition_parameters(context)
66
+ { context.any - @state_name => @state_name }
67
+ end
68
+ end
69
+ end
@@ -1,26 +1,98 @@
1
1
  # Extension for StateMachine::Machine to store and provide render blocks
2
2
  class StateMachine::Machine
3
- attr_accessor :render_blocks
4
- attr_accessor :flow_to_blocks
3
+ attr_accessor :response_blocks
4
+ attr_accessor :before_blocks
5
+ attr_accessor :after_blocks
6
+ attr_accessor :flow_actor_blocks
5
7
 
6
- def on_render(state_name, &blk)
7
- @render_blocks ||= {}
8
- @render_blocks[state_name] = blk
8
+ def response(state_name, &blk)
9
+ @response_blocks ||= {}
10
+ @response_blocks[state_name] = blk
9
11
  end
10
12
 
11
- def on_flow_to(state_name, &blk)
12
- @flow_to_blocks ||= {}
13
- @flow_to_blocks[state_name] = blk
13
+ def before(state_name, scope, options, &blk)
14
+ @before_blocks ||= []
15
+ @before_blocks << CallCenter::FlowCallback.create(state_name, :always, options, blk)
16
+ end
17
+
18
+ def after(state_name, scope, options, &blk)
19
+ @after_blocks ||= []
20
+ @after_blocks << CallCenter::FlowCallback.create(state_name, scope, options, blk)
21
+ end
22
+
23
+ def block_accessor(accessor, for_state)
24
+ return unless respond_to?(accessor)
25
+ blocks = send(accessor)
26
+ blocks[for_state] if blocks
27
+ end
28
+
29
+ def flow_actors(name, &blk)
30
+ @flow_actor_blocks ||= {}
31
+ @flow_actor_blocks[name] = blk
32
+ end
33
+
34
+ def setup_call_flow(flow)
35
+ setup_before_blocks
36
+ setup_after_blocks
37
+ setup_flow_actor_blocks(flow)
38
+ self
39
+ end
40
+
41
+ def setup_before_blocks
42
+ return unless @before_blocks
43
+ @before_blocks.each { |callback| callback.setup(self) }
44
+ end
45
+
46
+ def setup_after_blocks
47
+ return unless @after_blocks
48
+ @after_blocks.select(&:success).each { |callback| callback.setup(self) }
49
+
50
+ event_names = events.map(&:name)
51
+ event_names.each do |event_name|
52
+ after_failure :on => event_name do |call, transition|
53
+ callbacks = @after_blocks.select { |callback| callback.state_name == transition.to_name && callback.failure } || []
54
+ callbacks.each { |callback| callback.run(call, transition) }
55
+ end
56
+ end
57
+ end
58
+
59
+ def setup_flow_actor_blocks(flow_class)
60
+ return unless @flow_actor_blocks
61
+ @flow_actor_blocks.each do |actor, block|
62
+ flow_class.send(:define_method, actor) do |event|
63
+ self.instance_exec(self, event, &block) if block
64
+ end
65
+ end
14
66
  end
15
67
  end
16
68
 
17
69
  # Extension for StateMachine::AlternateMachine to provide render blocks inside a state definition
18
70
  class StateMachine::AlternateMachine
19
- def on_render(state_name = nil, &blk)
71
+ def response(state_name = nil, &blk)
20
72
  if @from_state
21
- @queued_sends << [[:on_render, @from_state], blk]
73
+ @queued_sends << [[:response, @from_state], blk]
22
74
  else
23
- @queued_sends << [[:on_render, state_name], blk]
75
+ @queued_sends << [[:response, state_name], blk]
76
+ end
77
+ end
78
+
79
+ def before(scope, options = {}, &blk)
80
+ if @from_state
81
+ @queued_sends << [[:before, @from_state, scope, options], blk]
82
+ end
83
+ end
84
+
85
+ def after(scope, options = {}, &blk)
86
+ if @from_state
87
+ @queued_sends << [[:after, @from_state, scope, options], blk]
88
+ end
89
+ end
90
+
91
+ def actor(name, &blk)
92
+ (class << self; self; end).send(:define_method, name.to_sym) do |event_name, options|
93
+ event_name = :"#{name}_#{event_name}"
94
+ event(event_name, options)
24
95
  end
96
+ @queued_sends << [[:flow_actors, name], blk]
25
97
  end
26
98
  end
@@ -16,7 +16,6 @@ class CallCenterTest < Test::Unit::TestCase
16
16
  klass = call_type.to_s.gsub('_', ' ').titleize.gsub(' ', '').constantize
17
17
  @call = klass.new
18
18
  @call.stubs(:notify)
19
- @call.stubs(:flow_url).returns('the_flow')
20
19
  end
21
20
 
22
21
  context "agents available" do
@@ -99,21 +98,28 @@ class CallCenterTest < Test::Unit::TestCase
99
98
  @call = Call.new
100
99
  end
101
100
 
102
- should "render xml for initial state" do
101
+ should "render xml for initial state" do
103
102
  @call.expects(:notify).with(:rendering_initial)
104
103
  body @call.render
105
104
  assert_select "Response>Say", "Hello World"
106
105
  end
107
106
 
108
- should "render xml for voicemail state" do
107
+ should "return customer url for event" do
108
+ assert_equal("/voice/calls/flow?event=voicemail_complete&actor=customer", @call.customer(:voicemail_complete))
109
+ end
110
+
111
+ should "return agent url for event" do
112
+ assert_equal("/voice/calls/flow?event=voicemail_complete&actor=agent", @call.agent(:voicemail_complete))
113
+ end
114
+
115
+ should "render xml for voicemail state" do
109
116
  @call.stubs(:agents_available?).returns(false)
110
117
  @call.incoming_call!
111
118
  @call.expects(:notify).with(:rendering_voicemail)
112
- @call.expects(:flow_url).with(:voicemail_complete).returns('the_flow')
113
119
 
114
120
  body @call.render
115
121
  assert_select "Response>Say"
116
- assert_select "Response>Record[action=the_flow]"
122
+ assert_select "Response>Record[action=/voice/calls/flow?event=voicemail_complete&actor=customer]"
117
123
  end
118
124
 
119
125
  should "render noop when no render block" do
@@ -124,15 +130,24 @@ class CallCenterTest < Test::Unit::TestCase
124
130
  assert_select "Response"
125
131
  end
126
132
 
127
- should "respond when flow to state (once)" do
128
- @call.state = 'routing'
129
- @call.expects(:notify).with(:cancelled).once
130
- @call.customer_hangs_up!
131
- assert @call.cancelled?
132
- @call.customer_hangs_up!
133
- assert @call.cancelled?
134
- @call.customer_hangs_up!
135
- assert @call.cancelled?
133
+ should "execute before callbacks" do
134
+ @call.state = 'cancelled'
135
+
136
+ @call.expects(:notify).with(:before_always).times(3)
137
+ @call.expects(:notify).with(:before_always_uniq).times(1)
138
+
139
+ @call.expects(:notify).with(:after_always).times(4)
140
+ @call.expects(:notify).with(:after_success).times(3)
141
+ @call.expects(:notify).with(:after_failure).times(1)
142
+
143
+ @call.expects(:notify).with(:after_always_uniq).times(1)
144
+ @call.expects(:notify).with { |notification, transition| notification == :after_success_uniq && transition.kind_of?(StateMachine::Transition) }.times(1)
145
+ @call.expects(:notify).with(:after_failure_uniq).times(0)
146
+
147
+ @call.customer_end!
148
+ @call.customer_end!
149
+ @call.customer_end!
150
+ assert(!@call.customer_hangs_up)
136
151
  end
137
152
 
138
153
  should "asynchronously perform event" do
@@ -166,10 +181,10 @@ class CallCenterTest < Test::Unit::TestCase
166
181
  should_flow :on => :incoming_call, :initial => :voicemail, :when => Proc.new {
167
182
  @call.stubs(:agents_available?).returns(false)
168
183
  @call.stubs(:notify)
169
- @call.stubs(:flow_url).returns('the_flow')
184
+ @call.stubs(:customer).returns('the_flow')
170
185
  } do
171
186
  should_also { assert_received(@call, :notify) { |e| e.with(:rendering_voicemail) } }
172
- and_also { assert_received(@call, :flow_url) { |e| e.with(:voicemail_complete) } }
187
+ and_also { assert_received(@call, :customer) { |e| e.with(:voicemail_complete) } }
173
188
  and_render { "Response>Say" }
174
189
  and_render { "Response>Record[action=the_flow]" }
175
190
  end
@@ -3,44 +3,63 @@ class Call
3
3
  include CommonCallMethods
4
4
 
5
5
  call_flow :state, :initial => :initial do
6
+ actor :customer do |call, event|
7
+ "/voice/calls/flow?event=#{event}&actor=customer"
8
+ end
9
+ actor :agent do |call, event|
10
+ "/voice/calls/flow?event=#{event}&actor=agent"
11
+ end
12
+
6
13
  state :initial do
14
+ response do |x, call| # To allow defining render blocks within a state
15
+ call.notify(:rendering_initial)
16
+ x.Say "Hello World"
17
+ end
18
+
7
19
  event :incoming_call, :to => :voicemail, :unless => :agents_available?
8
20
  event :incoming_call, :to => :routing, :if => :agents_available?
9
21
  event :something_crazy_happens, :to => :uh_oh
10
22
  end
11
23
 
12
24
  state :voicemail do
13
- event :customer_hangs_up, :to => :voicemail_completed
25
+ response do |x| # To allow defining render blocks outside a state
26
+ notify(:rendering_voicemail)
27
+ x.Say "Hello World"
28
+ x.Record :action => customer(:voicemail_complete)
29
+ end
30
+
31
+ customer :hangs_up, :to => :voicemail_completed
14
32
  end
15
33
 
16
34
  state :routing do
17
- event :customer_hangs_up, :to => :cancelled
35
+ customer :hangs_up, :to => :cancelled
18
36
  event :start_conference, :to => :in_conference
19
37
  end
20
38
 
21
39
  state :cancelled do
22
- event :customer_hangs_up, :to => same
23
- end
24
-
25
- # =================
26
- # = Render Blocks =
27
- # =================
40
+ after(:success, :uniq => true) { notify(:cancelled) }
28
41
 
29
- state :initial do
30
- on_render do |call, x| # To allow defining render blocks within a state
31
- call.notify(:rendering_initial)
32
- x.Say "Hello World"
33
- end
42
+ customer :hangs_up, :to => same
43
+ customer :end, :to => :ended
34
44
  end
35
45
 
36
- on_render(:voicemail) do |call, x| # To allow defining render blocks outside a state
37
- notify(:rendering_voicemail)
38
- x.Say "Hello World"
39
- x.Record :action => flow_url(:voicemail_complete)
46
+ response(:cancelled) do |x, call|
47
+ # Just for sake of comparison
40
48
  end
41
49
 
42
- on_flow_to(:cancelled) do |call, transition|
43
- notify(:cancelled)
50
+ state :ended do
51
+ after(:always) { notify(:after_always) }
52
+ after(:success) { notify(:after_success) }
53
+ after(:failure) { notify(:after_failure) }
54
+
55
+ after(:always, :uniq => true) { notify(:after_always_uniq) }
56
+ after(:success, :uniq => true) { |transition| notify(:after_success_uniq, transition) }
57
+ after(:failure, :uniq => true) { notify(:after_failure_uniq) }
58
+
59
+ before(:always) { notify(:before_always) }
60
+ before(:always, :uniq => true) { notify(:before_always_uniq) }
61
+
62
+ customer :end, :to => same
44
63
  end
45
64
  end
46
65
  end
@@ -4,7 +4,7 @@ class MultipleFlowCall
4
4
  call_flow :status, :initial => :ready do
5
5
  state :ready do
6
6
  event :go, :to => :done
7
- on_render do |call, x|
7
+ response do |x|
8
8
  x.Say "Hello World"
9
9
  end
10
10
  end
@@ -13,7 +13,7 @@ class MultipleFlowCall
13
13
  call_flow :outgoing_status, :initial => :outgoing_ready do
14
14
  state :outgoing_ready do
15
15
  event :outgoing_go, :to => :outgoing_done
16
- on_render do |call, x|
16
+ response do |x|
17
17
  x.Say "Hello Outgoing World"
18
18
  end
19
19
  end
@@ -4,7 +4,7 @@ class NonStandardCall
4
4
  call_flow :status, :initial => :ready do
5
5
  state :ready do
6
6
  event :go, :to => :done
7
- on_render do |call, x|
7
+ response do |x|
8
8
  x.Say "Hello World"
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: call_center
3
3
  version: !ruby/object:Gem::Version
4
- hash: 25
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 3
10
- version: 0.0.3
9
+ - 4
10
+ version: 0.0.4
11
11
  platform: ruby
12
12
  authors:
13
13
  - Henry Hsu
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-09-29 00:00:00 -07:00
18
+ date: 2011-10-23 00:00:00 -07:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -231,6 +231,7 @@ files:
231
231
  - init.rb
232
232
  - lib/call_center.rb
233
233
  - lib/call_center/core_ext/object_instance_exec.rb
234
+ - lib/call_center/flow_callback.rb
234
235
  - lib/call_center/state_machine_ext.rb
235
236
  - lib/call_center/test/dsl.rb
236
237
  - test/call_center_test.rb