/opt/mawid/apps/api/src/whatsapp
NameSizeModeActions
owner-notifier.service.ts17450644editdlrm
wa-core.module.ts13520644editdlrm
wa-http.client.ts17360644editdlrm
wa-inbound.service.spec.ts50660644editdlrm
wa-inbound.service.ts54310644editdlrm
wa-outbound.worker.ts27230644editdlrm
wa-queue.ts4470644editdlrm
wa-sender.service.ts49590644editdlrm
wa-signature.spec.ts12640644editdlrm
wa-signature.ts6620644editdlrm
wa-types.ts16830644editdlrm
wa-webhook.controller.spec.ts26840644editdlrm
wa-webhook.controller.ts23080644editdlrm
wa-window.spec.ts8600644editdlrm
wa-window.ts5090644editdlrm
whatsapp.module.ts5790644editdlrm
Edit: /opt/mawid/apps/api/src/whatsapp/wa-inbound.service.spec.ts (5066B)
/** * Integration test — requires the docker-compose postgres on localhost:5432. * The BullMQ queue is faked; everything else runs against the real DB. */ import { ConfigService } from '@nestjs/config'; import type { AgentService } from '../agent/agent.service'; import type { ReminderReplyService } from '../reminders/reminder-reply.service'; import type { WaitlistReplyService } from '../waitlist/waitlist-reply.service'; import type { WaitlistService } from '../waitlist/waitlist.service'; import { PrismaService } from '../prisma/prisma.service'; import { WaInboundService } from './wa-inbound.service'; import { WaSenderService } from './wa-sender.service'; import type { WaOutboundQueue } from './wa-queue'; import type { WaWebhookPayload } from './wa-types'; process.env.DATABASE_URL ??= 'postgresql://mawid:mawid@localhost:5432/mawid'; const PHONE_NUMBER_ID = `test-pnid-${Date.now()}`; const PATIENT_PHONE = `9055${String(Date.now()).slice(-9)}`; function webhookPayload(text: string, waMessageId: string): WaWebhookPayload { return { object: 'whatsapp_business_account', entry: [ { id: 'waba-1', changes: [ { field: 'messages', value: { messaging_product: 'whatsapp', metadata: { display_phone_number: '905550009999', phone_number_id: PHONE_NUMBER_ID }, contacts: [{ profile: { name: 'Test Patient' }, wa_id: PATIENT_PHONE }], messages: [ { from: PATIENT_PHONE, id: waMessageId, timestamp: String(Math.floor(Date.now() / 1000)), type: 'text', text: { body: text }, }, ], }, }, ], }, ], }; } describe('WaInboundService (integration)', () => { const prisma = new PrismaService(); const queueAdd = jest.fn(); const queue = { add: queueAdd } as unknown as WaOutboundQueue; const config = { get: (key: string) => ({ FEATURE_ECHO_MODE: true })[key], } as unknown as ConfigService; const sender = new WaSenderService(prisma, queue); const agent = { enabled: false } as AgentService; const reminderReplies = { tryHandle: async () => ({ handled: false }), } as unknown as ReminderReplyService; const waitlistReplies = { tryHandle: async () => false } as unknown as WaitlistReplyService; const waitlist = { onAppointmentCancelled: async () => {} } as unknown as WaitlistService; const service = new WaInboundService( prisma, sender, config, agent, reminderReplies, waitlistReplies, waitlist, ); let clinicId: string; beforeAll(async () => { const clinic = await prisma.clinic.create({ data: { name: `wa-inbound-test-${Date.now()}`, phone: '+905550009999', waPhoneNumberId: PHONE_NUMBER_ID, defaultLanguage: 'tr', }, }); clinicId = clinic.id; }); afterAll(async () => { await prisma.clinic.delete({ where: { id: clinicId } }); await prisma.$disconnect(); }); it('stores the inbound message, creates patient + conversation, and echoes back', async () => { await service.process(webhookPayload('merhaba', 'wamid.test.1')); const patient = await prisma.patient.findUnique({ where: { clinicId_waPhone: { clinicId, waPhone: `+${PATIENT_PHONE}` } }, }); expect(patient).not.toBeNull(); expect(patient?.name).toBe('Test Patient'); const conversation = await prisma.conversation.findFirst({ where: { clinicId, patientId: patient!.id }, include: { messages: { orderBy: { createdAt: 'asc' } } }, }); expect(conversation).not.toBeNull(); expect(conversation!.lastMessageAt).not.toBeNull(); const [inbound, echo] = conversation!.messages; expect(inbound.direction).toBe('inbound'); expect(inbound.body).toBe('merhaba'); expect(inbound.waMessageId).toBe('wamid.test.1'); // Echo mode: outbound reply persisted in the patient's language and queued. expect(echo.direction).toBe('outbound'); expect(echo.body).toBe('Alındı: merhaba'); expect(queueAdd).toHaveBeenCalledTimes(1); expect(queueAdd.mock.calls[0][1]).toEqual({ messageId: echo.id }); }); it('is idempotent on Meta webhook redelivery (same waMessageId)', async () => { queueAdd.mockClear(); await service.process(webhookPayload('merhaba', 'wamid.test.1')); const count = await prisma.message.count({ where: { conversation: { clinicId }, direction: 'inbound' }, }); expect(count).toBe(1); expect(queueAdd).not.toHaveBeenCalled(); }); it('ignores webhooks for unknown phone_number_id', async () => { const payload = webhookPayload('hi', 'wamid.test.2'); payload.entry![0].changes![0].value.metadata.phone_number_id = 'unknown-pnid'; await expect(service.process(payload)).resolves.toBeUndefined(); const count = await prisma.message.count({ where: { waMessageId: 'wamid.test.2' } }); expect(count).toBe(0); }); });