Files
medreport_mrb2b/packages/features/user-analyses/src/server/api.ts
2025-10-06 15:16:13 +03:00

496 lines
16 KiB
TypeScript

import { SupabaseClient } from '@supabase/supabase-js';
import { getLogger } from '@kit/shared/logger';
import type { UuringuVastus } from '@kit/shared/types/medipost-analysis';
import { toArray } from '@kit/shared/utils';
import { Database } from '@kit/supabase/database';
import type {
AnalysisOrder,
AnalysisOrderStatus,
} from '../types/analysis-orders';
import type {
AnalysisResultDetailsElement,
AnalysisResultDetailsMapped,
AnalysisResultLevel,
AnalysisResultsDetailsElementNested,
AnalysisResultsQuery,
AnalysisStatus,
UserAnalysis,
} from '../types/analysis-results';
/**
* Class representing an API for interacting with user accounts.
* @constructor
* @param {SupabaseClient<Database>} client - The Supabase client instance.
*/
class UserAnalysesApi {
constructor(private readonly client: SupabaseClient<Database>) {}
async getAnalysisOrder({
medusaOrderId,
analysisOrderId,
}: {
medusaOrderId?: string;
analysisOrderId?: number;
}) {
const query = this.client
.schema('medreport')
.from('analysis_orders')
.select('*');
if (medusaOrderId) {
query.eq('medusa_order_id', medusaOrderId);
} else if (analysisOrderId) {
query.eq('id', analysisOrderId);
} else {
throw new Error('Either medusaOrderId or orderId must be provided');
}
const { data: order, error } = await query.single();
if (error) {
throw new Error(
`Failed to get order by medusaOrderId=${medusaOrderId} or analysisOrderId=${analysisOrderId}, message=${error.message}, data=${JSON.stringify(order)}`,
);
}
return order as AnalysisOrder;
}
async getUserAnalysis(
analysisOrderId: number,
): Promise<AnalysisResultDetailsMapped | null> {
const analysisOrder = await this.getAnalysisOrder({ analysisOrderId });
const orderedAnalysisElementIds = analysisOrder.analysis_element_ids ?? [];
if (orderedAnalysisElementIds.length === 0) {
console.error(
'No ordered analysis element ids found for analysis order id=',
analysisOrderId,
);
return null;
}
const orderedAnalysisElements = await this.getOrderedAnalysisElements({
analysisOrderId,
orderedAnalysisElementIds,
});
const orderedAnalysisElementOriginalIds = orderedAnalysisElements.map(
({ analysis_id_original }) => analysis_id_original,
);
if (orderedAnalysisElementOriginalIds.length === 0) {
console.error(
'No ordered analysis element original ids found for analysis order id=',
analysisOrderId,
);
return null;
}
const responseWithElements = await this.getAnalysisResponseWithElements({
analysisOrderId,
});
if (!responseWithElements) {
return null;
}
const mappedOrderedAnalysisElements =
await this.getMappedOrderedAnalysisElements({
analysisResponseElements: responseWithElements.elements,
orderedAnalysisElements,
});
const feedback =
responseWithElements.summary?.doctor_analysis_feedback?.[0];
return {
id: analysisOrderId,
order: {
status: analysisOrder.status,
medusaOrderId: analysisOrder.medusa_order_id,
createdAt: analysisOrder.created_at,
},
orderedAnalysisElements: mappedOrderedAnalysisElements,
summary:
feedback?.status === 'COMPLETED'
? (responseWithElements.summary?.doctor_analysis_feedback?.[0] ??
null)
: null,
};
}
async getAnalysisResponseWithElements({
analysisOrderId,
}: {
analysisOrderId: number;
}) {
const { data, error: userError } = await this.client.auth.getUser();
if (userError) {
throw userError;
}
const { user } = data;
const { data: analysisResponse } = await this.client
.schema('medreport')
.from('analysis_responses')
.select(
`*,
elements:analysis_response_elements(analysis_name,norm_status,response_value,unit,norm_lower_included,norm_upper_included,norm_lower,norm_upper,response_time,status,analysis_element_original_id,original_response_element,response_value_is_negative,response_value_is_within_norm),
summary:analysis_order_id(doctor_analysis_feedback(*))`,
)
.eq('user_id', user.id)
.eq('analysis_order_id', analysisOrderId)
.throwOnError();
return analysisResponse?.[0] as AnalysisResultsQuery | null;
}
async getOrderedAnalysisElements({
analysisOrderId,
orderedAnalysisElementIds,
}: {
analysisOrderId: number;
orderedAnalysisElementIds: number[];
}) {
const {
data: orderedAnalysisElements,
error: orderedAnalysisElementsError,
} = await this.client
.schema('medreport')
.from('analysis_elements')
.select('analysis_id_original,analysis_name_lab')
.in('id', orderedAnalysisElementIds);
if (orderedAnalysisElementsError) {
console.error(
`Failed to get ordered analysis elements for analysis order id=${analysisOrderId}`,
orderedAnalysisElementsError,
);
throw orderedAnalysisElementsError;
}
return orderedAnalysisElements;
}
async getMappedOrderedAnalysisElements({
analysisResponseElements,
orderedAnalysisElements,
}: {
analysisResponseElements: AnalysisResultsQuery['elements'];
orderedAnalysisElements: {
analysis_id_original: string;
analysis_name_lab: string;
}[];
}): Promise<AnalysisResultDetailsElement[]> {
const mappedOrderedAnalysisElements = orderedAnalysisElements
.map(({ analysis_id_original, analysis_name_lab }) => {
return this.getOrderedAnalysisElement({
analysisIdOriginal: analysis_id_original,
analysisNameLab: analysis_name_lab,
analysisResponseElements,
});
})
.sort((a, b) => a.analysisName.localeCompare(b.analysisName));
const nestedAnalysisElementIds = mappedOrderedAnalysisElements
.map(({ results }) =>
results?.nestedElements.map(
({ analysisElementOriginalId }) => analysisElementOriginalId,
),
)
.flat()
.filter(Boolean);
if (nestedAnalysisElementIds.length > 0) {
const {
data: nestedAnalysisElements,
error: nestedAnalysisElementsError,
} = await this.client
.schema('medreport')
.from('analysis_elements')
.select('*')
.in('analysis_id_original', nestedAnalysisElementIds);
if (!nestedAnalysisElementsError && nestedAnalysisElements) {
for (const mappedOrderedAnalysisElement of mappedOrderedAnalysisElements) {
const { results } = mappedOrderedAnalysisElement;
if (!results) {
continue;
}
for (const nestedElement of results.nestedElements) {
const { analysisElementOriginalId } = nestedElement;
const nestedAnalysisElement = nestedAnalysisElements.find(
({ analysis_id_original }) =>
analysis_id_original === analysisElementOriginalId,
);
if (!nestedAnalysisElement) {
continue;
}
nestedElement.analysisElementOriginalId = analysisElementOriginalId;
nestedElement.analysisName =
nestedAnalysisElement.analysis_name_lab as string | undefined;
}
results.nestedElements = results.nestedElements.sort(
(a, b) => a.analysisName?.localeCompare(b.analysisName ?? '') ?? 0,
);
}
} else {
console.error(
'Failed to get nested analysis elements by ids=',
nestedAnalysisElementIds,
nestedAnalysisElementsError,
);
}
}
return mappedOrderedAnalysisElements;
}
getOrderedAnalysisElement({
analysisIdOriginal,
analysisNameLab,
analysisResponseElements,
}: {
analysisIdOriginal: string;
analysisNameLab: string;
analysisResponseElements: AnalysisResultsQuery['elements'];
}): AnalysisResultDetailsElement {
const elementResponse = analysisResponseElements.find(
(element) => element.analysis_element_original_id === analysisIdOriginal,
);
if (!elementResponse) {
return {
analysisIdOriginal,
isWaitingForResults: true,
analysisName: analysisNameLab,
};
}
const labComment =
elementResponse.original_response_element?.UuringuKommentaar;
return {
analysisIdOriginal,
isWaitingForResults: false,
analysisName: analysisNameLab,
results: {
nestedElements: ((): AnalysisResultsDetailsElementNested[] => {
const nestedElements = toArray(
elementResponse.original_response_element?.UuringuElement,
);
return nestedElements.map<AnalysisResultsDetailsElementNested>(
(element) => {
const mappedResponse = this.mapUuringVastus({
uuringVastus: element.UuringuVastus as
| UuringuVastus
| undefined,
});
return {
unit: element.Mootyhik ?? null,
normLower: mappedResponse.normLower,
normUpper: mappedResponse.normUpper,
normStatus: mappedResponse.normStatus,
responseTime: mappedResponse.responseTime,
responseValue: mappedResponse.responseValue,
responseValueIsNegative: mappedResponse.responseValueIsNegative,
responseValueIsWithinNorm:
mappedResponse.responseValueIsWithinNorm,
normLowerIncluded: mappedResponse.normLowerIncluded,
normUpperIncluded: mappedResponse.normUpperIncluded,
analysisElementOriginalId: element.UuringId,
status: Number(elementResponse.status) as AnalysisStatus,
analysisName: undefined,
};
},
);
})(),
labComment,
unit: elementResponse.unit,
normLower: elementResponse.norm_lower,
normUpper: elementResponse.norm_upper,
normStatus: elementResponse.norm_status,
responseTime: elementResponse.response_time,
responseValue: elementResponse.response_value,
responseValueIsNegative:
elementResponse.response_value_is_negative === null
? null
: elementResponse.response_value_is_negative === true,
responseValueIsWithinNorm:
elementResponse.response_value_is_within_norm === null
? null
: elementResponse.response_value_is_within_norm === true,
normLowerIncluded: elementResponse.norm_lower_included,
normUpperIncluded: elementResponse.norm_upper_included,
status: Number(elementResponse.status) as AnalysisStatus,
analysisElementOriginalId: elementResponse.analysis_element_original_id,
},
};
}
mapUuringVastus({ uuringVastus }: { uuringVastus?: UuringuVastus }) {
const vastuseVaartus = uuringVastus?.VastuseVaartus;
const responseValue = (() => {
const valueAsNumber = Number(vastuseVaartus);
if (isNaN(valueAsNumber)) {
return null;
}
return valueAsNumber;
})();
const responseValueNumber = Number(responseValue);
const responseValueIsNumeric = !isNaN(responseValueNumber);
const responseValueIsNegative = vastuseVaartus === 'Negatiivne';
const responseValueIsWithinNorm = vastuseVaartus === 'Normi piires';
return {
normLower: uuringVastus?.NormAlum?.['#text'] ?? null,
normUpper: uuringVastus?.NormYlem?.['#text'] ?? null,
normStatus: (uuringVastus?.NormiStaatus ??
null) as AnalysisResultLevel | null,
responseTime: uuringVastus?.VastuseAeg ?? null,
responseValue:
responseValueIsNegative || !responseValueIsNumeric
? null
: (responseValueNumber ?? null),
responseValueIsNegative: responseValueIsNumeric
? null
: responseValueIsNegative,
responseValueIsWithinNorm: responseValueIsNumeric
? null
: responseValueIsWithinNorm,
normLowerIncluded:
uuringVastus?.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah',
normUpperIncluded:
uuringVastus?.NormYlem?.['@_kaasaarvatud'].toLowerCase() === 'jah',
};
}
// @TODO unused currently
async getUserAnalyses(): Promise<UserAnalysis | null> {
const authUser = await this.client.auth.getUser();
const { data, error: userError } = authUser;
if (userError) {
console.error('Failed to get user', userError);
throw userError;
}
const { user } = data;
const { data: analysisResponses } = await this.client
.schema('medreport')
.from('analysis_responses')
.select('*')
.eq('user_id', user.id);
if (!analysisResponses) {
return null;
}
const analysisResponseIds = analysisResponses.map((r) => r.id);
const { data: analysisResponseElements } = await this.client
.schema('medreport')
.from('analysis_response_elements')
.select('*')
.in('analysis_response_id', analysisResponseIds);
if (!analysisResponseElements) {
return null;
}
return analysisResponses.map((r) => ({
...r,
elements: analysisResponseElements.filter(
(e) => e.analysis_response_id === r.id,
),
}));
}
async hasAccountTeamMembership(accountId?: string) {
if (!accountId) {
return false;
}
const { count, error } = await this.client
.schema('medreport')
.from('accounts_memberships')
.select('account_id', { count: 'exact', head: true })
.eq('account_id', accountId);
if (error) {
throw error;
}
return (count ?? 0) > 0;
}
async fetchBmiThresholds() {
// Fetch BMI
const { data, error } = await this.client
.schema('medreport')
.from('bmi_thresholds')
.select(
'age_min,age_max,underweight_max,normal_min,normal_max,overweight_min,strong_min,obesity_min',
)
.order('age_min', { ascending: true });
if (error) {
console.error('Error fetching BMI thresholds:', error);
throw error;
}
return data;
}
async getAllUserAnalysisResponses(): Promise<
Database['medreport']['Functions']['get_latest_analysis_response_elements_for_current_user']['Returns']
> {
const {
data: { user },
} = await this.client.auth.getUser();
if (!user) {
return [];
}
const { data, error } = await this.client
.schema('medreport')
.rpc('get_latest_analysis_response_elements_for_current_user', {
p_user_id: user.id,
});
if (error) {
console.error('Error fetching user analysis responses: ', error);
throw error;
}
return data;
}
async updateAnalysisOrderStatus({
orderId,
medusaOrderId,
orderStatus,
}: {
orderId?: number;
medusaOrderId?: string;
orderStatus: AnalysisOrderStatus;
}) {
const logger = await getLogger();
const orderIdParam = orderId;
const medusaOrderIdParam = medusaOrderId;
const ctx = {
action: 'update-analysis-order-status',
orderId,
medusaOrderId,
orderStatus,
};
logger.info(ctx, 'Updating order');
if (!orderIdParam && !medusaOrderIdParam) {
logger.error(ctx, 'Missing orderId or medusaOrderId');
throw new Error('Either orderId or medusaOrderId must be provided');
}
await this.client
.schema('medreport')
.rpc('update_analysis_order_status', {
order_id: orderIdParam ?? -1,
status_param: orderStatus,
medusa_order_id_param: medusaOrderIdParam ?? '',
})
.throwOnError();
}
}
export function createUserAnalysesApi(client: SupabaseClient<Database>) {
return new UserAnalysesApi(client);
}