/opt/mawid/apps/api/src/patients
Edit: /opt/mawid/apps/api/src/patients/patients.service.ts (4266B)
import { Injectable, NotFoundException } from '@nestjs/common';
import type { UpdatePatientInput } from '@mawid/shared';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class PatientsService {
constructor(private readonly prisma: PrismaService) {}
list(clinicId: string, search?: string) {
return this.prisma.patient.findMany({
where: {
clinicId,
...(search
? {
OR: [
{ name: { contains: search, mode: 'insensitive' } },
{ waPhone: { contains: search } },
],
}
: {}),
},
orderBy: [{ lastVisitAt: { sort: 'desc', nulls: 'last' } }, { createdAt: 'desc' }],
take: 100,
});
}
async get(clinicId: string, id: string) {
const patient = await this.prisma.patient.findFirst({
where: { id, clinicId },
include: {
appointments: {
include: {
service: { select: { id: true, name: true } },
staff: { select: { id: true, name: true } },
},
orderBy: { startsAt: 'desc' },
take: 50,
},
waitlistEntries: { where: { status: { in: ['active', 'notified'] } } },
},
});
if (!patient) throw new NotFoundException('Patient not found');
return patient;
}
async update(clinicId: string, id: string, input: UpdatePatientInput) {
await this.assertExists(clinicId, id);
return this.prisma.patient.update({ where: { id }, data: input });
}
/** KVKK/GDPR data export: every record we hold about the patient (Phase 7 task 4). */
async exportData(clinicId: string, id: string) {
const patient = await this.prisma.patient.findFirst({
where: { id, clinicId },
include: {
appointments: { include: { service: true, staff: true } },
waitlistEntries: true,
conversations: { include: { messages: { orderBy: { createdAt: 'asc' } } } },
},
});
if (!patient) throw new NotFoundException('Patient not found');
return {
exportedAt: new Date().toISOString(),
patient: {
id: patient.id,
waPhone: patient.waPhone,
name: patient.name,
language: patient.language,
notes: patient.notes,
tags: patient.tags,
createdAt: patient.createdAt,
},
appointments: patient.appointments,
waitlistEntries: patient.waitlistEntries,
conversations: patient.conversations,
};
}
/**
* KVKK/GDPR delete-on-request: hard-deletes the patient and everything keyed
* to them (messages, conversations, waitlist entries + holds, reminder jobs,
* appointments). Audited without PII.
*/
async deleteData(clinicId: string, id: string, actor: string) {
await this.assertExists(clinicId, id);
await this.prisma.$transaction(async (tx) => {
const conversations = await tx.conversation.findMany({
where: { clinicId, patientId: id },
select: { id: true },
});
await tx.message.deleteMany({
where: { conversationId: { in: conversations.map((c) => c.id) } },
});
await tx.conversation.deleteMany({ where: { clinicId, patientId: id } });
await tx.slotHold.deleteMany({ where: { clinicId, waitlistEntry: { patientId: id } } });
await tx.waitlistEntry.deleteMany({ where: { clinicId, patientId: id } });
const appointments = await tx.appointment.findMany({
where: { clinicId, patientId: id },
select: { id: true },
});
await tx.reminderJob.deleteMany({
where: { appointmentId: { in: appointments.map((a) => a.id) } },
});
await tx.appointment.deleteMany({ where: { clinicId, patientId: id } });
await tx.patient.delete({ where: { id } });
await tx.auditLog.create({
data: {
clinicId,
actor,
action: 'patient:data_deleted',
meta: { patientId: id }, // id only — no PII in audit logs
},
});
});
return { deleted: true };
}
private async assertExists(clinicId: string, id: string) {
const patient = await this.prisma.patient.findFirst({ where: { id, clinicId } });
if (!patient) throw new NotFoundException('Patient not found');
}
}