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 +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
|