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:
Helena
2025-06-18 17:06:24 +03:00
committed by GitHub
parent 084653b5ea
commit 538a17031a
14 changed files with 1582 additions and 1714 deletions

View File

@@ -9,6 +9,8 @@ MEDIPOST_USER=your-medipost-user
MEDIPOST_PASSWORD=your-medipost-password MEDIPOST_PASSWORD=your-medipost-password
MEDIPOST_RECIPIENT=your-medipost-recipient MEDIPOST_RECIPIENT=your-medipost-recipient
CONNECTED_ONLINE_URL=your-connected-online-url
EMAIL_SENDER= EMAIL_SENDER=
EMAIL_USER= # refer to your email provider's documentation EMAIL_USER= # refer to your email provider's documentation
EMAIL_PASSWORD= # refer to your email provider's documentation EMAIL_PASSWORD= # refer to your email provider's documentation

View File

@@ -25,26 +25,32 @@ async function syncData() {
config({ path: `.env.${process.env.NODE_ENV}` }); config({ path: `.env.${process.env.NODE_ENV}` });
} }
const baseUrl = process.env.MEDIPOST_URL!; const baseUrl = process.env.MEDIPOST_URL;
const user = process.env.MEDIPOST_USER!; const user = process.env.MEDIPOST_USER;
const password = process.env.MEDIPOST_PASSWORD!; const password = process.env.MEDIPOST_PASSWORD;
const sender = process.env.MEDIPOST_MESSAGE_SENDER!; const sender = process.env.MEDIPOST_MESSAGE_SENDER;
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseServiceRoleKey =
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY;
if (!baseUrl || !user || !password || !sender) { if (
!baseUrl ||
!supabaseUrl ||
!supabaseServiceRoleKey ||
!user ||
!password ||
!sender
) {
throw new Error('Could not access all necessary environment variables'); throw new Error('Could not access all necessary environment variables');
} }
const supabase = createCustomClient( const supabase = createCustomClient(supabaseUrl, supabaseServiceRoleKey, {
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!,
{
auth: { auth: {
persistSession: false, persistSession: false,
autoRefreshToken: false, autoRefreshToken: false,
detectSessionInUrl: false, detectSessionInUrl: false,
}, },
}, });
);
try { try {
// GET LATEST PUBLIC MESSAGE ID // GET LATEST PUBLIC MESSAGE ID

View File

@@ -0,0 +1,139 @@
import { createClient as createCustomClient } from '@supabase/supabase-js';
import axios from 'axios';
import { config } from 'dotenv';
async function syncData() {
if (process.env.NODE_ENV === 'local') {
config({ path: `.env.${process.env.NODE_ENV}` });
}
const baseUrl = process.env.CONNECTED_ONLINE_URL;
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseServiceRoleKey =
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY;
if (!baseUrl || !supabaseUrl || !supabaseServiceRoleKey) {
throw new Error('Could not access all necessary environment variables');
}
const supabase = createCustomClient(supabaseUrl, supabaseServiceRoleKey, {
auth: {
persistSession: false,
autoRefreshToken: false,
detectSessionInUrl: false,
},
});
try {
const response = await axios.post(
`${baseUrl}/Search_Load`,
{
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
param: "{'Value':'|et|-1'}", // get all available services in Estonian
},
);
const responseData: {
Value: any;
Data: any;
ErrorCode: number;
ErrorMessage: string;
} = JSON.parse(response.data.d);
if (responseData?.ErrorCode !== 0) {
throw new Error('Failed to get Connected Online data');
}
if (
!responseData.Data.T_Lic?.length ||
!responseData.Data.T_Service?.length
) {
return supabase.schema('audit').from('sync_entries').insert({
operation: 'CONNECTED_ONLINE_SYNC',
comment: 'No clinic or service data received',
status: 'FAIL',
changed_by_role: 'service_role',
});
}
const clinics = responseData.Data.T_Lic;
const services = responseData.Data.T_Service;
const mappedClinics = clinics.map((clinic) => {
return {
id: clinic.ID,
can_select_worker: !!clinic.OnlineCanSelectWorker,
email: clinic.Email || null,
name: clinic.Name,
personal_code_required: !!clinic.PersonalCodeRequired,
phone_number: clinic.Phone || null,
};
});
const mappedServices = services.map((service) => {
return {
id: service.ID,
clinic_id: service.ClinicID,
code: service.Code,
description: service.Description || null,
display: service.Display,
duration: service.Duration,
has_free_codes: !!service.HasFreeCodes,
name: service.Name,
neto_duration: service.NetoDuration,
online_hide_duration: service.OnlineHideDuration,
online_hide_price: service.OnlineHidePrice,
price: service.Price,
price_periods: service.PricePeriods || null,
requires_payment: !!service.RequiresPayment,
sync_id: service.SyncID,
};
});
const { error: providersError } = await supabase
.from('connected_online_providers')
.upsert(mappedClinics);
const { error: servicesError } = await supabase
.from('connected_online_services')
.upsert(mappedServices, { onConflict: 'id', ignoreDuplicates: false });
if (providersError || servicesError) {
return supabase
.schema('audit')
.from('sync_entries')
.insert({
operation: 'CONNECTED_ONLINE_SYNC',
comment: providersError
? 'Error saving providers: ' + JSON.stringify(providersError)
: 'Error saving services: ' + JSON.stringify(servicesError),
status: 'FAIL',
changed_by_role: 'service_role',
});
}
await supabase.schema('audit').from('sync_entries').insert({
operation: 'CONNECTED_ONLINE_SYNC',
status: 'SUCCESS',
changed_by_role: 'service_role',
});
} catch (e) {
await supabase
.schema('audit')
.from('sync_entries')
.insert({
operation: 'CONNECTED_ONLINE_SYNC',
status: 'FAIL',
comment: JSON.stringify(e),
changed_by_role: 'service_role',
});
throw new Error(
`Failed to sync Connected Online data, error: ${JSON.stringify(e)}`,
);
}
}
syncData();

File diff suppressed because it is too large Load Diff

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 { import {
SupabaseClient, SupabaseClient,
createClient as createCustomClient, createClient as createCustomClient,

View File

@@ -2,3 +2,8 @@ export enum SyncStatus {
Success = "SUCCESS", Success = "SUCCESS",
Fail = "FAIL", 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',
}

View File

@@ -19,21 +19,22 @@
"supabase": "supabase", "supabase": "supabase",
"supabase:start": "supabase status || supabase start", "supabase:start": "supabase status || supabase start",
"supabase:stop": "supabase stop", "supabase:stop": "supabase stop",
"supabase:reset": "supabase db reset",
"supabase:status": "supabase status", "supabase:status": "supabase status",
"supabase:test": "supabase db test", "supabase:test": "supabase db test",
"supabase:db:reset": "supabase db reset",
"supabase:db:lint": "supabase db lint", "supabase:db:lint": "supabase db lint",
"supabase:db:diff": "supabase db diff", "supabase:db:diff": "supabase db diff",
"supabase:deploy": "supabase link --project-ref $SUPABASE_PROJECT_REF && supabase db push", "supabase:deploy": "supabase link --project-ref $SUPABASE_PROJECT_REF && supabase db push",
"supabase:typegen": "pnpm run supabase:typegen:packages && pnpm run supabase:typegen:app", "supabase:typegen": "pnpm run supabase:typegen:packages && pnpm run supabase:typegen:app",
"supabase:typegen:packages": "supabase gen types typescript --local > ../../packages/supabase/src/database.types.ts", "supabase:typegen:packages": "supabase gen types typescript --local > ../../packages/supabase/src/database.types.ts",
"supabase:typegen:app": "supabase gen types typescript --local > ./lib/database.types.ts", "supabase:typegen:app": "supabase gen types typescript --local > ./supabase/database.types.ts",
"supabase:db:dump:local": "supabase db dump --local --data-only", "supabase:db:dump:local": "supabase db dump --local --data-only",
"sync-data:dev": "NODE_ENV=local ts-node jobs/sync-analysis-groups.ts" "sync-analysis-groups:dev": "NODE_ENV=local ts-node jobs/sync-analysis-groups.ts",
"sync-connected-online:dev": "NODE_ENV=local ts-node jobs/sync-connected-online.ts"
}, },
"dependencies": { "dependencies": {
"@edge-csrf/nextjs": "2.5.3-cloudflare-rc1", "@edge-csrf/nextjs": "2.5.3-cloudflare-rc1",
"@hookform/resolvers": "^5.0.1", "@hookform/resolvers": "^5.1.1",
"@kit/accounts": "workspace:*", "@kit/accounts": "workspace:*",
"@kit/admin": "workspace:*", "@kit/admin": "workspace:*",
"@kit/analytics": "workspace:*", "@kit/analytics": "workspace:*",
@@ -61,7 +62,7 @@
"@supabase/supabase-js": "2.49.4", "@supabase/supabase-js": "2.49.4",
"@tanstack/react-query": "5.76.1", "@tanstack/react-query": "5.76.1",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"axios": "^1.9.0", "axios": "^1.10.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"fast-xml-parser": "^5.2.5", "fast-xml-parser": "^5.2.5",
@@ -72,13 +73,13 @@
"next-themes": "0.4.6", "next-themes": "0.4.6",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-hook-form": "^7.56.3", "react-hook-form": "^7.58.0",
"react-i18next": "^15.5.1", "react-i18next": "^15.5.3",
"recharts": "2.15.3", "recharts": "2.15.3",
"sonner": "^2.0.3", "sonner": "^2.0.5",
"tailwind-merge": "^3.3.0", "tailwind-merge": "^3.3.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"zod": "^3.24.4" "zod": "^3.25.67"
}, },
"devDependencies": { "devDependencies": {
"@hookform/resolvers": "^5.0.1", "@hookform/resolvers": "^5.0.1",
@@ -86,9 +87,9 @@
"@kit/prettier-config": "workspace:*", "@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*", "@kit/tsconfig": "workspace:*",
"@next/bundle-analyzer": "15.3.2", "@next/bundle-analyzer": "15.3.2",
"@tailwindcss/postcss": "^4.1.7", "@tailwindcss/postcss": "^4.1.10",
"@types/lodash": "^4.17.17", "@types/lodash": "^4.17.17",
"@types/node": "^22.15.18", "@types/node": "^22.15.32",
"@types/react": "19.1.4", "@types/react": "19.1.4",
"@types/react-dom": "19.1.5", "@types/react-dom": "19.1.5",
"babel-plugin-react-compiler": "19.1.0-rc.2", "babel-plugin-react-compiler": "19.1.0-rc.2",
@@ -97,7 +98,7 @@
"pino-pretty": "^13.0.0", "pino-pretty": "^13.0.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"react-hook-form": "^7.57.0", "react-hook-form": "^7.57.0",
"supabase": "^2.22.12", "supabase": "^2.26.9",
"tailwindcss": "4.1.7", "tailwindcss": "4.1.7",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^5.8.3", "typescript": "^5.8.3",

574
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -48,6 +48,48 @@ export type Database = {
} }
Relationships: [] Relationships: []
} }
request_entries: {
Row: {
comment: string | null
created_at: string
id: number
personal_code: number | null
request_api: string
request_api_method: string
requested_end_date: string | null
requested_start_date: string | null
service_id: number | null
service_provider_id: number | null
status: Database["audit"]["Enums"]["request_status"]
}
Insert: {
comment?: string | null
created_at?: string
id?: number
personal_code?: number | null
request_api: string
request_api_method: string
requested_end_date?: string | null
requested_start_date?: string | null
service_id?: number | null
service_provider_id?: number | null
status: Database["audit"]["Enums"]["request_status"]
}
Update: {
comment?: string | null
created_at?: string
id?: number
personal_code?: number | null
request_api?: string
request_api_method?: string
requested_end_date?: string | null
requested_start_date?: string | null
service_id?: number | null
service_provider_id?: number | null
status?: Database["audit"]["Enums"]["request_status"]
}
Relationships: []
}
sync_entries: { sync_entries: {
Row: { Row: {
changed_by_role: string changed_by_role: string
@@ -83,6 +125,7 @@ export type Database = {
[_ in never]: never [_ in never]: never
} }
Enums: { Enums: {
request_status: "SUCCESS" | "FAIL"
sync_status: "SUCCESS" | "FAIL" sync_status: "SUCCESS" | "FAIL"
} }
CompositeTypes: { CompositeTypes: {
@@ -596,6 +639,158 @@ export type Database = {
} }
Relationships: [] Relationships: []
} }
connected_online_providers: {
Row: {
can_select_worker: boolean
created_at: string
email: string | null
id: number
name: string
personal_code_required: boolean
phone_number: string | null
updated_at: string | null
}
Insert: {
can_select_worker: boolean
created_at?: string
email?: string | null
id: number
name: string
personal_code_required: boolean
phone_number?: string | null
updated_at?: string | null
}
Update: {
can_select_worker?: boolean
created_at?: string
email?: string | null
id?: number
name?: string
personal_code_required?: boolean
phone_number?: string | null
updated_at?: string | null
}
Relationships: []
}
connected_online_reservation: {
Row: {
booking_code: string
clinic_id: number
comments: string | null
created_at: string
discount_code: string | null
id: number
lang: string
requires_payment: boolean
service_id: number
service_user_id: number | null
start_time: string
sync_user_id: number
updated_at: string | null
user_id: string
}
Insert: {
booking_code: string
clinic_id: number
comments?: string | null
created_at?: string
discount_code?: string | null
id?: number
lang: string
requires_payment: boolean
service_id: number
service_user_id?: number | null
start_time: string
sync_user_id: number
updated_at?: string | null
user_id: string
}
Update: {
booking_code?: string
clinic_id?: number
comments?: string | null
created_at?: string
discount_code?: string | null
id?: number
lang?: string
requires_payment?: boolean
service_id?: number
service_user_id?: number | null
start_time?: string
sync_user_id?: number
updated_at?: string | null
user_id?: string
}
Relationships: []
}
connected_online_services: {
Row: {
clinic_id: number
code: string
created_at: string
description: string | null
display: string | null
duration: number
has_free_codes: boolean
id: number
name: string
neto_duration: number | null
online_hide_duration: number | null
online_hide_price: number | null
price: number
price_periods: string | null
requires_payment: boolean
sync_id: number
updated_at: string | null
}
Insert: {
clinic_id: number
code: string
created_at?: string
description?: string | null
display?: string | null
duration: number
has_free_codes: boolean
id: number
name: string
neto_duration?: number | null
online_hide_duration?: number | null
online_hide_price?: number | null
price: number
price_periods?: string | null
requires_payment: boolean
sync_id: number
updated_at?: string | null
}
Update: {
clinic_id?: number
code?: string
created_at?: string
description?: string | null
display?: string | null
duration?: number
has_free_codes?: boolean
id?: number
name?: string
neto_duration?: number | null
online_hide_duration?: number | null
online_hide_price?: number | null
price?: number
price_periods?: string | null
requires_payment?: boolean
sync_id?: number
updated_at?: string | null
}
Relationships: [
{
foreignKeyName: "connected_online_services_clinic_id_fkey"
columns: ["clinic_id"]
isOneToOne: false
referencedRelation: "connected_online_providers"
referencedColumns: ["id"]
},
]
}
invitations: { invitations: {
Row: { Row: {
account_id: string account_id: string
@@ -661,6 +856,63 @@ export type Database = {
}, },
] ]
} }
nonces: {
Row: {
client_token: string
created_at: string
expires_at: string
id: string
last_verification_at: string | null
last_verification_ip: unknown | null
last_verification_user_agent: string | null
metadata: Json | null
nonce: string
purpose: string
revoked: boolean
revoked_reason: string | null
scopes: string[] | null
used_at: string | null
user_id: string | null
verification_attempts: number
}
Insert: {
client_token: string
created_at?: string
expires_at: string
id?: string
last_verification_at?: string | null
last_verification_ip?: unknown | null
last_verification_user_agent?: string | null
metadata?: Json | null
nonce: string
purpose: string
revoked?: boolean
revoked_reason?: string | null
scopes?: string[] | null
used_at?: string | null
user_id?: string | null
verification_attempts?: number
}
Update: {
client_token?: string
created_at?: string
expires_at?: string
id?: string
last_verification_at?: string | null
last_verification_ip?: unknown | null
last_verification_user_agent?: string | null
metadata?: Json | null
nonce?: string
purpose?: string
revoked?: boolean
revoked_reason?: string | null
scopes?: string[] | null
used_at?: string | null
user_id?: string | null
verification_attempts?: number
}
Relationships: []
}
notifications: { notifications: {
Row: { Row: {
account_id: string account_id: string
@@ -1058,6 +1310,17 @@ export type Database = {
updated_at: string updated_at: string
} }
} }
create_nonce: {
Args: {
p_user_id?: string
p_purpose?: string
p_expires_in_seconds?: number
p_metadata?: Json
p_scopes?: string[]
p_revoke_previous?: boolean
}
Returns: Json
}
create_team_account: { create_team_account: {
Args: { account_name: string } Args: { account_name: string }
Returns: { Returns: {
@@ -1110,6 +1373,10 @@ export type Database = {
Args: Record<PropertyKey, never> Args: Record<PropertyKey, never>
Returns: Json Returns: Json
} }
get_nonce_status: {
Args: { p_id: string }
Returns: Json
}
get_upper_system_role: { get_upper_system_role: {
Args: Record<PropertyKey, never> Args: Record<PropertyKey, never>
Returns: string Returns: string
@@ -1146,6 +1413,10 @@ export type Database = {
} }
Returns: boolean Returns: boolean
} }
is_aal2: {
Args: Record<PropertyKey, never>
Returns: boolean
}
is_account_owner: { is_account_owner: {
Args: { account_id: string } Args: { account_id: string }
Returns: boolean Returns: boolean
@@ -1154,14 +1425,26 @@ export type Database = {
Args: { target_account_id: string } Args: { target_account_id: string }
Returns: boolean Returns: boolean
} }
is_mfa_compliant: {
Args: Record<PropertyKey, never>
Returns: boolean
}
is_set: { is_set: {
Args: { field_name: string } Args: { field_name: string }
Returns: boolean Returns: boolean
} }
is_super_admin: {
Args: Record<PropertyKey, never>
Returns: boolean
}
is_team_member: { is_team_member: {
Args: { account_id: string; user_id: string } Args: { account_id: string; user_id: string }
Returns: boolean Returns: boolean
} }
revoke_nonce: {
Args: { p_id: string; p_reason?: string }
Returns: boolean
}
team_account_workspace: { team_account_workspace: {
Args: { account_slug: string } Args: { account_slug: string }
Returns: { Returns: {
@@ -1236,6 +1519,18 @@ export type Database = {
updated_at: string updated_at: string
} }
} }
verify_nonce: {
Args: {
p_token: string
p_purpose: string
p_user_id?: string
p_required_scopes?: string[]
p_max_verification_attempts?: number
p_ip?: unknown
p_user_agent?: string
}
Returns: Json
}
} }
Enums: { Enums: {
analysis_order_status: analysis_order_status:
@@ -1383,6 +1678,7 @@ export type CompositeTypes<
export const Constants = { export const Constants = {
audit: { audit: {
Enums: { Enums: {
request_status: ["SUCCESS", "FAIL"],
sync_status: ["SUCCESS", "FAIL"], sync_status: ["SUCCESS", "FAIL"],
}, },
}, },

View File

@@ -0,0 +1,227 @@
create table "public"."connected_online_providers" (
"id" bigint not null,
"name" text not null,
"email" text,
"phone_number" text,
"can_select_worker" boolean not null,
"personal_code_required" boolean not null,
"created_at" timestamp with time zone not null default now(),
"updated_at" timestamp without time zone default now()
);
alter table "public"."connected_online_providers" enable row level security;
create table "public"."connected_online_services" (
"id" bigint not null,
"clinic_id" bigint not null,
"sync_id" bigint not null,
"name" text not null,
"description" text,
"price" double precision not null,
"requires_payment" boolean not null,
"duration" bigint not null,
"neto_duration" bigint,
"display" text,
"price_periods" text,
"online_hide_duration" bigint,
"online_hide_price" bigint,
"code" text not null,
"has_free_codes" boolean not null,
"created_at" timestamp with time zone not null default now(),
"updated_at" timestamp with time zone default now()
);
alter table "public"."connected_online_services" enable row level security;
CREATE UNIQUE INDEX connected_online_providers_id_key ON public.connected_online_providers USING btree (id);
CREATE UNIQUE INDEX connected_online_providers_pkey ON public.connected_online_providers USING btree (id);
CREATE UNIQUE INDEX connected_online_services_id_key ON public.connected_online_services USING btree (id);
CREATE UNIQUE INDEX connected_online_services_pkey ON public.connected_online_services USING btree (id);
alter table "public"."connected_online_providers" add constraint "connected_online_providers_pkey" PRIMARY KEY using index "connected_online_providers_pkey";
alter table "public"."connected_online_services" add constraint "connected_online_services_pkey" PRIMARY KEY using index "connected_online_services_pkey";
alter table "public"."connected_online_providers" add constraint "connected_online_providers_id_key" UNIQUE using index "connected_online_providers_id_key";
alter table "public"."connected_online_services" add constraint "connected_online_services_clinic_id_fkey" FOREIGN KEY (clinic_id) REFERENCES connected_online_providers(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."connected_online_services" validate constraint "connected_online_services_clinic_id_fkey";
alter table "public"."connected_online_services" add constraint "connected_online_services_id_key" UNIQUE using index "connected_online_services_id_key";
grant delete on table "public"."connected_online_providers" to "service_role";
grant insert on table "public"."connected_online_providers" to "service_role";
grant references on table "public"."connected_online_providers" to "service_role";
grant select on table "public"."connected_online_providers" to "service_role";
grant trigger on table "public"."connected_online_providers" to "service_role";
grant truncate on table "public"."connected_online_providers" to "service_role";
grant update on table "public"."connected_online_providers" to "service_role";
grant select on table "public"."connected_online_providers" to "authenticated";
grant delete on table "public"."connected_online_services" to "service_role";
grant insert on table "public"."connected_online_services" to "service_role";
grant references on table "public"."connected_online_services" to "service_role";
grant select on table "public"."connected_online_services" to "service_role";
grant trigger on table "public"."connected_online_services" to "service_role";
grant truncate on table "public"."connected_online_services" to "service_role";
grant update on table "public"."connected_online_services" to "service_role";
grant select on table "public"."connected_online_services" to "authenticated";
create type "audit"."request_status" as enum ('SUCCESS', 'FAIL');
create table "audit"."request_entries" (
"id" bigint generated by default as identity not null,
"personal_code" bigint,
"request_api" text not null,
"request_api_method" text not null,
"status" audit.request_status not null,
"comment" text,
"service_provider_id" bigint,
"service_id" bigint,
"requested_start_date" timestamp with time zone,
"requested_end_date" timestamp with time zone,
"created_at" timestamp with time zone not null default now()
);
alter table "audit"."request_entries" enable row level security;
CREATE UNIQUE INDEX request_entries_pkey ON audit.request_entries USING btree (id);
alter table "audit"."request_entries" add constraint "request_entries_pkey" PRIMARY KEY using index "request_entries_pkey";
grant delete on table "audit"."request_entries" to "service_role";
grant insert on table "audit"."request_entries" to "service_role";
grant references on table "audit"."request_entries" to "service_role";
grant select on table "audit"."request_entries" to "service_role";
grant trigger on table "audit"."request_entries" to "service_role";
grant truncate on table "audit"."request_entries" to "service_role";
grant update on table "audit"."request_entries" to "service_role";
create policy "service_role_all"
on "audit"."request_entries"
as permissive
for all
to service_role
using (true);
create table "public"."connected_online_reservation" (
"id" bigint generated by default as identity not null,
"user_id" uuid not null,
"booking_code" text not null,
"service_id" bigint not null,
"clinic_id" bigint not null,
"service_user_id" bigint,
"sync_user_id" bigint not null,
"requires_payment" boolean not null,
"comments" text,
"start_time" timestamp with time zone not null,
"lang" text not null,
"discount_code" text,
"created_at" timestamp with time zone not null default now(),
"updated_at" timestamp with time zone default now()
);
alter table "public"."connected_online_reservation" enable row level security;
CREATE UNIQUE INDEX connected_online_reservation_booking_code_key ON public.connected_online_reservation USING btree (booking_code);
CREATE UNIQUE INDEX connected_online_reservation_pkey ON public.connected_online_reservation USING btree (id);
alter table "public"."connected_online_reservation" add constraint "connected_online_reservation_pkey" PRIMARY KEY using index "connected_online_reservation_pkey";
alter table "public"."connected_online_reservation" add constraint "connected_online_reservation_booking_code_key" UNIQUE using index "connected_online_reservation_booking_code_key";
alter table "public"."connected_online_reservation" add constraint "connected_online_reservation_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."connected_online_reservation" validate constraint "connected_online_reservation_user_id_fkey";
grant delete on table "public"."connected_online_reservation" to "service_role";
grant insert on table "public"."connected_online_reservation" to "service_role";
grant references on table "public"."connected_online_reservation" to "service_role";
grant select on table "public"."connected_online_reservation" to "service_role";
grant trigger on table "public"."connected_online_reservation" to "service_role";
grant truncate on table "public"."connected_online_reservation" to "service_role";
grant update on table "public"."connected_online_reservation" to "service_role";
create policy "service_role_all"
on "public"."connected_online_reservation"
as permissive
for all
to service_role
using (true);
CREATE TRIGGER connected_online_providers_change_record_timestamps AFTER INSERT OR UPDATE ON public.connected_online_providers FOR EACH ROW EXECUTE FUNCTION trigger_set_timestamps();
CREATE TRIGGER connected_online_services_change_record_timestamps AFTER INSERT OR UPDATE ON public.connected_online_services FOR EACH ROW EXECUTE FUNCTION trigger_set_timestamps();
create policy "service_role_all"
on "public"."connected_online_providers"
as permissive
for all
to service_role
using (true);
create policy "service_role_all"
on "public"."connected_online_services"
as permissive
for all
to service_role
using (true);
create policy "authenticated_select"
on "public"."connected_online_providers"
as permissive
for select
to authenticated
using (true);
create policy "authenticated_select"
on "public"."connected_online_services"
as permissive
for select
to authenticated
using (true);
create policy "own_all"
on "public"."connected_online_reservation"
as permissive
for all
to authenticated
using ((( SELECT auth.uid() AS uid) = user_id));