'use server'; import logRequestResult from '@/lib/services/audit.service'; import { RequestStatus } from '@/lib/types/audit'; import { AvailableAppointmentsResponse, BookTimeResponse, ConfirmedLoadResponse, ConnectedOnlineMethodName, FailureReason, } from '@/lib/types/connected-online'; import { ExternalApi } from '@/lib/types/external'; import { Tables } from '@/packages/supabase/src/database.types'; import { StoreOrder } from '@medusajs/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'; import { handleDeleteCartItem } from './medusaCart.service'; export async function getAvailableAppointmentsForService( serviceId: number, key: string, locationId: number | null, startTime?: Date, ) { 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: 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 = '', ) { 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') .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', }), }, ); 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, ); await logRequestResult( ExternalApi.ConnectedOnline, ConnectedOnlineMethodName.BookTime, RequestStatus.Success, JSON.stringify(connectedOnlineBookingResponseData), startTime.toString(), service.id, clinicId, ); return { success: true }; } 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 }; } } export async function getConfirmedService(reservationCode: string) { try { const response = await axios.post( `${process.env.CONNECTED_ONLINE_URL!}/${ConnectedOnlineMethodName.ConfirmedLoad}`, { headers: { 'Content-Type': 'application/json; charset=utf-8', }, param: JSON.stringify({ Value: `${reservationCode}|7T624nlu|et` }), }, ); const responseData: ConfirmedLoadResponse = JSON.parse(response.data.d); if (responseData?.ErrorCode !== 0) { await logRequestResult( ExternalApi.ConnectedOnline, ConnectedOnlineMethodName.ConfirmedLoad, RequestStatus.Fail, JSON.stringify(responseData), ); return null; } await logRequestResult( ExternalApi.ConnectedOnline, ConnectedOnlineMethodName.ConfirmedLoad, RequestStatus.Success, JSON.stringify(responseData), ); return responseData.Data; } 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 { 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', ), }; } export async function createInitialReservation( serviceId: number, clinicId: number, appointmentUserId: number, syncUserID: number, startTime: Date, medusaLineItemId: string, locationId?: number | null, comments = '', ) { const logger = await getLogger(); const supabase = getSupabaseServerClient(); const { data: { user }, } = await supabase.auth.getUser(); const userId = user?.id; if (!userId) { throw new Error('User not authenticated'); } logger.info( 'Creating reservation' + JSON.stringify({ serviceId, clinicId, startTime, userId }), ); try { const { data: createdReservation } = await supabase .schema('medreport') .from('connected_online_reservation') .insert({ clinic_id: clinicId, comments, lang: 'et', service_id: serviceId, service_user_id: appointmentUserId, start_time: startTime.toString(), sync_user_id: syncUserID, user_id: userId, status: 'PENDING', medusa_cart_line_item_id: medusaLineItemId, location_sync_id: locationId, }) .select('id') .single() .throwOnError(); logger.info( `Created reservation ${JSON.stringify({ createdReservation, userId })}`, ); return createdReservation; } catch (e) { logger.error( `Failed to create initial reservation ${JSON.stringify({ serviceId, clinicId, startTime })} ${e}`, ); await handleDeleteCartItem({ lineId: medusaLineItemId }); throw e; } } export async function cancelReservation(medusaLineItemId: string) { const supabase = getSupabaseServerClient(); return supabase .schema('medreport') .from('connected_online_reservation') .update({ status: 'CANCELLED', }) .eq('medusa_cart_line_item_id', medusaLineItemId) .throwOnError(); } export async function getOrderedTtoServices({ medusaOrder, }: { medusaOrder: StoreOrder; }) { const supabase = getSupabaseServerClient(); const ttoReservationIds: number[] = medusaOrder.items ?.filter(({ metadata }) => !!metadata?.connectedOnlineReservationId) .map(({ metadata }) => Number(metadata!.connectedOnlineReservationId)) ?? []; const { data: orderedTtoServices } = await supabase .schema('medreport') .from('connected_online_reservation') .select('*') .in('id', ttoReservationIds) .throwOnError(); return orderedTtoServices; }