/opt/mawid/apps/api/src/reminders
NameSizeModeActions
reminder-queue.ts5270644editdlrm
reminder-reply.service.ts34330644editdlrm
reminder-sender.service.ts21330644editdlrm
reminder.service.ts35900644editdlrm
reminder.worker.ts40600644editdlrm
reminders.e2e.spec.ts133040644editdlrm
reminders.module.ts10880644editdlrm
Edit: /opt/mawid/apps/api/src/reminders/reminder-reply.service.ts (3433B)
import { Injectable } from '@nestjs/common'; import { t, type Language } from '@mawid/shared'; import { PrismaService } from '../prisma/prisma.service'; import { WaSenderService } from '../whatsapp/wa-sender.service'; import { ReminderService } from './reminder.service'; const BUTTON_RE = /^(confirm|cancel|resched):([a-z0-9]+)$/i; export interface ReminderReplyResult { handled: boolean; /** Set when a cancel button freed a slot — caller triggers waitlist refill. */ cancelledAppointmentId?: string; } /** * Handles reminder button replies (PROJECT_PLAN Phase 4 task 3) before the * agent sees the message: confirm → status confirmed; cancel → cancelled + * job cleanup (caller triggers the Phase 5 waitlist refill); reschedule → * deterministic prompt, then the patient's reply flows to the agent. */ @Injectable() export class ReminderReplyService { constructor( private readonly prisma: PrismaService, private readonly sender: WaSenderService, private readonly reminders: ReminderService, ) {} /** Reports whether the message was a reminder button, and any freed slot. */ async tryHandle(clinicId: string, patientId: string, body: string): Promise { const match = BUTTON_RE.exec(body.trim()); if (!match) return { handled: false }; const [, action, appointmentId] = match; const appointment = await this.prisma.appointment.findFirst({ where: { id: appointmentId, clinicId, patientId, status: { in: ['pending', 'confirmed'] }, }, include: { patient: true }, }); if (!appointment) return { handled: false }; // stale button — let the agent respond const lang = this.lang(appointment.patient.language); switch (action.toLowerCase()) { case 'confirm': { if (appointment.status === 'pending') { await this.prisma.appointment.update({ where: { id: appointment.id }, data: { status: 'confirmed', unconfirmedRisk: false }, }); } await this.audit(clinicId, 'reminder:confirm', appointment.id); await this.sender.sendText(clinicId, patientId, t('confirmedThanks', lang)); return { handled: true }; } case 'cancel': { await this.prisma.appointment.update({ where: { id: appointment.id }, data: { status: 'cancelled', cancelReason: 'reminder_button' }, }); await this.reminders.cancelForAppointment(appointment.id); await this.audit(clinicId, 'reminder:cancel', appointment.id); await this.sender.sendText(clinicId, patientId, t('cancelledOk', lang)); return { handled: true, cancelledAppointmentId: appointment.id }; } case 'resched': { await this.audit(clinicId, 'reminder:reschedule_requested', appointment.id); await this.sender.sendText(clinicId, patientId, t('reschedulePrompt', lang)); return { handled: true }; } default: return { handled: false }; } } private lang(language: string): Language { return (['ar', 'tr', 'en'] as const).includes(language as Language) ? (language as Language) : 'en'; } private async audit(clinicId: string, action: string, appointmentId: string): Promise { await this.prisma.auditLog.create({ data: { clinicId, actor: 'patient', action, meta: { appointmentId } }, }); } }