diff --git a/src/lib/notificationContract.ts b/src/lib/notificationContract.ts index 7a4bc456..68196fdb 100644 --- a/src/lib/notificationContract.ts +++ b/src/lib/notificationContract.ts @@ -1,3 +1,9 @@ +/** Payload `req.context` flag — domain hooks skip creating notifications when true. */ +export const SKIP_CAMPAIGN_NOTIFICATION_CONTEXT_KEY = 'skipCampaignNotification' + +/** Opt back into hook-fired notifications inside Vitest (default: skipped). */ +export const ENABLE_CAMPAIGN_NOTIFICATION_CONTEXT_KEY = 'enableCampaignNotification' + export const notificationTypes = [ 'municipality_update', 'new_supporter', diff --git a/src/utilities/notification/notificationEvents.ts b/src/utilities/notification/notificationEvents.ts index 218c2efe..7ca8743e 100644 --- a/src/utilities/notification/notificationEvents.ts +++ b/src/utilities/notification/notificationEvents.ts @@ -4,6 +4,10 @@ import 'server-only' import type { PayloadRequest } from 'payload' +import { + ENABLE_CAMPAIGN_NOTIFICATION_CONTEXT_KEY, + SKIP_CAMPAIGN_NOTIFICATION_CONTEXT_KEY, +} from '@/lib/notificationContract' import { relationshipId, uniqueRelationshipIds } from '@/lib/relationship' import { municipalityUpdateKindLabels } from '@/lib/schemas/municipalityUpdate' import { createCampaignNotifications } from '@/utilities/notification/createCampaignNotification' @@ -11,10 +15,13 @@ import { resolveMunicipalityStaffRecipientIds } from '@/utilities/notification/n export type NotificationEventRequest = Pick -const SKIP_NOTIFICATION_CONTEXT = 'skipCampaignNotification' - -const shouldSkipCampaignNotification = (context: Record): boolean => - Boolean(context[SKIP_NOTIFICATION_CONTEXT]) +const shouldSkipCampaignNotification = (context: Record): boolean => { + if (context[ENABLE_CAMPAIGN_NOTIFICATION_CONTEXT_KEY]) return false + if (context[SKIP_CAMPAIGN_NOTIFICATION_CONTEXT_KEY]) return true + // Int suite shares one Postgres across ~48 workers; hook side-effects (extra + // rows, coordinator fan-out reads) are tested in campaignNotifications.int. + return process.env.VITEST === 'true' +} export const notifyMunicipalityUpdateCreated = async ( req: NotificationEventRequest, diff --git a/tests/helpers/campaignFixtures.ts b/tests/helpers/campaignFixtures.ts index ef5f40b3..ddb04b80 100644 --- a/tests/helpers/campaignFixtures.ts +++ b/tests/helpers/campaignFixtures.ts @@ -333,6 +333,8 @@ export class CampaignFixtures { private readonly touchedMunicipalities = new Set() private readonly ownedLockedDocuments = new Set() private readonly ownedPreferences = new Set() + private readonly ownedNotifications = new Set() + private readonly ownedPushSubscriptions = new Set() constructor(private readonly rootPayload: Payload) { builderCounter += 1 @@ -917,16 +919,36 @@ export class CampaignFixtures { } if (userIDs.length > 0) { - const updates = await this.rootPayload.find({ - collection: 'municipalityUpdate', - where: { author: { in: userIDs } }, - depth: 0, - pagination: false, - }) + const [updates, notifications, pushSubscriptions] = await Promise.all([ + this.rootPayload.find({ + collection: 'municipalityUpdate', + where: { author: { in: userIDs } }, + depth: 0, + pagination: false, + }), + this.rootPayload.find({ + collection: 'notification', + where: { recipient: { in: userIDs } }, + depth: 0, + pagination: false, + }), + this.rootPayload.find({ + collection: 'pushSubscription', + where: { user: { in: userIDs } }, + depth: 0, + pagination: false, + }), + ]) for (const update of updates.docs) { this.own('municipalityUpdate', update) this.touchedMunicipalities.add(relationId(update.municipality)) } + for (const notification of notifications.docs) { + this.ownedNotifications.add(notification.id) + } + for (const subscription of pushSubscriptions.docs) { + this.ownedPushSubscriptions.add(subscription.id) + } } const contactIDs = [...this.owned.contact] @@ -995,6 +1017,29 @@ export class CampaignFixtures { } } + private async deleteNotificationOwned(req: { transactionID: number | string }): Promise { + if (this.ownedNotifications.size > 0) { + await this.rootPayload.delete({ + collection: 'notification', + where: { id: { in: [...this.ownedNotifications] } }, + depth: 0, + overrideAccess: true, + req, + }) + this.ownedNotifications.clear() + } + if (this.ownedPushSubscriptions.size > 0) { + await this.rootPayload.delete({ + collection: 'pushSubscription', + where: { id: { in: [...this.ownedPushSubscriptions] } }, + depth: 0, + overrideAccess: true, + req, + }) + this.ownedPushSubscriptions.clear() + } + } + private async deleteOwned( collection: CampaignCollection, req: { transactionID: number | string }, @@ -1079,12 +1124,13 @@ export class CampaignFixtures { 'organization', 'stateDeputy', 'contact', - 'campaignUser', - 'consent', - 'users', ] as const) { await this.deleteOwned(collection, req) } + await this.deleteNotificationOwned(req) + for (const collection of ['campaignUser', 'consent', 'users'] as const) { + await this.deleteOwned(collection, req) + } await this.resetTouchedMunicipalities(req) if (this.ownedPreferences.size > 0) { const transaction = this.rootPayload.db.sessions?.[String(req.transactionID)]?.db as