diff --git a/pkg/coredata/document_version_approval_decision.go b/pkg/coredata/document_version_approval_decision.go index 984526fb2..12747b470 100644 --- a/pkg/coredata/document_version_approval_decision.go +++ b/pkg/coredata/document_version_approval_decision.go @@ -420,6 +420,7 @@ func (d *DocumentVersionApprovalDecisions) LoadNextDueGroupForNotification( debounceBefore time.Time, reminderInterval time.Duration, ) error { + // Reminders skip Sat/Sun; a weekend due time waits until Monday same UTC hour. q := ` WITH next_group AS ( SELECT @@ -432,7 +433,25 @@ WITH next_group AS ( AND notification_count < @max_notifications AND ( (notification_count = 0 AND created_at < @debounce_before) - OR (notification_count > 0 AND last_notified_at < @now::timestamptz - make_interval(secs => @reminder_interval_seconds * notification_count)) + OR ( + notification_count > 0 + AND EXTRACT(ISODOW FROM (@now::timestamptz AT TIME ZONE 'UTC')) BETWEEN 1 AND 5 + AND @now::timestamptz > ( + SELECT + (due_at_utc + CASE EXTRACT(ISODOW FROM due_at_utc) + WHEN 6 THEN interval '2 days' + WHEN 7 THEN interval '1 day' + ELSE interval '0' + END) AT TIME ZONE 'UTC' + FROM ( + SELECT + ( + last_notified_at + + make_interval(secs => @reminder_interval_seconds * notification_count) + ) AT TIME ZONE 'UTC' AS due_at_utc + ) AS reminder + ) + ) ) AND EXISTS ( SELECT 1 @@ -531,6 +550,7 @@ func (d DocumentVersionApprovalDecisions) ClaimForNotification( ids[i] = decision.ID } + // Reminders skip Sat/Sun; a weekend due time waits until Monday same UTC hour. q := ` UPDATE document_version_approval_decisions SET @@ -542,7 +562,25 @@ WHERE AND notification_count < @max_notifications AND ( (notification_count = 0 AND created_at < @debounce_before) - OR (notification_count > 0 AND last_notified_at < @now::timestamptz - make_interval(secs => @reminder_interval_seconds * notification_count)) + OR ( + notification_count > 0 + AND EXTRACT(ISODOW FROM (@now::timestamptz AT TIME ZONE 'UTC')) BETWEEN 1 AND 5 + AND @now::timestamptz > ( + SELECT + (due_at_utc + CASE EXTRACT(ISODOW FROM due_at_utc) + WHEN 6 THEN interval '2 days' + WHEN 7 THEN interval '1 day' + ELSE interval '0' + END) AT TIME ZONE 'UTC' + FROM ( + SELECT + ( + last_notified_at + + make_interval(secs => @reminder_interval_seconds * notification_count) + ) AT TIME ZONE 'UTC' AS due_at_utc + ) AS reminder + ) + ) ) RETURNING id ` diff --git a/pkg/coredata/document_version_signature.go b/pkg/coredata/document_version_signature.go index 8fd3b06a4..dbc94f7e7 100644 --- a/pkg/coredata/document_version_signature.go +++ b/pkg/coredata/document_version_signature.go @@ -412,7 +412,9 @@ WHERE // request gets: the first notice plus three reminders. A request is due for its // next email once it is past its scheduled offset — the first email after the // debounce delay, then reminders at 1x, 2x and 3x the reminder interval after -// the previous email — and stops once it reaches this cap. +// the previous email — and stops once it reaches this cap. Reminder sends that +// fall on Saturday or Sunday are deferred to Monday at the same clock time; +// the first debounced notice is unchanged. const documentNotificationMaxCount = 4 func remainingNotificationIDs(all []gid.GID, claimed []gid.GID) []gid.GID { @@ -445,6 +447,7 @@ func (pvss *DocumentVersionSignatures) LoadNextDueGroupForNotification( debounceBefore time.Time, reminderInterval time.Duration, ) error { + // Reminders skip Sat/Sun; a weekend due time waits until Monday same UTC hour. q := ` WITH next_group AS ( SELECT @@ -457,7 +460,25 @@ WITH next_group AS ( AND notification_count < @max_notifications AND ( (notification_count = 0 AND requested_at < @debounce_before) - OR (notification_count > 0 AND last_notified_at < @now::timestamptz - make_interval(secs => @reminder_interval_seconds * notification_count)) + OR ( + notification_count > 0 + AND EXTRACT(ISODOW FROM (@now::timestamptz AT TIME ZONE 'UTC')) BETWEEN 1 AND 5 + AND @now::timestamptz > ( + SELECT + (due_at_utc + CASE EXTRACT(ISODOW FROM due_at_utc) + WHEN 6 THEN interval '2 days' + WHEN 7 THEN interval '1 day' + ELSE interval '0' + END) AT TIME ZONE 'UTC' + FROM ( + SELECT + ( + last_notified_at + + make_interval(secs => @reminder_interval_seconds * notification_count) + ) AT TIME ZONE 'UTC' AS due_at_utc + ) AS reminder + ) + ) ) AND EXISTS ( SELECT 1 @@ -554,6 +575,7 @@ func (pvss DocumentVersionSignatures) ClaimForNotification( ids[i] = signature.ID } + // Reminders skip Sat/Sun; a weekend due time waits until Monday same UTC hour. q := ` UPDATE document_version_signatures SET @@ -565,7 +587,25 @@ WHERE AND notification_count < @max_notifications AND ( (notification_count = 0 AND requested_at < @debounce_before) - OR (notification_count > 0 AND last_notified_at < @now::timestamptz - make_interval(secs => @reminder_interval_seconds * notification_count)) + OR ( + notification_count > 0 + AND EXTRACT(ISODOW FROM (@now::timestamptz AT TIME ZONE 'UTC')) BETWEEN 1 AND 5 + AND @now::timestamptz > ( + SELECT + (due_at_utc + CASE EXTRACT(ISODOW FROM due_at_utc) + WHEN 6 THEN interval '2 days' + WHEN 7 THEN interval '1 day' + ELSE interval '0' + END) AT TIME ZONE 'UTC' + FROM ( + SELECT + ( + last_notified_at + + make_interval(secs => @reminder_interval_seconds * notification_count) + ) AT TIME ZONE 'UTC' AS due_at_utc + ) AS reminder + ) + ) ) RETURNING id ` diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index 51828dc74..1ea6f2142 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -179,7 +179,7 @@ func New() *Implm { Document: DocumentNotificationConfig{ Interval: 300, // 5 minutes DebounceDelay: 900, // 15 minutes - ReminderInterval: 86400, // 1 day base cadence (1x, 2x, 3x) + ReminderInterval: 86400, // 1 day base cadence (1x, 2x, 3x; weekend reminders → Monday) }, }, CustomDomains: CustomDomainsConfig{ diff --git a/pkg/probodconfig/notifications_config.go b/pkg/probodconfig/notifications_config.go index 4523edd4c..d1181ebaf 100644 --- a/pkg/probodconfig/notifications_config.go +++ b/pkg/probodconfig/notifications_config.go @@ -40,7 +40,9 @@ type DocumentNotificationConfig struct { // DebounceDelay is how long a request must have been pending before its // first notification is sent. DebounceDelay int `json:"debounce-delay"` - // ReminderInterval is the base reminder cadence. Reminders are sent at - // 1x, 2x and 3x this interval after the previous email, then stop. + // ReminderInterval is the base reminder cadence in seconds. Reminders are + // sent at 1x, 2x and 3x this interval after the previous email, then stop. + // Reminder sends that fall on Saturday or Sunday are deferred to Monday at + // the same clock time; the first (debounced) notice is unchanged. ReminderInterval int `json:"reminder-interval"` }