434 lines
12 KiB
TypeScript
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',
|
|
),
|
|
};
|
|
}
|