30 Commits

Author SHA1 Message Date
2df366c14a feat(MED-121): fix variant age range check 2025-09-01 14:29:14 +03:00
Danel Kungla
6ce60eacc7 Add data.sql to .gitignore 2025-08-29 18:05:51 +03:00
danelkungla
6e76e75e85 Merge pull request #66 from MR-medreport/main
Main
2025-08-29 16:17:54 +03:00
71c3e2ef1e Merge pull request #64 from MR-medreport/MED-85
feat(MED-85-105-123): some testing feedback, other improvements
2025-08-29 11:52:55 +03:00
d83319a094 Merge branch 'main' into MED-85 2025-08-29 11:45:36 +03:00
Helena
505ef0d91b Merge branch 'main' of https://github.com/MR-medreport/MRB2B 2025-08-29 10:28:15 +03:00
Helena
fce4355be8 Merge branch 'main' of https://github.com/MR-medreport/MRB2B 2025-08-29 10:27:46 +03:00
Helena
da9658ad7a fix email button 2025-08-29 10:27:12 +03:00
danelkungla
e023d54a2a Merge pull request #63 from MR-medreport/main
update develop
2025-08-29 09:50:32 +03:00
danelkungla
bdaacbe78a MED-104: booking page
MED-104
2025-08-29 09:48:26 +03:00
Danel Kungla
5479f310d7 Merge branch 'main' into MED-104 2025-08-29 09:46:04 +03:00
815b877b5b Merge branch 'main' into MED-85 2025-08-28 16:12:46 +03:00
0c28f9681b feat(MED-86): fix status check for fake responses 2025-08-28 15:49:03 +03:00
70b85dc967 feat(MED-86): don't prettify results sync log since aws will split it up 2025-08-28 15:37:29 +03:00
71f5a25632 Merge branch 'main' into MED-85 2025-08-28 14:57:09 +03:00
d072226a5c feat(MED-105): organize env specific required migrations 2025-08-28 14:54:51 +03:00
da7f574234 feat(MED-86): user can see in Medusa BO if sending order to medipost succeeded 2025-08-28 14:42:07 +03:00
b3505c1627 feat(MED-100): show cart line item quantities total instead of items total count 2025-08-28 14:41:59 +03:00
Danel Kungla
ad28352fc8 MED-104: create booking view with categories 2025-08-28 14:11:54 +03:00
b931035c3b feat(MED-86): add db fn to show medipost dispatch error for order in Medusa 2025-08-28 13:36:07 +03:00
f723633646 feat(MED-123): don't redirect to cart on single analysis select 2025-08-28 13:31:29 +03:00
6e6ad13b52 feat(MED-123): show toast on adding analysis package to cart 2025-08-28 13:30:25 +03:00
Danel Kungla
31bc4b6cff initial commit 2025-08-28 13:15:39 +03:00
3ddc0a2716 feat(MED-123): show toast on adding analysis to cart 2025-08-28 12:44:07 +03:00
49eeaa1876 feat(MED-123): update translations 2025-08-28 12:40:33 +03:00
2ffad84100 feat(MED-105): log opening analysis results from orders view 2025-08-28 12:35:30 +03:00
b4985afdf0 feat(MED-85): add logging for medipost response error xml 2025-08-28 12:25:18 +03:00
a37c4cad9c feat(MED-85): add logging for medipost actions with xml and related order id 2025-08-28 11:56:07 +03:00
47ab39172e feat(MED-85): run sending fake medipost results in dev from Medusa BO 2025-08-28 10:30:43 +03:00
d760f86632 feat(MED-85): run force medipost results sync from Medusa BO 2025-08-28 09:54:32 +03:00
45 changed files with 685 additions and 210 deletions

1
.gitignore vendored
View File

@@ -43,3 +43,4 @@ yarn-error.log*
next-env.d.ts next-env.d.ts
dump.sql dump.sql
data.sql

View File

@@ -71,19 +71,19 @@ export default async function syncConnectedOnline() {
return { return {
id: service.ID, id: service.ID,
clinic_id: service.ClinicID, clinic_id: service.ClinicID,
code: service.Code, sync_id: service.SyncID,
description: service.Description || null,
display: service.Display,
duration: service.Duration,
has_free_codes: !!service.HasFreeCodes,
name: service.Name, name: service.Name,
description: service.Description || null,
price: service.Price,
requires_payment: !!service.RequiresPayment,
duration: service.Duration,
neto_duration: service.NetoDuration, neto_duration: service.NetoDuration,
display: service.Display,
price_periods: service.PricePeriods || null,
online_hide_duration: service.OnlineHideDuration, online_hide_duration: service.OnlineHideDuration,
online_hide_price: service.OnlineHidePrice, online_hide_price: service.OnlineHidePrice,
price: service.Price, code: service.Code,
price_periods: service.PricePeriods || null, has_free_codes: !!service.HasFreeCodes,
requires_payment: !!service.RequiresPayment,
sync_id: service.SyncID,
}; };
}); });

View File

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

View File

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

View File

@@ -0,0 +1,41 @@
import { HomeLayoutPageHeader } from '@/app/home/(user)/_components/home-page-header';
import { loadCategory } from '@/app/home/(user)/_lib/server/load-category';
import { AppBreadcrumbs } from '@kit/ui/makerkit/app-breadcrumbs';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('booking:title');
return {
title,
};
};
async function BookingHandlePage({ params }: { params: { handle: string } }) {
const handle = await params.handle;
const { category } = await loadCategory({ handle });
return (
<>
<AppBreadcrumbs
values={{
[handle]: category?.name || handle,
}}
/>
<HomeLayoutPageHeader
title={<Trans i18nKey={'booking:title'} />}
description={<Trans i18nKey={'booking:description'} />}
/>
<PageBody></PageBody>
</>
);
}
export default withI18n(BookingHandlePage);

View File

@@ -1,12 +1,16 @@
import { use } from 'react';
import { AppBreadcrumbs } from '@kit/ui/makerkit/app-breadcrumbs';
import { PageBody } from '@kit/ui/page'; import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n'; import { withI18n } from '~/lib/i18n/with-i18n';
import { HomeLayoutPageHeader } from '../../_components/home-page-header'; import { HomeLayoutPageHeader } from '../../_components/home-page-header';
import OrderCards from '../../_components/order-cards'; import OrderCards from '../../_components/order-cards';
import ServiceCategories from '../../_components/service-categories';
import { loadTtoServices } from '../../_lib/server/load-tto-services';
export const generateMetadata = async () => { export const generateMetadata = async () => {
const i18n = await createI18nServerInstance(); const i18n = await createI18nServerInstance();
@@ -18,15 +22,30 @@ export const generateMetadata = async () => {
}; };
function BookingPage() { function BookingPage() {
const { heroCategories, ttoCategories } = use(loadTtoServices());
if (!heroCategories.length && !ttoCategories.length) {
return (
<>
<AppBreadcrumbs />
<h3 className="mt-8">
<Trans i18nKey="booking:noCategories" />
</h3>
</>
);
}
return ( return (
<> <>
<AppBreadcrumbs />
<HomeLayoutPageHeader <HomeLayoutPageHeader
title={<Trans i18nKey={'booking:title'} />} title={<Trans i18nKey={'booking:title'} />}
description={<Trans i18nKey={'booking:description'} />} description={<Trans i18nKey={'booking:description'} />}
/> />
<PageBody> <PageBody className="space-y-2">
<OrderCards /> <OrderCards heroCategories={heroCategories} />
<ServiceCategories categories={ttoCategories} />
</PageBody> </PageBody>
</> </>
); );

View File

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

View File

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

View File

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

View File

@@ -1,74 +1,68 @@
"use client"; 'use client';
import { ChevronRight, HeartPulse } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { cn } from '@/lib/utils';
import { pathsConfig } from '@/packages/shared/src/config';
import { ComponentInstanceIcon } from '@radix-ui/react-icons';
import { ChevronRight, HeartPulse } from 'lucide-react';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import { import {
Card, Card,
CardHeader,
CardDescription, CardDescription,
CardProps,
CardFooter, CardFooter,
CardHeader,
CardProps,
} from '@kit/ui/card'; } from '@kit/ui/card';
import { Trans } from '@kit/ui/trans';
import { cn } from '@/lib/utils';
const dummyCards = [ import { ServiceCategory } from './service-categories';
{
title: 'booking:analysisPackages.title',
description: 'booking:analysisPackages.description',
descriptionColor: 'text-primary',
icon: (
<Link href={'/home/order-analysis-package'}>
<Button size="icon" variant="outline" className="px-2 text-black">
<ChevronRight className="size-4 stroke-2" />
</Button>
</Link>
),
cardVariant: 'gradient-success' as CardProps['variant'],
iconBg: 'bg-warning',
},
];
export default function OrderCards() { export default function OrderCards({
heroCategories,
}: {
heroCategories: ServiceCategory[];
}) {
return ( return (
<div className="grid grid-cols-3 gap-6 mt-4"> <div className="xs:grid-cols-3 mt-4 grid grid-cols-1 gap-2">
{dummyCards.map(({ {heroCategories.map(({ name, description, color, handle }) => (
title,
description,
icon,
cardVariant,
descriptionColor,
iconBg,
}) => (
<Card <Card
key={title} key={name}
variant={cardVariant} variant={`gradient-${color}` as CardProps['variant']}
className="flex flex-col justify-between" className="flex flex-col justify-between"
> >
<CardHeader className="items-end-safe"> <CardHeader className="relative flex flex-row justify-between">
<div <div
className={cn( className={cn(
'flex size-8 items-center-safe justify-center-safe rounded-full text-white', 'flex size-8 items-center-safe justify-center-safe rounded-full',
iconBg, `text-${color}`,
`bg-${color}/10`,
{
'bg-primary/10': color === 'success',
},
)} )}
> >
{icon} <ComponentInstanceIcon
className={cn('size-4', `fill-${color}`)}
/>
</div>
<div className="absolute top-2 right-2 flex size-8 items-center-safe justify-center-safe rounded-xl text-white">
<Link
href={pathsConfig.app.bookingHandle.replace('[handle]', handle)}
>
<Button
size="icon"
variant="outline"
className="px-2 text-black"
>
<ChevronRight className="size-4 stroke-2" />
</Button>
</Link>
</div> </div>
</CardHeader> </CardHeader>
<CardFooter className="flex flex-col items-start gap-2"> <CardFooter className="mt-5 flex flex-col items-start gap-2">
<div <h5>{name}</h5>
className={'flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-primary\/10 mb-6'} <CardDescription>{description}</CardDescription>
>
<HeartPulse className="size-4 fill-green-500" />
</div>
<h5>
<Trans i18nKey={title} />
</h5>
<CardDescription className={descriptionColor}>
<Trans i18nKey={description} />
</CardDescription>
</CardFooter> </CardFooter>
</Card> </Card>
))} ))}

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

View File

@@ -0,0 +1,61 @@
'use client';
import React from 'react';
import { redirect } from 'next/navigation';
import { createPath, pathsConfig } from '@/packages/shared/src/config';
import { ComponentInstanceIcon } from '@radix-ui/react-icons';
import { cn } from '@kit/ui/shadcn';
import { Card, CardDescription, CardTitle } from '@kit/ui/shadcn/card';
export interface ServiceCategory {
name: string;
handle: string;
color: string;
description: string;
}
const ServiceCategories = ({
categories,
}: {
categories: ServiceCategory[];
}) => {
return (
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
{categories.map((category, index) => (
<Card
key={index}
className="flex cursor-pointer gap-2 p-4 shadow hover:shadow-md"
onClick={() => {
redirect(
pathsConfig.app.bookingHandle.replace(
'[handle]',
category.handle,
),
);
}}
>
<div
className={cn(
'flex size-8 items-center-safe justify-center-safe rounded-full',
`bg-${category.color}/10`,
`text-${category.color}`,
)}
>
<ComponentInstanceIcon />
</div>
<div>
<h5 className="mb-2 text-lg font-semibold">{category.name}</h5>
<CardDescription className="">
{category.description}
</CardDescription>
</div>
</Card>
))}
</div>
);
};
export default ServiceCategories;

View File

@@ -1,9 +1,11 @@
import { cache } from 'react'; import { cache } from 'react';
import { listProductTypes } from "@lib/data/products";
import { listRegions } from '@lib/data/regions';
import { getProductCategories } from '@lib/data/categories'; import { getProductCategories } from '@lib/data/categories';
import { listProductTypes } from '@lib/data/products';
import { listRegions } from '@lib/data/regions';
import { OrderAnalysisCard } from '../../_components/order-analyses-cards'; import { OrderAnalysisCard } from '../../_components/order-analyses-cards';
import { ServiceCategory } from '../../_components/service-categories';
async function countryCodesLoader() { async function countryCodesLoader() {
const countryCodes = await listRegions().then((regions) => const countryCodes = await listRegions().then((regions) =>
@@ -14,7 +16,9 @@ async function countryCodesLoader() {
export const loadCountryCodes = cache(countryCodesLoader); export const loadCountryCodes = cache(countryCodesLoader);
async function productCategoriesLoader() { async function productCategoriesLoader() {
const productCategories = await getProductCategories({ fields: "*products, *products.variants" }); const productCategories = await getProductCategories({
fields: '*products, *products.variants, is_active',
});
return productCategories.product_categories ?? []; return productCategories.product_categories ?? [];
} }
export const loadProductCategories = cache(productCategoriesLoader); export const loadProductCategories = cache(productCategoriesLoader);
@@ -29,25 +33,34 @@ async function analysesLoader() {
const [countryCodes, productCategories] = await Promise.all([ const [countryCodes, productCategories] = await Promise.all([
loadCountryCodes(), loadCountryCodes(),
loadProductCategories(), loadProductCategories(),
]); ]);
const countryCode = countryCodes[0]!; const countryCode = countryCodes[0]!;
const category = productCategories.find(({ metadata }) => metadata?.page === 'order-analysis'); const category = productCategories.find(
({ metadata }) => metadata?.page === 'order-analysis',
);
const serviceCategories = productCategories.filter(
({ parent_category }) => parent_category?.handle === 'tto-categories',
);
return { return {
analyses: category?.products?.map<OrderAnalysisCard>(({ title, description, subtitle, variants, status, metadata }) => { analyses:
const variant = variants![0]!; category?.products?.map<OrderAnalysisCard>(
return { ({ title, description, subtitle, variants, status, metadata }) => {
title, const variant = variants![0]!;
description, return {
subtitle, title,
variant: { description,
id: variant.id, subtitle,
variant: {
id: variant.id,
},
isAvailable:
status === 'published' && !!metadata?.analysisIdOriginal,
};
}, },
isAvailable: status === 'published' && !!metadata?.analysisIdOriginal, ) ?? [],
};
}) ?? [],
countryCode, countryCode,
} };
} }
export const loadAnalyses = cache(analysesLoader); export const loadAnalyses = cache(analysesLoader);

View File

@@ -38,8 +38,11 @@ function userSpecificVariantLoader({
if (age >= 18 && age <= 29) { if (age >= 18 && age <= 29) {
return '18-29'; return '18-29';
} }
if (age >= 30 && age <= 49) { if (age >= 30 && age <= 39) {
return '30-49'; return '30-39';
}
if (age >= 40 && age <= 49) {
return '40-49';
} }
if (age >= 50 && age <= 59) { if (age >= 50 && age <= 59) {
return '50-59'; return '50-59';

View File

@@ -0,0 +1,31 @@
import { cache } from 'react';
import { getProductCategories } from '@lib/data';
import { ServiceCategory } from '../../_components/service-categories';
async function categoryLoader({
handle,
}: {
handle: string;
}): Promise<{ category: ServiceCategory | null }> {
const response = await getProductCategories({
handle,
fields: '*products, is_active, metadata',
});
const category = response.product_categories[0];
return {
category: {
color:
typeof category?.metadata?.color === 'string'
? category?.metadata?.color
: 'primary',
description: category?.description || '',
handle: category?.handle || '',
name: category?.name || '',
},
};
}
export const loadCategory = cache(categoryLoader);

View File

@@ -0,0 +1,49 @@
import { cache } from 'react';
import { getProductCategories } from '@lib/data';
import { ServiceCategory } from '../../_components/service-categories';
async function ttoServicesLoader() {
const response = await getProductCategories({
fields: '*products, is_active, metadata',
});
const heroCategories = response.product_categories?.filter(
({ parent_category, is_active, metadata }) =>
parent_category?.handle === 'tto-categories' &&
is_active &&
metadata?.isHero,
);
const ttoCategories = response.product_categories?.filter(
({ parent_category, is_active, metadata }) =>
parent_category?.handle === 'tto-categories' &&
is_active &&
!metadata?.isHero,
);
return {
heroCategories:
heroCategories.map<ServiceCategory>(
({ name, handle, metadata, description }) => ({
name,
handle,
color:
typeof metadata?.color === 'string' ? metadata.color : 'primary',
description,
}),
) ?? [],
ttoCategories:
ttoCategories.map<ServiceCategory>(
({ name, handle, metadata, description }) => ({
name,
handle,
color:
typeof metadata?.color === 'string' ? metadata.color : 'primary',
description,
}),
) ?? [],
};
}
export const loadTtoServices = cache(ttoServicesLoader);

View File

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

View File

@@ -180,7 +180,10 @@ export async function getPrivateMessage(messageId: string) {
await validateMedipostResponse(data, { canHaveEmptyCode: true }); 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) { export async function deletePrivateMessage(messageId: string) {
@@ -211,7 +214,9 @@ export async function readPrivateMessageResponse({
try { try {
const privateMessage = await getLatestPrivateMessageListItem({ excludedMessageIds }); const privateMessage = await getLatestPrivateMessageListItem({ excludedMessageIds });
if (!privateMessage) { messageId = privateMessage?.messageId ?? null;
if (!privateMessage || !messageId) {
return { return {
messageId: null, messageId: null,
hasAnalysisResponse: false, hasAnalysisResponse: false,
@@ -221,40 +226,28 @@ export async function readPrivateMessageResponse({
}; };
} }
messageId = privateMessage.messageId; const { message: privateMessageContent, xml: privateMessageXml } = await getPrivateMessage(
if (!messageId) {
return {
messageId: null,
hasAnalysisResponse: false,
hasPartialAnalysisResponse: false,
hasFullAnalysisResponse: false,
medusaOrderId: undefined,
};
}
const privateMessageContent = await getPrivateMessage(
privateMessage.messageId, privateMessage.messageId,
); );
const messageResponse = privateMessageContent?.Saadetis?.Vastus; const messageResponse = privateMessageContent?.Saadetis?.Vastus;
medusaOrderId = privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId; medusaOrderId = privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId;
if (!medusaOrderId || !medusaOrderId.toString().startsWith('order_')) { const hasInvalidOrderId = !medusaOrderId || !medusaOrderId.toString().startsWith('order_');
return {
messageId,
hasAnalysisResponse: false,
hasPartialAnalysisResponse: false,
hasFullAnalysisResponse: false,
medusaOrderId: undefined,
};
}
if (!messageResponse) { if (hasInvalidOrderId || !messageResponse) {
await createMedipostActionLog({
action: 'sync_analysis_results_from_medipost',
xml: privateMessageXml,
hasAnalysisResults: false,
medusaOrderId: hasInvalidOrderId ? undefined : medusaOrderId,
});
return { return {
messageId, messageId,
hasAnalysisResponse: false, hasAnalysisResponse: false,
hasPartialAnalysisResponse: false, hasPartialAnalysisResponse: false,
hasFullAnalysisResponse: false, hasFullAnalysisResponse: false,
medusaOrderId, medusaOrderId: hasInvalidOrderId ? undefined : medusaOrderId,
}; };
} }
@@ -745,12 +738,36 @@ export async function sendOrderToMedipost({
await sendPrivateMessage(orderXml); await sendPrivateMessage(orderXml);
} catch (e) { } catch (e) {
const isMedipostError = e instanceof MedipostValidationError; const isMedipostError = e instanceof MedipostValidationError;
await logMedipostDispatch({ if (isMedipostError) {
medusaOrderId, await logMedipostDispatch({
isSuccess: false, medusaOrderId,
isMedipostError, isSuccess: false,
errorMessage: isMedipostError ? e.response : undefined, 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; throw e;
} }
await logMedipostDispatch({ await logMedipostDispatch({
@@ -758,6 +775,12 @@ export async function sendOrderToMedipost({
isSuccess: true, isSuccess: true,
isMedipostError: false, isMedipostError: false,
}); });
await createMedipostActionLog({
action: 'send_order_to_medipost',
xml: orderXml,
hasAnalysisResults: false,
medusaOrderId,
});
await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' }); await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' });
} }
@@ -825,3 +848,37 @@ export async function getOrderedAnalysisElementsIds({
return [...analysisPackageElements, ...orderedAnalysisElements]; 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({ export async function getAnalysisOrdersAdmin({
orderStatus, orderStatus,
medusaOrderId,
}: { }: {
orderStatus?: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status']; orderStatus?: Tables<{ schema: 'medreport' }, 'analysis_orders'>['status'];
medusaOrderId?: string | null;
} = {}) { } = {}) {
const query = getSupabaseServerAdminClient() const query = getSupabaseServerAdminClient()
.schema('medreport') .schema('medreport')
@@ -144,6 +146,9 @@ export async function getAnalysisOrdersAdmin({
if (orderStatus) { if (orderStatus) {
query.eq('status', orderStatus); query.eq('status', orderStatus);
} }
if (medusaOrderId) {
query.eq('medusa_order_id', medusaOrderId);
}
const orders = await query.order('created_at', { ascending: false }).throwOnError(); const orders = await query.order('created_at', { ascending: false }).throwOnError();
return orders.data; return orders.data;
} }

View File

@@ -0,0 +1,16 @@
import { Button } from '@react-email/components';
export function EmailButton(
props: React.PropsWithChildren<{
href: string;
}>,
) {
return (
<Button
className="hover:bg-primary/90 inline-flex w-full items-center justify-center gap-1 rounded bg-[#16a249] py-3 text-center text-[16px] font-semibold whitespace-nowrap text-white no-underline shadow-xs transition-colors focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
href={props.href}
>
{props.children}
</Button>
);
}

View File

@@ -1,9 +1,7 @@
import { import {
Body, Body,
Button,
Head, Head,
Html, Html,
Link,
Preview, Preview,
Tailwind, Tailwind,
Text, Text,
@@ -13,6 +11,7 @@ import {
import { BodyStyle } from '../components/body-style'; import { BodyStyle } from '../components/body-style';
import CommonFooter from '../components/common-footer'; import CommonFooter from '../components/common-footer';
import { EmailContent } from '../components/content'; import { EmailContent } from '../components/content';
import { EmailButton } from '../components/email-button';
import { EmailHeader } from '../components/header'; import { EmailHeader } from '../components/header';
import { EmailHeading } from '../components/heading'; import { EmailHeading } from '../components/heading';
import { EmailWrapper } from '../components/wrapper'; import { EmailWrapper } from '../components/wrapper';
@@ -72,11 +71,12 @@ export async function renderDoctorSummaryReceivedEmail({
<Text className="text-[16px] leading-[24px] text-[#242424]"> <Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:summaryReceivedForOrder`, { orderNr })} {t(`${namespace}:summaryReceivedForOrder`, { orderNr })}
</Text> </Text>
<Link
<EmailButton
href={`${process.env.NEXT_PUBLIC_SITE_URL}/home/order/${orderId}`} href={`${process.env.NEXT_PUBLIC_SITE_URL}/home/order/${orderId}`}
> >
<Button> {t(`${namespace}:linkText`, { orderNr })}</Button> {t(`${namespace}:linkText`, { orderNr })}
</Link> </EmailButton>
<Text> <Text>
{t(`${namespace}:ifButtonDisabled`)}{' '} {t(`${namespace}:ifButtonDisabled`)}{' '}
{`${process.env.NEXT_PUBLIC_SITE_URL}/home/order/${orderId}`} {`${process.env.NEXT_PUBLIC_SITE_URL}/home/order/${orderId}`}

View File

@@ -1,13 +1,13 @@
import { sdk } from "@lib/config" import { sdk } from "@lib/config";
import { HttpTypes } from "@medusajs/types" import { HttpTypes } from "@medusajs/types";
import { getCacheOptions } from "./cookies" import { getCacheOptions } from "./cookies";
export const listCategories = async (query?: Record<string, any>) => { export const listCategories = async (query?: Record<string, any>) => {
const next = { const next = {
...(await getCacheOptions("categories")), ...(await getCacheOptions("categories")),
} };
const limit = query?.limit || 100 const limit = query?.limit || 100;
return sdk.client return sdk.client
.fetch<{ product_categories: HttpTypes.StoreProductCategory[] }>( .fetch<{ product_categories: HttpTypes.StoreProductCategory[] }>(
@@ -23,8 +23,8 @@ export const listCategories = async (query?: Record<string, any>) => {
cache: "force-cache", cache: "force-cache",
} }
) )
.then(({ product_categories }) => product_categories) .then(({ product_categories }) => product_categories);
} };
export const getCategoryByHandle = async (categoryHandle: string[]) => { export const getCategoryByHandle = async (categoryHandle: string[]) => {
const { product_categories } = await getProductCategories({ const { product_categories } = await getProductCategories({
@@ -32,7 +32,7 @@ export const getCategoryByHandle = async (categoryHandle: string[]) => {
limit: 1, limit: 1,
}); });
return product_categories[0]; return product_categories[0];
} };
export const getProductCategories = async ({ export const getProductCategories = async ({
handle, handle,
@@ -45,19 +45,18 @@ export const getProductCategories = async ({
} = {}) => { } = {}) => {
const next = { const next = {
...(await getCacheOptions("categories")), ...(await getCacheOptions("categories")),
} };
return sdk.client return sdk.client.fetch<HttpTypes.StoreProductCategoryListResponse>(
.fetch<HttpTypes.StoreProductCategoryListResponse>( `/store/product-categories`,
`/store/product-categories`, {
{ query: {
query: { fields,
fields, handle,
handle, limit,
limit, },
}, next,
next, //cache: "force-cache",
//cache: "force-cache", }
} );
); };
}

View File

@@ -9,6 +9,7 @@ import { StoreProduct } from '@medusajs/types';
import { Button } from '@medusajs/ui'; import { Button } from '@medusajs/ui';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { handleAddToCart } from '../../../../lib/services/medusaCart.service'; import { handleAddToCart } from '../../../../lib/services/medusaCart.service';
import { toast } from '@kit/ui/sonner';
import { import {
Card, Card,
@@ -49,12 +50,19 @@ export default function SelectAnalysisPackage({
const handleSelect = async () => { const handleSelect = async () => {
setIsAddingToCart(true); setIsAddingToCart(true);
await handleAddToCart({ try {
selectedVariant: { id: variantId }, await handleAddToCart({
countryCode, selectedVariant: { id: variantId },
}); countryCode,
setIsAddingToCart(false); });
router.push('/home/cart'); 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 ( return (

View File

@@ -16,6 +16,7 @@ const PathsSchema = z.object({
home: z.string().min(1), home: z.string().min(1),
selectPackage: z.string().min(1), selectPackage: z.string().min(1),
booking: z.string().min(1), booking: z.string().min(1),
bookingHandle: z.string().min(1),
myOrders: z.string().min(1), myOrders: z.string().min(1),
analysisResults: z.string().min(1), analysisResults: z.string().min(1),
orderAnalysisPackage: z.string().min(1), orderAnalysisPackage: z.string().min(1),
@@ -64,6 +65,7 @@ const pathsConfig = PathsSchema.parse({
joinTeam: '/join', joinTeam: '/join',
selectPackage: '/select-package', selectPackage: '/select-package',
booking: '/home/booking', booking: '/home/booking',
bookingHandle: '/home/booking/[handle]',
orderAnalysisPackage: '/home/order-analysis-package', orderAnalysisPackage: '/home/order-analysis-package',
myOrders: '/home/order', myOrders: '/home/order',
analysisResults: '/home/analysis-results', analysisResults: '/home/analysis-results',

View File

@@ -199,6 +199,7 @@ export type Database = {
changed_by: string changed_by: string
created_at: string created_at: string
id: number id: number
extra_data?: Json | null
} }
Insert: { Insert: {
account_id: string account_id: string
@@ -206,6 +207,7 @@ export type Database = {
changed_by: string changed_by: string
created_at?: string created_at?: string
id?: number id?: number
extra_data?: Json | null
} }
Update: { Update: {
account_id?: string account_id?: string
@@ -213,6 +215,7 @@ export type Database = {
changed_by?: string changed_by?: string
created_at?: string created_at?: string
id?: number id?: number
extra_data?: Json | null
} }
Relationships: [] Relationships: []
} }
@@ -1088,7 +1091,7 @@ export type Database = {
price: number price: number
price_periods: string | null price_periods: string | null
requires_payment: boolean requires_payment: boolean
sync_id: number sync_id: string | null
updated_at: string | null updated_at: string | null
} }
Insert: { Insert: {
@@ -1107,7 +1110,7 @@ export type Database = {
price: number price: number
price_periods?: string | null price_periods?: string | null
requires_payment: boolean requires_payment: boolean
sync_id: number sync_id?: string | null
updated_at?: string | null updated_at?: string | null
} }
Update: { Update: {
@@ -1126,7 +1129,7 @@ export type Database = {
price?: number price?: number
price_periods?: string | null price_periods?: string | null
requires_payment?: boolean requires_payment?: boolean
sync_id?: number sync_id?: string | null
updated_at?: string | null updated_at?: string | null
} }
Relationships: [ Relationships: [
@@ -1147,7 +1150,7 @@ export type Database = {
doctor_user_id: string | null doctor_user_id: string | null
id: number id: number
status: Database["medreport"]["Enums"]["analysis_feedback_status"] status: Database["medreport"]["Enums"]["analysis_feedback_status"]
updated_at: string updated_at: string | null
updated_by: string | null updated_by: string | null
user_id: string user_id: string
value: string | null value: string | null
@@ -1159,7 +1162,7 @@ export type Database = {
doctor_user_id?: string | null doctor_user_id?: string | null
id?: number id?: number
status?: Database["medreport"]["Enums"]["analysis_feedback_status"] status?: Database["medreport"]["Enums"]["analysis_feedback_status"]
updated_at?: string updated_at?: string | null
updated_by?: string | null updated_by?: string | null
user_id: string user_id: string
value?: string | null value?: string | null
@@ -1171,7 +1174,7 @@ export type Database = {
doctor_user_id?: string | null doctor_user_id?: string | null
id?: number id?: number
status?: Database["medreport"]["Enums"]["analysis_feedback_status"] status?: Database["medreport"]["Enums"]["analysis_feedback_status"]
updated_at?: string updated_at?: string | null
updated_by?: string | null updated_by?: string | null
user_id?: string user_id?: string
value?: string | null value?: string | null
@@ -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: { medreport_product_groups: {
Row: { Row: {
created_at: string created_at: string
@@ -1850,9 +1881,7 @@ export type Database = {
Returns: Json Returns: Json
} }
create_team_account: { create_team_account: {
Args: Args: { account_name: string; new_personal_code: string }
| { account_name: string }
| { account_name: string; new_personal_code: string }
Returns: { Returns: {
application_role: Database["medreport"]["Enums"]["application_role"] application_role: Database["medreport"]["Enums"]["application_role"]
city: string | null city: string | null
@@ -1921,6 +1950,15 @@ export type Database = {
account_id: string 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: { get_medipost_dispatch_tries: {
Args: { p_medusa_order_id: string } Args: { p_medusa_order_id: string }
Returns: number Returns: number
@@ -2137,6 +2175,21 @@ export type Database = {
} }
Returns: Json 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: { Enums: {
analysis_feedback_status: "STARTED" | "DRAFT" | "COMPLETED" analysis_feedback_status: "STARTED" | "DRAFT" | "COMPLETED"

View File

@@ -1,7 +1,9 @@
{ {
"title": "Select analysis package", "title": "Select analysis package",
"noPackagesAvailable": "No packages available", "noPackagesAvailable": "No packages available",
"selectThisPackage": "Select this package", "selectThisPackage": "Select this package",
"selectPackage": "Select package", "selectPackage": "Select package",
"comparePackages": "Compare packages" "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", "title": "Select analysis",
"description": "Select the analysis that suits your needs", "description": "All analysis results will appear within 1-3 days after the blood test.",
"analysisNotAvailable": "Analysis is not available currently" "analysisNotAvailable": "Analysis is not available currently",
"analysisAddedToCart": "Analysis added to cart",
"analysisAddToCartError": "Adding analysis to cart failed"
} }

View File

@@ -1,8 +1,9 @@
{ {
"title": "Vali teenus", "title": "Vali teenus",
"description": "Vali sobiv teenus või pakett vastavalt oma tervisemurele või -eesmärgile.", "description": "Vali sobiv teenus või pakett vastavalt oma tervisemurele või -eesmärgile.",
"analysisPackages": { "analysisPackages": {
"title": "Analüüside paketid", "title": "Analüüside paketid",
"description": "Tutvu personaalsete analüüsi pakettidega ja telli" "description": "Tutvu personaalsete analüüsi pakettidega ja telli"
} },
} "noCategories": "Teenuste loetelu ei leitud, proovi hiljem uuesti"
}

View File

@@ -80,7 +80,8 @@
"dashboard": "Ülevaade", "dashboard": "Ülevaade",
"settings": "Settings", "settings": "Settings",
"profile": "Profile", "profile": "Profile",
"application": "Application" "application": "Application",
"pickTime": "Vali aeg"
}, },
"roles": { "roles": {
"owner": { "owner": {

View File

@@ -1,7 +1,9 @@
{ {
"title": "Vali analüüsi pakett", "title": "Vali analüüsi pakett",
"noPackagesAvailable": "Teenuste loetelu ei leitud, proovi hiljem uuesti", "noPackagesAvailable": "Teenuste loetelu ei leitud, proovi hiljem uuesti",
"selectThisPackage": "Vali see pakett", "selectThisPackage": "Vali see pakett",
"selectPackage": "Vali pakett", "selectPackage": "Vali pakett",
"comparePackages": "Võrdle pakette" "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", "title": "Vali analüüs",
"description": "Vali enda vajadustele sobiv analüüs", "description": "Kõikide analüüside tulemused ilmuvad 13 tööpäeva jooksul peale vere andmist.",
"analysisNotAvailable": "Analüüsi tellimine ei ole hetkel saadaval" "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( create or replace function medreport.medipost_retry_dispatch(
order_id text 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 -- Schedule the test-medipost-responses job to run every 15 minutes
select select
cron.schedule( cron.schedule(
'send-test-medipost-responses-every-15-minutes', -- Unique job name 'send-test-medipost-responses-every-15-minutes',
'*/15 * * * *', -- Cron schedule: every 15 minutes '*/15 * * * *',
$$ $$
select select
net.http_post( 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 -- Schedule the sync-analysis-results job to run every 15 minutes
select select
cron.schedule( 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,2 @@
ALTER TABLE medreport.connected_online_services
ALTER COLUMN sync_id TYPE text USING sync_id::text;

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;