diff --git a/.env b/.env index 04c866d..14a35b4 100644 --- a/.env +++ b/.env @@ -73,3 +73,6 @@ MONTONIO_API_URL=https://sandbox-stargate.montonio.com # MEDUSA MEDUSA_BACKEND_URL=http://localhost:9000 MEDUSA_BACKEND_PUBLIC_URL=http://localhost:9000 + +# JOBS +JOBS_API_TOKEN=73ce073c-6dd4-11f0-8e75-8fee89786197 diff --git a/.env.example b/.env.example index ce9f94c..091949f 100644 --- a/.env.example +++ b/.env.example @@ -23,3 +23,5 @@ NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY= NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead MONTONIO_SECRET_KEY=rNZkzwxOiH93mzkdV53AvhSsbGidrgO2Kl5lE/IT7cvo MONTONIO_API_URL=https://sandbox-stargate.montonio.com + +JOBS_API_TOKEN=73ce073c-6dd4-11f0-8e75-8fee89786197 diff --git a/app/api/job/handler/load-env.ts b/app/api/job/handler/load-env.ts new file mode 100644 index 0000000..9a218b6 --- /dev/null +++ b/app/api/job/handler/load-env.ts @@ -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}` }); + } +} diff --git a/jobs/sync-analysis-groups.ts b/app/api/job/handler/sync-analysis-groups.ts similarity index 88% rename from jobs/sync-analysis-groups.ts rename to app/api/job/handler/sync-analysis-groups.ts index d7e32d1..c0d56d8 100644 --- a/jobs/sync-analysis-groups.ts +++ b/app/api/job/handler/sync-analysis-groups.ts @@ -2,10 +2,10 @@ import { createClient as createCustomClient } from '@supabase/supabase-js'; import axios from 'axios'; import { format } from 'date-fns'; -import { config } from 'dotenv'; 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) { return null; } @@ -15,16 +15,12 @@ function getLatestMessage(messages) { ); } -export function toArray(input?: T | T[] | null): T[] { +function toArray(input?: T | T[] | null): T[] { if (!input) return []; return Array.isArray(input) ? input : [input]; } -async function syncData() { - if (process.env.NODE_ENV === 'local') { - config({ path: `.env.${process.env.NODE_ENV}` }); - } - +export default async function syncAnalysisGroups() { const baseUrl = process.env.MEDIPOST_URL; const user = process.env.MEDIPOST_USER; const password = process.env.MEDIPOST_PASSWORD; @@ -52,7 +48,7 @@ async function syncData() { }); try { - // GET LATEST PUBLIC MESSAGE ID + console.info('Getting latest public message id'); const { data: lastChecked } = await supabase .schema('audit') .from('sync_entries') @@ -60,34 +56,30 @@ async function syncData() { .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; - const lastCheckedDate = lastChecked?.length - ? { - LastChecked: format(lastChecked[0].created_at, 'yyyy-MM-dd HH:mm:ss'), - } - : {}; - - const { data, status } = await axios.get(baseUrl, { + console.info('Getting public message list'); + const { data, status } = await axios.get(baseUrl, { params: { Action: 'GetPublicMessageList', User: user, Password: password, - Sender: sender, - ...lastCheckedDate, + //Sender: sender, + // ...(lastCheckedDate && { LastChecked: lastCheckedDate }), MessageType: 'Teenus', }, }); - if (!data || status !== 200) { + 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.code && data.code !== 0) { - 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', @@ -96,9 +88,9 @@ async function syncData() { }); } - const latestMessage = getLatestMessage(data?.messages); - // 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: { @@ -266,8 +258,10 @@ async function syncData() { } } + 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', @@ -283,10 +277,9 @@ async function syncData() { 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)}`, ); } } - -syncData(); diff --git a/jobs/sync-connected-online.ts b/app/api/job/handler/sync-connected-online.ts similarity index 91% rename from jobs/sync-connected-online.ts rename to app/api/job/handler/sync-connected-online.ts index 4944bb7..2d0d9fb 100644 --- a/jobs/sync-connected-online.ts +++ b/app/api/job/handler/sync-connected-online.ts @@ -1,13 +1,10 @@ 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}` }); - } +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; @@ -27,19 +24,14 @@ async function syncData() { }); try { - const response = await axios.post(`${baseUrl}/Search_Load`, { + 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: { - Value: any; - Data: any; - ErrorCode: number; - ErrorMessage: string; - } = JSON.parse(response.data.d); + const responseData: IConnectedOnlineResponse_Search_Load = JSON.parse(response.data.d); if (responseData?.ErrorCode !== 0) { throw new Error('Failed to get Connected Online data'); @@ -147,5 +139,3 @@ async function syncData() { ); } } - -syncData(); diff --git a/app/api/job/handler/types.ts b/app/api/job/handler/types.ts new file mode 100644 index 0000000..0f32e9f --- /dev/null +++ b/app/api/job/handler/types.ts @@ -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; +} diff --git a/app/api/job/handler/validate-api-key.ts b/app/api/job/handler/validate-api-key.ts new file mode 100644 index 0000000..70af4ba --- /dev/null +++ b/app/api/job/handler/validate-api-key.ts @@ -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'); + } +} diff --git a/app/api/job/sync-analysis-groups/route.ts b/app/api/job/sync-analysis-groups/route.ts new file mode 100644 index 0000000..c4c193d --- /dev/null +++ b/app/api/job/sync-analysis-groups/route.ts @@ -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 }); + } +}; diff --git a/app/api/job/sync-connected-online/route.ts b/app/api/job/sync-connected-online/route.ts new file mode 100644 index 0000000..03a16ee --- /dev/null +++ b/app/api/job/sync-connected-online/route.ts @@ -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 }); + } +}; diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts index 7e25449..0c6998d 100644 --- a/lib/services/medipost.service.ts +++ b/lib/services/medipost.service.ts @@ -10,7 +10,6 @@ import { getClientInstitution, getClientPerson, getConfidentiality, - getOrderEnteredByPerson, getPais, getPatient, getProviderInstitution, @@ -149,6 +148,7 @@ export async function getPrivateMessage(messageId: string) { const parsed = parser.parse(data); if (parsed.ANSWER?.CODE && parsed.ANSWER?.CODE !== 0) { + console.error("Bad response", data); 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( - /* chosenAnalysisElements?: number[], - chosenAnalyses?: number[], */ + person: { + idCode: string, + firstName: string, + lastName: string, + phone: string, + }, comment?: string, ) { const supabase = createCustomClient( @@ -512,11 +515,9 @@ export async function composeOrderXML( ${getProviderInstitution()} - ${getClientPerson()} - - ${getOrderEnteredByPerson()} + ${getClientPerson(person)} ${comment ?? ''} - ${getPatient(49610230861, 'Surname', 'First Name', '1996-10-23', 'N')} + ${getPatient(person)} ${getConfidentiality()} ${specimenSection.join('')} ${analysisSection?.join('')} diff --git a/lib/templates/medipost-order.ts b/lib/templates/medipost-order.ts index 9439438..51a9765 100644 --- a/lib/templates/medipost-order.ts +++ b/lib/templates/medipost-order.ts @@ -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 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'; @@ -48,47 +49,60 @@ export const getProviderInstitution = () => { `; }; -export const getClientPerson = () => { +export const getClientPerson = ({ + idCode, + firstName, + lastName, + phone, +}: { + idCode: string, + firstName: string, + lastName: string, + phone: string, +}) => { if (isProd) { // return correct data } return ` 1.3.6.1.4.1.28284.6.2.4.9 - D07907 - Eduard - Tsvetkov - +37258131202 + ${idCode} + ${lastName} + ${firstName} + ${phone} `; }; -export const getOrderEnteredPerson = () => { - if (isProd) { - // return correct data - } - return ` - 1.3.6.1.4.1.28284.6.2.4.9 - D07907 - Eduard - Tsvetkov - +37258131202 - `; -}; +// export const getOrderEnteredPerson = () => { +// if (isProd) { +// // return correct data +// } +// return ` +// 1.3.6.1.4.1.28284.6.2.4.9 +// D07907 +// Eduard +// Tsvetkov +// +37258131202 +// `; +// }; -export const getPatient = ( - idCode: number, - surname: string, +export const getPatient = ({ + idCode, + lastName, + firstName, +}: { + idCode: string, + lastName: string, firstName: string, - birthDate: string, - genderLetter: string, -) => { +}) => { + const isikukood = new Isikukood(idCode); return ` 1.3.6.1.4.1.28284.6.2.2.1 ${idCode} - ${surname} + ${lastName} ${firstName} - ${birthDate} + ${format(isikukood.getBirthday(), DATE_FORMAT)} 1.3.6.1.4.1.28284.6.2.3.16.2 - ${genderLetter} + ${isikukood.getGender()} `; }; @@ -106,19 +120,19 @@ export const getConfidentiality = () => { `; }; -export const getOrderEnteredByPerson = () => { - if (isProd) { - // return correct data - } - return ` - - 1.3.6.1.4.1.28284.6.2.4.9 - D07907 - Eduard - Tsvetkov - +37258131202 - `; -}; +// export const getOrderEnteredByPerson = () => { +// if (isProd) { +// // return correct data +// } +// return ` +// +// 1.3.6.1.4.1.28284.6.2.4.9 +// D07907 +// Eduard +// Tsvetkov +// +37258131202 +// `; +// }; export const getSpecimen = ( materialTypeOid: string, diff --git a/package.json b/package.json index 4106646..11b1f81 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,7 @@ "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:typegen": "supabase gen types typescript --local > ./packages/supabase/src/database.types.ts", - "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" + "supabase:db:dump:local": "supabase db dump --local --data-only" }, "dependencies": { "@edge-csrf/nextjs": "2.5.3-cloudflare-rc1",