The typical telephony system processes incoming call-flow based on different times, and customers want to have control over this and handle time-ranges. The whole platform can be a multi-tenant server with different virtual PBXes.
Here are the different call-handling scenarios based on time:
- During the night, calls should be delivered to the voicemail.
- In the morning, lunchtime, and evening, calls should be forwarded to the cell phone.
- During business hours, calls should be processed following standard call-flow logic.
- On Fridays, the working hours are shorter.
- On Saturdays, the company works only until lunchtime.
- On Sundays, the company is closed, and calls should be routed to the voicemail.
- Calls on public holidays should also be forwarded to the voicemail.
A long time ago, I created the following Ruby classes and ActiveRecord migrations for flexible business time management. I am now sharing this, as it could be useful for anyone dealing with time conditions in backend applications, not just telephony-related ones.
The “Item” class is a polymorphic association that points to any voice-related entity in a PBX. By reusing polymorphic database associations, it is possible to create very flexible call flow building blocks. These blocks can point to any voice-related entity in a PBX, such as an extension, voicemail, conference, etc. This allows for customizable and dynamic call handling based on time conditions.
If we trigger the “operational?” method for a given business-time collection, it should return the “item_id” or nil if the time conditions are not met (i.e., the office is closed).
Tests
1.9.3-p327 :001 > Campaign.last.business_time.operational?
Campaign Load (0.1ms) SELECT "campaigns".* FROM "campaigns" ORDER BY "campaigns"."id" DESC LIMIT 1
BusinessTime Load (0.2ms) SELECT "business_times".* FROM "business_times" WHERE "business_times"."id" = 1 LIMIT 1
BusinessTimeMember Load (0.3ms) SELECT "business_time_members".* FROM "business_time_members" WHERE "business_time_members"."business_time_id" = 1 ORDER BY
business_time_id,
day_of_month desc,
month desc,
year desc,
weekday desc,
time_from desc,
time_to asc
=> 0
Migrations
create_table :business_time_members do |t|
t.string :name, :limit => 100
t.integer :business_time_id
t.time :time_from, :default => '2000-01-01 00:00:00'
t.time :time_to, :default => '2000-01-01 23:59:59'
t.integer :weekday # 0 - sunday, 1 - monday, 2 -tuesday, ...
t.integer :day_of_month # 1-31
t.integer :month # 1-12
t.integer :year # 2012
t.integer :item_id, :default => 0 # item with action!
t.timestamps
end
create_table :business_times do |t|
t.string :name, :limit => 100
t.timestamps
end
execute "INSERT INTO `business_times` (`id`, `name`)
VALUES (1, 'Call Mon-Fri 9 to 21; Sat, Sun 11 to 19')"
# Always closed!
# It is a default condition with lowest priority and will be executed it other conditions did not met.
# BusinessTimeMember.create :business_time => business_time, :item => closed_action
execute "INSERT INTO `business_time_members` (`id`, `business_time_id`, `item_id`) VALUES (?, 1, NULL)"
# Make calls on business days (Monday to Friday) from 9:00 to 21:00,
# on weekends (Sunday and Saturday) from 11:00 to 19:00
# 0 - Sun, 6 - Sat
0.upto(6) do |day|
time_from, time_to = if day == 0 || day == 6 then
#[Time.parse("11:00:00"), Time.parse("18:59:59")]
[Time.parse("00:00:00"), Time.parse("23:59:59")]
else
#[Time.parse("9:00:00"), Time.parse("20:59:59")]
[Time.parse("00:00:00"), Time.parse("23:59:59")]
end
# item_id 0 = continue dialplan!
# BusinessTimeMember.create :business_time => business_time,
# :item_id => 0, :weekday => day,
# :time_from => "6:00", :time_to => "17:00"
execute "INSERT INTO `business_time_members`
(`id`, `business_time_id`, `weekday`, `time_from`, `time_to`, `item_id`)
VALUES (?, 1, #{day}, '#{time_from}', '#{time_to}', 0)"
end
Models
class BusinessTimeMember < ActiveRecord::Base
#validations
validates_presence_of :business_time_id
validates_presence_of :item_id
# 0 (zero) means to continue dialplan after check!
validates_numericality_of :item_id #, :greater_than => 0
# relations
belongs_to :business_time
belongs_to :item
end
class BusinessTime < ActiveRecord::Base
has_many :business_time_members, :dependent => :destroy
has_many :items, :as => :routable
has_many :soho_pbxes
# return false if no records found,
# 0 if specified - means "continue" dialplan,
# or item_id
def operational?(details = { :datetime => Time.now.utc })
d = details[:datetime]
begin
if self.timezone
begin
tz = TZInfo::Timezone.get(self.timezone)
local = tz.utc_to_local(d)
puts "got timezone #{self.timezone} - utc #{d}, local #{local}"
d = local
rescue
puts "got exception for timezone #{self.timezone}"
end
end
end
tm = BusinessTimeMember.find(
:all,
:conditions => { :business_time_id => id },
:order => "
business_time_id,
day_of_month desc,
month desc,
year desc,
weekday desc,
time_from desc,
time_to asc
"
)
return false if tm.empty?
tm.each { |t|
tf = t[:time_from] ? Time.parse(t[:time_from].strftime("%H:%M:%S")) : Time.parse("00:00:00")
tt = t[:time_to] ? Time.parse(t[:time_to].strftime("%H:%M:%S")) : Time.parse("23:59:59")
tw = t[:weekday] ? t[:weekday].to_i : d.wday
td = t[:day_of_month] ? t[:day_of_month].to_i : d.day
tm = t[:month] ? t[:month].to_i : d.month
ty = t[:year] ? t[:year].to_i : d.year
if ty == d.year && tm == d.month && td == d.day && tw == d.wday && d >= tf && d <= tt then
return t.item_id
end
}
return false
end
end