import { beforeEach, describe, expect, it, vi } from 'vitest' const prismaMock = vi.hoisted(() => { const tx = { notificationEvent: { create: vi.fn().mockResolvedValue({ id: 'event_1' }), }, notificationRecipient: { create: vi.fn().mockResolvedValue({ id: 'recipient_1' }), }, notificationDelivery: { create: vi.fn().mockImplementation(({ data }) => Promise.resolve({ id: `delivery_${data.channel}`, ...data })), }, notificationOutbox: { create: vi.fn().mockResolvedValue({ id: 'outbox_1' }), }, } return { tx, prisma: { employee: { findFirst: vi.fn(), findMany: vi.fn() }, renter: { findUnique: vi.fn() }, notificationPreference: { findFirst: vi.fn() }, companyNotificationPreference: { findUnique: vi.fn() }, notificationEvent: { findUniqueOrThrow: vi.fn() }, $transaction: vi.fn((callback) => callback(tx)), }, } }) vi.mock('resend', () => ({ Resend: vi.fn() })) vi.mock('../lib/prisma', () => ({ prisma: prismaMock.prisma })) vi.mock('./notificationLocalizationService', async () => { const actual = await vi.importActual('./notificationLocalizationService') return { ...actual, resolveNotificationLocale: vi.fn().mockResolvedValue('en'), resolveNotificationTemplate: vi.fn(), } }) import { prisma } from '../lib/prisma' import { createNotification, sendNotification } from './notificationService' import { resolveNotificationLocale, resolveNotificationTemplate } from './notificationLocalizationService' describe('notificationService command boundaries', () => { beforeEach(() => { vi.clearAllMocks() vi.mocked(prisma.employee.findFirst).mockResolvedValue({ id: 'employee_1', email: 'agent@example.test', preferredLanguage: 'en', } as never) vi.mocked(prisma.notificationEvent.findUniqueOrThrow).mockResolvedValue({ recipients: [] } as never) vi.mocked(prisma.notificationPreference.findFirst).mockResolvedValue(null as never) vi.mocked(prisma.companyNotificationPreference.findUnique).mockResolvedValue(null as never) vi.mocked(resolveNotificationLocale).mockResolvedValue('en') vi.mocked(resolveNotificationTemplate).mockResolvedValue(null as never) }) it('creates an event, explicit recipient, delivery rows, and an outbox row without inline provider calls', async () => { const result = await createNotification({ companyId: 'company_1', type: 'NEW_BOOKING', audience: { type: 'EMPLOYEE', employeeId: 'employee_1' }, channels: ['IN_APP', 'EMAIL'], source: { type: 'reservation', id: 'reservation_1' }, idempotencyKey: 'reservation_1:new_booking:employee_1', title: 'New booking', body: 'A renter booked a vehicle.', data: { reservationId: 'reservation_1' }, }) expect(result.duplicate).toBe(false) expect(prisma.notificationEvent.findUniqueOrThrow).not.toHaveBeenCalled() expect(prismaMock.tx.notificationEvent.create).toHaveBeenCalledWith({ data: expect.objectContaining({ companyId: 'company_1', type: 'NEW_BOOKING', sourceType: 'reservation', sourceId: 'reservation_1', }), }) expect(prismaMock.tx.notificationRecipient.create).toHaveBeenCalledWith({ data: { notificationEventId: 'event_1', recipientType: 'EMPLOYEE', employeeId: 'employee_1', renterId: null, }, }) expect(prismaMock.tx.notificationDelivery.create).toHaveBeenCalledWith({ data: expect.objectContaining({ notificationRecipientId: 'recipient_1', channel: 'IN_APP', status: 'QUEUED', preferenceDecision: 'ENABLED_BY_PRODUCT_DEFAULT', }), }) expect(prismaMock.tx.notificationOutbox.create).toHaveBeenCalledWith({ data: expect.objectContaining({ notificationEventId: 'event_1', payload: expect.objectContaining({ notificationEventId: 'event_1', type: 'NEW_BOOKING', }), }), }) }) it('records skipped delivery decisions for unsupported optional channels', async () => { const result = await sendNotification({ type: 'BOOKING_CONFIRMED', title: 'Booking confirmed', body: 'Your booking is confirmed.', companyId: 'company_1', employeeId: 'employee_1', channels: ['PUSH'], data: { reservationId: 'reservation_1' }, }) expect(result).toEqual([ { channel: 'PUSH', success: false, error: 'Delivery channel disabled by notification policy or preference.' }, ]) expect(prismaMock.tx.notificationDelivery.create).toHaveBeenCalledWith({ data: expect.objectContaining({ channel: 'PUSH', status: 'SKIPPED', preferenceDecision: 'SKIPPED_UNSUPPORTED_CHANNEL', }), }) }) it('fails before persistence when an explicit recipient is missing', async () => { const result = await sendNotification({ type: 'NEW_BOOKING', title: 'Missing recipient', body: 'No target.', companyId: 'company_1', channels: ['IN_APP'], }) expect(result).toEqual([{ channel: 'IN_APP', success: false, error: 'An explicit employee or renter recipient is required' }]) expect(prismaMock.tx.notificationEvent.create).not.toHaveBeenCalled() }) })