/opt/mawid/apps/api/src/whatsapp
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);
});
});