import { inject } from '@loopback/core'; import { Filter } from '@loopback/repository'; import { post, requestBody, getModelSchemaRef, RestBindings, Request, get, HttpErrors, param, patch, getFilterSchemaFor, } from '@loopback/rest'; import { AlivioBindings, CONTENT_TYPE, ErrorCodes, getAge, IAuthUserWithPermissions, ILogger, LOGGER, PermissionKey, STATUS_CODE, SuccessResponse, UserStatus, } from '@sourcefuse-npm/alivio-lib'; import { AuditLogService, AuthenticationService, BadgeRewardsService, Caregiver, CareGroupService, ChatService, Ehr, ERROR_KEYS, NotifService, Patient, PatientAdapterService, PatientController, PatientOnboardingService, PatientService, TenantConfigHelperService, TenantConnector, User, UsersService, AuditLogs, AuditLogAction, AuditLogActionType, PatientCareGiver, } from '@sourcefuse-npm/patient-facade'; import { PatientUserService } from '@sourcefuse-npm/patient-facade/dist/services/patient-user.service'; import { authenticate, AuthenticationBindings, STRATEGY, } from 'loopback4-authentication'; import { authorize, AuthorizeErrorKeys } from 'loopback4-authorization'; import { PatientCarePlanDetails, PatientOthersDetailsDTO } from '../models'; import { PatientCareGiverOtherdetails, PatientOtherdetails, } from '../models/patient-caregiver-other-details'; import { DailyCheckInsService, PatientCarePlanDetailsService, PatientOthersDetailsService, } from '../services'; import { DiPatientCaregiverUserService } from '../services/di-patient-caregiver-user.service'; const patientsPath = '/patients'; const updateByIdDesc = 'To update only one user whose id is provided.'; const maxLength = 3; export class PatientAPIController extends PatientController { // sonarignore:start constructor( @inject('services.PatientOnboardingService') protected patientOnbService: PatientOnboardingService, @inject('services.PatientService') protected patientService: PatientService, @inject('services.UsersService') protected userService: UsersService, @inject('services.ChatService') protected chatService: ChatService, @inject('services.CareGroupService') protected cgpService: CareGroupService, @inject('services.AuthenticationService') protected authService: AuthenticationService, @inject('services.NotifService') protected notifService: NotifService, @inject('services.AuditLogService') protected auditLogService: AuditLogService, @inject('services.Ehr') protected ehrService: Ehr, @inject(LOGGER.LOGGER_INJECT) public logger: ILogger, @inject(AlivioBindings.i18n) protected i18n: i18nAPI, @inject('services.DiPatientCaregiverUserService') protected readonly patientCgUserService: DiPatientCaregiverUserService, @inject('services.TenantConfigHelperService') protected readonly tenantConfigHelper: TenantConfigHelperService, @inject('services.BadgeRewardsService') protected readonly badgeRewardsService: BadgeRewardsService, @inject('services.PatientAdapterService') protected readonly patientAdapterService: PatientAdapterService, @inject('services.PatientUserService') protected patientUserService: PatientUserService, @inject('services.TenantConnector') protected readonly tenantConnector: TenantConnector, @inject('services.PatientOthersDetailsService') protected readonly patientOthersDetailsService: PatientOthersDetailsService, @inject('services.PatientDetailsService') public patientCarePlanDetailsService: PatientCarePlanDetailsService, @inject('services.DailyCheckInsService') public dailyCheckIns: DailyCheckInsService, ) { super( patientOnbService, patientService, userService, chatService, cgpService, authService, notifService, auditLogService, ehrService, logger, i18n, patientCgUserService, tenantConfigHelper, badgeRewardsService, patientAdapterService, patientUserService, tenantConnector, ); } // sonarignore:end @authenticate(STRATEGY.BEARER, { passReqToCallback: true, }) @authorize({ permissions: [PermissionKey.CreatePatient, PermissionKey.CreateCaregiver], }) @post(patientsPath, { security: [ { bearerAuth: [], }, ], description: 'To add patient in patient database of tenant.', responses: { [STATUS_CODE.CREATED]: { description: PatientCareGiverOtherdetails, content: { [CONTENT_TYPE.JSON]: { schema: getModelSchemaRef(PatientCareGiverOtherdetails), }, }, }, ...ErrorCodes, }, }) async create( @requestBody({ content: { [CONTENT_TYPE.JSON]: { schema: getModelSchemaRef(PatientCareGiverOtherdetails), }, }, }) req: Omit, @inject(RestBindings.Http.REQUEST) request: Request, @inject(AuthenticationBindings.CURRENT_USER) currentUser: IAuthUserWithPermissions, ): Promise { req.patient.externalId = this.createExternalId(); if (req.careGiver) { req.careGiver.patientExternalId = req.patient.externalId; } const patientdetails = new PatientCareGiverOtherdetails(req); patientdetails.others = patientdetails.others ? patientdetails.others : new PatientOthersDetailsDTO(); const patientCareGiver = await this.createPatient( req, request, currentUser, ); patientdetails.others.userId = Number(patientCareGiver.patient.userId); patientdetails.others.patientId = Number(patientCareGiver.patient.id); patientdetails.others.icdCode = patientCareGiver.patient.icdCode; const otherDetails = new PatientOthersDetailsDTO(patientdetails.others); // sonarignore:start // eslint-disable-next-line @typescript-eslint/no-explicit-any otherDetails.symptoms = `{"${otherDetails.symptoms?.join('","')}"}` as any; // sonarignore:end const oters = await this.patientOthersDetailsService.create( otherDetails, request.headers.authorization, ); const careplan = new PatientCarePlanDetails({ externalId: patientCareGiver.patient.externalId, patientId: Number(patientCareGiver.patient.id), carePlanId: req.carePlanId, activated: true, deactivated: false, }); await this.patientCarePlanDetailsService.create( careplan, request.headers.authorization, ); await this.dailyCheckIns.createNewUserContentCards( patientCareGiver?.patient?.id ?? 0, request.headers.authorization ?? '', ); // eslint-disable-next-line @typescript-eslint/no-floating-promises this.dailyCheckIns.createNewUserPendingTasks( patientCareGiver?.patient?.id ?? 0, request.headers.authorization ?? '', ); const others = await this.patientOthersDetailsService.findById( Number(oters.id), request.headers.authorization, ); patientdetails.others = others; patientdetails.carePlanId = req.carePlanId; return patientdetails; } //Get details of a Patient by ID @authenticate(STRATEGY.BEARER, { passReqToCallback: true, }) @authorize({ permissions: [ PermissionKey.ViewPatient, PermissionKey.ViewCaregiver, PermissionKey.ViewOwnPatient, PermissionKey.ViewOwnCaregiver, ], }) @get(`${patientsPath}/{id}`, { description: 'To get one patient detail whom id is provided.', responses: { [STATUS_CODE.OK]: { description: 'Patient model instance', content: { [CONTENT_TYPE.JSON]: { schema: getModelSchemaRef(Patient) } }, }, ...ErrorCodes, [STATUS_CODE.NOT_FOUND]: { description: 'If given id for patient does not exist in database.', }, }, }) async findPatientById( @param.path.number('id') id: number, @inject(RestBindings.Http.REQUEST) request: Request, @inject(AuthenticationBindings.CURRENT_USER) currentUser: IAuthUserWithPermissions, ): Promise { // sonarignore:start const isAccessAllowed = await this.patientCgUserService.isPatientAccessAllowed( currentUser, id, // eslint-disable-next-line @typescript-eslint/no-explicit-any PermissionKey.ViewOwnPatient as any, request.headers.authorization, ); // sonarignore:end if (!isAccessAllowed) { throw new HttpErrors.Forbidden(AuthorizeErrorKeys.NotAllowedAccess); } const patientResp = await this.patientOnbService.findPatientById( id, request.headers.authorization, ); if ( !patientResp.userId || (patientResp.caregiver && !patientResp.caregiver.userId) ) { throw new HttpErrors.NotFound(ERROR_KEYS.PatientCGUserNotFound); } const patientUserInfo = await this.userService.getUserById( patientResp.userId, request.headers.authorization, ); delete patientUserInfo.id; const patientOther = new PatientOtherdetails(patientResp); const patient = Object.assign({}, patientOther, patientUserInfo); if (patientResp.caregiver) { const caregiver = new Caregiver(patientResp.caregiver); const caregiverUserInfo = await this.userService.getUserById( Number(patientResp.caregiver.userId), request.headers.authorization, ); delete caregiverUserInfo.id; Object.assign(patient.caregiver ?? {}, caregiver || {}, caregiverUserInfo || {}); } if (patient.dob) { const age = getAge(new Date(patient.dob)); patient.age = age; } if (patient?.caregiver?.dob) { const age = getAge(new Date(patient.caregiver.dob)); patient.caregiver.age = age; } // Update photo url if (patient.photoUrl) { patient.photoUrl = this.patientCgUserService.getPhotoUrl( patient.photoUrl, ); } if (patient?.caregiver?.photoUrl) { patient.caregiver.photoUrl = this.patientCgUserService.getPhotoUrl( patient.caregiver.photoUrl, ); } const carePlanArr = await this.patientCarePlanDetailsService.findByPatientId( Number(patient.id), request.headers.authorization, ); const careplan = carePlanArr.length ? carePlanArr[0] : ({} as PatientCarePlanDetails); patient.carePlanId = careplan.carePlanId; const otherDetailsArr = await this.patientOthersDetailsService.find( { where: { patientId: Number(patient.id), }, }, request.headers.authorization, ); const otherDetails = otherDetailsArr.length ? otherDetailsArr[0] : ({} as PatientOthersDetailsDTO); patient.others = otherDetails; return patient; } @authenticate(STRATEGY.BEARER, { passReqToCallback: true, }) @authorize({ permissions: [PermissionKey.ViewPatient, PermissionKey.ViewOwnPatient], }) @get(`${patientsPath}/other-details`, { description: 'To get all patients other details.', responses: { [STATUS_CODE.OK]: { description: 'Array of Patient model instances To get all patients other details.', content: { [CONTENT_TYPE.JSON]: { schema: { type: 'array', items: getModelSchemaRef(PatientOthersDetailsDTO), }, }, }, }, ...ErrorCodes, }, }) async findOtherplanDetails( @inject(RestBindings.Http.REQUEST) request: Request, @param.query.object('filter', getFilterSchemaFor(PatientOthersDetailsDTO)) filter?: Filter, ): Promise { return this.patientOthersDetailsService.find( filter, request.headers.authorization, ); } @authenticate(STRATEGY.BEARER, { passReqToCallback: true, }) @authorize({ permissions: [PermissionKey.UpdatePatient, PermissionKey.UpdateOwnPatient], }) @patch(`${patientsPath}/{id}`, { description: updateByIdDesc, responses: { '204': { description: 'Patient PATCH success', content: { [CONTENT_TYPE.JSON]: { schema: getModelSchemaRef(SuccessResponse) }, }, }, ...ErrorCodes, }, }) async updateByPatientId( @param.path.number('id') id: number, @requestBody({ content: { [CONTENT_TYPE.JSON]: { schema: getModelSchemaRef(Object), }, }, }) req: Omit, @inject(RestBindings.Http.REQUEST) request: Request, @inject(AuthenticationBindings.CURRENT_USER) currentUser: IAuthUserWithPermissions, ): Promise { const isAccessAllowed = await this.patientCgUserService.isPatientAccessAllowed( currentUser, id || 0, // sonarignore:start // eslint-disable-next-line @typescript-eslint/no-explicit-any PermissionKey.UpdateOwnPatient as any, // sonarignore:end request.headers.authorization, ); if (!isAccessAllowed) { throw new HttpErrors.Forbidden(AuthorizeErrorKeys.NotAllowedAccess); } const patientDetailsBeforeUpdate = await this.patientCgUserService.getPatientFullDetailsById( id, request.headers.authorization, ); const careGiverToUpdate = req.patient?.caregiver; delete req.patient.caregiver; //@ts-ignore delete req.patient.roleId; this.handleCaregiverAdd( careGiverToUpdate, patientDetailsBeforeUpdate, currentUser, request.headers.authorization, ); await this.patientCgUserService.updatePatientDetailsById( req.patient, currentUser, id, req.carePlanId, request.headers.authorization, ); const patientDetailsAfterUpdate = await this.patientCgUserService.getPatientFullDetailsById( id, request.headers.authorization, ); if (patientDetailsBeforeUpdate.phone !== patientDetailsAfterUpdate.phone) { const tenant = await this.tenantConfigHelper.findTenantProfile( request.headers.authorization, ); const locale = currentUser.userPreferences ? currentUser.userPreferences.locale : tenant.locale ?? process.env.LOCALE; const msgBody = this.i18n.__( { phrase: 'patientOnboardMsg-{{firstName}},{{tenant}},{{appInstallLink}}', locale: locale, }, { firstName: req.patient?.firstName ?? '', tenant: process.env.TENANT_NAME ?? '', appInstallLink: `${process.env.PATIENT_APP_INSTALL_LINK_ANDROID}?tenant_key=${tenant.key}`, app: process.env.APPLICATION ?? '', }, ); // eslint-disable-next-line @typescript-eslint/no-floating-promises this.patientUserService.sendNotification( req.patient, msgBody, request.headers.authorization, ); } if (req.carePlanId) { const carePlanArr = await this.patientCarePlanDetailsService.findByPatientId( id, request.headers.authorization, ); const careplan = carePlanArr.length ? carePlanArr[0] : ({} as PatientCarePlanDetails); const updatePlan = { carePlanId: req.carePlanId, } as PatientCarePlanDetails; await this.patientCarePlanDetailsService.updateById( Number(careplan.id), updatePlan, request.headers.authorization, ); } if (req.others) { // sonarignore:start // eslint-disable-next-line @typescript-eslint/no-explicit-any req.others.symptoms = `{"${req.others.symptoms?.join('","')}"}` as any; // sonarignore:end const otherDetailsArr = await this.patientOthersDetailsService.find( { where: { patientId: id, }, }, request.headers.authorization, ); const otherDetails = otherDetailsArr.length ? otherDetailsArr[0] : ({} as PatientOthersDetailsDTO); await this.patientOthersDetailsService.updateById( Number(otherDetails.id), req.others, request.headers.authorization, ); } return new SuccessResponse({ success: true, }); } createExternalId(length = maxLength) { let result = ''; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const charactersLength = characters.length; // sonarignore:start for (let i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * charactersLength)); } // sonarignore:end return 'ext-' + result; } @authenticate(STRATEGY.BEARER, { passReqToCallback: true, }) @authorize({ permissions: [PermissionKey.ViewPatient, PermissionKey.ViewOwnPatient], }) @get(`${patientsPath}`, { description: 'To get all patients.', responses: { [STATUS_CODE.OK]: { description: 'Array of Patient model instances', content: { [CONTENT_TYPE.JSON]: { schema: { type: 'array', items: getModelSchemaRef(Patient) }, }, }, }, ...ErrorCodes, }, }) async find( @inject(RestBindings.Http.REQUEST) request: Request, @param.query.object('filter', getFilterSchemaFor(Patient)) filter?: Filter, ): Promise<(PatientOtherdetails & User)[]> { const patients = await this.patientOnbService.findAllPatients( request.headers.authorization, filter, ); return Promise.all( patients.map(async p => { const patientUser = await this.patientCgUserService.getPatientDetails( p, request.headers.authorization, ); const patientOther = new PatientOtherdetails({}); const patient = Object.assign({}, patientOther, patientUser); if (patientUser.caregiver) { const caregiver = new Caregiver(patientUser.caregiver); const caregiverUserInfo = await this.userService.getUserById( Number(patientUser.caregiver.userId), request.headers.authorization, ); delete caregiverUserInfo.id; Object.assign(patient.caregiver ?? {}, caregiver || {}, caregiverUserInfo || {}); } if (patient.dob) { const age = getAge(new Date(patient.dob)); patient.age = age; } if (patient?.caregiver?.dob) { const age = getAge(new Date(patient.caregiver.dob)); patient.caregiver.age = age; } const carePlanArr = await this.patientCarePlanDetailsService.findByPatientId( Number(patient.id), request.headers.authorization, ); const careplan = carePlanArr.length ? carePlanArr[0] : ({} as PatientCarePlanDetails); patient.carePlanId = careplan.carePlanId; const otherDetailsArr = await this.patientOthersDetailsService.find( { where: { patientId: Number(patient.id), }, }, request.headers.authorization, ); const otherDetails = otherDetailsArr.length ? otherDetailsArr[0] : ({} as PatientOthersDetailsDTO); patient.others = otherDetails; return patient; }), ); } async createPatient( req: Omit, request: Request, currentUser: IAuthUserWithPermissions, ): Promise { const saved = await this.patientCgUserService.addPatientCaregiver( currentUser, req, request.headers.authorization, ); const user = await this.userService.getUserById( Number(saved.patient.userId), request.headers.authorization, ); const psychiatrist = await this.userService.getUserById( //@ts-ignore Number(saved.patient.createdBy), request.headers.authorization, ); const auditLog = new AuditLogs({ action: AuditLogAction.CreatePatientCaregiver, actionType: AuditLogActionType.Post, actedEntity: `${saved.patient.id}`, reference: 'Patient DB - Patient table, User Table', after: saved, }); // eslint-disable-next-line @typescript-eslint/no-floating-promises this.auditLogService.createAuditLog( auditLog, request.headers.authorization, ); const tenant = await this.tenantConfigHelper.findTenantProfile( request.headers.authorization, ); const locale = currentUser.userPreferences ? currentUser.userPreferences.locale : tenant.locale ?? process.env.LOCALE; const msgBody = this.i18n.__( { phrase: 'patientOnboardMsg-{{firstName}},{{tenant}},{{appInstallLink}},{{app}}', locale: locale, }, { firstName: user.firstName ?? user.phone, tenant: `Dr ${psychiatrist.firstName} `, appInstallLink: `${process.env.PATIENT_APP_INSTALL_LINK_ANDROID}?tenant_key=${tenant.key}`, app: `${process.env.APPLICATION ?? ''}`, }, ); // eslint-disable-next-line @typescript-eslint/no-floating-promises this.patientUserService.sendNotification( saved.patient, msgBody, request.headers.authorization, ); return saved; } // sonarignore:start async handleCaregiverAdd( careGiverToUpdate: any, patientDetailsBeforeUpdate: any, currentUser: IAuthUserWithPermissions, token?: string, ) { // sonarignore:end const caregiver = patientDetailsBeforeUpdate.caregiver; if (caregiver?.id) { if (careGiverToUpdate) { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.updateCaregiver(careGiverToUpdate, currentUser, token); } } else { if (careGiverToUpdate) { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.addCaregiver(careGiverToUpdate, patientDetailsBeforeUpdate, token); } } } // sonarignore:start async addCaregiver(req: any, patient: Patient, token?: string) { // sonarignore:end const careGiverUser = new User({ username: req.phone, phone: req.phone, firstName: req.firstName, middleName: req.middleName, lastName: req.lastName, roleId: req.roleId, dob: req.dob, email: req.email, gender: req.gender, relation: req.relation, externalId: req.patientExternalId, status: UserStatus.INVITATION_SMS_SENT.toString(), }); const newCareGiverUser = await this.userService.createuser( careGiverUser, token, ); delete req.firstName; delete req.middleName; delete req.lastName; delete req.roleId; delete req.dob; delete req.email; delete req.gender; delete req.relation; const caregiver = req; caregiver.userId = newCareGiverUser.id; caregiver.careGroupId = patient.careGroupId; caregiver.status = UserStatus.INVITATION_SMS_SENT.toString(); const newCareGiver = await this.patientOthersDetailsService.createCaregiver( caregiver, token, ); await this.patientOnbService.updatePatientById( patient.id ?? 1, { caregiverId: newCareGiver.id }, token, ); } async updateCaregiver( caregiver: Caregiver, currentUser: IAuthUserWithPermissions, token?: string, ) { //@ts-ignore delete caregiver.roleId; await this.patientCgUserService.updateCaregiverById( caregiver, currentUser, caregiver.id, token, ); } }