fix: 'Pause sending by' is off by default everywhere

The optional 'Pause sending by' deadline was defaulting to 18 (= 6 PM)
in three places:
  - reminders.delivery_window_end_hour schema default (NOT NULL DEFAULT 18)
  - createReminderAction / editScheduleAction fallback when the field
    is missing on the input
  - the Zod refine validator's secondary fallback

Net effect: any reminder created before this change has 18 in the DB,
so the edit form's checkbox flips ON automatically (the wizard treats
'value !== undefined && value !== 24' as 'opted in'). The wizard's
own create flow always sends 24 explicitly when the box is unchecked
— but legacy / direct API payloads + the schema default for older rows
don't carry that intent through.

Switch every default to 24 (the off-sentinel the wizard already uses)
so the optional toggle stays off until the operator ticks it. New
migration 0012 also backfills existing rows from 18 → 24 so editing
old reminders no longer auto-checks 'Pause sending by'.

Tests in when-form-deadline.test.tsx already lock in the UI contract
(off when initialDeliveryEndHour is undefined or 24, on for any other
value). No assertion changes needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yiekheng 2026-05-10 19:30:09 +08:00
parent 5c48e0e85f
commit e800882d15
5 changed files with 1170 additions and 92 deletions

View File

@ -271,7 +271,7 @@ const createReminderSchema = z
path: ["messages"],
},
)
.refine((d) => (d.deliveryWindowStartHour ?? 6) < (d.deliveryWindowEndHour ?? 18), {
.refine((d) => (d.deliveryWindowStartHour ?? 6) < (d.deliveryWindowEndHour ?? 24), {
message: "Delivery window start must be earlier than end",
path: ["deliveryWindowStartHour"],
});
@ -328,7 +328,11 @@ export async function createReminderAction(
timezone,
} = parsed.data;
const deliveryWindowStartHour = parsed.data.deliveryWindowStartHour ?? 6;
const deliveryWindowEndHour = parsed.data.deliveryWindowEndHour ?? 18;
// 24 = "no deadline" (off). The wizard sends 24 explicitly when the
// operator hasn't ticked the optional "Pause sending by" checkbox;
// fall back to 24 here so legacy payloads / direct API calls don't
// accidentally enable the deadline at 6pm.
const deliveryWindowEndHour = parsed.data.deliveryWindowEndHour ?? 24;
const parts = resolveMessageParts(parsed.data);
const op = await getSeededOperator();
@ -442,7 +446,11 @@ export async function updateReminderAction(
timezone,
} = parsed.data;
const deliveryWindowStartHour = parsed.data.deliveryWindowStartHour ?? 6;
const deliveryWindowEndHour = parsed.data.deliveryWindowEndHour ?? 18;
// 24 = "no deadline" (off). The wizard sends 24 explicitly when the
// operator hasn't ticked the optional "Pause sending by" checkbox;
// fall back to 24 here so legacy payloads / direct API calls don't
// accidentally enable the deadline at 6pm.
const deliveryWindowEndHour = parsed.data.deliveryWindowEndHour ?? 24;
const parts = resolveMessageParts(parsed.data);
const op = await getSeededOperator();

View File

@ -0,0 +1,10 @@
-- Switch the default to 24 ("no deadline" sentinel) so newly-created
-- reminders are off-by-default for the optional "Pause sending by"
-- toggle, matching the wizard's UX contract.
ALTER TABLE "reminders" ALTER COLUMN "delivery_window_end_hour" SET DEFAULT 24;
-- Existing rows still hold the old default (18). Treat those as
-- "schema-default, never opted in by the operator" and clear them to
-- 24 so editing an old reminder doesn't auto-check the deadline box.
-- Operators who actually wanted a 6pm deadline can re-enable it from
-- the edit form.
UPDATE "reminders" SET "delivery_window_end_hour" = 24 WHERE "delivery_window_end_hour" = 18;

File diff suppressed because it is too large Load Diff

View File

@ -85,6 +85,13 @@
"when": 1778464002000,
"tag": "0011_premium_grandmaster",
"breakpoints": true
},
{
"idx": 12,
"version": "7",
"when": 1778412502601,
"tag": "0012_lucky_masked_marvel",
"breakpoints": true
}
]
}

View File

@ -92,8 +92,11 @@ export const reminders = pgTable("reminders", {
// Delivery window (operator timezone). End hour is enforced at runtime
// by fire-reminder when window enforcement lands; start hour is documented
// here but not gated in v1.
// 24 is the "no deadline" sentinel — it's the off-by-default state so a
// reminder created without the operator explicitly opting into "Pause
// sending by" stays unbounded.
deliveryWindowStartHour: integer("delivery_window_start_hour").notNull().default(6),
deliveryWindowEndHour: integer("delivery_window_end_hour").notNull().default(18),
deliveryWindowEndHour: integer("delivery_window_end_hour").notNull().default(24),
});
export const reminderTargets = pgTable(