diff --git a/app/api/job/test-medipost-responses/route.ts b/app/api/job/test-medipost-responses/route.ts
index 4ca8be0..3b32794 100644
--- a/app/api/job/test-medipost-responses/route.ts
+++ b/app/api/job/test-medipost-responses/route.ts
@@ -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) {
diff --git a/app/api/order/medipost-test-response/route.ts b/app/api/order/medipost-test-response/route.ts
index 40cf4b2..a54e1fe 100644
--- a/app/api/order/medipost-test-response/route.ts
+++ b/app/api/order/medipost-test-response/route.ts
@@ -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);
diff --git a/app/home/(user)/_components/home-menu-navigation.tsx b/app/home/(user)/_components/home-menu-navigation.tsx
index fb91de9..473a681 100644
--- a/app/home/(user)/_components/home-menu-navigation.tsx
+++ b/app/home/(user)/_components/home-menu-navigation.tsx
@@ -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 (
@@ -64,7 +64,7 @@ export async function HomeMenuNavigation(props: {
diff --git a/app/home/(user)/_components/home-mobile-navigation.tsx b/app/home/(user)/_components/home-mobile-navigation.tsx
index a363a4c..7ead53a 100644
--- a/app/home/(user)/_components/home-mobile-navigation.tsx
+++ b/app/home/(user)/_components/home-mobile-navigation.tsx
@@ -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 (
@@ -83,7 +83,7 @@ export function HomeMobileNavigation(props: {
path="/home/cart"
label="common:shoppingCartCount"
Icon={}
- labelOptions={{ count: cartItemsCount }}
+ labelOptions={{ count: cartQuantityTotal }}
/>
diff --git a/app/home/(user)/_components/order-analyses-cards.tsx b/app/home/(user)/_components/order-analyses-cards.tsx
index b6a2d75..23b88b5 100644
--- a/app/home/(user)/_components/order-analyses-cards.tsx
+++ b/app/home/(user)/_components/order-analyses-cards.tsx
@@ -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();
setIsAddingToCart(false);
- router.push('/home/cart');
} catch (e) {
+ toast.error();
setIsAddingToCart(false);
console.error(e);
}
diff --git a/app/home/(user)/_components/orders/actions.ts b/app/home/(user)/_components/orders/actions.ts
new file mode 100644
index 0000000..d201507
--- /dev/null
+++ b/app/home/(user)/_components/orders/actions.ts
@@ -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,
+ },
+ });
+}
diff --git a/app/home/(user)/_components/orders/order-items-table.tsx b/app/home/(user)/_components/orders/order-items-table.tsx
index 48d502a..096ad06 100644
--- a/app/home/(user)/_components/orders/order-items-table.tsx
+++ b/app/home/(user)/_components/orders/order-items-table.tsx
@@ -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 (
@@ -60,13 +70,12 @@ export default function OrderItemsTable({ items, title, analysisOrder }: {
-
-
-
+
diff --git a/lib/services/audit/pageView.service.ts b/lib/services/audit/pageView.service.ts
index f4e70f6..efac5db 100644
--- a/lib/services/audit/pageView.service.ts
+++ b/lib/services/audit/pageView.service.ts
@@ -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;
}) => {
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) {
diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts
index 542f275..a32b624 100644
--- a/lib/services/medipost.service.ts
+++ b/lib/services/medipost.service.ts
@@ -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();
+}
diff --git a/lib/services/order.service.ts b/lib/services/order.service.ts
index 8afb4c4..e7ca441 100644
--- a/lib/services/order.service.ts
+++ b/lib/services/order.service.ts
@@ -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;
}
diff --git a/packages/shared/src/components/select-analysis-package.tsx b/packages/shared/src/components/select-analysis-package.tsx
index 1709138..9f87183 100644
--- a/packages/shared/src/components/select-analysis-package.tsx
+++ b/packages/shared/src/components/select-analysis-package.tsx
@@ -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();
+ router.push('/home/cart');
+ } catch (e) {
+ toast.error();
+ setIsAddingToCart(false);
+ console.error(e);
+ }
};
return (
diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts
index 5040c87..33eee12 100644
--- a/packages/supabase/src/database.types.ts
+++ b/packages/supabase/src/database.types.ts
@@ -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"
diff --git a/public/locales/en/order-analysis-package.json b/public/locales/en/order-analysis-package.json
index 9502bf1..7f93260 100644
--- a/public/locales/en/order-analysis-package.json
+++ b/public/locales/en/order-analysis-package.json
@@ -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"
}
\ No newline at end of file
diff --git a/public/locales/en/order-analysis.json b/public/locales/en/order-analysis.json
index 6ab9af8..2031316 100644
--- a/public/locales/en/order-analysis.json
+++ b/public/locales/en/order-analysis.json
@@ -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"
}
\ No newline at end of file
diff --git a/public/locales/et/order-analysis-package.json b/public/locales/et/order-analysis-package.json
index 2c44087..fb236a3 100644
--- a/public/locales/et/order-analysis-package.json
+++ b/public/locales/et/order-analysis-package.json
@@ -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"
}
\ No newline at end of file
diff --git a/public/locales/et/order-analysis.json b/public/locales/et/order-analysis.json
index 42f790e..9c7b750 100644
--- a/public/locales/et/order-analysis.json
+++ b/public/locales/et/order-analysis.json
@@ -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 1–3 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"
}
\ No newline at end of file
diff --git a/supabase/migrations-env-specific/20250824112611_enable_cron_net.sql b/supabase/migrations-env-specific/20250824112611_enable_cron_net.sql
new file mode 100644
index 0000000..e757d24
--- /dev/null
+++ b/supabase/migrations-env-specific/20250824112611_enable_cron_net.sql
@@ -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;
diff --git a/supabase/migrations/20250825120858_medipost_retry_dispatch.sql b/supabase/migrations-env-specific/20250825120858_medipost_retry_dispatch.sql
similarity index 96%
rename from supabase/migrations/20250825120858_medipost_retry_dispatch.sql
rename to supabase/migrations-env-specific/20250825120858_medipost_retry_dispatch.sql
index 73e6e27..c6e13e8 100644
--- a/supabase/migrations/20250825120858_medipost_retry_dispatch.sql
+++ b/supabase/migrations-env-specific/20250825120858_medipost_retry_dispatch.sql
@@ -1,5 +1,3 @@
-create extension if not exists pg_net;
-
create or replace function medreport.medipost_retry_dispatch(
order_id text
)
diff --git a/supabase/migrations-env-specific/20250827090151_bo_run_results_sync.sql b/supabase/migrations-env-specific/20250827090151_bo_run_results_sync.sql
new file mode 100644
index 0000000..3bddde6
--- /dev/null
+++ b/supabase/migrations-env-specific/20250827090151_bo_run_results_sync.sql
@@ -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;
diff --git a/supabase/migrations-env-specific/20250827090152_bo_run_dev_fake_results_sync.sql b/supabase/migrations-env-specific/20250827090152_bo_run_dev_fake_results_sync.sql
new file mode 100644
index 0000000..4ef9274
--- /dev/null
+++ b/supabase/migrations-env-specific/20250827090152_bo_run_dev_fake_results_sync.sql
@@ -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;
diff --git a/schedule-setup/setup_send_analysis_test_results_cron.sql b/supabase/migrations-env-specific/20250828142742_dev__send_analysis_results_cron.sql
similarity index 57%
rename from schedule-setup/setup_send_analysis_test_results_cron.sql
rename to supabase/migrations-env-specific/20250828142742_dev__send_analysis_results_cron.sql
index 2562939..e30f016 100644
--- a/schedule-setup/setup_send_analysis_test_results_cron.sql
+++ b/supabase/migrations-env-specific/20250828142742_dev__send_analysis_results_cron.sql
@@ -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(
diff --git a/schedule-setup/setup_sync_analysis_results_cron.sql b/supabase/migrations-env-specific/20250828142742_setup_sync_analysis_results_cron.sql
similarity index 76%
rename from schedule-setup/setup_sync_analysis_results_cron.sql
rename to supabase/migrations-env-specific/20250828142742_setup_sync_analysis_results_cron.sql
index 832f982..ed50da0 100644
--- a/schedule-setup/setup_sync_analysis_results_cron.sql
+++ b/supabase/migrations-env-specific/20250828142742_setup_sync_analysis_results_cron.sql
@@ -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(
diff --git a/supabase/migrations-env-specific/README.md b/supabase/migrations-env-specific/README.md
new file mode 100644
index 0000000..6d6e833
--- /dev/null
+++ b/supabase/migrations-env-specific/README.md
@@ -0,0 +1,4 @@
+Migrations that require env specific parameters.
+
+- JOBS_API_TOKEN
+- app deploy public or internal URL
diff --git a/supabase/migrations/20250828105844_medipost_actions.sql b/supabase/migrations/20250828105844_medipost_actions.sql
new file mode 100644
index 0000000..3eea037
--- /dev/null
+++ b/supabase/migrations/20250828105844_medipost_actions.sql
@@ -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;
diff --git a/supabase/migrations/20250828121900_medipost_actions_extra_fields.sql b/supabase/migrations/20250828121900_medipost_actions_extra_fields.sql
new file mode 100644
index 0000000..6c1f9df
--- /dev/null
+++ b/supabase/migrations/20250828121900_medipost_actions_extra_fields.sql
@@ -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;
diff --git a/supabase/migrations/20250828123119_page_views_extra_data.sql b/supabase/migrations/20250828123119_page_views_extra_data.sql
new file mode 100644
index 0000000..69f1e3d
--- /dev/null
+++ b/supabase/migrations/20250828123119_page_views_extra_data.sql
@@ -0,0 +1 @@
+ALTER TABLE audit.page_views ADD COLUMN extra_data JSONB;
diff --git a/supabase/migrations/20250828133240_show_medipost_dispatch_error_for_order_in_medusa.sql b/supabase/migrations/20250828133240_show_medipost_dispatch_error_for_order_in_medusa.sql
new file mode 100644
index 0000000..d72dc89
--- /dev/null
+++ b/supabase/migrations/20250828133240_show_medipost_dispatch_error_for_order_in_medusa.sql
@@ -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;
diff --git a/supabase/migrations/20250828142742_show_medipost_dispatch_success_for_order_in_medusa.sql b/supabase/migrations/20250828142742_show_medipost_dispatch_success_for_order_in_medusa.sql
new file mode 100644
index 0000000..2838327
--- /dev/null
+++ b/supabase/migrations/20250828142742_show_medipost_dispatch_success_for_order_in_medusa.sql
@@ -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;