Add a value object for mobile push working hours

What does this MR do and why?

Adds Gitlab::MobilePush::WorkingHours, a value object for a user's working hours: a daily window in minutes after local midnight, the weekdays it applies to and the IANA zone it is defined in.

  • deliver_now?(now) says whether the window is open. The end is exclusive, so 09:00 to 17:00 stops at 16:59. An end before the start means the window crosses midnight.
  • next_window_start(now) is the next opening in UTC, used later to schedule the summary push. On the fall-back DST overlap it pins the first occurrence.
  • It fails open: a schedule it cannot evaluate (unknown zone, missing time, start equal to end, no day) delivers rather than mutes.

Pure Ruby under fast_spec_helper. Nothing references it yet; the user preference that stores the schedule follows in the next MR.

References

Split from !260440 (closed), as agreed with the reviewer there: seven merge requests of 200 to 300 lines each, cut where the code does not reference the other parts. Each one targets its parent branch and is retargeted to master when the parent merges.

Merge request Targets
A Extract the mobile push recipient checks and the APNs fan-out master
B1 (this MR) Add a value object for mobile push working hours A
B2 Store mobile push working hours on the user preference B1
C1 Hold mobile pushes for to-dos outside working hours B2
C2a Add the held summary that resumes one user's mobile pushes C1
C2b Send the mobile push summaries when working hours begin C2a
D Add the mobile push working hours to the user preferences API C2b

Merge order is the chain order: A, B1, B2, C1, C2a, C2b, then D. D is the switch: it targets C2b, so the API cannot reach master before the server can hold a push and send its summary. Any other order would open a window on GitLab.com where users who enabled the schedule have their pushes held but not summarized until C2b deploys.

Screenshots or screen recordings

No UI changes.

How to set up and validate locally

In a Rails console:

schedule = Gitlab::MobilePush::WorkingHours.new(
  window: Gitlab::MobilePush::WorkingHours::Window.new(start_minute: 9 * 60, end_minute: 17 * 60),
  days: %w[monday tuesday wednesday thursday friday],
  zone: ActiveSupport::TimeZone['Europe/Berlin']
)
schedule.deliver_now?                                  # true during Berlin office hours on a weekday
schedule.next_window_start(Time.utc(2026, 10, 10, 12)) # Saturday noon => 2026-10-12 07:00:00 UTC, Monday 09:00 Berlin

MR acceptance checklist

Evaluated against the MR acceptance checklist.

🤖 Generated with Claude Code

Edited by Marcel van Remmerden

Merge request reports

Loading
Loading