# frozen_string_literal: true # Business-hours decision: Mon-Fri 08:00-16:00 Europe/Prague, excluding Czech public holidays. # This is the authoritative backend check (the device's util/BusinessHours.java is only a local # reflex when the network is down). Pure functions over a supplied local Time — deterministic + testable. # Note: prague_now uses a TZ-env trick for DST; a tz library would be sturdier if this grows. require "date" module Helpdesk module BusinessHours OPEN_HOUR = 8 CLOSE_HOUR = 16 # closed at 16:00 sharp module_function # `local` must be a Time already in Europe/Prague. def open?(local) return false if weekend?(local) return false if holiday?(local.to_date) local.hour >= OPEN_HOUR && local.hour < CLOSE_HOUR end def weekend?(local) = [0, 6].include?(local.wday) # Sun=0, Sat=6 # Czech public holidays (fixed) + movable Good Friday & Easter Monday. def holiday?(date) y = date.year fixed = [[1, 1], [5, 1], [5, 8], [7, 5], [7, 6], [9, 28], [10, 28], [11, 17], [12, 24], [12, 25], [12, 26]] return true if fixed.include?([date.month, date.day]) easter = easter_date(y) date == (easter - 2) || date == (easter + 1) # Good Friday, Easter Monday end # Anonymous Gregorian algorithm. def easter_date(y) a = y % 19; b = y / 100; c = y % 100; d = b / 4; e = b % 4 f = (b + 8) / 25; g = (b - f + 1) / 3 h = (19 * a + b - d - g + 15) % 30 i = c / 4; k = c % 4 l = (32 + 2 * e + 2 * i - h - k) % 7 m = (a + 11 * h + 22 * l) / 451 month = (h + l - 7 * m + 114) / 31 day = ((h + l - 7 * m + 114) % 31) + 1 Date.new(y, month, day) end # Prague "now" without a tz gem AND without mutating the process-global ENV["TZ"] (the old # `ENV["TZ"]=` trick raced across WEBrick request threads: concurrent /status polls could # permanently leak TZ, and libc setenv isn't thread-safe against getenv in other threads). Instead # apply the Europe/Prague UTC offset (CET/CEST) directly to a UTC clock — pure, thread-safe. def prague_now utc = Time.now.utc utc.getlocal(prague_offset(utc)) end # "+02:00" during EU summer time (CEST), else "+01:00" (CET). EU DST runs from 01:00 UTC on the # last Sunday of March to 01:00 UTC on the last Sunday of October. def prague_offset(utc) y = utc.year dst_start = Time.utc(y, 3, last_sunday(y, 3).day, 1) dst_end = Time.utc(y, 10, last_sunday(y, 10).day, 1) (utc >= dst_start && utc < dst_end) ? "+02:00" : "+01:00" end def last_sunday(y, month) d = Date.new(y, month, -1) # last day of the month d - d.wday # step back to Sunday (Date#wday: Sunday=0) end # Out-of-hours SMS text (Czech). OUT_OF_HOURS_SMS = "Mimo pracovní dobu (Po–Pá 8–16). Zavolejte prosím zítra v 8:00. Děkujeme." end end