call_center 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +81 -69
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/call_center.gemspec +3 -2
- data/lib/call_center.rb +22 -30
- data/lib/call_center/flow_callback.rb +69 -0
- data/lib/call_center/state_machine_ext.rb +83 -11
- data/test/call_center_test.rb +31 -16
- data/test/examples/call.rb +38 -19
- data/test/examples/multiple_flow_call.rb +2 -2
- data/test/examples/non_standard_call.rb +1 -1
- metadata +5 -4
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 => :
|
36
|
-
|
37
|
-
event
|
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 :
|
42
|
-
|
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
|
-
|
46
|
-
|
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
|
-
|
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.
|
62
|
-
@call.
|
63
|
-
@call.
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
115
|
+
<Say>This is Henry!</Say>
|
111
116
|
</Response>
|
112
117
|
|
113
118
|
Callbacks
|
114
119
|
---------
|
115
120
|
|
116
|
-
|
121
|
+
You have control over what you want to happen before/after state transitions:
|
117
122
|
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
136
|
+
For example,
|
129
137
|
|
130
|
-
|
131
|
-
|
132
|
-
state :answered do
|
133
|
-
...
|
134
|
-
end
|
138
|
+
state :voicemail do
|
139
|
+
before(:always) { log_start_event }
|
135
140
|
|
136
|
-
|
137
|
-
|
138
|
-
end
|
141
|
+
after(:always) { log_end_event }
|
142
|
+
after(:failure) { notify_airbrake }
|
139
143
|
|
140
|
-
|
141
|
-
|
142
|
-
end
|
144
|
+
after(:success, :uniq => true) { notify_browser }
|
145
|
+
after(:failure, :uniq => true) { notify_cleanup_browser }
|
143
146
|
end
|
144
|
-
...
|
145
147
|
|
146
|
-
|
148
|
+
Motivation
|
149
|
+
----------
|
147
150
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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.
|
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.
|
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-
|
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(
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
76
|
-
|
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
|
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 :
|
4
|
-
attr_accessor :
|
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
|
7
|
-
@
|
8
|
-
@
|
8
|
+
def response(state_name, &blk)
|
9
|
+
@response_blocks ||= {}
|
10
|
+
@response_blocks[state_name] = blk
|
9
11
|
end
|
10
12
|
|
11
|
-
def
|
12
|
-
@
|
13
|
-
@
|
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
|
71
|
+
def response(state_name = nil, &blk)
|
20
72
|
if @from_state
|
21
|
-
@queued_sends << [[:
|
73
|
+
@queued_sends << [[:response, @from_state], blk]
|
22
74
|
else
|
23
|
-
@queued_sends << [[:
|
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
|
data/test/call_center_test.rb
CHANGED
@@ -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
|
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
|
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=
|
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 "
|
128
|
-
@call.state = '
|
129
|
-
|
130
|
-
@call.
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
@call.
|
135
|
-
|
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(:
|
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, :
|
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
|
data/test/examples/call.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
23
|
-
end
|
24
|
-
|
25
|
-
# =================
|
26
|
-
# = Render Blocks =
|
27
|
-
# =================
|
40
|
+
after(:success, :uniq => true) { notify(:cancelled) }
|
28
41
|
|
29
|
-
|
30
|
-
|
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
|
-
|
37
|
-
|
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
|
-
|
43
|
-
notify(:
|
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
|
-
|
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
|
-
|
16
|
+
response do |x|
|
17
17
|
x.Say "Hello Outgoing World"
|
18
18
|
end
|
19
19
|
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:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
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-
|
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
|