B2B-52: add Connected Online syncing, tables and functions (#18)
* B2B-52: add Connected Online syncing, tables and functions * clean up * improve autogenerated types * add use server directive --------- Co-authored-by: Helena <helena@Helenas-MacBook-Pro.local>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
48
lib/services/audit.service.ts
Normal file
48
lib/services/audit.service.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
'use server'
|
||||
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
import { RequestStatus } from '@/lib/types/audit';
|
||||
import { ConnectedOnlineMethodName } from '@/lib/types/connected-online';
|
||||
import { ExternalApi } from '@/lib/types/external';
|
||||
import { MedipostAction } from '@/lib/types/medipost';
|
||||
|
||||
export default async function logRequestResult(
|
||||
/* personalCode: string, */ requestApi: keyof typeof ExternalApi,
|
||||
requestApiMethod: `${ConnectedOnlineMethodName}` | `${MedipostAction}`,
|
||||
status: RequestStatus,
|
||||
comment?: string,
|
||||
startTime?: string,
|
||||
serviceId?: number,
|
||||
serviceProviderId?: number,
|
||||
) {
|
||||
const supabaseServiceUser = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!,
|
||||
{
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { error } = await supabaseServiceUser
|
||||
.schema('audit')
|
||||
.from('request_entries')
|
||||
.insert({
|
||||
/* personal_code: personalCode, */
|
||||
request_api: requestApi,
|
||||
request_api_method: requestApiMethod,
|
||||
requested_start_date: startTime,
|
||||
status,
|
||||
service_id: serviceId,
|
||||
service_provider_id: serviceProviderId,
|
||||
comment,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error('Failed to insert log entry, error: ' + error.message);
|
||||
}
|
||||
}
|
||||
268
lib/services/connected-online.service.ts
Normal file
268
lib/services/connected-online.service.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
'use server'
|
||||
|
||||
import logRequestResult from '@/lib/services/audit.service';
|
||||
import { RequestStatus } from '@/lib/types/audit';
|
||||
import {
|
||||
AvailableAppointmentsResponse,
|
||||
BookTimeResponse,
|
||||
ConfirmedLoadResponse,
|
||||
ConnectedOnlineMethodName,
|
||||
} from '@/lib/types/connected-online';
|
||||
import { ExternalApi } from '@/lib/types/external';
|
||||
import { Tables } from '@/supabase/database.types';
|
||||
import { createClient } from '@/utils/supabase/server';
|
||||
import axios from 'axios';
|
||||
|
||||
export async function getAvailableAppointmentsForService(
|
||||
serviceId: number,
|
||||
startTime?: Date,
|
||||
) {
|
||||
try {
|
||||
const showTimesFrom = 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: '7T624nlu',
|
||||
Lang: 'et',
|
||||
...showTimesFrom,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
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(
|
||||
serviceSyncId: number,
|
||||
clinicId: number,
|
||||
appointmentUserId: number,
|
||||
syncUserID: number,
|
||||
startTime: string,
|
||||
locationId = 0,
|
||||
comments = '',
|
||||
isEarlierTimeRequested = false,
|
||||
earlierTimeRequestComment = '',
|
||||
) {
|
||||
const supabase = await createClient();
|
||||
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (!user?.id) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
const [
|
||||
{ data: dbClinic, error: clinicError },
|
||||
{ data: dbService, error: serviceError },
|
||||
] = await Promise.all([
|
||||
supabase
|
||||
.from('connected_online_providers')
|
||||
.select('*')
|
||||
.eq('id', clinicId)
|
||||
.limit(1),
|
||||
supabase
|
||||
.from('connected_online_services')
|
||||
.select('*')
|
||||
.eq('sync_id', serviceSyncId)
|
||||
.eq('clinic_id', clinicId)
|
||||
.limit(1),
|
||||
]);
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
const clinic: Tables<'connected_online_providers'> = dbClinic![0];
|
||||
const service: Tables<'connected_online_services'> = dbService![0];
|
||||
|
||||
// TODO the dummy data needs to be replaced with real values once they're present on the user/account
|
||||
const response = 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,
|
||||
UserID: appointmentUserId,
|
||||
SyncUserID: syncUserID,
|
||||
StartTime: startTime,
|
||||
FirstName: 'Test',
|
||||
LastName: 'User',
|
||||
PersonalCode: '4',
|
||||
Email: user.email,
|
||||
Phone: 'phone',
|
||||
Comments: comments,
|
||||
Location: locationId,
|
||||
FreeCode: '',
|
||||
AddToBasket: false,
|
||||
Key: '7T624nlu',
|
||||
Lang: 'et', // update when integrated into app, if needed
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const responseData: BookTimeResponse = JSON.parse(response.data.d);
|
||||
|
||||
if (responseData?.ErrorCode !== 0 || !responseData.Value) {
|
||||
return logRequestResult(
|
||||
ExternalApi.ConnectedOnline,
|
||||
ConnectedOnlineMethodName.BookTime,
|
||||
RequestStatus.Fail,
|
||||
JSON.stringify(responseData),
|
||||
startTime,
|
||||
service.id,
|
||||
clinicId,
|
||||
);
|
||||
}
|
||||
|
||||
const responseParts = responseData.Value.split(',');
|
||||
|
||||
const { error } = await supabase
|
||||
.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,
|
||||
});
|
||||
|
||||
await logRequestResult(
|
||||
ExternalApi.ConnectedOnline,
|
||||
ConnectedOnlineMethodName.BookTime,
|
||||
RequestStatus.Success,
|
||||
JSON.stringify(responseData),
|
||||
startTime,
|
||||
service.id,
|
||||
clinicId,
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
return responseData.Value;
|
||||
} catch (error) {
|
||||
return logRequestResult(
|
||||
ExternalApi.ConnectedOnline,
|
||||
ConnectedOnlineMethodName.BookTime,
|
||||
RequestStatus.Fail,
|
||||
JSON.stringify(error),
|
||||
startTime,
|
||||
serviceSyncId,
|
||||
clinicId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
JSON.stringify(error),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
'use server';
|
||||
|
||||
import {
|
||||
SupabaseClient,
|
||||
createClient as createCustomClient,
|
||||
|
||||
@@ -2,3 +2,8 @@ export enum SyncStatus {
|
||||
Success = "SUCCESS",
|
||||
Fail = "FAIL",
|
||||
}
|
||||
|
||||
export enum RequestStatus {
|
||||
Success = "SUCCESS",
|
||||
Fail = "FAIL",
|
||||
}
|
||||
|
||||
226
lib/types/connected-online.ts
Normal file
226
lib/types/connected-online.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
export const BookTimeResponseSchema = z.object({
|
||||
Value: z.string(),
|
||||
Data: z.null(),
|
||||
ErrorCode: z.number(),
|
||||
ErrorMessage: z.null(),
|
||||
});
|
||||
export type BookTimeResponse = z.infer<typeof BookTimeResponseSchema>;
|
||||
|
||||
export enum ConnectedOnlineMethodName {
|
||||
SearchLoad = 'Search_Load',
|
||||
GetAvailabilities = 'GetAvailabilities',
|
||||
BookTime = 'BookTime',
|
||||
ConfirmedLoad = 'Confirmed_Load',
|
||||
}
|
||||
|
||||
export const AvailableAppointmentTBookingSchema = z.object({
|
||||
ClinicID: z.string(),
|
||||
LocationID: z.number(),
|
||||
UserID: z.number(),
|
||||
SyncUserID: z.number(),
|
||||
ServiceID: z.number(),
|
||||
HKServiceID: z.number(),
|
||||
StartTime: z.coerce.date(),
|
||||
EndTime: z.coerce.date(),
|
||||
PayorCode: z.string(),
|
||||
});
|
||||
export type AvailableAppointment = z.infer<
|
||||
typeof AvailableAppointmentTBookingSchema
|
||||
>;
|
||||
|
||||
export const TDocsASchema = z.object({
|
||||
List: z.string(),
|
||||
});
|
||||
export type TDocsA = z.infer<typeof TDocsASchema>;
|
||||
|
||||
export const TServiceSchema = z.object({
|
||||
Code: z.string(),
|
||||
Name: z.string(),
|
||||
Price: z.number(),
|
||||
VATType: z.number(),
|
||||
HKServiceID: z.union([z.number(), z.null()]),
|
||||
Duration: z.union([z.number(), z.null()]),
|
||||
Description: z.union([z.string(), z.null()]),
|
||||
OnlinePaymentRequired: z.number(),
|
||||
TehikServiceCode: z.union([z.string(), z.null()]),
|
||||
OnlineHideDuration: z.number(),
|
||||
OnlineHidePrice: z.number(),
|
||||
PricePeriods: z.string(),
|
||||
});
|
||||
export type TService = z.infer<typeof TServiceSchema>;
|
||||
|
||||
export const AvailableAppointmentsDataSchema = z.object({
|
||||
T_ScheduleType: z.array(z.any()),
|
||||
T_Service: z.array(TServiceSchema),
|
||||
T_Booking: z.array(AvailableAppointmentTBookingSchema),
|
||||
T_DocsAvailable: z.array(TDocsASchema),
|
||||
T_DocsAll: z.array(TDocsASchema),
|
||||
});
|
||||
export type AvailableAppointmentsData = z.infer<
|
||||
typeof AvailableAppointmentsDataSchema
|
||||
>;
|
||||
|
||||
export const AvailableAppointmentsResponseSchema = z.object({
|
||||
Value: z.null(),
|
||||
Data: AvailableAppointmentsDataSchema,
|
||||
ErrorCode: z.number(),
|
||||
ErrorMessage: z.union([z.string(), z.null()]),
|
||||
});
|
||||
export type AvailableAppointmentsResponse = z.infer<
|
||||
typeof AvailableAppointmentsResponseSchema
|
||||
>;
|
||||
|
||||
export const APaymentRequestSchema = z.object({
|
||||
ID: z.number(),
|
||||
Ref: z.string(),
|
||||
AmountTotal: z.number(),
|
||||
AmountVat: z.number(),
|
||||
PaidAmount: z.union([z.number(), z.null()]),
|
||||
Paid: z.union([z.number(), z.null()]),
|
||||
Failed: z.union([z.number(), z.null()]),
|
||||
PaidAmount1: z.union([z.number(), z.null()]),
|
||||
ScheduleID: z.number(),
|
||||
PaymentID: z.union([z.number(), z.null()]),
|
||||
Created: z.coerce.date(),
|
||||
Nonce: z.string(),
|
||||
DiscountPercent: z.number(),
|
||||
DiscountCode: z.string(),
|
||||
});
|
||||
export type APaymentRequest = z.infer<typeof APaymentRequestSchema>;
|
||||
|
||||
export const PClinicSchema = z.object({
|
||||
ID: z.number(),
|
||||
LicenseID: z.number(),
|
||||
Name: z.string(),
|
||||
Email: z.string(),
|
||||
Address: z.string(),
|
||||
Address2: z.string(),
|
||||
Phone: z.string(),
|
||||
VatNo: z.string(),
|
||||
RegistryCode: z.string(),
|
||||
OID: z.string(),
|
||||
PersonalCodeRequired: z.number(),
|
||||
OnlineCommentRequired: z.number(),
|
||||
OnlineLoginRequired: z.number(),
|
||||
OnlineSenderEmail: z.string(),
|
||||
OnlineReplyToEmail: z.string(),
|
||||
OnlineCCEmail: z.string(),
|
||||
OnlineRedirectkUrl: z.string(), // the typo is on their side
|
||||
OnlineAllowQueue: z.number(),
|
||||
});
|
||||
export type PClinic = z.infer<typeof PClinicSchema>;
|
||||
|
||||
export const PJobTitleTranslationSchema = z.object({
|
||||
ID: z.number(),
|
||||
SyncID: z.number(),
|
||||
TextEN: z.string(),
|
||||
TextET: z.string(),
|
||||
TextFI: z.string(),
|
||||
TextRU: z.string(),
|
||||
TextLT: z.string(),
|
||||
ClinicID: z.number(),
|
||||
Deleted: z.number(),
|
||||
});
|
||||
export type PJobTitleTranslation = z.infer<typeof PJobTitleTranslationSchema>;
|
||||
|
||||
export const PServiceSchema = z.object({
|
||||
ID: z.number(),
|
||||
NameET: z.string(),
|
||||
NameEN: z.string(),
|
||||
NameRU: z.string(),
|
||||
NameFI: z.string(),
|
||||
DescriptionET: z.string(),
|
||||
DescriptionEN: z.string(),
|
||||
DescriptionRU: z.string(),
|
||||
DescriptionFI: z.string(),
|
||||
ExtraEmailTextET: z.string(),
|
||||
ExtraEmailTextEN: z.string(),
|
||||
ExtraEmailTextRU: z.string(),
|
||||
ExtraEmailTextFI: z.string(),
|
||||
});
|
||||
export type PService = z.infer<typeof PServiceSchema>;
|
||||
|
||||
export const ParamSchema = z.object({
|
||||
PersonalCodeRequired: z.number(),
|
||||
OnlineCommentRequired: z.number(),
|
||||
OnlineLoginRequired: z.number(),
|
||||
ClinicName: z.string(),
|
||||
ClinicID: z.number(),
|
||||
OnlineRedirectkUrl: z.string(),
|
||||
OnlineAllowQueue: z.number(),
|
||||
Key: z.string(),
|
||||
});
|
||||
export type Param = z.infer<typeof ParamSchema>;
|
||||
|
||||
export const TBookingSchema = z.object({
|
||||
ID: z.number(),
|
||||
ClinicID: z.number(),
|
||||
FirstName: z.string(),
|
||||
LastName: z.string(),
|
||||
Email: z.string(),
|
||||
Phone: z.string(),
|
||||
PersonalCode: z.string(),
|
||||
Comments: z.string(),
|
||||
ServiceName: z.string(),
|
||||
DoctorName: z.string(),
|
||||
StartTime: z.coerce.date(),
|
||||
EndTime: z.coerce.date(),
|
||||
Status: z.number(),
|
||||
PaymentRequestID: z.number(),
|
||||
MailResponse: z.null(), // was not present in test data, might need to be specified in the future
|
||||
BookingCode: z.string(),
|
||||
LocationName: z.union([z.string(), z.null()]),
|
||||
isDR: z.number(),
|
||||
LocationID: z.number(),
|
||||
ScheduleID: z.number(),
|
||||
ServiceID: z.number(),
|
||||
ServiceSyncID: z.number(),
|
||||
ServiceCode: z.string(),
|
||||
ServiceIsRemote: z.number(),
|
||||
IsRegistered: z.number(),
|
||||
OfferEarlierTime: z.number(),
|
||||
EarlierTimeComment: z.string(),
|
||||
PartnerCode: z.union([z.number(), z.null()]),
|
||||
PartnerCallBackUrl: z.union([z.string(), z.null()]),
|
||||
LocationOfficialName: z.union([z.string(), z.null()]),
|
||||
LocationAddress1: z.union([z.string(), z.null()]),
|
||||
LocationAddress2: z.union([z.string(), z.null()]),
|
||||
LocationPhone: z.union([z.string(), z.null()]),
|
||||
});
|
||||
export type TBooking = z.infer<typeof TBookingSchema>;
|
||||
|
||||
export const TDoctorSchema = z.object({
|
||||
ID: z.number(),
|
||||
Name: z.string(),
|
||||
Prefix: z.string(),
|
||||
Photo: z.string(),
|
||||
SpokenLanguages: z.string(),
|
||||
JobTitleID: z.union([z.number(), z.null()]),
|
||||
ClinicID: z.number(),
|
||||
Deleted: z.number(),
|
||||
});
|
||||
export type TDoctor = z.infer<typeof TDoctorSchema>;
|
||||
|
||||
export const ConfirmedLoadDataSchema = z.object({
|
||||
T_Booking: z.array(TBookingSchema),
|
||||
P_Location: z.array(z.any()),
|
||||
P_Clinic: z.array(PClinicSchema),
|
||||
A_PaymentRequest: z.array(APaymentRequestSchema),
|
||||
P_Service: z.array(PServiceSchema),
|
||||
T_Service: z.array(z.any()),
|
||||
T_ServiceHK: z.array(z.any()),
|
||||
P_JobTitleTranslations: z.array(PJobTitleTranslationSchema),
|
||||
T_Doctor: z.array(TDoctorSchema),
|
||||
Params: z.array(ParamSchema),
|
||||
});
|
||||
export type ConfirmedLoadData = z.infer<typeof ConfirmedLoadDataSchema>;
|
||||
|
||||
export const ConfirmedLoadResponseSchema = z.object({
|
||||
Value: z.null(),
|
||||
Data: ConfirmedLoadDataSchema,
|
||||
ErrorCode: z.number(),
|
||||
ErrorMessage: z.union([z.string(), z.null()]),
|
||||
});
|
||||
export type ConfirmedLoadResponse = z.infer<typeof ConfirmedLoadResponseSchema>;
|
||||
4
lib/types/external.ts
Normal file
4
lib/types/external.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum ExternalApi {
|
||||
Medipost = 'Medipost',
|
||||
ConnectedOnline = 'ConnectedOnline',
|
||||
}
|
||||
Reference in New Issue
Block a user