improvement: Nothing done is related to subscriptions!!!
This commit is contained in:
@@ -84,6 +84,7 @@ export const ErrorCode = {
|
||||
APPOINTMENT_NOT_PROVIDER: 'APPOINTMENT_NOT_PROVIDER',
|
||||
|
||||
PATIENT_NOT_FOUND: 'PATIENT_NOT_FOUND',
|
||||
PATIENT_MOBILE_UNAVAILABLE: 'PATIENT_MOBILE_UNAVAILABLE',
|
||||
|
||||
WORKING_HOURS_INVALID: 'WORKING_HOURS_INVALID',
|
||||
WORKING_HOURS_OWNER_NOT_ALLOWED: 'WORKING_HOURS_OWNER_NOT_ALLOWED',
|
||||
@@ -110,6 +111,8 @@ export const ErrorCode = {
|
||||
STAFF_CANNOT_ENABLE_OWNER: 'STAFF_CANNOT_ENABLE_OWNER',
|
||||
STAFF_CANNOT_DISABLE_OWNER: 'STAFF_CANNOT_DISABLE_OWNER',
|
||||
STAFF_CANNOT_REMOVE_OWNER: 'STAFF_CANNOT_REMOVE_OWNER',
|
||||
STAFF_CANNOT_CLEAR_OWN_PASSWORD: 'STAFF_CANNOT_CLEAR_OWN_PASSWORD',
|
||||
STAFF_PASSWORD_CLEAR_ACTIVE_ONLY: 'STAFF_PASSWORD_CLEAR_ACTIVE_ONLY',
|
||||
|
||||
ORG_CANNOT_LINK_SELF: 'ORG_CANNOT_LINK_SELF',
|
||||
ORG_LINK_WRONG_TYPE: 'ORG_LINK_WRONG_TYPE',
|
||||
|
||||
@@ -341,12 +341,16 @@ export class AppointmentsService {
|
||||
}
|
||||
}
|
||||
|
||||
private async ensurePatientInOrg(patientId: string, _organizationId: string) {
|
||||
const patient = await this.prisma.patient.findUnique({
|
||||
where: { id: patientId },
|
||||
select: { id: true, isWalkIn: true },
|
||||
private async ensurePatientInOrg(patientId: string, organizationId: string) {
|
||||
const patient = await this.prisma.patient.findFirst({
|
||||
where: {
|
||||
id: patientId,
|
||||
isWalkIn: false,
|
||||
createdByOrganizationId: organizationId,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (!patient || patient.isWalkIn) {
|
||||
if (!patient) {
|
||||
throw new AppException(ErrorCode.PATIENT_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,16 +25,17 @@ export class PatientsController {
|
||||
constructor(private readonly patientsService: PatientsService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create or return existing global patient by mobile' })
|
||||
@ApiOperation({ summary: 'Create or return this clinic’s patient by mobile' })
|
||||
create(@Body() createPatientDto: CreatePatientDto, @Req() req) {
|
||||
const organizationId = this.patientsService.getOrganizationIdFromUser(req.user);
|
||||
return this.patientsService.create(createPatientDto, organizationId);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Search all patients globally' })
|
||||
findAll(@Query() query: ListPatientsDto) {
|
||||
return this.patientsService.findAll(query);
|
||||
@ApiOperation({ summary: 'Search patients created by the current clinic' })
|
||||
findAll(@Query() query: ListPatientsDto, @Req() req) {
|
||||
const organizationId = this.patientsService.getOrganizationIdFromUser(req.user);
|
||||
return this.patientsService.findAll(query, organizationId);
|
||||
}
|
||||
|
||||
@Get(':id/appointments')
|
||||
@@ -48,14 +49,20 @@ export class PatientsController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get one patient by id' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.patientsService.findOne(id);
|
||||
@ApiOperation({ summary: 'Get one patient created by the current clinic' })
|
||||
findOne(@Param('id') id: string, @Req() req) {
|
||||
const organizationId = this.patientsService.getOrganizationIdFromUser(req.user);
|
||||
return this.patientsService.findOne(id, organizationId);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ summary: 'Update global patient record' })
|
||||
update(@Param('id') id: string, @Body() updatePatientDto: UpdatePatientDto) {
|
||||
return this.patientsService.update(id, updatePatientDto);
|
||||
@ApiOperation({ summary: 'Update a patient created by the current clinic' })
|
||||
update(
|
||||
@Param('id') id: string,
|
||||
@Body() updatePatientDto: UpdatePatientDto,
|
||||
@Req() req,
|
||||
) {
|
||||
const organizationId = this.patientsService.getOrganizationIdFromUser(req.user);
|
||||
return this.patientsService.update(id, updatePatientDto, organizationId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,13 @@ export class PatientsService {
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return { success: true, data: existing, existing: true as const };
|
||||
if (
|
||||
!existing.isWalkIn &&
|
||||
existing.createdByOrganizationId === organizationId
|
||||
) {
|
||||
return { success: true, data: existing, existing: true as const };
|
||||
}
|
||||
throw new AppException(ErrorCode.PATIENT_MOBILE_UNAVAILABLE, HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
const patient = await this.prisma.patient.create({
|
||||
@@ -39,12 +45,13 @@ export class PatientsService {
|
||||
return { success: true, data: patient, existing: false as const };
|
||||
}
|
||||
|
||||
async findAll(query: ListPatientsDto) {
|
||||
async findAll(query: ListPatientsDto, organizationId: string) {
|
||||
const { page = 1, limit = 10, q } = query;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where = {
|
||||
isWalkIn: false,
|
||||
createdByOrganizationId: organizationId,
|
||||
...(q?.trim() ? this.buildSearchWhere(q.trim()) : {}),
|
||||
};
|
||||
|
||||
@@ -72,27 +79,13 @@ export class PatientsService {
|
||||
};
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const patient = await this.prisma.patient.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!patient || patient.isWalkIn) {
|
||||
throw new AppException(ErrorCode.PATIENT_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
async findOne(id: string, organizationId: string) {
|
||||
const patient = await this.findNamedPatientInOrg(id, organizationId);
|
||||
return { success: true, data: patient };
|
||||
}
|
||||
|
||||
async update(id: string, updatePatientDto: UpdatePatientDto) {
|
||||
await this.ensurePatient(id);
|
||||
const patient = await this.prisma.patient.findUnique({
|
||||
where: { id },
|
||||
select: { isWalkIn: true },
|
||||
});
|
||||
if (patient?.isWalkIn) {
|
||||
throw new AppException(ErrorCode.PATIENT_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
async update(id: string, updatePatientDto: UpdatePatientDto, organizationId: string) {
|
||||
await this.findNamedPatientInOrg(id, organizationId);
|
||||
|
||||
const data: {
|
||||
firstName?: string;
|
||||
@@ -110,7 +103,15 @@ export class PatientsService {
|
||||
data.lastName = this.requireNonEmptyName(updatePatientDto.lastName, 'lastName');
|
||||
}
|
||||
if (updatePatientDto.mobile !== undefined) {
|
||||
data.mobile = this.resolveMobile(updatePatientDto.mobile);
|
||||
const mobile = this.resolveMobile(updatePatientDto.mobile);
|
||||
const taken = await this.prisma.patient.findUnique({
|
||||
where: { mobile },
|
||||
select: { id: true, createdByOrganizationId: true, isWalkIn: true },
|
||||
});
|
||||
if (taken && taken.id !== id) {
|
||||
throw new AppException(ErrorCode.PATIENT_MOBILE_UNAVAILABLE, HttpStatus.CONFLICT);
|
||||
}
|
||||
data.mobile = mobile;
|
||||
}
|
||||
if (updatePatientDto.email !== undefined) {
|
||||
data.email = updatePatientDto.email?.trim() || null;
|
||||
@@ -138,7 +139,7 @@ export class PatientsService {
|
||||
actorUserId: string,
|
||||
) {
|
||||
await this.assertCanViewPatients(actorUserId, organizationId);
|
||||
await this.ensurePatient(patientId);
|
||||
await this.findNamedPatientInOrg(patientId, organizationId);
|
||||
|
||||
const items = await this.prisma.appointment.findMany({
|
||||
where: { organizationId, patientId },
|
||||
@@ -241,14 +242,18 @@ export class PatientsService {
|
||||
}
|
||||
}
|
||||
|
||||
private async ensurePatient(id: string) {
|
||||
const patient = await this.prisma.patient.findUnique({
|
||||
where: { id },
|
||||
select: { id: true },
|
||||
private async findNamedPatientInOrg(id: string, organizationId: string) {
|
||||
const patient = await this.prisma.patient.findFirst({
|
||||
where: {
|
||||
id,
|
||||
isWalkIn: false,
|
||||
createdByOrganizationId: organizationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!patient) {
|
||||
throw new AppException(ErrorCode.PATIENT_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return patient;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,20 @@ export class StaffController {
|
||||
return this.staffService.invite(req.user.id, organizationId, dto);
|
||||
}
|
||||
|
||||
@Post('members/:membershipId/clear-password')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Clear a staff member password and return a setup link (owner or TAB_STAFF_EDIT; active members only)',
|
||||
})
|
||||
clearPassword(
|
||||
@Req() req: { user: { id: string; organizationId?: string } },
|
||||
@Param('membershipId') membershipId: string,
|
||||
) {
|
||||
const organizationId = this.staffService.getOrganizationIdFromUser(req.user);
|
||||
return this.staffService.clearPassword(req.user.id, organizationId, membershipId);
|
||||
}
|
||||
|
||||
@Post('members/:membershipId/invitation-link')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({
|
||||
|
||||
@@ -49,7 +49,7 @@ export class StaffService {
|
||||
this.prisma.membership.findMany({
|
||||
where: { organizationId },
|
||||
include: {
|
||||
user: { select: { id: true, email: true, name: true } },
|
||||
user: { select: { id: true, email: true, name: true, passwordHash: true } },
|
||||
permissions: { include: { permission: true } },
|
||||
invitations: {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
@@ -80,6 +80,7 @@ export class StaffService {
|
||||
isOwner: m.isOwner,
|
||||
isActive: m.isOwner ? true : m.isActive,
|
||||
invitationStatus: this.getInvitationStatus(m),
|
||||
hasPassword: Boolean(m.user.passwordHash),
|
||||
invitedAt: m.invitations[0]?.createdAt?.toISOString() || null,
|
||||
acceptedAt: m.invitations[0]?.acceptedAt?.toISOString() || null,
|
||||
permissions: m.isOwner
|
||||
@@ -310,6 +311,77 @@ export class StaffService {
|
||||
organizationName: org.name,
|
||||
expiresAt: invitation.expiresAt.toISOString(),
|
||||
status: invitation.acceptedAt ? 'ACCEPTED' : 'PENDING',
|
||||
mode: invitation.membership.isActive ? 'password_setup' : 'join',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async clearPassword(
|
||||
actorUserId: string,
|
||||
organizationId: string,
|
||||
membershipId: string,
|
||||
) {
|
||||
const actor = await this.getActorMembership(actorUserId, organizationId);
|
||||
if (!actor || !this.canEditStaff(actor)) {
|
||||
throw new AppException(ErrorCode.PERMISSION_EDIT_STAFF, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
const membership = await this.prisma.membership.findFirst({
|
||||
where: { id: membershipId, organizationId },
|
||||
include: {
|
||||
user: { select: { id: true, email: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new AppException(ErrorCode.STAFF_MEMBER_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (membership.isOwner) {
|
||||
throw new AppException(ErrorCode.STAFF_CANNOT_EDIT_OWNER, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
if (membership.userId === actorUserId) {
|
||||
throw new AppException(ErrorCode.STAFF_CANNOT_CLEAR_OWN_PASSWORD, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
if (!membership.isActive) {
|
||||
throw new AppException(ErrorCode.STAFF_PASSWORD_CLEAR_ACTIVE_ONLY, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const plainToken = this.generateInviteToken();
|
||||
const tokenHash = this.hashInviteToken(plainToken);
|
||||
|
||||
const invitation = await this.prisma.$transaction(async (tx) => {
|
||||
await tx.user.update({
|
||||
where: { id: membership.userId },
|
||||
data: { passwordHash: null },
|
||||
});
|
||||
await tx.session.deleteMany({
|
||||
where: { userId: membership.userId },
|
||||
});
|
||||
await tx.staffInvitation.updateMany({
|
||||
where: {
|
||||
membershipId: membership.id,
|
||||
acceptedAt: null,
|
||||
revokedAt: null,
|
||||
},
|
||||
data: { revokedAt: new Date() },
|
||||
});
|
||||
return tx.staffInvitation.create({
|
||||
data: {
|
||||
membershipId: membership.id,
|
||||
invitedById: actorUserId,
|
||||
tokenHash,
|
||||
expiresAt: this.getInviteExpiryDate(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
membershipId: membership.id,
|
||||
invitationId: invitation.id,
|
||||
email: membership.user.email,
|
||||
invitationUrl: this.buildInviteUrl(plainToken),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -222,14 +222,7 @@ export class TreatmentsService {
|
||||
const sentinel = await ensureWalkInPatient(this.prisma, organizationId);
|
||||
patientId = sentinel.id;
|
||||
} else {
|
||||
await this.ensurePatientExists(dto.patientId!);
|
||||
const patient = await this.prisma.patient.findUnique({
|
||||
where: { id: dto.patientId! },
|
||||
select: { isWalkIn: true },
|
||||
});
|
||||
if (patient?.isWalkIn) {
|
||||
throw new AppException(ErrorCode.TREATMENT_PATIENT_OR_WALK_IN, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
await this.ensurePatientInOrg(dto.patientId!, organizationId);
|
||||
patientId = dto.patientId!;
|
||||
}
|
||||
|
||||
@@ -1670,6 +1663,20 @@ export class TreatmentsService {
|
||||
}
|
||||
}
|
||||
|
||||
private async ensurePatientInOrg(patientId: string, organizationId: string) {
|
||||
const patient = await this.prisma.patient.findFirst({
|
||||
where: {
|
||||
id: patientId,
|
||||
isWalkIn: false,
|
||||
createdByOrganizationId: organizationId,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (!patient) {
|
||||
throw new AppException(ErrorCode.PATIENT_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureTreatmentProvider(
|
||||
treatmentId: string,
|
||||
organizationId: string,
|
||||
|
||||
Reference in New Issue
Block a user