Typical telephony system process incoming call-flow based on different times (and of course customer do want to have control on that, handle time-ranges; also whole platform could be an multi-tenant server with different virtual PBX’es):
-
During night call should be delivered to the voicemail;
-
In the morning, lunch time and evening should be forwarded to the cell phone;
-
During business hours, call should be processed following standard call-flow logic;
-
Friday’s time is shorter;
-
at Saturday company works only until lunch time;
-
and it’s closed at Sunday (calls should be routed to the voicemail);
-
Calls on public holidays should be forwarded to the voicemail too.
Long time ago I’ve created an following Ruby classes and ActiveRecord migrations for flexible business times management. And now sharing this, it could be useful for anyone who deal with time conditions in the backend apps (and that’s not only telephony-related).
Item
is and kind polymorphic association that points to any voice-related “thing” in a PBX. I.e. to the extension, or voicemail, conference, whatever…
If we would trigger method operational?
for given business-time collection, it should return “item_id” or nil if time conditions did not met (office 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