Merge branch 'develop' into MED-97
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
'use server';
|
||||
|
||||
import { RequestStatus } from '@/lib/types/audit';
|
||||
import { RequestStatus, SyncStatus } from '@/lib/types/audit';
|
||||
import { ConnectedOnlineMethodName } from '@/lib/types/connected-online';
|
||||
import { ExternalApi } from '@/lib/types/external';
|
||||
import { MedipostAction } from '@/lib/types/medipost';
|
||||
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
||||
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
||||
|
||||
export default async function logRequestResult(
|
||||
/* personalCode: string, */ requestApi: keyof typeof ExternalApi,
|
||||
requestApi: keyof typeof ExternalApi,
|
||||
requestApiMethod: `${ConnectedOnlineMethodName}` | `${MedipostAction}`,
|
||||
status: RequestStatus,
|
||||
comment?: string,
|
||||
@@ -16,11 +15,10 @@ export default async function logRequestResult(
|
||||
serviceId?: number,
|
||||
serviceProviderId?: number,
|
||||
) {
|
||||
const { error } = await getSupabaseServerClient()
|
||||
const { error } = await getSupabaseServerAdminClient()
|
||||
.schema('audit')
|
||||
.from('request_entries')
|
||||
.insert({
|
||||
/* personal_code: personalCode, */
|
||||
request_api: requestApi,
|
||||
request_api_method: requestApiMethod,
|
||||
requested_start_date: startTime,
|
||||
@@ -69,3 +67,29 @@ export async function getMedipostDispatchTries(medusaOrderId: string) {
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function logSyncResult({
|
||||
operation,
|
||||
comment,
|
||||
status,
|
||||
changed_by_role,
|
||||
}: {
|
||||
operation: string;
|
||||
comment?: string;
|
||||
status: SyncStatus;
|
||||
changed_by_role: string;
|
||||
}) {
|
||||
const { error } = await getSupabaseServerAdminClient()
|
||||
.schema('audit')
|
||||
.from('sync_entries')
|
||||
.insert({
|
||||
operation,
|
||||
comment,
|
||||
status,
|
||||
changed_by_role,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error('Failed to insert log entry, error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
40
lib/services/audit/cartEntries.ts
Normal file
40
lib/services/audit/cartEntries.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
export const createCartEntriesLog = async ({
|
||||
operation,
|
||||
accountId,
|
||||
cartId,
|
||||
variantId,
|
||||
comment,
|
||||
}: {
|
||||
operation: string;
|
||||
accountId: string;
|
||||
cartId: string;
|
||||
variantId?: string;
|
||||
comment?: string;
|
||||
}) => {
|
||||
try {
|
||||
const supabase = getSupabaseServerClient();
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
error: userError,
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (userError || !user) {
|
||||
console.error('No authenticated user found; skipping audit insert');
|
||||
return;
|
||||
}
|
||||
|
||||
return supabase.schema('audit').from('cart_entries').insert({
|
||||
operation,
|
||||
account_id: accountId,
|
||||
cart_id: cartId,
|
||||
changed_by: user.id,
|
||||
variant_id: variantId,
|
||||
comment,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to insert doctor page view log', error);
|
||||
}
|
||||
};
|
||||
@@ -6,6 +6,7 @@ export enum PageViewAction {
|
||||
REGISTRATION_SUCCESS = 'REGISTRATION_SUCCESS',
|
||||
VIEW_ORDER_ANALYSIS = 'VIEW_ORDER_ANALYSIS',
|
||||
VIEW_TEAM_ACCOUNT_DASHBOARD = 'VIEW_TEAM_ACCOUNT_DASHBOARD',
|
||||
VIEW_TTO_SERVICE_BOOKING = 'VIEW_TTO_SERVICE_BOOKING',
|
||||
}
|
||||
|
||||
export const createPageViewLog = async ({
|
||||
@@ -37,6 +38,7 @@ export const createPageViewLog = async ({
|
||||
account_id: accountId,
|
||||
action,
|
||||
changed_by: user.id,
|
||||
extra_data: extraData,
|
||||
})
|
||||
.throwOnError();
|
||||
} catch (error) {
|
||||
|
||||
@@ -7,19 +7,30 @@ import {
|
||||
BookTimeResponse,
|
||||
ConfirmedLoadResponse,
|
||||
ConnectedOnlineMethodName,
|
||||
FailureReason,
|
||||
} from '@/lib/types/connected-online';
|
||||
import { ExternalApi } from '@/lib/types/external';
|
||||
import { Tables } from '@/packages/supabase/src/database.types';
|
||||
import { createClient } from '@/utils/supabase/server';
|
||||
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 showTimesFrom = startTime ? { StartTime: startTime } : {};
|
||||
|
||||
const start = startTime ? { StartTime: startTime } : {};
|
||||
const response = await axios.post(
|
||||
`${process.env.CONNECTED_ONLINE_URL!}/${ConnectedOnlineMethodName.GetAvailabilities}`,
|
||||
{
|
||||
@@ -28,9 +39,11 @@ export async function getAvailableAppointmentsForService(
|
||||
},
|
||||
param: JSON.stringify({
|
||||
ServiceID: serviceId,
|
||||
Key: '7T624nlu',
|
||||
Key: key,
|
||||
Lang: 'et',
|
||||
...showTimesFrom,
|
||||
MaxDays: maxDays ?? 120,
|
||||
LocationId: locationId ?? -1,
|
||||
...start,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -80,157 +93,210 @@ export async function getAvailableAppointmentsForService(
|
||||
}
|
||||
|
||||
export async function bookAppointment(
|
||||
serviceSyncId: number,
|
||||
serviceId: number,
|
||||
clinicId: number,
|
||||
appointmentUserId: number,
|
||||
syncUserID: number,
|
||||
startTime: string,
|
||||
locationId = 0,
|
||||
comments = '',
|
||||
isEarlierTimeRequested = false,
|
||||
earlierTimeRequestComment = '',
|
||||
) {
|
||||
const supabase = await createClient();
|
||||
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)
|
||||
.limit(1),
|
||||
.single(),
|
||||
supabase
|
||||
.schema('medreport')
|
||||
.from('connected_online_services')
|
||||
.select('*')
|
||||
.eq('sync_id', serviceSyncId)
|
||||
.eq('id', serviceId)
|
||||
.eq('clinic_id', clinicId)
|
||||
.limit(1),
|
||||
.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?.length || !dbService?.length) {
|
||||
return logRequestResult(
|
||||
ExternalApi.ConnectedOnline,
|
||||
ConnectedOnlineMethodName.BookTime,
|
||||
RequestStatus.Fail,
|
||||
dbClinic?.length
|
||||
? `Could not find clinic with id ${clinicId}, error: ${JSON.stringify(clinicError)}`
|
||||
: `Could not find service with sync id ${serviceSyncId} and clinic id ${clinicId}, error: ${JSON.stringify(serviceError)}`,
|
||||
startTime,
|
||||
serviceSyncId,
|
||||
clinicId,
|
||||
);
|
||||
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![0];
|
||||
> = dbClinic;
|
||||
const service: Tables<
|
||||
{ schema: 'medreport' },
|
||||
'connected_online_services'
|
||||
> = dbService![0];
|
||||
> = dbService;
|
||||
|
||||
// TODO the dummy data needs to be replaced with real values once they're present on the user/account
|
||||
const response = await axios.post(
|
||||
const connectedOnlineBookingResponse = await axios.post(
|
||||
`${process.env.CONNECTED_ONLINE_URL!}/${ConnectedOnlineMethodName.BookTime}`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
param: JSON.stringify({
|
||||
EarlierTime: isEarlierTimeRequested, // once we have the e-shop functionality we can let the user select if she would like to be offered earlier time slots if they become available
|
||||
EarlierTimeComment: earlierTimeRequestComment,
|
||||
ClinicID: clinic.id,
|
||||
ServiceID: service.id,
|
||||
ClinicServiceID: service.sync_id,
|
||||
ServiceID: service.sync_id,
|
||||
ClinicServiceID: service.id,
|
||||
UserID: appointmentUserId,
|
||||
SyncUserID: syncUserID,
|
||||
StartTime: startTime,
|
||||
FirstName: 'Test',
|
||||
LastName: 'User',
|
||||
PersonalCode: '4',
|
||||
Email: user.email,
|
||||
Phone: 'phone',
|
||||
FirstName: account.name,
|
||||
LastName: account.last_name,
|
||||
PersonalCode: account.personal_code,
|
||||
Email: account.email ?? user.email,
|
||||
Phone: account.phone,
|
||||
Comments: comments,
|
||||
Location: locationId,
|
||||
FreeCode: '',
|
||||
AddToBasket: false,
|
||||
Key: '7T624nlu',
|
||||
Lang: 'et', // update when integrated into app, if needed
|
||||
Key: dbClinic.key,
|
||||
Lang: 'et',
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const responseData: BookTimeResponse = JSON.parse(response.data.d);
|
||||
const connectedOnlineBookingResponseData: BookTimeResponse = JSON.parse(
|
||||
connectedOnlineBookingResponse.data.d,
|
||||
);
|
||||
|
||||
if (responseData?.ErrorCode !== 0 || !responseData.Value) {
|
||||
return logRequestResult(
|
||||
ExternalApi.ConnectedOnline,
|
||||
ConnectedOnlineMethodName.BookTime,
|
||||
RequestStatus.Fail,
|
||||
JSON.stringify(responseData),
|
||||
startTime,
|
||||
service.id,
|
||||
clinicId,
|
||||
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 }),
|
||||
);
|
||||
}
|
||||
|
||||
const responseParts = responseData.Value.split(',');
|
||||
|
||||
const { error } = await supabase
|
||||
.schema('medreport')
|
||||
.from('connected_online_reservation')
|
||||
.insert({
|
||||
booking_code: responseParts[1],
|
||||
clinic_id: clinic.id,
|
||||
comments,
|
||||
lang: 'et', // change later, if needed
|
||||
service_id: service.id,
|
||||
service_user_id: appointmentUserId,
|
||||
start_time: startTime,
|
||||
sync_user_id: syncUserID,
|
||||
requires_payment: !!responseParts[0],
|
||||
user_id: user.id,
|
||||
});
|
||||
logger.info(
|
||||
'Booked time, updated reservation with id ' + updatedReservation?.id,
|
||||
);
|
||||
|
||||
await logRequestResult(
|
||||
ExternalApi.ConnectedOnline,
|
||||
ConnectedOnlineMethodName.BookTime,
|
||||
RequestStatus.Success,
|
||||
JSON.stringify(responseData),
|
||||
startTime,
|
||||
JSON.stringify(connectedOnlineBookingResponseData),
|
||||
startTime.toString(),
|
||||
service.id,
|
||||
clinicId,
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
return responseData.Value;
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return logRequestResult(
|
||||
logger.error(`Failed to book time, error: ${JSON.stringify(error)}`);
|
||||
await logRequestResult(
|
||||
ExternalApi.ConnectedOnline,
|
||||
ConnectedOnlineMethodName.BookTime,
|
||||
RequestStatus.Fail,
|
||||
JSON.stringify(error),
|
||||
startTime,
|
||||
serviceSyncId,
|
||||
startTime.toString(),
|
||||
serviceId,
|
||||
clinicId,
|
||||
);
|
||||
return { success: false, reason };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,8 +336,83 @@ export async function getConfirmedService(reservationCode: string) {
|
||||
ExternalApi.ConnectedOnline,
|
||||
ConnectedOnlineMethodName.ConfirmedLoad,
|
||||
RequestStatus.Fail,
|
||||
JSON.stringify(error),
|
||||
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',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ import axios from 'axios';
|
||||
import { toArray } from '@kit/shared/utils';
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
|
||||
import type { AnalysisOrder } from '~/lib/types/analysis-order';
|
||||
import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element';
|
||||
import type { AnalysisOrder } from '~/lib/types/order';
|
||||
|
||||
import { getAccountAdmin } from '../account.service';
|
||||
import { getAnalyses } from '../analyses.service';
|
||||
|
||||
@@ -2,12 +2,19 @@
|
||||
|
||||
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
|
||||
import { MontonioOrderHandlerService } from '@/packages/billing/montonio/src';
|
||||
import { addToCart, deleteLineItem } from '@lib/data/cart';
|
||||
import { addToCart, deleteLineItem, retrieveCart } from '@lib/data/cart';
|
||||
import { getCartId } from '@lib/data/cookies';
|
||||
import { StoreCartLineItem, StoreProductVariant } from '@medusajs/types';
|
||||
import { isSameMinute } from 'date-fns';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import {
|
||||
cancelReservation,
|
||||
getOrderedTtoServices,
|
||||
} from '~/lib/services/reservation.service';
|
||||
|
||||
import { createCartEntriesLog } from './audit/cartEntries';
|
||||
import { getAvailableAppointmentsForService } from './connected-online.service';
|
||||
|
||||
const env = () =>
|
||||
z
|
||||
@@ -35,53 +42,44 @@ export async function handleAddToCart({
|
||||
selectedVariant: Pick<StoreProductVariant, 'id'>;
|
||||
countryCode: string;
|
||||
}) {
|
||||
const supabase = getSupabaseServerClient();
|
||||
const { account, user } = await loadCurrentUserAccount();
|
||||
const { account } = await loadCurrentUserAccount();
|
||||
if (!account) {
|
||||
throw new Error('Account not found');
|
||||
}
|
||||
|
||||
const quantity = 1;
|
||||
const cart = await addToCart({
|
||||
const { newCart, addedItem } = await addToCart({
|
||||
variantId: selectedVariant.id,
|
||||
quantity,
|
||||
countryCode,
|
||||
});
|
||||
|
||||
const { error } = await supabase.schema('audit').from('cart_entries').insert({
|
||||
variant_id: selectedVariant.id,
|
||||
await createCartEntriesLog({
|
||||
variantId: selectedVariant.id,
|
||||
operation: 'ADD_TO_CART',
|
||||
account_id: account.id,
|
||||
cart_id: cart.id,
|
||||
changed_by: user.id,
|
||||
accountId: account.id,
|
||||
cartId: newCart.id,
|
||||
});
|
||||
if (error) {
|
||||
throw new Error('Error logging cart entry: ' + error.message);
|
||||
}
|
||||
|
||||
return cart;
|
||||
return { cart: newCart, addedItem };
|
||||
}
|
||||
|
||||
export async function handleDeleteCartItem({ lineId }: { lineId: string }) {
|
||||
await deleteLineItem(lineId);
|
||||
await cancelReservation(lineId);
|
||||
|
||||
const supabase = getSupabaseServerClient();
|
||||
const cartId = await getCartId();
|
||||
const { account, user } = await loadCurrentUserAccount();
|
||||
const { account } = await loadCurrentUserAccount();
|
||||
if (!account) {
|
||||
throw new Error('Account not found');
|
||||
}
|
||||
|
||||
const { error } = await supabase.schema('audit').from('cart_entries').insert({
|
||||
variant_id: lineId,
|
||||
await createCartEntriesLog({
|
||||
variantId: lineId,
|
||||
operation: 'REMOVE_FROM_CART',
|
||||
account_id: account.id,
|
||||
cart_id: cartId!,
|
||||
changed_by: user.id,
|
||||
accountId: account.id,
|
||||
cartId: cartId!,
|
||||
});
|
||||
if (error) {
|
||||
throw new Error('Error logging cart entry: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleNavigateToPayment({
|
||||
@@ -97,12 +95,43 @@ export async function handleNavigateToPayment({
|
||||
currencyCode: string;
|
||||
cartId: string;
|
||||
}) {
|
||||
const supabase = getSupabaseServerClient();
|
||||
const { account, user } = await loadCurrentUserAccount();
|
||||
const { account } = await loadCurrentUserAccount();
|
||||
if (!account) {
|
||||
throw new Error('Account not found');
|
||||
}
|
||||
|
||||
const cart = await retrieveCart();
|
||||
if (!cart) {
|
||||
throw new Error('No cart found');
|
||||
}
|
||||
const orderedTtoServices = await getOrderedTtoServices({ cart });
|
||||
|
||||
if (orderedTtoServices?.length) {
|
||||
const unavailableLineItemIds: string[] = [];
|
||||
for (const ttoService of orderedTtoServices) {
|
||||
const availabilities = await getAvailableAppointmentsForService(
|
||||
ttoService.service_id,
|
||||
ttoService.provider.key,
|
||||
ttoService.location_sync_id,
|
||||
new Date(ttoService.start_time),
|
||||
1,
|
||||
);
|
||||
const isAvailable = availabilities?.T_Booking?.length
|
||||
? availabilities.T_Booking.find((timeSlot) =>
|
||||
isSameMinute(ttoService.start_time, timeSlot.StartTime),
|
||||
)
|
||||
: false;
|
||||
|
||||
if (!isAvailable) {
|
||||
unavailableLineItemIds.push(ttoService.medusa_cart_line_item_id!);
|
||||
}
|
||||
}
|
||||
|
||||
if (unavailableLineItemIds.length) {
|
||||
return { unavailableLineItemIds };
|
||||
}
|
||||
}
|
||||
|
||||
const paymentLink =
|
||||
await new MontonioOrderHandlerService().getMontonioPaymentLink({
|
||||
notificationUrl: `${env().medusaBackendPublicUrl}/hooks/payment/montonio_montonio`,
|
||||
@@ -114,17 +143,13 @@ export async function handleNavigateToPayment({
|
||||
merchantReference: `${account.id}:${paymentSessionId}:${cartId}`,
|
||||
});
|
||||
|
||||
const { error } = await supabase.schema('audit').from('cart_entries').insert({
|
||||
await createCartEntriesLog({
|
||||
operation: 'NAVIGATE_TO_PAYMENT',
|
||||
account_id: account.id,
|
||||
cart_id: cartId,
|
||||
changed_by: user.id,
|
||||
accountId: account.id,
|
||||
cartId: cart.id,
|
||||
});
|
||||
if (error) {
|
||||
throw new Error('Error logging cart entry: ' + error.message);
|
||||
}
|
||||
|
||||
return paymentLink;
|
||||
return { url: paymentLink };
|
||||
}
|
||||
|
||||
export async function handleLineItemTimeout({
|
||||
@@ -132,21 +157,16 @@ export async function handleLineItemTimeout({
|
||||
}: {
|
||||
lineItem: StoreCartLineItem;
|
||||
}) {
|
||||
const supabase = getSupabaseServerClient();
|
||||
const { account, user } = await loadCurrentUserAccount();
|
||||
const { account } = await loadCurrentUserAccount();
|
||||
if (!account) {
|
||||
throw new Error('Account not found');
|
||||
}
|
||||
|
||||
await deleteLineItem(lineItem.id);
|
||||
|
||||
const { error } = await supabase.schema('audit').from('cart_entries').insert({
|
||||
await createCartEntriesLog({
|
||||
operation: 'LINE_ITEM_TIMEOUT',
|
||||
account_id: account.id,
|
||||
cart_id: lineItem.cart_id,
|
||||
changed_by: user.id,
|
||||
accountId: account.id,
|
||||
cartId: lineItem.cart_id,
|
||||
});
|
||||
if (error) {
|
||||
throw new Error('Error logging cart entry: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Tables } from '@kit/supabase/database';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import type { AnalysisOrder } from '../types/analysis-order';
|
||||
import type { AnalysisOrder, TTOOrder } from '../types/order';
|
||||
|
||||
export async function createAnalysisOrder({
|
||||
medusaOrder,
|
||||
@@ -129,3 +129,39 @@ export async function getAnalysisOrdersAdmin({
|
||||
.throwOnError();
|
||||
return orders.data;
|
||||
}
|
||||
|
||||
export async function getTtoOrders({
|
||||
orderStatus,
|
||||
lineItemIds,
|
||||
}: {
|
||||
orderStatus?: TTOOrder['status'];
|
||||
lineItemIds?: string[];
|
||||
} = {}) {
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await client.auth.getUser();
|
||||
if (!user) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
const query = client
|
||||
.schema('medreport')
|
||||
.from('connected_online_reservation')
|
||||
.select('*')
|
||||
.eq('user_id', user.id);
|
||||
|
||||
if (orderStatus) {
|
||||
query.eq('status', orderStatus);
|
||||
}
|
||||
|
||||
if (lineItemIds?.length) {
|
||||
query.in('medusa_cart_line_item_id', lineItemIds);
|
||||
}
|
||||
|
||||
const orders = await query
|
||||
.order('created_at', { ascending: false })
|
||||
.throwOnError();
|
||||
return orders.data;
|
||||
}
|
||||
|
||||
343
lib/services/reservation.service.ts
Normal file
343
lib/services/reservation.service.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
'use server';
|
||||
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
import { listRegions } from '@lib/data/regions';
|
||||
import { StoreCart, StoreOrder } from '@medusajs/types';
|
||||
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { EnrichedCartItem } from '../../app/home/(user)/_components/cart/types';
|
||||
import { loadCurrentUserAccount } from '../../app/home/(user)/_lib/server/load-user-account';
|
||||
import { createCartEntriesLog } from './audit/cartEntries';
|
||||
import { handleDeleteCartItem } from './medusaCart.service';
|
||||
|
||||
type Locations = Tables<{ schema: 'medreport' }, 'connected_online_locations'>;
|
||||
type Services = Tables<{ schema: 'medreport' }, 'connected_online_services'>;
|
||||
type ServiceProviders = Tables<
|
||||
{ schema: 'medreport' },
|
||||
'connected_online_service_providers'
|
||||
>;
|
||||
|
||||
export async function getCartReservations(
|
||||
medusaCart: StoreCart,
|
||||
): Promise<EnrichedCartItem[]> {
|
||||
const supabase = getSupabaseServerClient();
|
||||
|
||||
const cartLineItemIds = medusaCart.items?.map(({ id }) => id);
|
||||
|
||||
if (!cartLineItemIds?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { data: reservations } = await supabase
|
||||
.schema('medreport')
|
||||
.from('connected_online_reservation')
|
||||
.select(
|
||||
'id, startTime:start_time, service:service_id, location:location_sync_id, serviceProvider:service_user_id, medusaCartLineItemId:medusa_cart_line_item_id',
|
||||
)
|
||||
.in('medusa_cart_line_item_id', cartLineItemIds)
|
||||
.throwOnError();
|
||||
|
||||
const locationSyncIds: number[] =
|
||||
reservations
|
||||
?.filter((reservation) => !!reservation.location)
|
||||
.map((reservation) => reservation.location!) ?? [];
|
||||
const serviceIds =
|
||||
reservations?.map((reservation) => reservation.service) ?? [];
|
||||
const serviceProviderIds =
|
||||
reservations.map((reservation) => reservation.serviceProvider) ?? [];
|
||||
|
||||
let locations:
|
||||
| {
|
||||
syncId: Locations['sync_id'];
|
||||
name: Locations['name'];
|
||||
address: Locations['address'];
|
||||
}[]
|
||||
| null = null;
|
||||
if (locationSyncIds.length) {
|
||||
({ data: locations } = await supabase
|
||||
.schema('medreport')
|
||||
.from('connected_online_locations')
|
||||
.select('syncId:sync_id, name, address')
|
||||
.in('sync_id', locationSyncIds)
|
||||
.throwOnError());
|
||||
}
|
||||
|
||||
let services:
|
||||
| {
|
||||
id: Services['id'];
|
||||
name: Services['name'];
|
||||
}[]
|
||||
| null = null;
|
||||
if (serviceIds.length) {
|
||||
({ data: services } = await supabase
|
||||
.schema('medreport')
|
||||
.from('connected_online_services')
|
||||
.select('name, id')
|
||||
.in('id', serviceIds)
|
||||
.throwOnError());
|
||||
}
|
||||
|
||||
let serviceProviders:
|
||||
| {
|
||||
id: ServiceProviders['id'];
|
||||
name: ServiceProviders['name'];
|
||||
jobTitleEt: ServiceProviders['job_title_et'];
|
||||
jobTitleEn: ServiceProviders['job_title_en'];
|
||||
jobTitleRu: ServiceProviders['job_title_ru'];
|
||||
spokenLanguages: ServiceProviders['spoken_languages'];
|
||||
}[]
|
||||
| null = null;
|
||||
if (serviceProviderIds.length) {
|
||||
({ data: serviceProviders } = await supabase
|
||||
.schema('medreport')
|
||||
.from('connected_online_service_providers')
|
||||
.select(
|
||||
'id, name, jobTitleEt:job_title_et, jobTitleEn:job_title_en, jobTitleRu:job_title_ru, spokenLanguages:spoken_languages',
|
||||
)
|
||||
.in('id', serviceProviderIds)
|
||||
.throwOnError());
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const reservation of reservations) {
|
||||
if (reservation.medusaCartLineItemId === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cartLineItem = medusaCart.items?.find(
|
||||
(item) => item.id === reservation.medusaCartLineItemId,
|
||||
);
|
||||
|
||||
if (!cartLineItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const location = locations?.find(
|
||||
(location) => location.syncId === reservation.location,
|
||||
);
|
||||
const service = services?.find(
|
||||
(service) => service.id === reservation.service,
|
||||
);
|
||||
const serviceProvider = serviceProviders?.find(
|
||||
(serviceProvider) => serviceProvider.id === reservation.serviceProvider,
|
||||
);
|
||||
|
||||
const countryCodes = await listRegions().then((regions) =>
|
||||
regions?.map((r) => r.countries?.map((c) => c.iso_2)).flat(),
|
||||
);
|
||||
|
||||
const enrichedReservation = {
|
||||
...reservation,
|
||||
location,
|
||||
service,
|
||||
serviceProvider,
|
||||
};
|
||||
|
||||
results.push({
|
||||
...cartLineItem,
|
||||
reservation: {
|
||||
...enrichedReservation,
|
||||
medusaCartLineItemId: reservation.medusaCartLineItemId!,
|
||||
countryCode: countryCodes[0],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function createInitialReservation({
|
||||
serviceId,
|
||||
clinicId,
|
||||
appointmentUserId,
|
||||
syncUserID,
|
||||
startTime,
|
||||
medusaLineItemId,
|
||||
locationId,
|
||||
comments = '',
|
||||
}: {
|
||||
serviceId: number;
|
||||
clinicId: number;
|
||||
appointmentUserId: number;
|
||||
syncUserID: number;
|
||||
startTime: Date;
|
||||
medusaLineItemId: string;
|
||||
locationId?: number | null;
|
||||
comments?: string;
|
||||
}) {
|
||||
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({
|
||||
cart,
|
||||
medusaOrder,
|
||||
}: {
|
||||
cart?: StoreCart;
|
||||
medusaOrder?: StoreOrder;
|
||||
}) {
|
||||
const supabase = getSupabaseServerClient();
|
||||
|
||||
if (!medusaOrder && !cart) {
|
||||
throw new Error('No cart or medusa order provided');
|
||||
}
|
||||
|
||||
const ttoReservationIds: number[] =
|
||||
(medusaOrder?.items ?? cart?.items)
|
||||
?.filter(({ metadata }) => !!metadata?.connectedOnlineReservationId)
|
||||
.map(({ metadata }) => Number(metadata!.connectedOnlineReservationId)) ??
|
||||
[];
|
||||
|
||||
const { data: orderedTtoServices } = await supabase
|
||||
.schema('medreport')
|
||||
.from('connected_online_reservation')
|
||||
.select('*, provider:connected_online_providers(key)')
|
||||
.in('id', ttoReservationIds)
|
||||
.throwOnError();
|
||||
|
||||
return orderedTtoServices;
|
||||
}
|
||||
|
||||
export async function updateReservationTime({
|
||||
reservationId,
|
||||
newStartTime,
|
||||
newServiceId,
|
||||
newAppointmentUserId,
|
||||
newSyncUserId,
|
||||
newLocationId, // TODO stop allowing null when Connected starts returning the correct ids instead of -1
|
||||
cartId,
|
||||
}: {
|
||||
reservationId: number;
|
||||
newStartTime: Date;
|
||||
newServiceId: number;
|
||||
newAppointmentUserId: number;
|
||||
newSyncUserId: number;
|
||||
newLocationId: number | null;
|
||||
cartId: string;
|
||||
}) {
|
||||
const logger = await getLogger();
|
||||
const supabase = getSupabaseServerClient();
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
const userId = user?.id;
|
||||
const { account } = await loadCurrentUserAccount();
|
||||
|
||||
if (!userId || !account) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
const reservationData = JSON.stringify({
|
||||
reservationId,
|
||||
newStartTime,
|
||||
newServiceId,
|
||||
newAppointmentUserId,
|
||||
newSyncUserId,
|
||||
newLocationId,
|
||||
userId,
|
||||
cartId,
|
||||
});
|
||||
|
||||
logger.info('Updating reservation' + reservationData);
|
||||
try {
|
||||
await supabase
|
||||
.schema('medreport')
|
||||
.from('connected_online_reservation')
|
||||
.update({
|
||||
service_id: newServiceId,
|
||||
service_user_id: newAppointmentUserId,
|
||||
sync_user_id: newSyncUserId,
|
||||
start_time: newStartTime.toString(),
|
||||
location_sync_id: newLocationId,
|
||||
})
|
||||
.eq('id', reservationId)
|
||||
.eq('user_id', user.id)
|
||||
.throwOnError();
|
||||
|
||||
logger.info(`Successfully updated reservation ${reservationData}`);
|
||||
await createCartEntriesLog({
|
||||
operation: 'CHANGE_RESERVATION',
|
||||
accountId: account.id,
|
||||
cartId: cartId,
|
||||
comment: `${reservationData}`,
|
||||
});
|
||||
revalidatePath('/home/cart', 'layout');
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update reservation ${reservationData}`);
|
||||
await createCartEntriesLog({
|
||||
operation: 'CHANGE_RESERVATION',
|
||||
accountId: account.id,
|
||||
cartId: cartId,
|
||||
comment: `${e}`,
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user