Class: Insika::FollowupPolicy
- Inherits:
-
Object
- Object
- Insika::FollowupPolicy
- Defined in:
- lib/insika/followup_policy.rb
Overview
the parsed follow-up policy of ONE agent — the ONLY shape the engine accepts, shared by the schedule/cancel tools, the FollowupEngine, the doctor and the Studio. Pure value object: it never touches a store (D8 — the policy is pack data on the profile, no platform settings layer).
parse returns nil on a malformed hash (D9 — the firer BLOCKS, the doctor
explains, the tools refuse); parse! raises Insika::ValidationError naming
the exact defect.
Defined Under Namespace
Classes: QuietHours
Constant Summary collapse
- FREQUENCY_RE =
/\A(\d+)\/(\d+)(m|h|d)s?\z/- HH_MM_RE =
/\A\d{2}:\d{2}\z/- DEFAULT_ARM =
"schedule"- DEFAULT_SILENCE_AFTER_SENDS =
3- KEYWORD_MAX =
200
Instance Attribute Summary collapse
-
#arm ⇒ Object
readonly
Returns the value of attribute arm.
-
#cancel_keywords ⇒ Object
readonly
Returns the value of attribute cancel_keywords.
-
#max_frequency ⇒ Object
readonly
Returns the value of attribute max_frequency.
-
#quiet_hours ⇒ Object
readonly
Returns the value of attribute quiet_hours.
-
#silence_after_sends ⇒ Object
readonly
Returns the value of attribute silence_after_sends.
Class Method Summary collapse
Instance Method Summary collapse
-
#frequency_window ⇒ Object
"2/24h" -> { count: 2, seconds: 86_400 }.
-
#initialize(hash) ⇒ FollowupPolicy
constructor
A new instance of FollowupPolicy.
-
#match_keyword(text) ⇒ Object
The first cancel keyword matched (case/accent-insensitive substring); nil when none matches.
-
#quiet?(time) ⇒ Boolean
Is the given UTC Time inside quiet hours in the policy's timezone? (nil quiet_hours -> false: no quiet window.).
- #to_h ⇒ Object
Constructor Details
#initialize(hash) ⇒ FollowupPolicy
Returns a new instance of FollowupPolicy.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/insika/followup_policy.rb', line 33 def initialize(hash) raise Insika::ValidationError, "followup: declaration must be a Hash" unless hash.is_a?(Hash) h = hash.transform_keys(&:to_s) @arm = arm_of(h) policy = h["policy"] raise Insika::ValidationError, "followup.policy must be a Hash" unless policy.is_a?(Hash) policy = policy.transform_keys(&:to_s) @quiet_hours = quiet_hours_of(policy) @max_frequency = frequency_of(policy) @cancel_keywords = keywords_of(policy) @silence_after_sends = silence_of(policy) freeze end |
Instance Attribute Details
#arm ⇒ Object (readonly)
Returns the value of attribute arm.
21 22 23 |
# File 'lib/insika/followup_policy.rb', line 21 def arm @arm end |
#cancel_keywords ⇒ Object (readonly)
Returns the value of attribute cancel_keywords.
21 22 23 |
# File 'lib/insika/followup_policy.rb', line 21 def cancel_keywords @cancel_keywords end |
#max_frequency ⇒ Object (readonly)
Returns the value of attribute max_frequency.
21 22 23 |
# File 'lib/insika/followup_policy.rb', line 21 def max_frequency @max_frequency end |
#quiet_hours ⇒ Object (readonly)
Returns the value of attribute quiet_hours.
21 22 23 |
# File 'lib/insika/followup_policy.rb', line 21 def quiet_hours @quiet_hours end |
#silence_after_sends ⇒ Object (readonly)
Returns the value of attribute silence_after_sends.
21 22 23 |
# File 'lib/insika/followup_policy.rb', line 21 def silence_after_sends @silence_after_sends end |
Class Method Details
.parse(hash) ⇒ Object
23 24 25 26 27 |
# File 'lib/insika/followup_policy.rb', line 23 def self.parse(hash) new(hash) rescue Insika::ValidationError nil end |
.parse!(hash) ⇒ Object
29 30 31 |
# File 'lib/insika/followup_policy.rb', line 29 def self.parse!(hash) new(hash) end |
Instance Method Details
#frequency_window ⇒ Object
"2/24h" -> { count: 2, seconds: 86_400 }. nil when the policy has no
ceiling (absent max_frequency — the frequency gate is off). Always valid
once parsed.
count is the sends allowed per window; seconds is the WINDOW's
duration (the value after the "/"), never count*window.
54 55 56 57 58 59 60 |
# File 'lib/insika/followup_policy.rb', line 54 def frequency_window return nil if @max_frequency.nil? match = FREQUENCY_RE.match(@max_frequency) multiplier = { "m" => 60, "h" => 3600, "d" => 86_400 }.fetch(match[3]) { count: match[1].to_i, seconds: match[2].to_i * multiplier } end |
#match_keyword(text) ⇒ Object
The first cancel keyword matched (case/accent-insensitive substring); nil when none matches.
87 88 89 90 91 92 93 |
# File 'lib/insika/followup_policy.rb', line 87 def match_keyword(text) folded = fold(text) @cancel_keywords.each do |kw| return kw if folded.include?(fold(kw)) end nil end |
#quiet?(time) ⇒ Boolean
Is the given UTC Time inside quiet hours in the policy's timezone? (nil quiet_hours -> false: no quiet window.)
IANA names are resolved through the OS tz database by pointing Ruby's
TZ at the zone for the computation (Ruby stdlib's Time#getlocal
only takes an offset, not a zone name). Save/restore keeps the global
intact; under the engine's cooperative fiber model — no IO between the
save and the restore — the mutation is atomic on the calling fiber.
70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/insika/followup_policy.rb', line 70 def quiet?(time) return false unless @quiet_hours local = in_zone(@quiet_hours.timezone, time) { |t| t.getlocal } minutes = local.hour * 60 + local.min start_min = minutes_of(@quiet_hours.start) end_min = minutes_of(@quiet_hours.end) if start_min <= end_min minutes >= start_min && minutes < end_min else # an overnight window: 21:30-09:00 minutes >= start_min || minutes < end_min end end |
#to_h ⇒ Object
95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/insika/followup_policy.rb', line 95 def to_h { "arm" => @arm, "policy" => { "quiet_hours" => @quiet_hours && { "timezone" => @quiet_hours.timezone, "start" => @quiet_hours.start, "end" => @quiet_hours.end }, "max_frequency" => @max_frequency, "cancel_keywords" => @cancel_keywords, "silence_after_sends" => @silence_after_sends }.compact } end |