feat(MED-131): move jobs to /api/job/* secured with key
This commit is contained in:
8
app/api/job/handler/load-env.ts
Normal file
8
app/api/job/handler/load-env.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { config } from 'dotenv';
|
||||
|
||||
export default function loadEnv() {
|
||||
config({ path: `.env` });
|
||||
if (['local', 'test', 'development', 'production'].includes(process.env.NODE_ENV!)) {
|
||||
config({ path: `.env.${process.env.NODE_ENV}` });
|
||||
}
|
||||
}
|
||||
285
app/api/job/handler/sync-analysis-groups.ts
Normal file
285
app/api/job/handler/sync-analysis-groups.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
import { createClient as createCustomClient } from '@supabase/supabase-js';
|
||||
|
||||
import axios from 'axios';
|
||||
import { format } from 'date-fns';
|
||||
import { XMLParser } from 'fast-xml-parser';
|
||||
import { IMedipostResponse_GetPublicMessageList } from './types';
|
||||
|
||||
function getLatestMessage(messages: IMedipostResponse_GetPublicMessageList['messages']): IMedipostResponse_GetPublicMessageList['messages'][number] | null {
|
||||
if (!messages?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return messages.reduce((prev, current) =>
|
||||
Number(prev.messageId) > Number(current.messageId) ? prev : current,
|
||||
);
|
||||
}
|
||||
|
||||
function toArray<T>(input?: T | T[] | null): T[] {
|
||||
if (!input) return [];
|
||||
return Array.isArray(input) ? input : [input];
|
||||
}
|
||||
|
||||
export default async function syncAnalysisGroups() {
|
||||
const baseUrl = process.env.MEDIPOST_URL;
|
||||
const user = process.env.MEDIPOST_USER;
|
||||
const password = process.env.MEDIPOST_PASSWORD;
|
||||
const sender = process.env.MEDIPOST_MESSAGE_SENDER;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
|
||||
if (
|
||||
!baseUrl ||
|
||||
!supabaseUrl ||
|
||||
!supabaseServiceRoleKey ||
|
||||
!user ||
|
||||
!password ||
|
||||
!sender
|
||||
) {
|
||||
throw new Error('Could not access all necessary environment variables');
|
||||
}
|
||||
|
||||
const supabase = createCustomClient(supabaseUrl, supabaseServiceRoleKey, {
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
console.info('Getting latest public message id');
|
||||
const { data: lastChecked } = await supabase
|
||||
.schema('audit')
|
||||
.from('sync_entries')
|
||||
.select('created_at')
|
||||
.eq('status', 'SUCCESS')
|
||||
.order('created_at')
|
||||
.limit(1);
|
||||
const lastEntry = lastChecked?.[0];
|
||||
const lastCheckedDate = lastEntry
|
||||
? format(lastEntry.created_at, 'yyyy-MM-dd HH:mm:ss')
|
||||
: null;
|
||||
|
||||
console.info('Getting public message list');
|
||||
const { data, status } = await axios.get<IMedipostResponse_GetPublicMessageList>(baseUrl, {
|
||||
params: {
|
||||
Action: 'GetPublicMessageList',
|
||||
User: user,
|
||||
Password: password,
|
||||
//Sender: sender,
|
||||
// ...(lastCheckedDate && { LastChecked: lastCheckedDate }),
|
||||
MessageType: 'Teenus',
|
||||
},
|
||||
});
|
||||
|
||||
if (!data || status !== 200 || data.code !== 0) {
|
||||
console.error("Failed to get public message list, status: ", status, data);
|
||||
throw new Error('Failed to get public message list');
|
||||
}
|
||||
|
||||
if (!data.messages?.length) {
|
||||
console.info('No new data received');
|
||||
return supabase.schema('audit').from('sync_entries').insert({
|
||||
operation: 'ANALYSES_SYNC',
|
||||
comment: 'No new data received',
|
||||
status: 'SUCCESS',
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
}
|
||||
|
||||
// GET PUBLIC MESSAGE WITH GIVEN ID
|
||||
const latestMessage = getLatestMessage(data?.messages)!;
|
||||
console.info('Getting public message with id: ', latestMessage.messageId);
|
||||
|
||||
const { data: publicMessageData } = await axios.get(baseUrl, {
|
||||
params: {
|
||||
Action: 'GetPublicMessage',
|
||||
User: user,
|
||||
Password: password,
|
||||
MessageId: latestMessage.messageId,
|
||||
},
|
||||
headers: {
|
||||
Accept: 'application/xml',
|
||||
},
|
||||
});
|
||||
|
||||
const parser = new XMLParser({ ignoreAttributes: false });
|
||||
const parsed = parser.parse(publicMessageData);
|
||||
|
||||
if (parsed.ANSWER?.CODE && parsed.ANSWER?.CODE !== 0) {
|
||||
throw new Error(
|
||||
`Failed to get public message (id: ${latestMessage.messageId})`,
|
||||
);
|
||||
}
|
||||
|
||||
// SAVE PUBLIC MESSAGE DATA
|
||||
|
||||
const providers = toArray(parsed?.Saadetis?.Teenused.Teostaja);
|
||||
const analysisGroups = providers.flatMap((provider) =>
|
||||
toArray(provider.UuringuGrupp),
|
||||
);
|
||||
|
||||
if (!parsed || !analysisGroups.length) {
|
||||
return supabase.schema('audit').from('sync_entries').insert({
|
||||
operation: 'ANALYSES_SYNC',
|
||||
comment: 'No data received',
|
||||
status: 'FAIL',
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
}
|
||||
|
||||
const codes: any[] = [];
|
||||
for (const analysisGroup of analysisGroups) {
|
||||
// SAVE ANALYSIS GROUP
|
||||
const { data: insertedAnalysisGroup, error } = await supabase
|
||||
.schema('medreport')
|
||||
.from('analysis_groups')
|
||||
.upsert(
|
||||
{
|
||||
original_id: analysisGroup.UuringuGruppId,
|
||||
name: analysisGroup.UuringuGruppNimi,
|
||||
order: analysisGroup.UuringuGruppJarjekord,
|
||||
},
|
||||
{ onConflict: 'original_id', ignoreDuplicates: false },
|
||||
)
|
||||
.select('id');
|
||||
|
||||
if (error || !insertedAnalysisGroup[0]?.id) {
|
||||
throw new Error(
|
||||
`Failed to insert analysis group (id: ${analysisGroup.UuringuGruppId}), error: ${error?.message}`,
|
||||
);
|
||||
}
|
||||
const analysisGroupId = insertedAnalysisGroup[0].id;
|
||||
|
||||
const analysisGroupCodes = toArray(analysisGroup.Kood);
|
||||
codes.push(
|
||||
...analysisGroupCodes.map((kood) => ({
|
||||
hk_code: kood.HkKood,
|
||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||
coefficient: kood.Koefitsient,
|
||||
price: kood.Hind,
|
||||
analysis_group_id: analysisGroupId,
|
||||
analysis_element_id: null,
|
||||
analysis_id: null,
|
||||
})),
|
||||
);
|
||||
|
||||
const analysisGroupItems = toArray(analysisGroup.Uuring);
|
||||
|
||||
for (const item of analysisGroupItems) {
|
||||
const analysisElement = item.UuringuElement;
|
||||
|
||||
const { data: insertedAnalysisElement, error } = await supabase
|
||||
.schema('medreport')
|
||||
.from('analysis_elements')
|
||||
.upsert(
|
||||
{
|
||||
analysis_id_oid: analysisElement.UuringIdOID,
|
||||
analysis_id_original: analysisElement.UuringId,
|
||||
tehik_short_loinc: analysisElement.TLyhend,
|
||||
tehik_loinc_name: analysisElement.KNimetus,
|
||||
analysis_name_lab: analysisElement.UuringNimi,
|
||||
order: analysisElement.Jarjekord,
|
||||
parent_analysis_group_id: analysisGroupId,
|
||||
material_groups: toArray(item.MaterjalideGrupp),
|
||||
},
|
||||
{ onConflict: 'analysis_id_original', ignoreDuplicates: false },
|
||||
)
|
||||
.select('id');
|
||||
|
||||
if (error || !insertedAnalysisElement[0]?.id) {
|
||||
throw new Error(
|
||||
`Failed to insert analysis element (id: ${analysisElement.UuringId}), error: ${error?.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
const insertedAnalysisElementId = insertedAnalysisElement[0].id;
|
||||
|
||||
if (analysisElement.Kood) {
|
||||
const analysisElementCodes = toArray(analysisElement.Kood);
|
||||
codes.push(
|
||||
...analysisElementCodes.map((kood) => ({
|
||||
hk_code: kood.HkKood,
|
||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||
coefficient: kood.Koefitsient,
|
||||
price: kood.Hind,
|
||||
analysis_group_id: null,
|
||||
analysis_element_id: insertedAnalysisElementId,
|
||||
analysis_id: null,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const analyses = analysisElement.UuringuElement;
|
||||
if (analyses?.length) {
|
||||
for (const analysis of analyses) {
|
||||
const { data: insertedAnalysis, error } = await supabase
|
||||
.schema('medreport')
|
||||
.from('analyses')
|
||||
.upsert(
|
||||
{
|
||||
analysis_id_oid: analysis.UuringIdOID,
|
||||
analysis_id_original: analysis.UuringId,
|
||||
tehik_short_loinc: analysis.TLyhend,
|
||||
tehik_loinc_name: analysis.KNimetus,
|
||||
analysis_name_lab: analysis.UuringNimi,
|
||||
order: analysis.Jarjekord,
|
||||
parent_analysis_element_id: insertedAnalysisElementId,
|
||||
},
|
||||
{ onConflict: 'analysis_id_original', ignoreDuplicates: false },
|
||||
)
|
||||
.select('id');
|
||||
|
||||
if (error || !insertedAnalysis[0]?.id) {
|
||||
throw new Error(
|
||||
`Failed to insert analysis (id: ${analysis.UuringId}) error: ${error?.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
const insertedAnalysisId = insertedAnalysis[0].id;
|
||||
if (analysis.Kood) {
|
||||
const analysisCodes = toArray(analysis.Kood);
|
||||
|
||||
codes.push(
|
||||
...analysisCodes.map((kood) => ({
|
||||
hk_code: kood.HkKood,
|
||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||
coefficient: kood.Koefitsient,
|
||||
price: kood.Hind,
|
||||
analysis_group_id: null,
|
||||
analysis_element_id: null,
|
||||
analysis_id: insertedAnalysisId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.info('Inserting codes');
|
||||
await supabase.schema('medreport').from('codes').upsert(codes);
|
||||
|
||||
console.info('Inserting sync entry');
|
||||
await supabase.schema('audit').from('sync_entries').insert({
|
||||
operation: 'ANALYSES_SYNC',
|
||||
status: 'SUCCESS',
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
} catch (e) {
|
||||
await supabase
|
||||
.schema('audit')
|
||||
.from('sync_entries')
|
||||
.insert({
|
||||
operation: 'ANALYSES_SYNC',
|
||||
status: 'FAIL',
|
||||
comment: JSON.stringify(e),
|
||||
changed_by_role: 'service_role',
|
||||
});
|
||||
console.error(e);
|
||||
throw new Error(
|
||||
`Failed to sync public message data, error: ${JSON.stringify(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
141
app/api/job/handler/sync-connected-online.ts
Normal file
141
app/api/job/handler/sync-connected-online.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { createClient as createCustomClient } from '@supabase/supabase-js';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import type { IConnectedOnlineResponse_Search_Load } from './types';
|
||||
|
||||
export default async function syncConnectedOnline() {
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
const baseUrl = process.env.CONNECTED_ONLINE_URL;
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseServiceRoleKey = process.env.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<{ d: string }>(`${baseUrl}/Search_Load`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
param: "{'Value':'|et|-1'}", // get all available services in Estonian
|
||||
});
|
||||
|
||||
const responseData: IConnectedOnlineResponse_Search_Load = 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',
|
||||
});
|
||||
}
|
||||
|
||||
let clinics;
|
||||
let services;
|
||||
// Filter out "Dentas Demo OÜ" in prod or only sync "Dentas Demo OÜ" in any other environment
|
||||
if (isProd) {
|
||||
clinics = responseData.Data.T_Lic.filter((clinic) => clinic.ID !== 2);
|
||||
services = responseData.Data.T_Service.filter(
|
||||
(service) => service.ClinicID !== 2,
|
||||
);
|
||||
} else {
|
||||
clinics = responseData.Data.T_Lic.filter((clinic) => clinic.ID === 2);
|
||||
services = responseData.Data.T_Service.filter(
|
||||
(service) => service.ClinicID === 2,
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
.schema('medreport')
|
||||
.from('connected_online_providers')
|
||||
.upsert(mappedClinics);
|
||||
|
||||
const { error: servicesError } = await supabase
|
||||
.schema('medreport')
|
||||
.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)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
39
app/api/job/handler/types.ts
Normal file
39
app/api/job/handler/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export interface IMedipostResponse_GetPublicMessageList {
|
||||
code: number;
|
||||
messages: {
|
||||
messageId: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface IConnectedOnlineResponse_Search_Load {
|
||||
Value: string;
|
||||
Data: {
|
||||
T_Lic: {
|
||||
ID: number;
|
||||
Name: string;
|
||||
OnlineCanSelectWorker: boolean;
|
||||
Email: string | null;
|
||||
PersonalCodeRequired: boolean;
|
||||
Phone: string | null;
|
||||
}[];
|
||||
T_Service: {
|
||||
ID: number;
|
||||
ClinicID: number;
|
||||
Code: string;
|
||||
Description: string | null;
|
||||
Display: string;
|
||||
Duration: number;
|
||||
HasFreeCodes: boolean;
|
||||
Name: string;
|
||||
NetoDuration: number;
|
||||
OnlineHideDuration: number;
|
||||
OnlineHidePrice: number;
|
||||
Price: number;
|
||||
PricePeriods: string | null;
|
||||
RequiresPayment: boolean;
|
||||
SyncID: string;
|
||||
}[];
|
||||
};
|
||||
ErrorCode: number;
|
||||
ErrorMessage: string;
|
||||
}
|
||||
9
app/api/job/handler/validate-api-key.ts
Normal file
9
app/api/job/handler/validate-api-key.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export default function validateApiKey(request: NextRequest) {
|
||||
const envApiKey = process.env.JOBS_API_TOKEN;
|
||||
const requestApiKey = request.headers.get('x-jobs-api-key');
|
||||
if (requestApiKey !== envApiKey) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user