Typical telephony systems process incoming call flows based on various time conditions, and customers often require control over these time ranges. The platform might also be a multi-tenant server hosting different virtual PBXes.
Common time-based call-handling scenarios include:
- Night: Calls are routed to voicemail.
- Morning, Lunchtime, Evening: Calls are forwarded to a mobile phone.
- Business Hours: Calls follow standard call-flow logic.
- Fridays: Working hours are shorter.
- Saturdays: The company operates only until lunchtime.
- Sundays: The company is closed; calls go to voicemail.
- Public Holidays: Calls are forwarded to voicemail.
Some time ago, I developed the following Ruby classes and ActiveRecord migrations for flexible business time management. I am sharing them now, as they could be beneficial for anyone implementing time-based conditions in backend applications, not limited to telephony.
The Item
class utilizes a polymorphic association that can point to any voice-related entity within a PBX (e.g., extension, voicemail, conference). This approach allows for the creation of highly flexible call flow building blocks. These blocks can be linked to various PBX entities, enabling customizable and dynamic call handling based on specific time conditions.
When the operational?
method is triggered for a given business-time collection, it returns the item_id
if the time conditions are met, or nil
otherwise (indicating, for example, that 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