Files
medreport_mrb2b/lib/services/connected-online.service.ts
2025-10-21 17:28:55 +03:00

434 lines
12 KiB
TypeScript

'use server';
import logRequestResult from '@/lib/services/audit.service';
import { RequestStatus } from '@/lib/types/audit';
import {
AvailableAppointmentsResponse,
BookTimeResponse,
ConnectedOnlineMethodName,
FailureReason,
} from '@/lib/types/connected-online';
import { ExternalApi } from '@/lib/types/external';
import { Tables } from '@/packages/supabase/src/database.types';
import axios from 'axios';
import { uniq, uniqBy } from 'lodash';
import { renderBookTimeFailedEmail } from '@kit/email-templates';
import { getLogger } from '@kit/shared/logger';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { TimeSlotResponse } from '~/home/(user)/_components/booking/booking.context';
import { sendEmailFromTemplate } from './mailer.service';
export async function getAvailableAppointmentsForService(
serviceId: number,
key: string,
locationId: number | null,
startTime?: Date,
maxDays?: number,
) {
try {
const start = startTime ? { StartTime: startTime } : {};
const response = await axios.post(
`${process.env.CONNECTED_ONLINE_URL!}/${ConnectedOnlineMethodName.GetAvailabilities}`,
{
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
param: JSON.stringify({
ServiceID: serviceId,
Key: key,
Lang: 'et',
MaxDays: maxDays ?? 120,
LocationId: locationId ?? -1,
...start,
}),
},
);
const responseData: AvailableAppointmentsResponse = JSON.parse(
response.data.d,
);
if (
responseData?.ErrorCode !== 0 ||
!responseData.Data.T_Service?.length ||
!responseData.Data.T_Booking?.length
) {
let comment = `Response returned error code ${responseData.ErrorCode}, message: ${responseData.ErrorMessage}`;
if (responseData?.ErrorCode === 0) {
comment = responseData.Data.T_Service?.length
? `No service present in appointment availability response, service id: ${serviceId}, start time: ${startTime}`
: `No booking times present in appointment availability response, service id: ${serviceId}, start time: ${startTime}`;
}
await logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.GetAvailabilities,
RequestStatus.Fail,
comment,
);
return null;
}
await logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.GetAvailabilities,
RequestStatus.Success,
JSON.stringify(responseData),
);
return responseData.Data;
} catch (error) {
await logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.GetAvailabilities,
RequestStatus.Fail,
JSON.stringify(error),
);
return null;
}
}
export async function bookAppointment(
serviceId: number,
clinicId: number,
appointmentUserId: number,
syncUserID: number,
startTime: string,
comments = '',
): Promise<{
success: boolean;
reason?: FailureReason;
bookingCode: string | null;
clinicKey: string | null;
}> {
const logger = await getLogger();
const supabase = getSupabaseServerClient();
let reason = FailureReason.BOOKING_FAILED;
try {
const {
data: { user },
} = await supabase.auth.getUser();
logger.info(
`Booking time slot ${JSON.stringify({ serviceId, clinicId, startTime, userId: user?.id })}`,
);
if (!user?.id) {
throw new Error('User not authenticated');
}
const formattedStartTime = startTime.replace('T', ' ');
const [
{ data: dbClinic, error: clinicError },
{ data: dbService, error: serviceError },
{ data: account, error: accountError },
{ data: dbReservation, error: dbReservationError },
] = await Promise.all([
supabase
.schema('medreport')
.from('connected_online_providers')
.select('*')
.eq('id', clinicId)
.single(),
supabase
.schema('medreport')
.from('connected_online_services')
.select('*')
.eq('id', serviceId)
.eq('clinic_id', clinicId)
.single(),
supabase
.schema('medreport')
.from('accounts')
.select('name, last_name, personal_code, phone, email')
.eq('is_personal_account', true)
.eq('primary_owner_user_id', user.id)
.single(),
supabase
.schema('medreport')
.from('connected_online_reservation')
.select('id,location_sync_id')
.eq('clinic_id', clinicId)
.eq('service_id', serviceId)
.eq('start_time', formattedStartTime)
.eq('user_id', user.id)
.eq('status', 'PENDING')
.single(),
]);
if (!dbClinic || !dbService) {
const errorMessage = dbClinic
? `Could not find clinic with id ${clinicId}, error: ${JSON.stringify(clinicError)}`
: `Could not find service with sync id ${serviceId} and clinic id ${clinicId}, error: ${JSON.stringify(serviceError)}`;
logger.error(errorMessage);
throw new Error(errorMessage);
}
if (clinicError || serviceError || accountError) {
const stringifiedErrors = JSON.stringify({
clinicError,
serviceError,
accountError,
});
const errorMessage = `Failed to book time, error: ${stringifiedErrors}`;
logger.error(errorMessage);
throw new Error(errorMessage);
}
if (!dbReservation) {
const errorMessage = `No reservation found in db with data ${JSON.stringify({ clinicId, serviceId, startTime, userId: user.id })}, got error ${JSON.stringify(dbReservationError)}`;
logger.error(errorMessage);
throw new Error(errorMessage);
}
const clinic: Tables<
{ schema: 'medreport' },
'connected_online_providers'
> = dbClinic;
const service: Tables<
{ schema: 'medreport' },
'connected_online_services'
> = dbService;
const connectedOnlineBookingResponse = await axios.post(
`${process.env.CONNECTED_ONLINE_URL!}/${ConnectedOnlineMethodName.BookTime}`,
{
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
param: JSON.stringify({
ClinicID: clinic.id,
ServiceID: service.id,
ClinicServiceID: service.sync_id,
UserID: appointmentUserId,
SyncUserID: syncUserID,
StartTime: startTime,
FirstName: account.name,
LastName: account.last_name,
PersonalCode: account.personal_code,
Email: account.email ?? user.email,
Phone: account.phone,
Comments: comments,
AddToBasket: false,
Key: dbClinic.key,
Lang: 'et',
Location: dbReservation.location_sync_id,
}),
},
);
const connectedOnlineBookingResponseData: BookTimeResponse = JSON.parse(
connectedOnlineBookingResponse.data.d,
);
const errorCode = connectedOnlineBookingResponseData?.ErrorCode;
if (errorCode !== 0 || !connectedOnlineBookingResponseData.Value) {
const errorMessage = `Received invalid result from external api, error: ${JSON.stringify(connectedOnlineBookingResponseData)}`;
logger.error(errorMessage);
if (process.env.SUPPORT_EMAIL) {
await sendEmailFromTemplate(
renderBookTimeFailedEmail,
{ reservationId: dbReservation.id, error: errorMessage },
process.env.SUPPORT_EMAIL,
);
}
await supabase
.schema('medreport')
.from('connected_online_reservation')
.update({
status: 'REJECTED',
})
.eq('id', dbReservation.id)
.throwOnError();
if (errorCode === 1) {
reason = FailureReason.TIME_SLOT_UNAVAILABLE;
}
throw new Error(errorMessage);
}
const responseParts = connectedOnlineBookingResponseData.Value.split(',');
const { data: updatedReservation, error } = await supabase
.schema('medreport')
.from('connected_online_reservation')
.update({
booking_code: responseParts[1],
requires_payment: !!responseParts[0],
status: 'CONFIRMED',
})
.eq('id', dbReservation.id)
.select('id')
.single();
if (error) {
throw new Error(
JSON.stringify({ connectedOnlineBookingResponseData, error }),
);
}
logger.info(
'Booked time, updated reservation with id ' + updatedReservation?.id,
);
if (responseParts[1]) {
await logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.BookTime,
RequestStatus.Success,
JSON.stringify(connectedOnlineBookingResponseData),
startTime.toString(),
service.id,
clinicId,
);
return {
success: true,
bookingCode: responseParts[1],
clinicKey: dbClinic.key,
};
} else {
logger.error(`Missing booking code: ${responseParts}`);
await logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.BookTime,
RequestStatus.Fail,
JSON.stringify(error),
startTime.toString(),
serviceId,
clinicId,
);
return { success: false, bookingCode: null, clinicKey: null };
}
} catch (error) {
logger.error(`Failed to book time, error: ${JSON.stringify(error)}`);
await logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.BookTime,
RequestStatus.Fail,
JSON.stringify(error),
startTime.toString(),
serviceId,
clinicId,
);
return { success: false, reason, bookingCode: null, clinicKey: null };
}
}
export async function getConfirmedService(
reservationCode: string,
clinicKey: string,
) {
try {
const url = new URL(process.env.CONNECTED_ONLINE_CONFIRMED_URL!);
url.searchParams.set('code1', reservationCode);
url.searchParams.set('key', clinicKey);
url.searchParams.set('lang', 'et');
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
});
await logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.ConfirmedLoad,
RequestStatus.Success,
'ok',
);
return;
} catch (error) {
await logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.ConfirmedLoad,
RequestStatus.Fail,
error?.toString(),
);
return null;
}
}
export async function getAvailableTimeSlotsForDisplay(
serviceIds: number[],
locationId: number | null,
date?: Date,
): Promise<TimeSlotResponse> {
const supabase = getSupabaseServerClient();
const { data: syncedServices } = await supabase
.schema('medreport')
.from('connected_online_services')
.select(
'*, providerClinic:clinic_id(*,locations:connected_online_locations(*))',
)
.in('id', serviceIds)
.throwOnError();
const timeSlotPromises = [];
for (const syncedService of syncedServices) {
const timeSlotsPromise = getAvailableAppointmentsForService(
syncedService.id,
syncedService.providerClinic.key,
locationId,
date,
);
timeSlotPromises.push(timeSlotsPromise);
}
const timeSlots = await Promise.all(timeSlotPromises);
const mappedTimeSlots = [];
for (const timeSlotGroup of timeSlots) {
const { data: serviceProviders } = await supabase
.schema('medreport')
.from('connected_online_service_providers')
.select(
'name, id, jobTitleEn: job_title_en, jobTitleEt: job_title_et, jobTitleRu: job_title_ru, clinicId: clinic_id',
)
.in(
'clinic_id',
uniq(timeSlotGroup?.T_Booking.map(({ ClinicID }) => ClinicID)),
)
.throwOnError();
const timeSlots =
timeSlotGroup?.T_Booking?.map((item) => {
return {
...item,
serviceProvider: serviceProviders.find(
({ id }) => id === item.UserID,
),
syncedService: syncedServices.find(
(syncedService) => syncedService.sync_id === item.ServiceID,
),
location: syncedServices
.find(
({ providerClinic }) =>
providerClinic.id === Number(item.ClinicID),
)
?.providerClinic?.locations?.find(
(location) => location.sync_id === item.LocationID,
),
};
}) ?? [];
mappedTimeSlots.push(...timeSlots);
}
return {
timeSlots: mappedTimeSlots,
locations: uniqBy(
syncedServices.flatMap(({ providerClinic }) => providerClinic.locations),
'id',
),
};
}