Merge branch 'main' into B2B-34

This commit is contained in:
Danel Kungla
2025-06-26 16:11:11 +03:00
13 changed files with 1521 additions and 271 deletions

View 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);
}
}

View 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;
}
}

View File

@@ -1,3 +1,5 @@
'use server';
import {
SupabaseClient,
createClient as createCustomClient,

View File

@@ -2,3 +2,8 @@ export enum SyncStatus {
Success = "SUCCESS",
Fail = "FAIL",
}
export enum RequestStatus {
Success = "SUCCESS",
Fail = "FAIL",
}

View 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
View File

@@ -0,0 +1,4 @@
export enum ExternalApi {
Medipost = 'Medipost',
ConnectedOnline = 'ConnectedOnline',
}