Defer weekend document reminders to Monday

Reminders that fall due on Saturday or Sunday no longer send
over the weekend or spend the escalation ladder unused. SQL
keeps calendar cadence and rolls weekend due times to Monday
at the same clock hour. The first debounced notice is unchanged.

Co-authored-by: Sacha Al Himdani <sacha@probo.com>
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
Cursor Agent
2026-07-30 14:04:37 +00:00
parent eaff3c9dda
commit a26a4ad898
4 changed files with 88 additions and 8 deletions

View File

@@ -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
`

View File

@@ -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
`

View File

@@ -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{

View File

@@ -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"`
}