add cart functionality for tto services

This commit is contained in:
Helena
2025-09-19 16:23:19 +03:00
parent 3c272505d6
commit b59148630a
26 changed files with 921 additions and 221 deletions

View File

@@ -11,7 +11,6 @@ import {
} 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';
@@ -20,14 +19,15 @@ 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,
maxDays?: number
) {
try {
const start = startTime ? { StartTime: startTime } : {};
@@ -41,7 +41,7 @@ export async function getAvailableAppointmentsForService(
ServiceID: serviceId,
Key: key,
Lang: 'et',
MaxDays: 120,
MaxDays: maxDays ?? 120,
LocationId: locationId ?? -1,
...start,
}),
@@ -202,8 +202,8 @@ export async function bookAppointment(
},
param: JSON.stringify({
ClinicID: clinic.id,
ServiceID: service.id,
ClinicServiceID: service.sync_id,
ServiceID: service.sync_id,
ClinicServiceID: service.id,
UserID: appointmentUserId,
SyncUserID: syncUserID,
StartTime: startTime,
@@ -416,102 +416,3 @@ export async function getAvailableTimeSlotsForDisplay(
),
};
}
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;
}

View File

@@ -6,8 +6,23 @@ import { addToCart, deleteLineItem, retrieveCart } from '@lib/data/cart';
import { getCartId } from '@lib/data/cookies';
import { StoreCartLineItem, StoreProductVariant } from '@medusajs/types';
import { z } from 'zod';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { cancelReservation } from './connected-online.service';
import { cancelReservation, getOrderedTtoServices } from '~/lib/services/reservation.service';
import { isSameMinute } from 'date-fns';
import { getAvailableAppointmentsForService } from './connected-online.service';
const env = () =>
z
@@ -102,6 +117,30 @@ export async function handleNavigateToPayment({
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({
@@ -120,11 +159,12 @@ export async function handleNavigateToPayment({
cart_id: cart.id,
changed_by: user.id,
});
if (error) {
throw new Error('Error logging cart entry: ' + error.message);
}
return paymentLink;
return { url: paymentLink };
}
export async function handleLineItemTimeout({
@@ -149,4 +189,4 @@ export async function handleLineItemTimeout({
if (error) {
throw new Error('Error logging cart entry: ' + error.message);
}
}
}

View File

@@ -154,8 +154,10 @@ export async function getAnalysisOrdersAdmin({
export async function getTtoOrders({
orderStatus,
lineItemIds
}: {
orderStatus?: Tables<{ schema: 'medreport' }, 'connected_online_reservation'>['status'];
lineItemIds?: string[]
} = {}) {
const client = getSupabaseServerClient();
@@ -171,9 +173,15 @@ export async function getTtoOrders({
.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;
}

View File

@@ -0,0 +1,333 @@
'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 { 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: 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({
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: number,
newStartTime: Date,
newServiceId: number,
newAppointmentUserId: number,
newSyncUserId: number,
newLocationId: number | null, // TODO stop allowing null when Connected starts returning the correct ids instead of -1
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 supabase
.schema('audit')
.from('cart_entries')
.insert({
operation: 'CHANGE_RESERVATION',
account_id: account.id,
cart_id: cartId,
changed_by: user.id,
comment: `${reservationData}`,
});
revalidatePath('/home/cart', 'layout');
} catch (e) {
logger.error(`Failed to update reservation ${reservationData}`);
await supabase
.schema('audit')
.from('cart_entries')
.insert({
operation: 'CHANGE_RESERVATION',
account_id: account.id,
cart_id: cartId,
changed_by: user.id,
comment: `${e}`,
});
throw e;
}
}