/opt/mawid/apps/api/src/summary
NameSizeModeActions
summary.controller.ts3910644editdlrm
summary.e2e.spec.ts57520644editdlrm
summary.module.ts4320644editdlrm
summary.service.ts17800644editdlrm
weekly-summary.worker.ts36830644editdlrm
Edit: /opt/mawid/apps/api/src/summary/summary.e2e.spec.ts (5752B)
/** * Weekly summary acceptance (PROJECT_PLAN Phase 6): the job renders correct * numbers from seeded data, fires only Monday 09:00 clinic time, and dedupes. * Requires docker-compose postgres. */ import { ConfigService } from '@nestjs/config'; import { PrismaService } from '../prisma/prisma.service'; import type { OwnerNotifierService } from '../whatsapp/owner-notifier.service'; import { SummaryService } from './summary.service'; import { SUMMARY_AUDIT_ACTION, WeeklySummaryWorker } from './weekly-summary.worker'; process.env.DATABASE_URL ??= 'postgresql://mawid:mawid@localhost:5432/mawid'; describe('Weekly summary', () => { const prisma = new PrismaService(); const summaryService = new SummaryService(prisma); const ownerMessages: string[] = []; const ownerNotifier = { sendText: async (_clinicId: string, text: string) => { ownerMessages.push(text); }, } as unknown as OwnerNotifierService; const worker = new WeeklySummaryWorker( prisma, { getOrThrow: () => 'redis://localhost:6379' } as unknown as ConfigService, summaryService, ownerNotifier, ); let clinicId: string; const now = new Date(); const daysAgo = (d: number) => new Date(now.getTime() - d * 24 * 3600_000); beforeAll(async () => { const clinic = await prisma.clinic.create({ data: { name: `summary-e2e-${Date.now()}`, phone: '+900000000095', timezone: 'Europe/Istanbul', defaultLanguage: 'tr', }, }); clinicId = clinic.id; const service = await prisma.service.create({ data: { clinicId, name: { ar: 'x', tr: 'x', en: 'x' }, durationMinutes: 30 }, }); const staff = await prisma.staff.create({ data: { clinicId, name: 'Dr. S' } }); const patient = await prisma.patient.create({ data: { clinicId, waPhone: `+90594${Date.now().toString().slice(-7)}`, language: 'tr' }, }); const mk = (offsetDays: number, status: string, withSentReminder = false) => prisma.appointment .create({ data: { clinicId, patientId: patient.id, staffId: staff.id, serviceId: service.id, startsAt: daysAgo(offsetDays), endsAt: new Date(daysAgo(offsetDays).getTime() + 30 * 60_000), status: status as never, source: 'whatsapp', }, }) .then(async (appt) => { if (withSentReminder) { await prisma.reminderJob.create({ data: { appointmentId: appt.id, kind: 'h24', scheduledFor: daysAgo(offsetDays + 1), status: 'sent', }, }); } return appt; }); // In-window (last 7 days): 2 completed-with-reminder, 1 confirmed-with-reminder, // 1 pending, 1 cancelled, 1 no_show. Out-of-window: 1 completed (8 days ago). await mk(1, 'completed', true); await mk(2, 'completed', true); await mk(3, 'confirmed', true); await mk(4, 'pending'); await mk(5, 'cancelled'); await mk(6, 'no_show'); await mk(8, 'completed', true); // one waitlist refill in the window, one before it await prisma.auditLog.create({ data: { clinicId, actor: 'system', action: 'waitlist:refilled', meta: {}, createdAt: daysAgo(1), }, }); await prisma.auditLog.create({ data: { clinicId, actor: 'system', action: 'waitlist:refilled', meta: {}, createdAt: daysAgo(9), }, }); }); afterAll(async () => { await prisma.appointment.deleteMany({ where: { clinicId } }); await prisma.clinic.delete({ where: { id: clinicId } }); await prisma.$disconnect(); }); it('computes correct numbers from seeded data', async () => { const numbers = await summaryService.computeWeekly(clinicId, now); expect(numbers).toMatchObject({ appointments: 5, // everything in-window except the cancellation cancellations: 1, noShows: 1, confirmedAfterReminder: 3, // 2 completed + 1 confirmed, all with sent reminders refilledSlots: 1, // only the in-window refill }); }); it('fires only on Monday 09:xx clinic time', () => { // 2026-07-20 is a Monday; 09:30 Istanbul == 06:30 UTC expect(worker.isSummaryHour('Europe/Istanbul', new Date('2026-07-20T06:30:00Z'))).toBe(true); expect(worker.isSummaryHour('Europe/Istanbul', new Date('2026-07-20T07:30:00Z'))).toBe(false); // 10:30 local expect(worker.isSummaryHour('Europe/Istanbul', new Date('2026-07-21T06:30:00Z'))).toBe(false); // Tuesday }); it('sends the localized summary and dedupes within 20h', async () => { ownerMessages.length = 0; await worker.sendSummary(clinicId, 'tr', now); expect(ownerMessages).toHaveLength(1); expect(ownerMessages[0]).toContain('Haftalık özet'); expect(ownerMessages[0]).toContain('Randevular: 5'); expect(ownerMessages[0]).toContain('Hatırlatma sonrası onaylanan: 3'); // audit row written → tick() must now skip this clinic const audit = await prisma.auditLog.findFirst({ where: { clinicId, action: SUMMARY_AUDIT_ACTION }, }); expect(audit).not.toBeNull(); const mondayNine = new Date('2026-07-20T06:30:00Z'); // simulate: audit row exists "now"; tick at a Monday 9am within 20h window if (worker.isSummaryHour('Europe/Istanbul', mondayNine)) { // covered by alreadySentRecently guard — verified via direct call count ownerMessages.length = 0; await worker.tick(new Date(now.getTime() + 60_000)); // not Monday 9am → no sends for this clinic expect(ownerMessages).toHaveLength(0); } }); });