Merge pull request #64 from MR-medreport/MED-85

feat(MED-85-105-123): some testing feedback, other improvements
This commit is contained in:
2025-08-29 11:52:55 +03:00
committed by GitHub
28 changed files with 328 additions and 84 deletions

View File

@@ -41,8 +41,6 @@ export async function POST(request: NextRequest) {
orderCreatedAt: new Date(medreportOrder.created_at),
});
console.info("SEND XML", messageXml);
try {
await sendPrivateMessageTestResponse({ messageXml });
} catch (error) {

View File

@@ -3,7 +3,7 @@ import { getOrder } from "~/lib/services/order.service";
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service";
import { retrieveOrder } from "@lib/data";
import { getAccountAdmin } from "~/lib/services/account.service";
import { getOrderedAnalysisElementsIds } from "~/lib/services/medipost.service";
import { createMedipostActionLog, getOrderedAnalysisElementsIds } from "~/lib/services/medipost.service";
export async function POST(request: Request) {
// const isDev = process.env.NODE_ENV === 'development';
@@ -35,6 +35,11 @@ export async function POST(request: Request) {
});
try {
await createMedipostActionLog({
action: 'send_fake_analysis_results_to_medipost',
xml: messageXml,
medusaOrderId,
});
await sendPrivateMessageTestResponse({ messageXml });
} catch (error) {
console.error("Error sending private message test response: ", error);

View File

@@ -31,8 +31,8 @@ export async function HomeMenuNavigation(props: {
})
: 0;
const cartItemsCount = props.cart?.items?.length ?? 0;
const hasCartItems = cartItemsCount > 0;
const cartQuantityTotal = props.cart?.items?.reduce((acc, item) => acc + item.quantity, 0) ?? 0;
const hasCartItems = cartQuantityTotal > 0;
return (
<div className={'flex w-full flex-1 items-center justify-between gap-3'}>
@@ -64,7 +64,7 @@ export async function HomeMenuNavigation(props: {
<ShoppingCart className="stroke-[1.5px]" />
<Trans
i18nKey="common:shoppingCartCount"
values={{ count: cartItemsCount }}
values={{ count: cartQuantityTotal }}
/>
</Button>
</Link>

View File

@@ -51,8 +51,8 @@ export function HomeMobileNavigation(props: {
}
});
const cartItemsCount = props.cart?.items?.length ?? 0;
const hasCartItems = cartItemsCount > 0;
const cartQuantityTotal = props.cart?.items?.reduce((acc, item) => acc + item.quantity, 0) ?? 0;
const hasCartItems = cartQuantityTotal > 0;
return (
<DropdownMenu>
@@ -83,7 +83,7 @@ export function HomeMobileNavigation(props: {
path="/home/cart"
label="common:shoppingCartCount"
Icon={<ShoppingCart className="stroke-[1.5px]" />}
labelOptions={{ count: cartItemsCount }}
labelOptions={{ count: cartQuantityTotal }}
/>
</DropdownMenuGroup>
<DropdownMenuSeparator />

View File

@@ -12,9 +12,9 @@ import {
import { StoreProduct } from '@medusajs/types';
import { useState } from 'react';
import { handleAddToCart } from '~/lib/services/medusaCart.service';
import { useRouter } from 'next/navigation';
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
import { Trans } from '@kit/ui/trans';
import { toast } from '@kit/ui/sonner';
export type OrderAnalysisCard = Pick<
StoreProduct, 'title' | 'description' | 'subtitle'
@@ -30,8 +30,6 @@ export default function OrderAnalysesCards({
analyses: OrderAnalysisCard[];
countryCode: string;
}) {
const router = useRouter();
const [isAddingToCart, setIsAddingToCart] = useState(false);
const handleSelect = async (variantId: string) => {
if (isAddingToCart) {
@@ -44,9 +42,10 @@ export default function OrderAnalysesCards({
selectedVariant: { id: variantId },
countryCode,
});
toast.success(<Trans i18nKey={'order-analysis:analysisAddedToCart'} />);
setIsAddingToCart(false);
router.push('/home/cart');
} catch (e) {
toast.error(<Trans i18nKey={'order-analysis:analysisAddToCartError'} />);
setIsAddingToCart(false);
console.error(e);
}

View File

@@ -0,0 +1,18 @@
'use server';
import { createPageViewLog, PageViewAction } from "~/lib/services/audit/pageView.service";
import { loadCurrentUserAccount } from "../../_lib/server/load-user-account";
export async function logAnalysisResultsNavigateAction(analysisOrderId: string) {
const account = await loadCurrentUserAccount();
if (!account) {
throw new Error('Account not found');
}
await createPageViewLog({
accountId: account.id,
action: PageViewAction.VIEW_ANALYSIS_RESULTS_FROM_ORDER,
extraData: {
analysisOrderId,
},
});
}

View File

@@ -1,3 +1,5 @@
'use client';
import { Trans } from '@kit/ui/trans';
import {
Table,
@@ -10,18 +12,26 @@ import {
import { StoreOrderLineItem } from "@medusajs/types";
import { AnalysisOrder } from '~/lib/services/order.service';
import { formatDate } from 'date-fns';
import Link from 'next/link';
import { Eye } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { logAnalysisResultsNavigateAction } from './actions';
export default function OrderItemsTable({ items, title, analysisOrder }: {
items: StoreOrderLineItem[];
title: string;
analysisOrder: AnalysisOrder;
}) {
const router = useRouter();
if (!items || items.length === 0) {
return null;
}
const openAnalysisResults = async () => {
await logAnalysisResultsNavigateAction(analysisOrder.medusa_order_id);
router.push(`/home/analysis-results`);
}
return (
<Table className="rounded-lg border border-separate">
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
@@ -60,13 +70,12 @@ export default function OrderItemsTable({ items, title, analysisOrder }: {
<TableCell className="text-right px-6">
<span className="flex gap-x-1 justify-end w-[30px]">
<Link href={`/home/analysis-results`} className="flex items-center justify-between text-small-regular">
<button
className="flex gap-x-1 text-ui-fg-subtle hover:text-ui-fg-base cursor-pointer"
>
<Eye />
</button>
</Link>
<button
className="flex gap-x-1 text-ui-fg-subtle hover:text-ui-fg-base cursor-pointer "
onClick={openAnalysisResults}
>
<Eye />
</button>
</span>
</TableCell>
</TableRow>

View File

@@ -2,6 +2,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
export enum PageViewAction {
VIEW_ANALYSIS_RESULTS = 'VIEW_ANALYSIS_RESULTS',
VIEW_ANALYSIS_RESULTS_FROM_ORDER = 'VIEW_ANALYSIS_RESULTS_FROM_ORDER',
REGISTRATION_SUCCESS = 'REGISTRATION_SUCCESS',
VIEW_ORDER_ANALYSIS = 'VIEW_ORDER_ANALYSIS',
VIEW_TEAM_ACCOUNT_DASHBOARD = 'VIEW_TEAM_ACCOUNT_DASHBOARD',
@@ -10,9 +11,11 @@ export enum PageViewAction {
export const createPageViewLog = async ({
accountId,
action,
extraData,
}: {
accountId: string;
action: PageViewAction;
extraData?: Record<string, any>;
}) => {
try {
const supabase = getSupabaseServerClient();
@@ -34,6 +37,7 @@ export const createPageViewLog = async ({
account_id: accountId,
action,
changed_by: user.id,
extra_data: extraData,
})
.throwOnError();
} catch (error) {

View File

@@ -180,7 +180,10 @@ export async function getPrivateMessage(messageId: string) {
await validateMedipostResponse(data, { canHaveEmptyCode: true });
return parseXML(data) as MedipostOrderResponse;
return {
message: parseXML(data) as MedipostOrderResponse,
xml: data as string,
};
}
export async function deletePrivateMessage(messageId: string) {
@@ -211,7 +214,9 @@ export async function readPrivateMessageResponse({
try {
const privateMessage = await getLatestPrivateMessageListItem({ excludedMessageIds });
if (!privateMessage) {
messageId = privateMessage?.messageId ?? null;
if (!privateMessage || !messageId) {
return {
messageId: null,
hasAnalysisResponse: false,
@@ -221,40 +226,28 @@ export async function readPrivateMessageResponse({
};
}
messageId = privateMessage.messageId;
if (!messageId) {
return {
messageId: null,
hasAnalysisResponse: false,
hasPartialAnalysisResponse: false,
hasFullAnalysisResponse: false,
medusaOrderId: undefined,
};
}
const privateMessageContent = await getPrivateMessage(
const { message: privateMessageContent, xml: privateMessageXml } = await getPrivateMessage(
privateMessage.messageId,
);
const messageResponse = privateMessageContent?.Saadetis?.Vastus;
medusaOrderId = privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId;
if (!medusaOrderId || !medusaOrderId.toString().startsWith('order_')) {
return {
messageId,
hasAnalysisResponse: false,
hasPartialAnalysisResponse: false,
hasFullAnalysisResponse: false,
medusaOrderId: undefined,
};
}
const hasInvalidOrderId = !medusaOrderId || !medusaOrderId.toString().startsWith('order_');
if (!messageResponse) {
if (hasInvalidOrderId || !messageResponse) {
await createMedipostActionLog({
action: 'sync_analysis_results_from_medipost',
xml: privateMessageXml,
hasAnalysisResults: false,
medusaOrderId: hasInvalidOrderId ? undefined : medusaOrderId,
});
return {
messageId,
hasAnalysisResponse: false,
hasPartialAnalysisResponse: false,
hasFullAnalysisResponse: false,
medusaOrderId,
medusaOrderId: hasInvalidOrderId ? undefined : medusaOrderId,
};
}
@@ -745,12 +738,36 @@ export async function sendOrderToMedipost({
await sendPrivateMessage(orderXml);
} catch (e) {
const isMedipostError = e instanceof MedipostValidationError;
await logMedipostDispatch({
medusaOrderId,
isSuccess: false,
isMedipostError,
errorMessage: isMedipostError ? e.response : undefined,
});
if (isMedipostError) {
await logMedipostDispatch({
medusaOrderId,
isSuccess: false,
isMedipostError,
errorMessage: e.response,
});
await createMedipostActionLog({
action: 'send_order_to_medipost',
xml: orderXml,
hasAnalysisResults: false,
medusaOrderId,
responseXml: e.response,
hasError: true,
});
} else {
await logMedipostDispatch({
medusaOrderId,
isSuccess: false,
isMedipostError,
});
await createMedipostActionLog({
action: 'send_order_to_medipost',
xml: orderXml,
hasAnalysisResults: false,
medusaOrderId,
hasError: true,
});
}
throw e;
}
await logMedipostDispatch({
@@ -758,6 +775,12 @@ export async function sendOrderToMedipost({
isSuccess: true,
isMedipostError: false,
});
await createMedipostActionLog({
action: 'send_order_to_medipost',
xml: orderXml,
hasAnalysisResults: false,
medusaOrderId,
});
await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' });
}
@@ -825,3 +848,37 @@ export async function getOrderedAnalysisElementsIds({
return [...analysisPackageElements, ...orderedAnalysisElements];
}
export async function createMedipostActionLog({
action,
xml,
hasAnalysisResults = false,
medusaOrderId,
responseXml,
hasError = false,
}: {
action:
| 'send_order_to_medipost'
| 'sync_analysis_results_from_medipost'
| 'send_fake_analysis_results_to_medipost'
| 'send_analysis_results_to_medipost';
xml: string;
hasAnalysisResults?: boolean;
medusaOrderId?: string | null;
responseXml?: string | null;
hasError?: boolean;
}) {
await getSupabaseServerAdminClient()
.schema('medreport')
.from('medipost_actions')
.insert({
action,
xml,
has_analysis_results: hasAnalysisResults,
medusa_order_id: medusaOrderId,
response_xml: responseXml,
has_error: hasError,
})
.select('id')
.throwOnError();
}

View File

@@ -134,8 +134,10 @@ export async function getAnalysisOrders({
export async function getAnalysisOrdersAdmin({
orderStatus,
medusaOrderId,
}: {
orderStatus?: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status'];
medusaOrderId?: string | null;
} = {}) {
const query = getSupabaseServerAdminClient()
.schema('medreport')
@@ -144,6 +146,9 @@ export async function getAnalysisOrdersAdmin({
if (orderStatus) {
query.eq('status', orderStatus);
}
if (medusaOrderId) {
query.eq('medusa_order_id', medusaOrderId);
}
const orders = await query.order('created_at', { ascending: false }).throwOnError();
return orders.data;
}

View File

@@ -9,6 +9,7 @@ import { StoreProduct } from '@medusajs/types';
import { Button } from '@medusajs/ui';
import { useTranslation } from 'react-i18next';
import { handleAddToCart } from '../../../../lib/services/medusaCart.service';
import { toast } from '@kit/ui/sonner';
import {
Card,
@@ -49,12 +50,19 @@ export default function SelectAnalysisPackage({
const handleSelect = async () => {
setIsAddingToCart(true);
await handleAddToCart({
selectedVariant: { id: variantId },
countryCode,
});
setIsAddingToCart(false);
router.push('/home/cart');
try {
await handleAddToCart({
selectedVariant: { id: variantId },
countryCode,
});
setIsAddingToCart(false);
toast.success(<Trans i18nKey={'order-analysis-package:analysisPackageAddedToCart'} />);
router.push('/home/cart');
} catch (e) {
toast.error(<Trans i18nKey={'order-analysis-package:analysisPackageAddToCartError'} />);
setIsAddingToCart(false);
console.error(e);
}
};
return (

View File

@@ -199,6 +199,7 @@ export type Database = {
changed_by: string
created_at: string
id: number
extra_data?: Json | null
}
Insert: {
account_id: string
@@ -206,6 +207,7 @@ export type Database = {
changed_by: string
created_at?: string
id?: number
extra_data?: Json | null
}
Update: {
account_id?: string
@@ -213,6 +215,7 @@ export type Database = {
changed_by?: string
created_at?: string
id?: number
extra_data?: Json | null
}
Relationships: []
}
@@ -1254,6 +1257,34 @@ export type Database = {
},
]
}
medipost_actions: {
Row: {
id: string
action: string
xml: string
has_analysis_results: boolean
created_at: string
medusa_order_id: string
response_xml: string
has_error: boolean
}
Insert: {
action: string
xml: string
has_analysis_results: boolean
medusa_order_id: string
response_xml: string
has_error: boolean
}
Update: {
action?: string
xml?: string
has_analysis_results?: boolean
medusa_order_id?: string
response_xml?: string
has_error?: boolean
}
}
medreport_product_groups: {
Row: {
created_at: string
@@ -1919,6 +1950,15 @@ export type Database = {
account_id: string
}[]
}
get_latest_medipost_dispatch_state_for_order: {
Args: {
medusa_order_id: string
}
Returns: {
has_success: boolean
action_date: string
}
}
get_medipost_dispatch_tries: {
Args: { p_medusa_order_id: string }
Returns: number
@@ -2135,6 +2175,21 @@ export type Database = {
}
Returns: Json
}
sync_analysis_results: {
}
send_medipost_test_response_for_order: {
Args: {
medusa_order_id: string
}
}
order_has_medipost_dispatch_error: {
Args: {
medusa_order_id: string
}
Returns: {
success: boolean
}
}
}
Enums: {
analysis_feedback_status: "STARTED" | "DRAFT" | "COMPLETED"

View File

@@ -1,7 +1,9 @@
{
"title": "Select analysis package",
"noPackagesAvailable": "No packages available",
"selectThisPackage": "Select this package",
"selectPackage": "Select package",
"comparePackages": "Compare packages"
"title": "Select analysis package",
"noPackagesAvailable": "No packages available",
"selectThisPackage": "Select this package",
"selectPackage": "Select package",
"comparePackages": "Compare packages",
"analysisPackageAddedToCart": "Analysis package added to cart",
"analysisPackageAddToCartError": "Adding analysis package to cart failed"
}

View File

@@ -1,5 +1,7 @@
{
"title": "Select analysis",
"description": "Select the analysis that suits your needs",
"analysisNotAvailable": "Analysis is not available currently"
"description": "All analysis results will appear within 1-3 days after the blood test.",
"analysisNotAvailable": "Analysis is not available currently",
"analysisAddedToCart": "Analysis added to cart",
"analysisAddToCartError": "Adding analysis to cart failed"
}

View File

@@ -1,7 +1,9 @@
{
"title": "Vali analüüsi pakett",
"noPackagesAvailable": "Teenuste loetelu ei leitud, proovi hiljem uuesti",
"selectThisPackage": "Vali see pakett",
"selectPackage": "Vali pakett",
"comparePackages": "Võrdle pakette"
"title": "Vali analüüsi pakett",
"noPackagesAvailable": "Teenuste loetelu ei leitud, proovi hiljem uuesti",
"selectThisPackage": "Vali see pakett",
"selectPackage": "Vali pakett",
"comparePackages": "Võrdle pakette",
"analysisPackageAddedToCart": "Analüüsi pakett lisatud ostukorvi",
"analysisPackageAddToCartError": "Analüüsi paketi lisamine ostukorvi ebaõnnestus"
}

View File

@@ -1,5 +1,7 @@
{
"title": "Vali analüüs",
"description": "Vali enda vajadustele sobiv analüüs",
"analysisNotAvailable": "Analüüsi tellimine ei ole hetkel saadaval"
"description": "Kõikide analüüside tulemused ilmuvad 13 tööpäeva jooksul peale vere andmist.",
"analysisNotAvailable": "Analüüsi tellimine ei ole hetkel saadaval",
"analysisAddedToCart": "Analüüs lisatud ostukorvi",
"analysisAddToCartError": "Analüüsi lisamine ostukorvi ebaõnnestus"
}

View File

@@ -0,0 +1,3 @@
-- Enable required extensions for cron jobs and HTTP requests
create extension if not exists pg_cron;
create extension if not exists pg_net;

View File

@@ -1,5 +1,3 @@
create extension if not exists pg_net;
create or replace function medreport.medipost_retry_dispatch(
order_id text
)

View File

@@ -0,0 +1,16 @@
CREATE OR REPLACE FUNCTION medreport.sync_analysis_results()
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
select net.http_post(
url := 'https://test.medreport.ee/api/job/sync-analysis-results',
headers := jsonb_build_object(
'Content-Type', 'application/json',
'x-jobs-api-key', 'fd26ec26-70ed-11f0-9e95-431ac3b15a84'
)
) as request_id;
END;
$$;
grant execute on function medreport.sync_analysis_results() to service_role;

View File

@@ -0,0 +1,19 @@
CREATE OR REPLACE FUNCTION medreport.send_medipost_test_response_for_order(medusa_order_id text)
RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
select net.http_post(
url := 'https://test.medreport.ee/api/order/medipost-test-response',
headers := jsonb_build_object(
'Content-Type', 'application/json',
'x-jobs-api-key', 'fd26ec26-70ed-11f0-9e95-431ac3b15a84'
),
body := jsonb_build_object(
'medusaOrderId', medusa_order_id
)
) as request_id;
END;
$$;
grant execute on function medreport.send_medipost_test_response_for_order(text) to service_role;

View File

@@ -1,12 +1,8 @@
-- Enable required extensions for cron jobs and HTTP requests
create extension if not exists pg_cron;
create extension if not exists pg_net;
-- Schedule the test-medipost-responses job to run every 15 minutes
select
cron.schedule(
'send-test-medipost-responses-every-15-minutes', -- Unique job name
'*/15 * * * *', -- Cron schedule: every 15 minutes
'send-test-medipost-responses-every-15-minutes',
'*/15 * * * *',
$$
select
net.http_post(

View File

@@ -1,7 +1,3 @@
-- Enable required extensions for cron jobs and HTTP requests
create extension if not exists pg_cron;
create extension if not exists pg_net;
-- Schedule the sync-analysis-results job to run every 15 minutes
select
cron.schedule(

View File

@@ -0,0 +1,4 @@
Migrations that require env specific parameters.
- JOBS_API_TOKEN
- app deploy public or internal URL

View File

@@ -0,0 +1,16 @@
CREATE TABLE medreport.medipost_actions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
action VARCHAR(255) NOT NULL,
xml VARCHAR(131072),
has_analysis_results BOOLEAN NOT NULL DEFAULT FALSE,
medusa_order_id VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
ALTER TABLE medreport.medipost_actions ENABLE ROW LEVEL SECURITY;
CREATE POLICY "service_role_select" ON medreport.medipost_actions FOR SELECT TO service_role USING (true);
CREATE POLICY "service_role_insert" ON medreport.medipost_actions FOR INSERT TO service_role WITH CHECK (true);
CREATE POLICY "service_role_update" ON medreport.medipost_actions FOR UPDATE TO service_role USING (true);
CREATE POLICY "service_role_delete" ON medreport.medipost_actions FOR DELETE TO service_role USING (true);
grant select, insert, update, delete on table medreport.medipost_actions to service_role;

View File

@@ -0,0 +1,2 @@
ALTER TABLE medreport.medipost_actions ADD COLUMN response_xml VARCHAR(131072);
ALTER TABLE medreport.medipost_actions ADD COLUMN has_error BOOLEAN NOT NULL DEFAULT FALSE;

View File

@@ -0,0 +1 @@
ALTER TABLE audit.page_views ADD COLUMN extra_data JSONB;

View File

@@ -0,0 +1,11 @@
CREATE OR REPLACE FUNCTION medreport.order_has_medipost_dispatch_error(medusa_order_id text)
RETURNS boolean AS $$
SELECT EXISTS (
SELECT 1 FROM medreport.medipost_actions
WHERE medusa_order_id = $1
AND action = 'send_order_to_medipost'
AND has_error = true
);
$$ LANGUAGE sql STABLE;
grant execute on function medreport.order_has_medipost_dispatch_error(text) to service_role;

View File

@@ -0,0 +1,16 @@
CREATE OR REPLACE FUNCTION medreport.get_latest_medipost_dispatch_state_for_order(medusa_order_id text)
RETURNS TABLE(has_success boolean, action_date timestamp with time zone) AS $$
SELECT
CASE
WHEN ma.has_error = false THEN true
ELSE false
END as has_success,
ma.created_at as action_date
FROM medreport.medipost_actions ma
WHERE ma.medusa_order_id = $1
AND ma.action = 'send_order_to_medipost'
ORDER BY ma.created_at DESC
LIMIT 1;
$$ LANGUAGE sql STABLE;
grant execute on function medreport.get_latest_medipost_dispatch_state_for_order(text) to service_role;