diff --git a/internal/models/notification_settings.go b/internal/models/notification_settings.go index df66ec837..972421e93 100644 --- a/internal/models/notification_settings.go +++ b/internal/models/notification_settings.go @@ -1,8 +1,8 @@ package models // NotificationSettings are a person's recipient-side push filters (TASK-3937). -// The dispatcher (TASK-3939) is to apply them; the enqueue does not, so -// changing one never rewrites the queue. +// The dispatcher (TASK-3939) is to apply them and the enqueue (TASK-3937 +// part B) is not to, so changing one will never rewrite the queue. type NotificationSettings struct { PushEnabled bool `json:"push_enabled"` NotifyAssigned bool `json:"notify_assigned"` diff --git a/internal/store/migrated.go b/internal/store/migrated.go index eb94c56ed..dc8fadc1a 100644 --- a/internal/store/migrated.go +++ b/internal/store/migrated.go @@ -114,9 +114,9 @@ const MigratedMarker = "PAD_MIGRATED_TO_POSTGRES" // database. Refusing them would claim a loss the migration never promised // to prevent. // - PUSH DELIVERY STATE (push_devices, user_notifications; TASK-3937): -// a device row is bound to the session that registered it, and the -// migration does not carry sessions, so every device is re-registered -// after the operator signs in again; the queue is delivery state, not a +// a device row is bound to the session that registers it (TASK-3938), +// and the migration does not carry sessions, so every device is to +// re-register after the operator signs in again; the queue is delivery state, not a // person's data: a lost row is one push not sent. (Its dispatcher and // 7-day sweep land with TASK-3939; until then nothing reads or deletes // it.) Both are excluded as "push-delivery" in migratedExcludedTables, @@ -282,8 +282,8 @@ var migratedExcludedTables = map[string]string{ "sqlite_sequence": "bookkeeping", // PUSH DELIVERY STATE (TASK-3937): a device row is bound to a session the - // migration does not carry, so it is re-registered after sign-in; a queue - // row lost is one push not sent. user_notification_settings is refused. + // migration does not carry, so it is to re-register after sign-in + // (TASK-3938); a queue row lost is one push not sent. user_notification_settings is refused. "push_devices": "push-delivery", "user_notifications": "push-delivery", diff --git a/internal/store/migrations/155_push_notifications.sql b/internal/store/migrations/155_push_notifications.sql index 70d29ade3..d644c4f64 100644 --- a/internal/store/migrations/155_push_notifications.sql +++ b/internal/store/migrations/155_push_notifications.sql @@ -1,16 +1,18 @@ -- Migration 155: push notifications (TASK-3937, PLAN-3908 phase 2 unit 1). -- --- push_devices: one row per signed-in app session that registered a push --- token. Every token is an FCM registration token (iOS routes through FCM), +-- push_devices: one row per signed-in app session that registers a push +-- token (the register endpoint lands with TASK-3938; nothing writes this +-- table yet). Every token is an FCM registration token (iOS routes through FCM), -- so there is no environment column: FCM picks the APNs gateway itself. -- platform stays because the FCM payload differs per platform. The row dies -- with its session or its user. -- --- user_notifications: the per-event push queue. A row is written ONLY for a --- user who has at least one device, on the transaction of the write that --- caused it. dedupe_key makes the enqueue idempotent per (user, key). item_id --- and comment_id carry no foreign key: the dispatcher skips a row whose --- subject is gone, and retention removes the rest. Retention is NOT in this +-- user_notifications: the per-event push queue. Nothing writes it yet: the +-- enqueue is TASK-3937 part B, and it is to write a row ONLY for a user who +-- has at least one device, on the transaction of the write that caused it. +-- dedupe_key is what makes that enqueue idempotent per (user, key). item_id +-- and comment_id carry no foreign key: the dispatcher is to skip a row whose +-- subject is gone, and retention to remove the rest. Retention is NOT in this -- migration: the 7-day sweep lands with the dispatcher (TASK-3939), and until -- then nothing deletes a queue row. workspace_id cascades, so a purged -- workspace leaves no queue behind. actor_user_id is indexed because deleting @@ -18,7 +20,7 @@ -- -- user_notification_settings: a person's recipient-side filters. No row means -- the defaults. The dispatcher (TASK-3939) is to apply them; the enqueue --- does not. +-- (part B) is not to. -- -- workspace_read_cursors (149) stays separate: it must exist for every user, -- with or without a device, and this queue is to be deleted after 7 days. diff --git a/internal/store/nulcolumns.go b/internal/store/nulcolumns.go index 02395f4cb..eae8bd7c2 100644 --- a/internal/store/nulcolumns.go +++ b/internal/store/nulcolumns.go @@ -303,7 +303,8 @@ var nulColumns = []nulColumn{ {"item_verifications", "created_by", classText}, {"item_verifications", "agent_name", classText}, - // push_devices (TASK-3937): all three arrive in a registration request body. + // push_devices (TASK-3937): all three are to arrive in a registration + // request body, whose endpoint lands with TASK-3938. {"push_devices", "connection_id", classText}, {"push_devices", "token", classText}, {"push_devices", "app_version", classText}, @@ -433,27 +434,34 @@ func NULProtectedColumns() []nulColumn { // column someone looked at and excluded" — without it, the test's only options // on an unlisted column are to fail on every known exclusion or to ignore the // class entirely. +// +// The push_devices and user_notifications reasons (TASK-3937) state the +// CONTRACT their writers must keep, and none of those writers exists yet: the +// device register endpoint is TASK-3938, the enqueue is TASK-3937 part B, and +// the dispatcher's claim and send columns are TASK-3939. Each names the unit +// that owes it. user_notification_settings is written today, by +// PutNotificationSettings. var nulExcluded = map[string]string{ - "push_devices.id": "server-minted device UUID (TASK-3937, migration 155)", - "push_devices.user_id": "the authenticated user's server-minted UUID (TASK-3937)", - "push_devices.session_id": "server-minted session id read from the authenticated session (TASK-3937)", - "push_devices.platform": "fixed enum ios | android, a CHECK constraint (TASK-3937)", - "push_devices.created_at": "server timestamp from now() (TASK-3937)", - "push_devices.updated_at": "server timestamp from now() (TASK-3937)", - "user_notifications.id": "server-minted notification UUID (TASK-3937, migration 155)", - "user_notifications.user_id": "server-minted user UUID read from items and comments rows (TASK-3937)", - "user_notifications.workspace_id": "server-minted workspace UUID from the written row (TASK-3937)", - "user_notifications.item_id": "server-minted item UUID from the written row (TASK-3937)", - "user_notifications.comment_id": "server-minted comment UUID from the written row (TASK-3937)", - "user_notifications.kind": "fixed server constant, a CHECK constraint (TASK-3937)", - "user_notifications.actor_user_id": "server-minted user UUID of the authenticated writer (TASK-3937)", - "user_notifications.dedupe_key": "server-built from a fixed kind and server-minted ids and timestamps (TASK-3937)", - "user_notifications.created_at": "server timestamp from now() (TASK-3937)", - "user_notifications.push_state": "fixed server constant, a CHECK constraint (TASK-3937)", + "push_devices.id": "server-minted device UUID (TASK-3938, migration 155)", + "push_devices.user_id": "the authenticated user's server-minted UUID (TASK-3938)", + "push_devices.session_id": "server-minted session id read from the authenticated session (TASK-3938)", + "push_devices.platform": "fixed enum ios | android, a CHECK constraint (TASK-3938)", + "push_devices.created_at": "server timestamp from now() (TASK-3938)", + "push_devices.updated_at": "server timestamp from now() (TASK-3938)", + "user_notifications.id": "server-minted notification UUID (TASK-3937 part B, migration 155)", + "user_notifications.user_id": "server-minted user UUID read from items and comments rows (TASK-3937 part B)", + "user_notifications.workspace_id": "server-minted workspace UUID from the written row (TASK-3937 part B)", + "user_notifications.item_id": "server-minted item UUID from the written row (TASK-3937 part B)", + "user_notifications.comment_id": "server-minted comment UUID from the written row (TASK-3937 part B)", + "user_notifications.kind": "fixed server constant, a CHECK constraint (TASK-3937 part B)", + "user_notifications.actor_user_id": "server-minted user UUID of the authenticated writer (TASK-3937 part B)", + "user_notifications.dedupe_key": "server-built from a fixed kind and server-minted ids and timestamps (TASK-3937 part B)", + "user_notifications.created_at": "server timestamp from now() (TASK-3937 part B)", + "user_notifications.push_state": "fixed server constant, a CHECK constraint (TASK-3939)", "user_notifications.skip_reason": "fixed server constant set by the dispatcher (TASK-3939)", - "user_notifications.claim_token": "server-minted UUID (TASK-3937)", - "user_notifications.claimed_at": "server timestamp from now() (TASK-3937)", - "user_notifications.sent_at": "server timestamp from now() (TASK-3937)", + "user_notifications.claim_token": "server-minted UUID (TASK-3939)", + "user_notifications.claimed_at": "server timestamp from now() (TASK-3939)", + "user_notifications.sent_at": "server timestamp from now() (TASK-3939)", "user_notification_settings.user_id": "the authenticated user's server-minted UUID (TASK-3937, migration 155)", "user_notification_settings.updated_at": "server timestamp from now() (TASK-3937)", "workspace_import_status.workspace_id": "server-minted workspace UUID, a foreign key to workspaces(id) (TASK-896, migration 131)",