feat(MED-131): move jobs to /api/job/* secured with key

This commit is contained in:
2025-08-04 11:51:11 +03:00
parent 5746e1b087
commit 43493c261c
12 changed files with 204 additions and 93 deletions

3
.env
View File

@@ -73,3 +73,6 @@ MONTONIO_API_URL=https://sandbox-stargate.montonio.com
# MEDUSA # MEDUSA
MEDUSA_BACKEND_URL=http://localhost:9000 MEDUSA_BACKEND_URL=http://localhost:9000
MEDUSA_BACKEND_PUBLIC_URL=http://localhost:9000 MEDUSA_BACKEND_PUBLIC_URL=http://localhost:9000
# JOBS
JOBS_API_TOKEN=73ce073c-6dd4-11f0-8e75-8fee89786197

View File

@@ -23,3 +23,5 @@ NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
MONTONIO_SECRET_KEY=rNZkzwxOiH93mzkdV53AvhSsbGidrgO2Kl5lE/IT7cvo MONTONIO_SECRET_KEY=rNZkzwxOiH93mzkdV53AvhSsbGidrgO2Kl5lE/IT7cvo
MONTONIO_API_URL=https://sandbox-stargate.montonio.com MONTONIO_API_URL=https://sandbox-stargate.montonio.com
JOBS_API_TOKEN=73ce073c-6dd4-11f0-8e75-8fee89786197

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

View File

@@ -2,10 +2,10 @@ import { createClient as createCustomClient } from '@supabase/supabase-js';
import axios from 'axios'; import axios from 'axios';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { config } from 'dotenv';
import { XMLParser } from 'fast-xml-parser'; import { XMLParser } from 'fast-xml-parser';
import { IMedipostResponse_GetPublicMessageList } from './types';
function getLatestMessage(messages) { function getLatestMessage(messages: IMedipostResponse_GetPublicMessageList['messages']): IMedipostResponse_GetPublicMessageList['messages'][number] | null {
if (!messages?.length) { if (!messages?.length) {
return null; return null;
} }
@@ -15,16 +15,12 @@ function getLatestMessage(messages) {
); );
} }
export function toArray<T>(input?: T | T[] | null): T[] { function toArray<T>(input?: T | T[] | null): T[] {
if (!input) return []; if (!input) return [];
return Array.isArray(input) ? input : [input]; return Array.isArray(input) ? input : [input];
} }
async function syncData() { export default async function syncAnalysisGroups() {
if (process.env.NODE_ENV === 'local') {
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;
@@ -52,7 +48,7 @@ async function syncData() {
}); });
try { try {
// GET LATEST PUBLIC MESSAGE ID console.info('Getting latest public message id');
const { data: lastChecked } = await supabase const { data: lastChecked } = await supabase
.schema('audit') .schema('audit')
.from('sync_entries') .from('sync_entries')
@@ -60,34 +56,30 @@ async function syncData() {
.eq('status', 'SUCCESS') .eq('status', 'SUCCESS')
.order('created_at') .order('created_at')
.limit(1); .limit(1);
const lastEntry = lastChecked?.[0];
const lastCheckedDate = lastEntry
? format(lastEntry.created_at, 'yyyy-MM-dd HH:mm:ss')
: null;
const lastCheckedDate = lastChecked?.length console.info('Getting public message list');
? { const { data, status } = await axios.get<IMedipostResponse_GetPublicMessageList>(baseUrl, {
LastChecked: format(lastChecked[0].created_at, 'yyyy-MM-dd HH:mm:ss'),
}
: {};
const { data, status } = await axios.get(baseUrl, {
params: { params: {
Action: 'GetPublicMessageList', Action: 'GetPublicMessageList',
User: user, User: user,
Password: password, Password: password,
Sender: sender, //Sender: sender,
...lastCheckedDate, // ...(lastCheckedDate && { LastChecked: lastCheckedDate }),
MessageType: 'Teenus', MessageType: 'Teenus',
}, },
}); });
if (!data || status !== 200) { if (!data || status !== 200 || data.code !== 0) {
console.error("Failed to get public message list, status: ", status, data); console.error("Failed to get public message list, status: ", status, data);
throw new Error('Failed to get public message list'); throw new Error('Failed to get public message list');
} }
if (data.code && data.code !== 0) {
throw new Error('Failed to get public message list');
}
if (!data.messages?.length) { if (!data.messages?.length) {
console.info('No new data received');
return supabase.schema('audit').from('sync_entries').insert({ return supabase.schema('audit').from('sync_entries').insert({
operation: 'ANALYSES_SYNC', operation: 'ANALYSES_SYNC',
comment: 'No new data received', comment: 'No new data received',
@@ -96,9 +88,9 @@ async function syncData() {
}); });
} }
const latestMessage = getLatestMessage(data?.messages);
// GET PUBLIC MESSAGE WITH GIVEN ID // 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, { const { data: publicMessageData } = await axios.get(baseUrl, {
params: { params: {
@@ -266,8 +258,10 @@ async function syncData() {
} }
} }
console.info('Inserting codes');
await supabase.schema('medreport').from('codes').upsert(codes); await supabase.schema('medreport').from('codes').upsert(codes);
console.info('Inserting sync entry');
await supabase.schema('audit').from('sync_entries').insert({ await supabase.schema('audit').from('sync_entries').insert({
operation: 'ANALYSES_SYNC', operation: 'ANALYSES_SYNC',
status: 'SUCCESS', status: 'SUCCESS',
@@ -283,10 +277,9 @@ async function syncData() {
comment: JSON.stringify(e), comment: JSON.stringify(e),
changed_by_role: 'service_role', changed_by_role: 'service_role',
}); });
console.error(e);
throw new Error( throw new Error(
`Failed to sync public message data, error: ${JSON.stringify(e)}`, `Failed to sync public message data, error: ${JSON.stringify(e)}`,
); );
} }
} }
syncData();

View File

@@ -1,13 +1,10 @@
import { createClient as createCustomClient } from '@supabase/supabase-js'; import { createClient as createCustomClient } from '@supabase/supabase-js';
import axios from 'axios'; import axios from 'axios';
import { config } from 'dotenv';
async function syncData() { import type { IConnectedOnlineResponse_Search_Load } from './types';
if (process.env.NODE_ENV === 'local') {
config({ path: `.env.${process.env.NODE_ENV}` });
}
export default async function syncConnectedOnline() {
const isProd = process.env.NODE_ENV === 'production'; const isProd = process.env.NODE_ENV === 'production';
const baseUrl = process.env.CONNECTED_ONLINE_URL; const baseUrl = process.env.CONNECTED_ONLINE_URL;
@@ -27,19 +24,14 @@ async function syncData() {
}); });
try { try {
const response = await axios.post(`${baseUrl}/Search_Load`, { const response = await axios.post<{ d: string }>(`${baseUrl}/Search_Load`, {
headers: { headers: {
'Content-Type': 'application/json; charset=utf-8', 'Content-Type': 'application/json; charset=utf-8',
}, },
param: "{'Value':'|et|-1'}", // get all available services in Estonian param: "{'Value':'|et|-1'}", // get all available services in Estonian
}); });
const responseData: { const responseData: IConnectedOnlineResponse_Search_Load = JSON.parse(response.data.d);
Value: any;
Data: any;
ErrorCode: number;
ErrorMessage: string;
} = JSON.parse(response.data.d);
if (responseData?.ErrorCode !== 0) { if (responseData?.ErrorCode !== 0) {
throw new Error('Failed to get Connected Online data'); throw new Error('Failed to get Connected Online data');
@@ -147,5 +139,3 @@ async function syncData() {
); );
} }
} }
syncData();

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

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

View File

@@ -0,0 +1,27 @@
import { NextRequest, NextResponse } from "next/server";
import syncAnalysisGroups from "../handler/sync-analysis-groups";
import loadEnv from "../handler/load-env";
import validateApiKey from "../handler/validate-api-key";
export const GET = async (request: NextRequest) => {
loadEnv();
try {
validateApiKey(request);
} catch (e) {
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
}
try {
await syncAnalysisGroups();
console.info("Successfully synced analysis groups");
return NextResponse.json({
message: 'Successfully synced analysis groups',
}, { status: 200 });
} catch (e) {
console.error("Error syncing analysis groups", e);
return NextResponse.json({
message: 'Failed to sync analysis groups',
}, { status: 500 });
}
};

View File

@@ -0,0 +1,27 @@
import { NextRequest, NextResponse } from "next/server";
import loadEnv from "../handler/load-env";
import validateApiKey from "../handler/validate-api-key";
import syncConnectedOnline from "../handler/sync-connected-online";
export const GET = async (request: NextRequest) => {
loadEnv();
try {
validateApiKey(request);
} catch (e) {
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
}
try {
await syncConnectedOnline();
console.info("Successfully synced connected-online");
return NextResponse.json({
message: 'Successfully synced connected-online',
}, { status: 200 });
} catch (e) {
console.error("Error syncing connected-online", e);
return NextResponse.json({
message: 'Failed to sync connected-online',
}, { status: 500 });
}
};

View File

@@ -10,7 +10,6 @@ import {
getClientInstitution, getClientInstitution,
getClientPerson, getClientPerson,
getConfidentiality, getConfidentiality,
getOrderEnteredByPerson,
getPais, getPais,
getPatient, getPatient,
getProviderInstitution, getProviderInstitution,
@@ -149,6 +148,7 @@ export async function getPrivateMessage(messageId: string) {
const parsed = parser.parse(data); const parsed = parser.parse(data);
if (parsed.ANSWER?.CODE && parsed.ANSWER?.CODE !== 0) { if (parsed.ANSWER?.CODE && parsed.ANSWER?.CODE !== 0) {
console.error("Bad response", data);
throw new Error(`Failed to get private message (id: ${messageId})`); throw new Error(`Failed to get private message (id: ${messageId})`);
} }
@@ -378,10 +378,13 @@ export async function syncPublicMessage(
} }
} }
// TODO use actual parameters
export async function composeOrderXML( export async function composeOrderXML(
/* chosenAnalysisElements?: number[], person: {
chosenAnalyses?: number[], */ idCode: string,
firstName: string,
lastName: string,
phone: string,
},
comment?: string, comment?: string,
) { ) {
const supabase = createCustomClient( const supabase = createCustomClient(
@@ -512,11 +515,9 @@ export async function composeOrderXML(
<!--<TeostajaAsutus>--> <!--<TeostajaAsutus>-->
${getProviderInstitution()} ${getProviderInstitution()}
<!--<TellijaIsik>--> <!--<TellijaIsik>-->
${getClientPerson()} ${getClientPerson(person)}
<!--<SisestajaIsik>-->
${getOrderEnteredByPerson()}
<TellijaMarkused>${comment ?? ''}</TellijaMarkused> <TellijaMarkused>${comment ?? ''}</TellijaMarkused>
${getPatient(49610230861, 'Surname', 'First Name', '1996-10-23', 'N')} ${getPatient(person)}
${getConfidentiality()} ${getConfidentiality()}
${specimenSection.join('')} ${specimenSection.join('')}
${analysisSection?.join('')} ${analysisSection?.join('')}

View File

@@ -1,6 +1,7 @@
import { DATE_TIME_FORMAT } from '@/lib/constants';
import { Tables } from '@/packages/supabase/src/database.types';
import { format } from 'date-fns'; import { format } from 'date-fns';
import Isikukood from 'isikukood';
import { Tables } from '@/packages/supabase/src/database.types';
import { DATE_FORMAT, DATE_TIME_FORMAT } from '@/lib/constants';
const isProd = process.env.NODE_ENV === 'production'; const isProd = process.env.NODE_ENV === 'production';
@@ -48,47 +49,60 @@ export const getProviderInstitution = () => {
</Asutus>`; </Asutus>`;
}; };
export const getClientPerson = () => { export const getClientPerson = ({
idCode,
firstName,
lastName,
phone,
}: {
idCode: string,
firstName: string,
lastName: string,
phone: string,
}) => {
if (isProd) { if (isProd) {
// return correct data // return correct data
} }
return `<Personal tyyp="TELLIJA" jarjenumber="1"> return `<Personal tyyp="TELLIJA" jarjenumber="1">
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID> <PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
<PersonalKood>D07907</PersonalKood> <PersonalKood>${idCode}</PersonalKood>
<PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi> <PersonalPerekonnaNimi>${lastName}</PersonalPerekonnaNimi>
<PersonalEesNimi>Tsvetkov</PersonalEesNimi> <PersonalEesNimi>${firstName}</PersonalEesNimi>
<Telefon>+37258131202</Telefon> <Telefon>${phone}</Telefon>
</Personal>`; </Personal>`;
}; };
export const getOrderEnteredPerson = () => { // export const getOrderEnteredPerson = () => {
if (isProd) { // if (isProd) {
// return correct data // // return correct data
} // }
return `<Personal tyyp="SISESTAJA" jarjenumber="1"> // return `<Personal tyyp="SISESTAJA" jarjenumber="1">
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID> // <PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
<PersonalKood>D07907</PersonalKood> // <PersonalKood>D07907</PersonalKood>
<PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi> // <PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
<PersonalEesNimi>Tsvetkov</PersonalEesNimi> // <PersonalEesNimi>Tsvetkov</PersonalEesNimi>
<Telefon>+37258131202</Telefon> // <Telefon>+37258131202</Telefon>
</Personal>`; // </Personal>`;
}; // };
export const getPatient = ( export const getPatient = ({
idCode: number, idCode,
surname: string, lastName,
firstName,
}: {
idCode: string,
lastName: string,
firstName: string, firstName: string,
birthDate: string, }) => {
genderLetter: string, const isikukood = new Isikukood(idCode);
) => {
return `<Patsient> return `<Patsient>
<IsikukoodiOID>1.3.6.1.4.1.28284.6.2.2.1</IsikukoodiOID> <IsikukoodiOID>1.3.6.1.4.1.28284.6.2.2.1</IsikukoodiOID>
<Isikukood>${idCode}</Isikukood> <Isikukood>${idCode}</Isikukood>
<PerekonnaNimi>${surname}</PerekonnaNimi> <PerekonnaNimi>${lastName}</PerekonnaNimi>
<EesNimi>${firstName}</EesNimi> <EesNimi>${firstName}</EesNimi>
<SynniAeg>${birthDate}</SynniAeg> <SynniAeg>${format(isikukood.getBirthday(), DATE_FORMAT)}</SynniAeg>
<SuguOID>1.3.6.1.4.1.28284.6.2.3.16.2</SuguOID> <SuguOID>1.3.6.1.4.1.28284.6.2.3.16.2</SuguOID>
<Sugu>${genderLetter}</Sugu> <Sugu>${isikukood.getGender()}</Sugu>
</Patsient>`; </Patsient>`;
}; };
@@ -106,19 +120,19 @@ export const getConfidentiality = () => {
</Konfidentsiaalsus>`; </Konfidentsiaalsus>`;
}; };
export const getOrderEnteredByPerson = () => { // export const getOrderEnteredByPerson = () => {
if (isProd) { // if (isProd) {
// return correct data // // return correct data
} // }
return ` // return `
<Personal tyyp="SISESTAJA" jarjenumber="1"> // <Personal tyyp="SISESTAJA" jarjenumber="1">
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID> // <PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
<PersonalKood>D07907</PersonalKood> // <PersonalKood>D07907</PersonalKood>
<PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi> // <PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
<PersonalEesNimi>Tsvetkov</PersonalEesNimi> // <PersonalEesNimi>Tsvetkov</PersonalEesNimi>
<Telefon>+37258131202</Telefon> // <Telefon>+37258131202</Telefon>
</Personal>`; // </Personal>`;
}; // };
export const getSpecimen = ( export const getSpecimen = (
materialTypeOid: string, materialTypeOid: string,

View File

@@ -26,9 +26,7 @@
"supabase:db:diff": "supabase db diff --schema auth --schema audit --schema medreport", "supabase:db:diff": "supabase db diff --schema auth --schema audit --schema medreport",
"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": "supabase gen types typescript --local > ./packages/supabase/src/database.types.ts", "supabase:typegen": "supabase gen types typescript --local > ./packages/supabase/src/database.types.ts",
"supabase:db:dump:local": "supabase db dump --local --data-only", "supabase:db:dump:local": "supabase db dump --local --data-only"
"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",