Merge pull request #60 from MR-medreport/MED-85
feat(MED-85): testing feedback updates, some fixes+improvements
This commit is contained in:
@@ -1,29 +1,62 @@
|
||||
import { readPrivateMessageResponse } from "~/lib/services/medipost.service";
|
||||
|
||||
type ProcessedMessage = {
|
||||
messageId: string;
|
||||
hasAnalysisResponse: boolean;
|
||||
hasPartialAnalysisResponse: boolean;
|
||||
hasFullAnalysisResponse: boolean;
|
||||
medusaOrderId: string | undefined;
|
||||
};
|
||||
|
||||
type GroupedResults = {
|
||||
processed: Pick<ProcessedMessage, 'messageId' | 'medusaOrderId'>[];
|
||||
waitingForResults: Pick<ProcessedMessage, 'messageId' | 'medusaOrderId'>[];
|
||||
};
|
||||
|
||||
export default async function syncAnalysisResults() {
|
||||
console.info("Syncing analysis results");
|
||||
|
||||
let processedMessageIds: string[] = [];
|
||||
let processedMessages: ProcessedMessage[] = [];
|
||||
const excludedMessageIds: string[] = [];
|
||||
while (true) {
|
||||
console.info("Fetching private messages");
|
||||
const { messageIdErrored, messageIdProcessed } = await readPrivateMessageResponse({ excludedMessageIds });
|
||||
if (messageIdProcessed) {
|
||||
processedMessageIds.push(messageIdProcessed);
|
||||
const result = await readPrivateMessageResponse({ excludedMessageIds });
|
||||
if (result.messageId) {
|
||||
processedMessages.push(result as ProcessedMessage);
|
||||
}
|
||||
|
||||
if (!messageIdErrored) {
|
||||
if (!result.messageId) {
|
||||
console.info("No more messages to process");
|
||||
break;
|
||||
}
|
||||
|
||||
if (excludedMessageIds.includes(messageIdErrored)) {
|
||||
console.info(`Message id=${messageIdErrored} has already been processed, stopping`);
|
||||
if (!excludedMessageIds.includes(result.messageId)) {
|
||||
excludedMessageIds.push(result.messageId);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
excludedMessageIds.push(messageIdErrored);
|
||||
}
|
||||
|
||||
console.info(`Processed ${processedMessageIds.length} messages, ids: ${processedMessageIds.join(', ')}`);
|
||||
const groupedResults = processedMessages.reduce((acc, result) => {
|
||||
if (result.medusaOrderId) {
|
||||
if (result.hasAnalysisResponse) {
|
||||
if (!acc.processed) {
|
||||
acc.processed = [];
|
||||
}
|
||||
acc.processed.push({
|
||||
messageId: result.messageId,
|
||||
medusaOrderId: result.medusaOrderId,
|
||||
});
|
||||
} else {
|
||||
if (!acc.waitingForResults) {
|
||||
acc.waitingForResults = [];
|
||||
}
|
||||
acc.waitingForResults.push({
|
||||
messageId: result.messageId,
|
||||
medusaOrderId: result.medusaOrderId,
|
||||
});
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, {} as GroupedResults);
|
||||
console.info(`Processed ${processedMessages.length} messages, results: ${JSON.stringify(groupedResults, undefined, 2)}`);
|
||||
}
|
||||
|
||||
40
app/api/job/medipost-retry-dispatch/route.ts
Normal file
40
app/api/job/medipost-retry-dispatch/route.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import loadEnv from "../handler/load-env";
|
||||
import validateApiKey from "../handler/validate-api-key";
|
||||
import { getOrderedAnalysisElementsIds, sendOrderToMedipost } from "~/lib/services/medipost.service";
|
||||
import { retrieveOrder } from "@lib/data/orders";
|
||||
import { getMedipostDispatchTries } from "~/lib/services/audit.service";
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
loadEnv();
|
||||
|
||||
const { medusaOrderId } = await request.json();
|
||||
|
||||
try {
|
||||
validateApiKey(request);
|
||||
} catch (e) {
|
||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const tries = await getMedipostDispatchTries(medusaOrderId);
|
||||
if (tries >= 3) {
|
||||
return NextResponse.json({
|
||||
message: 'Order has been retried too many times',
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const medusaOrder = await retrieveOrder(medusaOrderId);
|
||||
const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder });
|
||||
await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
|
||||
console.info("Successfully sent order to medipost");
|
||||
return NextResponse.json({
|
||||
message: 'Successfully sent order to medipost',
|
||||
}, { status: 200 });
|
||||
} catch (e) {
|
||||
console.error("Error sending order to medipost", e);
|
||||
return NextResponse.json({
|
||||
message: 'Failed to send order to medipost',
|
||||
}, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -18,8 +18,8 @@ async function AnalysisPage({
|
||||
id: string;
|
||||
}>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const analysisResultDetails = await loadResult(Number(id));
|
||||
const { id: analysisResponseId } = await params;
|
||||
const analysisResultDetails = await loadResult(Number(analysisResponseId));
|
||||
|
||||
if (!analysisResultDetails) {
|
||||
return null;
|
||||
|
||||
@@ -85,35 +85,43 @@ const AnalysisLevelBar = ({
|
||||
return calculated;
|
||||
}, [value, upper, lower]);
|
||||
|
||||
const [isVeryLow, isLow, isHigh, isVeryHigh] = useMemo(() => [
|
||||
level === AnalysisResultLevel.VERY_LOW,
|
||||
level === AnalysisResultLevel.LOW,
|
||||
level === AnalysisResultLevel.HIGH,
|
||||
level === AnalysisResultLevel.VERY_HIGH,
|
||||
], [level, value, upper, lower]);
|
||||
|
||||
const hasAbnormalLevel = isVeryLow || isLow || isHigh || isVeryHigh;
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex h-3 w-[35%] max-w-[360px] gap-1 sm:mt-0">
|
||||
{normLowerIncluded && (
|
||||
<>
|
||||
<Level
|
||||
isActive={level === AnalysisResultLevel.VERY_LOW}
|
||||
isActive={isVeryLow}
|
||||
color="destructive"
|
||||
isFirst
|
||||
/>
|
||||
<Level isActive={level === AnalysisResultLevel.LOW} color="warning" />
|
||||
<Level isActive={isLow} color="warning" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Level
|
||||
isFirst={!normLowerIncluded}
|
||||
isLast={!normUpperIncluded}
|
||||
color={level === AnalysisResultLevel.NORMAL ? "success" : "warning"}
|
||||
isActive
|
||||
{...(hasAbnormalLevel ? { color: "warning", isActive: false } : { color: "success", isActive: true })}
|
||||
arrowLocation={arrowLocation}
|
||||
/>
|
||||
|
||||
{normUpperIncluded && (
|
||||
<>
|
||||
<Level
|
||||
isActive={level === AnalysisResultLevel.HIGH}
|
||||
isActive={isHigh}
|
||||
color="warning"
|
||||
/>
|
||||
<Level
|
||||
isActive={level === AnalysisResultLevel.VERY_HIGH}
|
||||
isActive={isVeryHigh}
|
||||
color="destructive"
|
||||
isLast
|
||||
/>
|
||||
|
||||
@@ -39,9 +39,11 @@ const Analysis = ({
|
||||
results,
|
||||
startIcon,
|
||||
endIcon,
|
||||
isCancelled,
|
||||
}: {
|
||||
analysisElement: Pick<AnalysisElement, 'analysis_name_lab'>;
|
||||
results?: AnalysisResultForDisplay;
|
||||
isCancelled?: boolean;
|
||||
startIcon?: ReactElement | null;
|
||||
endIcon?: ReactNode | null;
|
||||
}) => {
|
||||
@@ -128,7 +130,7 @@ const Analysis = ({
|
||||
/>
|
||||
{endIcon || <div className="mx-2 w-4" />}
|
||||
</>
|
||||
) : (
|
||||
) : (isCancelled ? null : (
|
||||
<>
|
||||
<div className="flex items-center gap-3 sm:ml-auto">
|
||||
<div className="font-semibold">
|
||||
@@ -138,7 +140,7 @@ const Analysis = ({
|
||||
<div className="mx-8 w-[60px]"></div>
|
||||
<AnalysisLevelBarSkeleton />
|
||||
</>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -104,7 +104,7 @@ async function AnalysisResultsPage() {
|
||||
&& analysisResponseElements?.find((element) => element.analysis_element_original_id === analysisElement.analysis_id_original);
|
||||
if (!results) {
|
||||
return (
|
||||
<Analysis key={`${analysisOrder.id}-${analysisElement.id}`} analysisElement={analysisElement} />
|
||||
<Analysis key={`${analysisOrder.id}-${analysisElement.id}`} analysisElement={analysisElement} isCancelled={analysisOrder.status === 'CANCELLED'}/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -143,7 +143,7 @@ export async function processMontonioCallback(orderToken: string) {
|
||||
console.error("Missing email or analysisPackageName", orderResult);
|
||||
}
|
||||
|
||||
sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
|
||||
await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
|
||||
|
||||
return { success: true, orderId };
|
||||
} catch (error) {
|
||||
|
||||
@@ -25,21 +25,28 @@ export default async function CartPage() {
|
||||
|
||||
const { productTypes } = await listProductTypes();
|
||||
const analysisPackagesType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages');
|
||||
const analysisPackages = analysisPackagesType && cart?.items
|
||||
? cart.items.filter((item) => item.product?.type_id === analysisPackagesType.id)
|
||||
const synlabAnalysisType = productTypes.find(({ metadata }) => metadata?.handle === 'synlab-analysis');
|
||||
const synlabAnalyses = analysisPackagesType && synlabAnalysisType && cart?.items
|
||||
? cart.items.filter((item) => {
|
||||
const productTypeId = item.product?.type_id;
|
||||
if (!productTypeId) {
|
||||
return false;
|
||||
}
|
||||
return [analysisPackagesType.id, synlabAnalysisType.id].includes(productTypeId);
|
||||
})
|
||||
: [];
|
||||
const otherItems = cart?.items?.filter((item) => item.product?.type_id !== analysisPackagesType?.id) ?? [];
|
||||
const ttoServiceItems = cart?.items?.filter((item) => !synlabAnalyses.some((analysis) => analysis.id === item.id)) ?? [];
|
||||
|
||||
const otherItemsSorted = otherItems.sort((a, b) => (a.updated_at ?? "") > (b.updated_at ?? "") ? -1 : 1);
|
||||
const otherItemsSorted = ttoServiceItems.sort((a, b) => (a.updated_at ?? "") > (b.updated_at ?? "") ? -1 : 1);
|
||||
const item = otherItemsSorted[0];
|
||||
const hasItemsWithTimer = false as boolean;
|
||||
const isTimerShown = ttoServiceItems.length > 0 && !!item && !!item.updated_at;
|
||||
|
||||
return (
|
||||
<PageBody>
|
||||
<PageHeader title={<Trans i18nKey="cart:title" />}>
|
||||
{hasItemsWithTimer && item && item.updated_at && <CartTimer cartItem={item} />}
|
||||
{isTimerShown && <CartTimer cartItem={item} />}
|
||||
</PageHeader>
|
||||
<Cart cart={cart} analysisPackages={analysisPackages} otherItems={otherItems} />
|
||||
<Cart cart={cart} synlabAnalyses={synlabAnalyses} ttoServiceItems={ttoServiceItems} />
|
||||
</PageBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,17 +19,13 @@ import {
|
||||
} from '@kit/ui/select';
|
||||
import { updateCartPartnerLocation } from '../../_lib/server/update-cart-partner-location';
|
||||
|
||||
import partnerLocations from './partner-locations.json';
|
||||
|
||||
const AnalysisLocationSchema = z.object({
|
||||
locationId: z.string().min(1),
|
||||
});
|
||||
|
||||
const MOCK_LOCATIONS: { id: string, name: string }[] = [
|
||||
{ id: "synlab-tallinn-1", name: "SYNLAB - Tallinn" },
|
||||
{ id: "synlab-tartu-1", name: "SYNLAB - Tartu" },
|
||||
{ id: "synlab-parnu-1", name: "SYNLAB - Pärnu" },
|
||||
]
|
||||
|
||||
export default function AnalysisLocation({ cart, analysisPackages }: { cart: StoreCart, analysisPackages: StoreCartLineItem[] }) {
|
||||
export default function AnalysisLocation({ cart, synlabAnalyses }: { cart: StoreCart, synlabAnalyses: StoreCartLineItem[] }) {
|
||||
const { t } = useTranslation('cart');
|
||||
|
||||
const form = useForm<z.infer<typeof AnalysisLocationSchema>>({
|
||||
@@ -39,12 +35,16 @@ export default function AnalysisLocation({ cart, analysisPackages }: { cart: Sto
|
||||
resolver: zodResolver(AnalysisLocationSchema),
|
||||
});
|
||||
|
||||
const getLocation = (locationId: string) => partnerLocations.find(({ name }) => name === locationId);
|
||||
|
||||
const selectedLocation = getLocation(form.watch('locationId'));
|
||||
|
||||
const onSubmit = async ({ locationId }: z.infer<typeof AnalysisLocationSchema>) => {
|
||||
const promise = updateCartPartnerLocation({
|
||||
cartId: cart.id,
|
||||
lineIds: analysisPackages.map(({ id }) => id),
|
||||
lineIds: synlabAnalyses.map(({ id }) => id),
|
||||
partnerLocationId: locationId,
|
||||
partnerLocationName: MOCK_LOCATIONS.find((location) => location.id === locationId)?.name ?? '',
|
||||
partnerLocationName: getLocation(locationId)?.name ?? '',
|
||||
});
|
||||
|
||||
toast.promise(promise, {
|
||||
@@ -55,7 +55,7 @@ export default function AnalysisLocation({ cart, analysisPackages }: { cart: Sto
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full bg-white flex flex-col txt-medium">
|
||||
<div className="w-full bg-white flex flex-col txt-medium gap-y-2">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) => onSubmit(data))}
|
||||
@@ -78,18 +78,35 @@ export default function AnalysisLocation({ cart, analysisPackages }: { cart: Sto
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t('cart:locations.locationSelect')}</SelectLabel>
|
||||
|
||||
{MOCK_LOCATIONS.map((location) => (
|
||||
<SelectItem key={location.id} value={location.id}>{location.name}</SelectItem>
|
||||
{Object.entries(partnerLocations
|
||||
.reduce((acc, curr) => ({
|
||||
...acc,
|
||||
[curr.city]: [...((acc[curr.city] as typeof partnerLocations) ?? []), curr],
|
||||
}), {} as Record<string, typeof partnerLocations>))
|
||||
.map(([city, locations]) => (
|
||||
<SelectGroup key={city}>
|
||||
<SelectLabel>{city}</SelectLabel>
|
||||
{locations.map((location) => (
|
||||
<SelectItem key={location.name} value={location.name}>{location.name}</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{selectedLocation && (
|
||||
<div className="flex flex-col gap-y-2 mb-4">
|
||||
<p className="text-sm">
|
||||
{selectedLocation.address}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
{selectedLocation.hours}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Trans i18nKey={'cart:locations.description'} />
|
||||
</p>
|
||||
|
||||
@@ -22,12 +22,12 @@ const IS_DISCOUNT_SHOWN = false as boolean;
|
||||
|
||||
export default function Cart({
|
||||
cart,
|
||||
analysisPackages,
|
||||
otherItems,
|
||||
synlabAnalyses,
|
||||
ttoServiceItems,
|
||||
}: {
|
||||
cart: StoreCart | null
|
||||
analysisPackages: StoreCartLineItem[];
|
||||
otherItems: StoreCartLineItem[];
|
||||
synlabAnalyses: StoreCartLineItem[];
|
||||
ttoServiceItems: StoreCartLineItem[];
|
||||
}) {
|
||||
const { i18n: { language } } = useTranslation();
|
||||
|
||||
@@ -68,13 +68,13 @@ export default function Cart({
|
||||
}
|
||||
|
||||
const hasCartItems = Array.isArray(cart.items) && cart.items.length > 0;
|
||||
const isLocationsShown = analysisPackages.length > 0;
|
||||
const isLocationsShown = synlabAnalyses.length > 0;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40 lg:px-4">
|
||||
<div className="flex flex-col bg-white gap-y-6">
|
||||
<CartItems cart={cart} items={analysisPackages} productColumnLabelKey="cart:items.analysisPackages.productColumnLabel" />
|
||||
<CartItems cart={cart} items={otherItems} productColumnLabelKey="cart:items.services.productColumnLabel" />
|
||||
<CartItems cart={cart} items={synlabAnalyses} productColumnLabelKey="cart:items.synlabAnalyses.productColumnLabel" />
|
||||
<CartItems cart={cart} items={ttoServiceItems} productColumnLabelKey="cart:items.ttoServices.productColumnLabel" />
|
||||
</div>
|
||||
{hasCartItems && (
|
||||
<div className="flex justify-end gap-x-4 px-6 py-4">
|
||||
@@ -121,7 +121,7 @@ export default function Cart({
|
||||
</h5>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AnalysisLocation cart={{ ...cart }} analysisPackages={analysisPackages} />
|
||||
<AnalysisLocation cart={{ ...cart }} synlabAnalyses={synlabAnalyses} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
122
app/home/(user)/_components/cart/partner-locations.json
Normal file
122
app/home/(user)/_components/cart/partner-locations.json
Normal file
@@ -0,0 +1,122 @@
|
||||
[
|
||||
{
|
||||
"name": "SYNLAB Eesti Veerenni verevõtupunkt",
|
||||
"address": "Veerenni 53a, VI korrus Tel: 17123",
|
||||
"hours": "Verevõtt E-R 7.30-15.30 ja L 8.00-16.00 (lõunapaus 13.00-13.30)",
|
||||
"city": "Tallinn"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti kesklinna verevõtupunkt",
|
||||
"address": "Pärnu mnt 15 (Kawe Plaza), I korrus Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti 8.00-16.00 (lõunapaus 12.00-12.30)",
|
||||
"city": "Tallinn"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Lasnamäe verevõtupunkt",
|
||||
"address": "Linnamäe tee 3 (Lasnamäe Tervisemaja), II korrus Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti 8.00-16.00 (lõunapaus 12.00-12.30)",
|
||||
"city": "Tallinn"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Ülemiste verevõtupunkt",
|
||||
"address": "Valukoja 7 (Ülemiste Tervisemaja), II korrus Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti 8.00-16.00 (lõunapaus 12.00-12.30)",
|
||||
"city": "Tallinn"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Sepapaja verevõtupunkt",
|
||||
"address": "Sepapaja 12/1 (Ülemiste Tervisemaja 2, Karl Ernst von Baeri maja), III korrus Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti 8.00-16.00 (lõunapaus 12.30-13.00)",
|
||||
"city": "Tallinn"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Viimsi verevõtupunkt",
|
||||
"address": "Ravi tee 4 (Viimsi Fertilitas) Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti 7.30-15.30 (lõunapaus 12.30-13.00)",
|
||||
"city": "Viimsi"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Maardu verevõtupunkt",
|
||||
"address": "Kallasmaa 4 (Maardu Tervisekeskus) Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti 8.00-15.00 (lõunapaus 12.00-12.30)",
|
||||
"city": "Maardu"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Tartu kliiniline labor",
|
||||
"address": "Raatuse 21, II korrus Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti 8.00-16.30",
|
||||
"city": "Tartu"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Tasku verevõtupunkt",
|
||||
"address": "Turu 2, IV korrus (Tasku Meditsiinikeskus) Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti 8.00-15.30",
|
||||
"city": "Tartu"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Tartu Tervisekeskuse verevõtupunkt",
|
||||
"address": "Mõisavahe 34b, I korrus Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti 8.00-15.00 (lõunapaus 12:30-13:00)",
|
||||
"city": "Tartu"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Pärnu Tervis SPA verevõtupunkt",
|
||||
"address": "Seedri 6 (Tervis SPA), kabinet 202 Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti E-N 8.00-15.00 R 8.00-13.00",
|
||||
"city": "Pärnu"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Suur-Sepa verevõtupunkt",
|
||||
"address": "Suur-Sepa 14, kabinet 102 Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti E-N 8.00-16.00 (lõunapaus 12.30-13.00) ja R 8.00-14.00 (lõunapaus 12.30-13.00)",
|
||||
"city": "Pärnu"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Narva verevõtupunkt",
|
||||
"address": "Fama 10/2, kabinet 14 Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti 7.30-12.00",
|
||||
"city": "Narva"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Sillamäe verevõtupunkt",
|
||||
"address": "Kajaka 9, IV korrus, kabinet 404 Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti 8.00-11.30",
|
||||
"city": "Sillamäe"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Jõhvi verevõtupunkt",
|
||||
"address": "Jaama 34, I korrus, kabinet 15 Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti 8.00-13.00",
|
||||
"city": "Jõhvi"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Viljandi verevõtupunkt",
|
||||
"address": "Tallinna 19, II korrus, kabinet 210 Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti E-N 8.00-15.00 ja R 8.00-12.00",
|
||||
"city": "Viljandi"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Võru labor",
|
||||
"address": "Tartu tn 9 Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti E-N 8.00-15.30 R 8.00-13.00",
|
||||
"city": "Võru"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Elva labor",
|
||||
"address": "Supelranna 21, kabinet 133 Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti 8.00-14.00",
|
||||
"city": "Elva"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Põltsamaa labor",
|
||||
"address": "Lossi 49 Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti E-N 8.00-15.00 (lõunapaus 12:00-12:30) ja R 8.00-12.00",
|
||||
"city": "Põltsamaa"
|
||||
},
|
||||
{
|
||||
"name": "SYNLAB Eesti Otepää labor",
|
||||
"address": "Tartu mnt 2 Tel: 17123",
|
||||
"hours": "Verevõtt tööpäeviti 8.00-12.00",
|
||||
"city": "Otepää"
|
||||
}
|
||||
]
|
||||
@@ -22,8 +22,14 @@ import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { PackageHeader } from '@kit/shared/components/package-header';
|
||||
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
|
||||
import { StoreProduct } from '@medusajs/types';
|
||||
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
||||
import { withI18n } from '@/lib/i18n/with-i18n';
|
||||
import { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
export type AnalysisPackageElement = Pick<StoreProduct, 'title' | 'id' | 'description'> & {
|
||||
isIncludedInStandard: boolean;
|
||||
isIncludedInStandardPlus: boolean;
|
||||
isIncludedInPremium: boolean;
|
||||
};
|
||||
|
||||
const CheckWithBackground = () => {
|
||||
return (
|
||||
@@ -33,15 +39,15 @@ const CheckWithBackground = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const PackageTableHead = async ({ product, nrOfAnalyses }: { product: StoreProduct, nrOfAnalyses: number }) => {
|
||||
const PackageTableHead = async ({ product }: { product: AnalysisPackageWithVariant }) => {
|
||||
const { t, language } = await createI18nServerInstance();
|
||||
const variant = product.variants?.[0];
|
||||
const titleKey = product.title;
|
||||
const price = variant?.calculated_price?.calculated_amount ?? 0;
|
||||
|
||||
const { title, price, nrOfAnalyses } = product;
|
||||
|
||||
return (
|
||||
<TableHead className="py-2">
|
||||
<PackageHeader
|
||||
title={t(titleKey)}
|
||||
title={t(title)}
|
||||
tagColor='bg-cyan'
|
||||
analysesNr={t('product:nrOfAnalyses', { nr: nrOfAnalyses })}
|
||||
language={language}
|
||||
@@ -56,24 +62,20 @@ const ComparePackagesModal = async ({
|
||||
analysisPackageElements,
|
||||
triggerElement,
|
||||
}: {
|
||||
analysisPackages: StoreProduct[];
|
||||
analysisPackageElements: StoreProduct[];
|
||||
analysisPackages: AnalysisPackageWithVariant[];
|
||||
analysisPackageElements: AnalysisPackageElement[];
|
||||
triggerElement: JSX.Element;
|
||||
}) => {
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
const standardPackage = analysisPackages.find(({ metadata }) => metadata?.analysisPackageTier === 'standard')!;
|
||||
const standardPlusPackage = analysisPackages.find(({ metadata }) => metadata?.analysisPackageTier === 'standard-plus')!;
|
||||
const premiumPackage = analysisPackages.find(({ metadata }) => metadata?.analysisPackageTier === 'premium')!;
|
||||
const standardPackage = analysisPackages.find(({ isStandard }) => isStandard);
|
||||
const standardPlusPackage = analysisPackages.find(({ isStandardPlus }) => isStandardPlus);
|
||||
const premiumPackage = analysisPackages.find(({ isPremium }) => isPremium);
|
||||
|
||||
if (!standardPackage || !standardPlusPackage || !premiumPackage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const standardPackageAnalyses = getAnalysisElementMedusaProductIds([standardPackage]);
|
||||
const standardPlusPackageAnalyses = getAnalysisElementMedusaProductIds([standardPlusPackage]);
|
||||
const premiumPackageAnalyses = getAnalysisElementMedusaProductIds([premiumPackage]);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>{triggerElement}</DialogTrigger>
|
||||
@@ -103,9 +105,9 @@ const ComparePackagesModal = async ({
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead></TableHead>
|
||||
<PackageTableHead product={standardPackage} nrOfAnalyses={standardPackageAnalyses.length} />
|
||||
<PackageTableHead product={standardPlusPackage} nrOfAnalyses={standardPlusPackageAnalyses.length} />
|
||||
<PackageTableHead product={premiumPackage} nrOfAnalyses={premiumPackageAnalyses.length} />
|
||||
<PackageTableHead product={standardPackage} />
|
||||
<PackageTableHead product={standardPlusPackage} />
|
||||
<PackageTableHead product={premiumPackage} />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -115,29 +117,29 @@ const ComparePackagesModal = async ({
|
||||
title,
|
||||
id,
|
||||
description,
|
||||
isIncludedInStandard,
|
||||
isIncludedInStandardPlus,
|
||||
isIncludedInPremium,
|
||||
},
|
||||
index,
|
||||
) => {
|
||||
if (!title) {
|
||||
return null;
|
||||
}
|
||||
const includedInStandard = standardPackageAnalyses.includes(id);
|
||||
const includedInStandardPlus = standardPlusPackageAnalyses.includes(id);
|
||||
const includedInPremium = premiumPackageAnalyses.includes(id);
|
||||
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableRow key={id}>
|
||||
<TableCell className="py-6">
|
||||
{title}{' '}
|
||||
{description && (<InfoTooltip content={description} icon={<QuestionMarkCircledIcon />} />)}
|
||||
</TableCell>
|
||||
<TableCell align="center" className="py-6">
|
||||
{includedInStandard && <CheckWithBackground />}
|
||||
{isIncludedInStandard && <CheckWithBackground />}
|
||||
</TableCell>
|
||||
<TableCell align="center" className="py-6">
|
||||
{(includedInStandard || includedInStandardPlus) && <CheckWithBackground />}
|
||||
{(isIncludedInStandard || isIncludedInStandardPlus) && <CheckWithBackground />}
|
||||
</TableCell>
|
||||
<TableCell align="center" className="py-6">
|
||||
{(includedInStandard || includedInStandardPlus || includedInPremium) && <CheckWithBackground />}
|
||||
{(isIncludedInStandard || isIncludedInStandardPlus || isIncludedInPremium) && <CheckWithBackground />}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -9,30 +9,39 @@ import {
|
||||
CardFooter,
|
||||
CardDescription,
|
||||
} from '@kit/ui/card';
|
||||
import { StoreProduct, StoreProductVariant } from '@medusajs/types';
|
||||
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';
|
||||
|
||||
export type OrderAnalysisCard = Pick<
|
||||
StoreProduct, 'title' | 'description' | 'subtitle'
|
||||
> & {
|
||||
isAvailable: boolean;
|
||||
variant: { id: string };
|
||||
};
|
||||
|
||||
export default function OrderAnalysesCards({
|
||||
analyses,
|
||||
countryCode,
|
||||
}: {
|
||||
analyses: StoreProduct[];
|
||||
analyses: OrderAnalysisCard[];
|
||||
countryCode: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
const [isAddingToCart, setIsAddingToCart] = useState(false);
|
||||
const handleSelect = async (selectedVariant: StoreProductVariant) => {
|
||||
if (!selectedVariant?.id || isAddingToCart) return null
|
||||
const handleSelect = async (variantId: string) => {
|
||||
if (isAddingToCart) {
|
||||
return null;
|
||||
}
|
||||
|
||||
setIsAddingToCart(true);
|
||||
try {
|
||||
await handleAddToCart({
|
||||
selectedVariant,
|
||||
selectedVariant: { id: variantId },
|
||||
countryCode,
|
||||
});
|
||||
setIsAddingToCart(false);
|
||||
@@ -47,13 +56,11 @@ export default function OrderAnalysesCards({
|
||||
<div className="grid grid-cols-3 gap-6 mt-4">
|
||||
{analyses.map(({
|
||||
title,
|
||||
variants,
|
||||
variant,
|
||||
description,
|
||||
subtitle,
|
||||
status,
|
||||
metadata,
|
||||
isAvailable,
|
||||
}) => {
|
||||
const isAvailable = status === 'published' && !!metadata?.analysisIdOriginal;
|
||||
return (
|
||||
<Card
|
||||
key={title}
|
||||
@@ -72,7 +79,7 @@ export default function OrderAnalysesCards({
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="px-2 text-black"
|
||||
onClick={() => handleSelect(variants![0]!)}
|
||||
onClick={() => handleSelect(variant.id)}
|
||||
>
|
||||
{isAddingToCart ? <Loader2 className="size-4 stroke-2 animate-spin" /> : <ShoppingCart className="size-4 stroke-2" />}
|
||||
</Button>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { cache } from 'react';
|
||||
import { listProductTypes } from "@lib/data/products";
|
||||
import { listRegions } from '@lib/data/regions';
|
||||
import { getProductCategories } from '@lib/data/categories';
|
||||
import { OrderAnalysisCard } from '../../_components/order-analyses-cards';
|
||||
|
||||
async function countryCodesLoader() {
|
||||
const countryCodes = await listRegions().then((regions) =>
|
||||
@@ -34,7 +35,18 @@ async function analysesLoader() {
|
||||
const category = productCategories.find(({ metadata }) => metadata?.page === 'order-analysis');
|
||||
|
||||
return {
|
||||
analyses: category?.products ?? [],
|
||||
analyses: category?.products?.map<OrderAnalysisCard>(({ title, description, subtitle, variants, status, metadata }) => {
|
||||
const variant = variants![0]!;
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
subtitle,
|
||||
variant: {
|
||||
id: variant.id,
|
||||
},
|
||||
isAvailable: status === 'published' && !!metadata?.analysisIdOriginal,
|
||||
};
|
||||
}) ?? [],
|
||||
countryCode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { cache } from 'react';
|
||||
import Isikukood, { Gender } from 'isikukood';
|
||||
|
||||
import { listProductTypes, listProducts } from "@lib/data/products";
|
||||
import { listRegions } from '@lib/data/regions';
|
||||
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
||||
import type { StoreProduct } from '@medusajs/types';
|
||||
import { loadCurrentUserAccount } from './load-user-account';
|
||||
import { AnalysisPackageWithVariant } from '~/components/select-analysis-package';
|
||||
import { AccountWithParams } from '@/packages/features/accounts/src/server/api';
|
||||
|
||||
async function countryCodesLoader() {
|
||||
const countryCodes = await listRegions().then((regions) =>
|
||||
@@ -19,26 +23,64 @@ async function productTypesLoader() {
|
||||
}
|
||||
export const loadProductTypes = cache(productTypesLoader);
|
||||
|
||||
async function analysisPackagesLoader() {
|
||||
const [countryCodes, productTypes] = await Promise.all([loadCountryCodes(), loadProductTypes()]);
|
||||
const countryCode = countryCodes[0]!;
|
||||
function userSpecificVariantLoader({
|
||||
account,
|
||||
}: {
|
||||
account: AccountWithParams;
|
||||
}) {
|
||||
const { personal_code: personalCode } = account;
|
||||
if (!personalCode) {
|
||||
throw new Error('Personal code not found');
|
||||
}
|
||||
const parsed = new Isikukood(personalCode);
|
||||
const ageRange = (() => {
|
||||
const age = parsed.getAge();
|
||||
if (age >= 18 && age <= 29) {
|
||||
return '18-29';
|
||||
}
|
||||
if (age >= 30 && age <= 49) {
|
||||
return '30-49';
|
||||
}
|
||||
if (age >= 50 && age <= 59) {
|
||||
return '50-59';
|
||||
}
|
||||
if (age >= 60) {
|
||||
return '60';
|
||||
}
|
||||
throw new Error('Age range not supported');
|
||||
})();
|
||||
const gender = parsed.getGender() === Gender.MALE ? 'M' : 'F';
|
||||
|
||||
let analysisPackages: StoreProduct[] = [];
|
||||
let analysisPackageElements: StoreProduct[] = [];
|
||||
|
||||
const productType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages');
|
||||
if (!productType) {
|
||||
return { analysisPackageElements, analysisPackages, countryCode };
|
||||
return ({
|
||||
product,
|
||||
}: {
|
||||
product: StoreProduct;
|
||||
}) => {
|
||||
const variants = product.variants;
|
||||
if (!variants) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const analysisPackagesResponse = await listProducts({
|
||||
countryCode,
|
||||
queryParams: { limit: 100, "type_id[0]": productType.id },
|
||||
});
|
||||
analysisPackages = analysisPackagesResponse.response.products;
|
||||
const variant = variants.find((v) => v.options?.every((o) => [ageRange, gender].includes(o.value)));
|
||||
if (!variant) {
|
||||
return null;
|
||||
}
|
||||
return variant;
|
||||
}
|
||||
}
|
||||
|
||||
async function analysisPackageElementsLoader({
|
||||
analysisPackagesWithVariant,
|
||||
countryCode,
|
||||
}: {
|
||||
analysisPackagesWithVariant: AnalysisPackageWithVariant[];
|
||||
countryCode: string;
|
||||
}) {
|
||||
const analysisElementMedusaProductIds = getAnalysisElementMedusaProductIds(analysisPackagesWithVariant);
|
||||
if (analysisElementMedusaProductIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const analysisElementMedusaProductIds = getAnalysisElementMedusaProductIds(analysisPackages);
|
||||
if (analysisElementMedusaProductIds.length > 0) {
|
||||
const { response: { products } } = await listProducts({
|
||||
countryCode,
|
||||
queryParams: {
|
||||
@@ -46,9 +88,88 @@ async function analysisPackagesLoader() {
|
||||
limit: 100,
|
||||
},
|
||||
});
|
||||
analysisPackageElements = products;
|
||||
|
||||
const standardPackage = analysisPackagesWithVariant.find(({ isStandard }) => isStandard);
|
||||
const standardPlusPackage = analysisPackagesWithVariant.find(({ isStandardPlus }) => isStandardPlus);
|
||||
const premiumPackage = analysisPackagesWithVariant.find(({ isPremium }) => isPremium);
|
||||
if (!standardPackage || !standardPlusPackage || !premiumPackage) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return { analysisPackageElements, analysisPackages, countryCode };
|
||||
const standardPackageAnalyses = getAnalysisElementMedusaProductIds([standardPackage]);
|
||||
const standardPlusPackageAnalyses = getAnalysisElementMedusaProductIds([standardPlusPackage]);
|
||||
const premiumPackageAnalyses = getAnalysisElementMedusaProductIds([premiumPackage]);
|
||||
|
||||
return products.map(({ id, title, description }) => ({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
isIncludedInStandard: standardPackageAnalyses.includes(id),
|
||||
isIncludedInStandardPlus: standardPlusPackageAnalyses.includes(id),
|
||||
isIncludedInPremium: premiumPackageAnalyses.includes(id),
|
||||
}));
|
||||
}
|
||||
|
||||
async function analysisPackagesWithVariantLoader({
|
||||
account,
|
||||
countryCode,
|
||||
}: {
|
||||
account: AccountWithParams;
|
||||
countryCode: string;
|
||||
}) {
|
||||
const productTypes = await loadProductTypes();
|
||||
const productType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages');
|
||||
if (!productType) {
|
||||
return null;
|
||||
}
|
||||
const analysisPackagesResponse = await listProducts({
|
||||
countryCode,
|
||||
queryParams: { limit: 100, "type_id[0]": productType.id },
|
||||
});
|
||||
|
||||
const getVariant = userSpecificVariantLoader({ account });
|
||||
const analysisPackagesWithVariant = analysisPackagesResponse.response.products
|
||||
.reduce((acc, product) => {
|
||||
const variant = getVariant({ product });
|
||||
if (!variant) {
|
||||
return acc;
|
||||
}
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
variantId: variant.id,
|
||||
nrOfAnalyses: getAnalysisElementMedusaProductIds([product]).length,
|
||||
price: variant.calculated_price?.calculated_amount ?? 0,
|
||||
title: product.title,
|
||||
subtitle: product.subtitle,
|
||||
description: product.description,
|
||||
metadata: product.metadata,
|
||||
isStandard: product.metadata?.analysisPackageTier === 'standard',
|
||||
isStandardPlus: product.metadata?.analysisPackageTier === 'standard-plus',
|
||||
isPremium: product.metadata?.analysisPackageTier === 'premium',
|
||||
},
|
||||
];
|
||||
}, [] as AnalysisPackageWithVariant[]);
|
||||
|
||||
return analysisPackagesWithVariant;
|
||||
}
|
||||
|
||||
async function analysisPackagesLoader() {
|
||||
const account = await loadCurrentUserAccount();
|
||||
if (!account) {
|
||||
throw new Error('Account not found');
|
||||
}
|
||||
|
||||
const countryCodes = await loadCountryCodes();
|
||||
const countryCode = countryCodes[0]!;
|
||||
|
||||
const analysisPackagesWithVariant = await analysisPackagesWithVariantLoader({ account, countryCode });
|
||||
if (!analysisPackagesWithVariant) {
|
||||
return { analysisPackageElements: [], analysisPackages: [], countryCode };
|
||||
}
|
||||
|
||||
const analysisPackageElements = await analysisPackageElementsLoader({ analysisPackagesWithVariant, countryCode });
|
||||
|
||||
return { analysisPackageElements, analysisPackages: analysisPackagesWithVariant, countryCode };
|
||||
}
|
||||
export const loadAnalysisPackages = cache(analysisPackagesLoader);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use server'
|
||||
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
import { RequestStatus } from '@/lib/types/audit';
|
||||
import { ConnectedOnlineMethodName } from '@/lib/types/connected-online';
|
||||
import { ExternalApi } from '@/lib/types/external';
|
||||
import { MedipostAction } from '@/lib/types/medipost';
|
||||
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
||||
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
||||
|
||||
export default async function logRequestResult(
|
||||
/* personalCode: string, */ requestApi: keyof typeof ExternalApi,
|
||||
@@ -16,19 +16,7 @@ export default async function logRequestResult(
|
||||
serviceId?: number,
|
||||
serviceProviderId?: number,
|
||||
) {
|
||||
const supabaseServiceUser = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
||||
{
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { error } = await supabaseServiceUser
|
||||
const { error } = await getSupabaseServerClient()
|
||||
.schema('audit')
|
||||
.from('request_entries')
|
||||
.insert({
|
||||
@@ -46,3 +34,38 @@ export default async function logRequestResult(
|
||||
throw new Error('Failed to insert log entry, error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logMedipostDispatch({
|
||||
medusaOrderId,
|
||||
isSuccess,
|
||||
isMedipostError,
|
||||
errorMessage,
|
||||
}: {
|
||||
medusaOrderId: string;
|
||||
isSuccess: boolean;
|
||||
isMedipostError: boolean;
|
||||
errorMessage?: string;
|
||||
}) {
|
||||
const { error } = await getSupabaseServerAdminClient()
|
||||
.schema('audit')
|
||||
.from('medipost_dispatch')
|
||||
.insert({
|
||||
medusa_order_id: medusaOrderId,
|
||||
is_success: isSuccess,
|
||||
is_medipost_error: isMedipostError,
|
||||
error_message: errorMessage,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error('Failed to insert log entry, error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMedipostDispatchTries(medusaOrderId: string) {
|
||||
const { data } = await getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.rpc('get_medipost_dispatch_tries', { p_medusa_order_id: medusaOrderId })
|
||||
.throwOnError();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ import { StoreOrder } from '@medusajs/types';
|
||||
import { listProducts } from '@lib/data/products';
|
||||
import { listRegions } from '@lib/data/regions';
|
||||
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
|
||||
import { MedipostValidationError } from './medipost/MedipostValidationError';
|
||||
import { logMedipostDispatch } from './audit.service';
|
||||
|
||||
const BASE_URL = process.env.MEDIPOST_URL!;
|
||||
const USER = process.env.MEDIPOST_USER!;
|
||||
@@ -63,14 +65,14 @@ export async function validateMedipostResponse(response: string, { canHaveEmptyC
|
||||
if (canHaveEmptyCode) {
|
||||
if (code && code !== 0) {
|
||||
console.error("Bad response", response);
|
||||
throw new Error(`Medipost response is invalid`);
|
||||
throw new MedipostValidationError(response);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof code !== 'number' || (code !== 0 && !canHaveEmptyCode)) {
|
||||
console.error("Bad response", response);
|
||||
throw new Error(`Medipost response is invalid`);
|
||||
throw new MedipostValidationError(response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,28 +202,60 @@ export async function readPrivateMessageResponse({
|
||||
excludedMessageIds,
|
||||
}: {
|
||||
excludedMessageIds: string[];
|
||||
}) {
|
||||
let messageIdErrored: string | null = null;
|
||||
let messageIdProcessed: string | null = null;
|
||||
}): Promise<{ messageId: string | null; hasAnalysisResponse: boolean; hasPartialAnalysisResponse: boolean; hasFullAnalysisResponse: boolean; medusaOrderId: string | undefined }> {
|
||||
let messageId: string | null = null;
|
||||
let hasAnalysisResponse = false;
|
||||
let hasPartialAnalysisResponse = false;
|
||||
let hasFullAnalysisResponse = false;
|
||||
let medusaOrderId: string | undefined = undefined;
|
||||
|
||||
try {
|
||||
const privateMessage = await getLatestPrivateMessageListItem({ excludedMessageIds });
|
||||
if (!privateMessage) {
|
||||
throw new Error(`No private message found`);
|
||||
return {
|
||||
messageId: null,
|
||||
hasAnalysisResponse: false,
|
||||
hasPartialAnalysisResponse: false,
|
||||
hasFullAnalysisResponse: false,
|
||||
medusaOrderId: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
messageIdErrored = privateMessage.messageId;
|
||||
if (!messageIdErrored) {
|
||||
throw new Error(`No message id found`);
|
||||
messageId = privateMessage.messageId;
|
||||
if (!messageId) {
|
||||
return {
|
||||
messageId: null,
|
||||
hasAnalysisResponse: false,
|
||||
hasPartialAnalysisResponse: false,
|
||||
hasFullAnalysisResponse: false,
|
||||
medusaOrderId: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const privateMessageContent = await getPrivateMessage(
|
||||
privateMessage.messageId,
|
||||
);
|
||||
const messageResponse = privateMessageContent?.Saadetis?.Vastus;
|
||||
const medusaOrderId = privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId;
|
||||
medusaOrderId = privateMessageContent?.Saadetis?.Tellimus?.ValisTellimuseId || messageResponse?.ValisTellimuseId;
|
||||
|
||||
if (!medusaOrderId || !medusaOrderId.toString().startsWith('order_')) {
|
||||
return {
|
||||
messageId,
|
||||
hasAnalysisResponse: false,
|
||||
hasPartialAnalysisResponse: false,
|
||||
hasFullAnalysisResponse: false,
|
||||
medusaOrderId: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (!messageResponse) {
|
||||
throw new Error(`Private message response has no results yet for order=${medusaOrderId}`);
|
||||
return {
|
||||
messageId,
|
||||
hasAnalysisResponse: false,
|
||||
hasPartialAnalysisResponse: false,
|
||||
hasFullAnalysisResponse: false,
|
||||
medusaOrderId,
|
||||
};
|
||||
}
|
||||
|
||||
let order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
||||
@@ -236,17 +270,19 @@ export async function readPrivateMessageResponse({
|
||||
|
||||
if (status.isPartial) {
|
||||
await updateOrderStatus({ medusaOrderId, orderStatus: 'PARTIAL_ANALYSIS_RESPONSE' });
|
||||
messageIdProcessed = privateMessage.messageId;
|
||||
hasAnalysisResponse = true;
|
||||
hasPartialAnalysisResponse = true;
|
||||
} else if (status.isCompleted) {
|
||||
await updateOrderStatus({ medusaOrderId, orderStatus: 'FULL_ANALYSIS_RESPONSE' });
|
||||
await deletePrivateMessage(privateMessage.messageId);
|
||||
messageIdProcessed = privateMessage.messageId;
|
||||
hasAnalysisResponse = true;
|
||||
hasFullAnalysisResponse = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to process private message id=${messageIdErrored}, message=${(e as Error).message}`);
|
||||
console.warn(`Failed to process private message id=${messageId}, message=${(e as Error).message}`);
|
||||
}
|
||||
|
||||
return { messageIdErrored, messageIdProcessed };
|
||||
return { messageId, hasAnalysisResponse, hasPartialAnalysisResponse, hasFullAnalysisResponse, medusaOrderId };
|
||||
}
|
||||
|
||||
async function saveAnalysisGroup(
|
||||
@@ -705,7 +741,23 @@ export async function sendOrderToMedipost({
|
||||
comment: '',
|
||||
});
|
||||
|
||||
try {
|
||||
await sendPrivateMessage(orderXml);
|
||||
} catch (e) {
|
||||
const isMedipostError = e instanceof MedipostValidationError;
|
||||
await logMedipostDispatch({
|
||||
medusaOrderId,
|
||||
isSuccess: false,
|
||||
isMedipostError,
|
||||
errorMessage: isMedipostError ? e.response : undefined,
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
await logMedipostDispatch({
|
||||
medusaOrderId,
|
||||
isSuccess: true,
|
||||
isMedipostError: false,
|
||||
});
|
||||
await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' });
|
||||
}
|
||||
|
||||
|
||||
9
lib/services/medipost/MedipostValidationError.ts
Normal file
9
lib/services/medipost/MedipostValidationError.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export class MedipostValidationError extends Error {
|
||||
response: string;
|
||||
|
||||
constructor(response: string) {
|
||||
super('Invalid Medipost response');
|
||||
this.name = 'MedipostValidationError';
|
||||
this.response = response;
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export async function handleAddToCart({
|
||||
selectedVariant,
|
||||
countryCode,
|
||||
}: {
|
||||
selectedVariant: StoreProductVariant
|
||||
selectedVariant: Pick<StoreProductVariant, 'id'>
|
||||
countryCode: string
|
||||
}) {
|
||||
const supabase = getSupabaseServerClient();
|
||||
|
||||
@@ -99,7 +99,10 @@ export async function getOrder({
|
||||
throw new Error('Either medusaOrderId or orderId must be provided');
|
||||
}
|
||||
|
||||
const { data: order } = await query.single().throwOnError();
|
||||
const { data: order, error } = await query.single();
|
||||
if (error) {
|
||||
throw new Error(`Failed to get order by medusaOrderId=${medusaOrderId} or orderId=${orderId}, message=${error.message}, data=${JSON.stringify(order)}`);
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import { createAdminAccountsService } from './services/admin-accounts.service';
|
||||
import { createAdminAuthUserService } from './services/admin-auth-user.service';
|
||||
import { createCreateCompanyAccountService } from './services/admin-create-company-account.service';
|
||||
import { adminAction } from './utils/admin-action';
|
||||
import { getAdminSdk } from './utils/medusa-sdk';
|
||||
|
||||
/**
|
||||
* @name banUserAction
|
||||
@@ -138,7 +139,24 @@ export const deleteAccountAction = adminAction(
|
||||
|
||||
logger.info({ accountId }, `Super Admin is deleting account...`);
|
||||
|
||||
const { name: customerGroupName } = await service.getAccount(accountId);
|
||||
try {
|
||||
await service.deleteAccount(accountId);
|
||||
} catch (e) {
|
||||
logger.error({ accountId }, `Error deleting company account`);
|
||||
throw e;
|
||||
}
|
||||
const medusa = getAdminSdk();
|
||||
const { customer_groups } = await medusa.admin.customerGroup.list();
|
||||
const customerGroup = customer_groups.find(({ name }) => name === customerGroupName);
|
||||
if (customerGroup) {
|
||||
try {
|
||||
await medusa.admin.customerGroup.delete(customerGroup.id);
|
||||
} catch (e) {
|
||||
logger.error({ accountId }, `Error deleting Medusa customer group for company ${customerGroupName}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{ accountId },
|
||||
@@ -267,6 +285,40 @@ export const createCompanyAccountAction = enhanceAction(
|
||||
}
|
||||
|
||||
logger.info(ctx, `Company account created`);
|
||||
|
||||
logger.info(ctx, `Creating Medusa customer group`);
|
||||
const medusa = getAdminSdk();
|
||||
const { customer_groups: existingCustomerGroups } = await medusa.admin.customerGroup.list();
|
||||
const isExisting = existingCustomerGroups.find((group) => group.name === name);
|
||||
if (isExisting) {
|
||||
logger.info(ctx, `Customer group already exists`);
|
||||
} else {
|
||||
logger.info(ctx, `Creating Medusa customer group`);
|
||||
const { data: account } = await client
|
||||
.schema('medreport').from('accounts')
|
||||
.select('medusa_account_id')
|
||||
.eq('personal_code', ownerPersonalCode)
|
||||
.single().throwOnError();
|
||||
const medusaAccountId = account.medusa_account_id;
|
||||
if (!medusaAccountId) {
|
||||
logger.error(ctx, `User has no Medusa account ID`);
|
||||
} else {
|
||||
const { customer_group: { id: customerGroupId } } = await medusa.admin.customerGroup.create({ name });
|
||||
const { customers } = await medusa.admin.customer.list({
|
||||
id: medusaAccountId,
|
||||
});
|
||||
if (customers.length !== 1) {
|
||||
logger.error(ctx, `Customer not found`);
|
||||
} else {
|
||||
const customerId = customers[0]!.id;
|
||||
await medusa.admin.customer.batchCustomerGroups(customerId, {
|
||||
add: [customerGroupId],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
redirect(`/admin/accounts/${data.id}`);
|
||||
},
|
||||
{
|
||||
|
||||
@@ -37,4 +37,15 @@ class AdminAccountsService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getAccount(accountId: string) {
|
||||
const { data } = await this.adminClient
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.select('*')
|
||||
.eq('id', accountId)
|
||||
.single().throwOnError();
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
16
packages/features/admin/src/lib/server/utils/medusa-sdk.ts
Normal file
16
packages/features/admin/src/lib/server/utils/medusa-sdk.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import Medusa from "@medusajs/js-sdk"
|
||||
|
||||
export const getAdminSdk = () => {
|
||||
const medusaBackendUrl = process.env.MEDUSA_BACKEND_PUBLIC_URL!;
|
||||
const medusaPublishableApiKey = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!;
|
||||
const key = process.env.MEDUSA_SECRET_API_KEY!;
|
||||
|
||||
if (!medusaBackendUrl || !medusaPublishableApiKey) {
|
||||
throw new Error('Medusa environment variables not set');
|
||||
}
|
||||
return new Medusa({
|
||||
baseUrl: medusaBackendUrl,
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
apiKey: key,
|
||||
});
|
||||
}
|
||||
@@ -359,7 +359,7 @@ export async function getOtherResponses({
|
||||
}
|
||||
|
||||
export async function getAnalysisResultsForDoctor(
|
||||
id: number,
|
||||
analysisResponseId: number,
|
||||
): Promise<AnalysisResultDetails> {
|
||||
const supabase = getSupabaseServerClient();
|
||||
|
||||
@@ -370,7 +370,7 @@ export async function getAnalysisResultsForDoctor(
|
||||
`*,
|
||||
analysis_responses(user_id, analysis_order_id(id,medusa_order_id, analysis_element_ids))`,
|
||||
)
|
||||
.eq('analysis_response_id', id);
|
||||
.eq('analysis_response_id', analysisResponseId);
|
||||
|
||||
if (error) {
|
||||
throw new Error('Something went wrong.');
|
||||
|
||||
@@ -276,6 +276,12 @@ export async function medusaLoginOrRegister(credentials: {
|
||||
|
||||
const customerCacheTag = await getCacheTag("customers");
|
||||
revalidateTag(customerCacheTag);
|
||||
|
||||
const customer = await retrieveCustomer();
|
||||
if (!customer) {
|
||||
throw new Error("Customer not found");
|
||||
}
|
||||
return customer.id;
|
||||
} catch (error) {
|
||||
console.error("Failed to login customer, attempting to register", error);
|
||||
try {
|
||||
@@ -302,6 +308,12 @@ export async function medusaLoginOrRegister(credentials: {
|
||||
const customerCacheTag = await getCacheTag("customers");
|
||||
revalidateTag(customerCacheTag);
|
||||
await transferCart();
|
||||
|
||||
const customer = await retrieveCustomer();
|
||||
if (!customer) {
|
||||
throw new Error("Customer not found");
|
||||
}
|
||||
return customer.id;
|
||||
} catch (registerError) {
|
||||
throw medusaError(registerError);
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@ import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { StoreProduct, StoreProductVariant } from '@medusajs/types';
|
||||
import { StoreProduct } from '@medusajs/types';
|
||||
import { Button } from '@medusajs/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { handleAddToCart } from '../../../../lib/services/medusaCart.service';
|
||||
|
||||
import {
|
||||
Card,
|
||||
@@ -17,25 +18,24 @@ import {
|
||||
CardHeader,
|
||||
} from '@kit/ui/card';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { handleAddToCart } from '../../../../lib/services/medusaCart.service';
|
||||
import { getAnalysisElementMedusaProductIds } from '../../../../utils/medusa-product';
|
||||
import { PackageHeader } from './package-header';
|
||||
import { ButtonTooltip } from './ui/button-tooltip';
|
||||
import { PackageHeader } from './package-header';
|
||||
|
||||
export interface IAnalysisPackage {
|
||||
titleKey: string;
|
||||
export type AnalysisPackageWithVariant = Pick<StoreProduct, 'title' | 'description' | 'subtitle' | 'metadata'> & {
|
||||
variantId: string;
|
||||
nrOfAnalyses: number;
|
||||
price: number;
|
||||
tagColor: string;
|
||||
descriptionKey: string;
|
||||
}
|
||||
isStandard: boolean;
|
||||
isStandardPlus: boolean;
|
||||
isPremium: boolean;
|
||||
};
|
||||
|
||||
export default function SelectAnalysisPackage({
|
||||
analysisPackage,
|
||||
countryCode,
|
||||
}: {
|
||||
analysisPackage: StoreProduct;
|
||||
countryCode: string;
|
||||
analysisPackage: AnalysisPackageWithVariant;
|
||||
countryCode: string,
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const {
|
||||
@@ -44,35 +44,21 @@ export default function SelectAnalysisPackage({
|
||||
} = useTranslation();
|
||||
|
||||
const [isAddingToCart, setIsAddingToCart] = useState(false);
|
||||
const handleSelect = async (selectedVariant: StoreProductVariant) => {
|
||||
if (!selectedVariant?.id) return null;
|
||||
|
||||
const { nrOfAnalyses, variantId, title, subtitle = '', description = '', price } = analysisPackage;
|
||||
|
||||
const handleSelect = async () => {
|
||||
setIsAddingToCart(true);
|
||||
await handleAddToCart({
|
||||
selectedVariant,
|
||||
selectedVariant: { id: variantId },
|
||||
countryCode,
|
||||
});
|
||||
setIsAddingToCart(false);
|
||||
router.push('/home/cart');
|
||||
};
|
||||
|
||||
const titleKey = analysisPackage.title;
|
||||
const analysisElementMedusaProductIds = getAnalysisElementMedusaProductIds([
|
||||
analysisPackage,
|
||||
]);
|
||||
const nrOfAnalyses = analysisElementMedusaProductIds.length;
|
||||
const description = analysisPackage.description ?? '';
|
||||
const subtitle = analysisPackage.subtitle ?? '';
|
||||
const variant = analysisPackage.variants?.[0];
|
||||
|
||||
if (!variant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const price = variant.calculated_price?.calculated_amount ?? 0;
|
||||
|
||||
return (
|
||||
<Card key={titleKey}>
|
||||
<Card key={title}>
|
||||
<CardHeader className="relative">
|
||||
{description && (
|
||||
<ButtonTooltip
|
||||
@@ -90,8 +76,8 @@ export default function SelectAnalysisPackage({
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1 text-center">
|
||||
<PackageHeader
|
||||
title={t(titleKey)}
|
||||
tagColor="bg-cyan"
|
||||
title={title}
|
||||
tagColor='bg-cyan'
|
||||
analysesNr={t('product:nrOfAnalyses', { nr: nrOfAnalyses })}
|
||||
language={language}
|
||||
price={price}
|
||||
@@ -99,14 +85,8 @@ export default function SelectAnalysisPackage({
|
||||
<CardDescription>{subtitle}</CardDescription>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
className="w-full text-[10px] sm:text-sm"
|
||||
onClick={() => handleSelect(variant)}
|
||||
isLoading={isAddingToCart}
|
||||
>
|
||||
{!isAddingToCart && (
|
||||
<Trans i18nKey="order-analysis-package:selectThisPackage" />
|
||||
)}
|
||||
<Button className="w-full text-[10px] sm:text-sm" onClick={handleSelect} isLoading={isAddingToCart}>
|
||||
{!isAddingToCart && <Trans i18nKey='order-analysis-package:selectThisPackage' />}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { StoreProduct } from '@medusajs/types';
|
||||
|
||||
import SelectAnalysisPackage from './select-analysis-package';
|
||||
import SelectAnalysisPackage, { AnalysisPackageWithVariant } from './select-analysis-package';
|
||||
|
||||
export default function SelectAnalysisPackages({ analysisPackages, countryCode }: { analysisPackages: StoreProduct[], countryCode: string }) {
|
||||
export default function SelectAnalysisPackages({
|
||||
analysisPackages,
|
||||
countryCode,
|
||||
}: {
|
||||
analysisPackages: AnalysisPackageWithVariant[];
|
||||
countryCode: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
{analysisPackages.length > 0 ? analysisPackages.map(
|
||||
(product) => (
|
||||
<SelectAnalysisPackage key={product.title} analysisPackage={product} countryCode={countryCode} />
|
||||
(analysisPackage) => (
|
||||
<SelectAnalysisPackage key={analysisPackage.title} analysisPackage={analysisPackage} countryCode={countryCode} />
|
||||
)) : (
|
||||
<h4>
|
||||
<Trans i18nKey="order-analysis-package:noPackagesAvailable" />
|
||||
|
||||
@@ -201,6 +201,28 @@ export type Database = {
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
medusa_action: {
|
||||
Row: {
|
||||
id: number
|
||||
medusa_user_id: string
|
||||
user_email: string
|
||||
action: string
|
||||
page: string
|
||||
created_at: string
|
||||
}
|
||||
Insert: {
|
||||
medusa_user_id: string
|
||||
user_email: string
|
||||
action: string
|
||||
page: string
|
||||
}
|
||||
Update: {
|
||||
medusa_user_id?: string
|
||||
user_email?: string
|
||||
action?: string
|
||||
page?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
@@ -314,6 +336,7 @@ export type Database = {
|
||||
primary_owner_user_id: string
|
||||
public_data: Json
|
||||
slug: string | null
|
||||
medusa_account_id: string | null
|
||||
updated_at: string | null
|
||||
updated_by: string | null
|
||||
}
|
||||
@@ -335,6 +358,7 @@ export type Database = {
|
||||
primary_owner_user_id?: string
|
||||
public_data?: Json
|
||||
slug?: string | null
|
||||
medusa_account_id?: string | null
|
||||
updated_at?: string | null
|
||||
updated_by?: string | null
|
||||
}
|
||||
@@ -356,6 +380,7 @@ export type Database = {
|
||||
primary_owner_user_id?: string
|
||||
public_data?: Json
|
||||
slug?: string | null
|
||||
medusa_account_id?: string | null
|
||||
updated_at?: string | null
|
||||
updated_by?: string | null
|
||||
}
|
||||
@@ -2032,6 +2057,21 @@ export type Database = {
|
||||
}
|
||||
Returns: Json
|
||||
}
|
||||
medipost_retry_dispatch: {
|
||||
Args: {
|
||||
order_id: string
|
||||
}
|
||||
Returns: {
|
||||
success: boolean
|
||||
error: string | null
|
||||
}
|
||||
}
|
||||
get_medipost_dispatch_tries: {
|
||||
Args: {
|
||||
p_medusa_order_id: string
|
||||
}
|
||||
Returns: number
|
||||
}
|
||||
}
|
||||
Enums: {
|
||||
analysis_feedback_status: "STARTED" | "DRAFT" | "COMPLETED"
|
||||
|
||||
@@ -20,15 +20,20 @@ export function useSignInWithEmailPassword() {
|
||||
const identities = user?.identities ?? [];
|
||||
|
||||
if (identities.length === 0) {
|
||||
throw new Error('User already registered');
|
||||
throw new Error('Invalid user');
|
||||
}
|
||||
|
||||
if ('email' in credentials) {
|
||||
try {
|
||||
await medusaLoginOrRegister({
|
||||
const medusaAccountId = await medusaLoginOrRegister({
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
});
|
||||
await client
|
||||
.schema('medreport').from('accounts')
|
||||
.update({ medusa_account_id: medusaAccountId })
|
||||
.eq('primary_owner_user_id', user.id)
|
||||
.eq('is_personal_account', true);
|
||||
} catch (error) {
|
||||
await client.auth.signOut();
|
||||
throw error;
|
||||
|
||||
@@ -40,10 +40,15 @@ export function useSignUpWithEmailAndPassword() {
|
||||
|
||||
if ('email' in credentials) {
|
||||
try {
|
||||
await medusaLoginOrRegister({
|
||||
const medusaAccountId = await medusaLoginOrRegister({
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
});
|
||||
await client
|
||||
.schema('medreport').from('accounts')
|
||||
.update({ medusa_account_id: medusaAccountId })
|
||||
.eq('primary_owner_user_id', user!.id)
|
||||
.eq('is_personal_account', true);
|
||||
} catch (error) {
|
||||
await client.auth.signOut();
|
||||
throw error;
|
||||
|
||||
@@ -30,10 +30,10 @@
|
||||
"placeholder": "Enter promotion code"
|
||||
},
|
||||
"items": {
|
||||
"analysisPackages": {
|
||||
"productColumnLabel": "Package name"
|
||||
"synlabAnalyses": {
|
||||
"productColumnLabel": "Analysis name"
|
||||
},
|
||||
"services": {
|
||||
"ttoServices": {
|
||||
"productColumnLabel": "Service name"
|
||||
},
|
||||
"delete": {
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
"ON_HOLD": "Waiting for analysis results",
|
||||
"PROCESSING": "In progress",
|
||||
"PARTIAL_ANALYSIS_RESPONSE": "Partial analysis response",
|
||||
"FULL_ANALYSIS_RESPONSE": "All analysis responses",
|
||||
"WAITING_FOR_DOCTOR_RESPONSE": "Waiting for doctor response",
|
||||
"FULL_ANALYSIS_RESPONSE": "All analysis responses received, waiting for doctor response",
|
||||
"COMPLETED": "Completed",
|
||||
"REJECTED": "Rejected",
|
||||
"CANCELLED": "Cancelled"
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
"placeholder": "Sisesta promo kood"
|
||||
},
|
||||
"items": {
|
||||
"analysisPackages": {
|
||||
"productColumnLabel": "Paketi nimi"
|
||||
"synlabAnalyses": {
|
||||
"productColumnLabel": "Analüüsi nimi"
|
||||
},
|
||||
"services": {
|
||||
"ttoServices": {
|
||||
"productColumnLabel": "Teenuse nimi"
|
||||
},
|
||||
"delete": {
|
||||
|
||||
@@ -12,9 +12,8 @@
|
||||
"ON_HOLD": "Makstud",
|
||||
"PROCESSING": "Synlabile edastatud",
|
||||
"PARTIAL_ANALYSIS_RESPONSE": "Osalised tulemused",
|
||||
"FULL_ANALYSIS_RESPONSE": "Kõik tulemused käes",
|
||||
"WAITING_FOR_DOCTOR_RESPONSE": "Ootab arsti kokkuvõtet",
|
||||
"COMPLETED": "Lõplikud tulemused",
|
||||
"FULL_ANALYSIS_RESPONSE": "Kõik tulemused käes, ootab arsti kokkuvõtet",
|
||||
"COMPLETED": "Kinnitatud",
|
||||
"REJECTED": "Tagastatud",
|
||||
"CANCELLED": "Tühistatud"
|
||||
}
|
||||
|
||||
1
supabase/migrations/20250825065821_medusa_account_id.sql
Normal file
1
supabase/migrations/20250825065821_medusa_account_id.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE medreport.accounts ADD COLUMN medusa_account_id TEXT;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- user_id+account_id primary key -> separate id column for audit
|
||||
ALTER TABLE medreport.accounts_memberships DROP CONSTRAINT accounts_memberships_pkey;
|
||||
ALTER TABLE medreport.accounts_memberships ADD COLUMN id UUID DEFAULT gen_random_uuid();
|
||||
ALTER TABLE medreport.accounts_memberships ADD CONSTRAINT accounts_memberships_pkey PRIMARY KEY (id);
|
||||
ALTER TABLE medreport.accounts_memberships ADD CONSTRAINT unique_user_account UNIQUE (user_id, account_id);
|
||||
19
supabase/migrations/20250825081751_medusa_audit_logs.sql
Normal file
19
supabase/migrations/20250825081751_medusa_audit_logs.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
create table "audit"."medusa_action" (
|
||||
"id" bigint generated by default as identity not null,
|
||||
"medusa_user_id" text not null,
|
||||
"user_email" text not null,
|
||||
"action" text not null,
|
||||
"page" text,
|
||||
"created_at" timestamp with time zone not null default now()
|
||||
);
|
||||
|
||||
grant usage on schema audit to authenticated;
|
||||
grant select, insert, update, delete on table audit.medusa_action to authenticated;
|
||||
|
||||
alter table "audit"."medusa_action" enable row level security;
|
||||
|
||||
create policy "service_role_select" on "audit"."medusa_action" for select to service_role using (true);
|
||||
create policy "service_role_insert" on "audit"."medusa_action" for insert to service_role with check (true);
|
||||
create policy "service_role_update" on "audit"."medusa_action" for update to service_role using (true);
|
||||
create policy "service_role_delete" on "audit"."medusa_action" for delete to service_role using (true);
|
||||
grant select, insert, update, delete on table audit.medusa_action to service_role;
|
||||
@@ -0,0 +1,36 @@
|
||||
CREATE TABLE "audit"."medipost_dispatch" (
|
||||
"id" bigint generated by default as identity not null,
|
||||
"medusa_order_id" text not null,
|
||||
"is_medipost_error" boolean not null,
|
||||
"is_success" boolean not null,
|
||||
"error_message" text,
|
||||
"created_at" timestamp with time zone not null default now(),
|
||||
"changed_by" uuid default auth.uid()
|
||||
);
|
||||
|
||||
grant usage on schema audit to authenticated;
|
||||
grant select, insert, update, delete on table audit.medipost_dispatch to authenticated;
|
||||
grant usage on schema medreport to service_role;
|
||||
|
||||
alter table "audit"."medipost_dispatch" enable row level security;
|
||||
|
||||
create policy "service_role_select" on "audit"."medipost_dispatch" for select to service_role using (true);
|
||||
create policy "service_role_insert" on "audit"."medipost_dispatch" for insert to service_role with check (true);
|
||||
create policy "service_role_update" on "audit"."medipost_dispatch" for update to service_role using (true);
|
||||
create policy "service_role_delete" on "audit"."medipost_dispatch" for delete to service_role using (true);
|
||||
|
||||
CREATE OR REPLACE FUNCTION medreport.get_medipost_dispatch_tries(p_medusa_order_id text)
|
||||
returns integer
|
||||
language plpgsql
|
||||
security definer
|
||||
as $function$
|
||||
declare
|
||||
tries integer;
|
||||
begin
|
||||
select count(*) from audit.medipost_dispatch m where m.medusa_order_id = p_medusa_order_id and m.created_at > now() - interval '1 day' and m.is_success = false into tries;
|
||||
return tries;
|
||||
end;
|
||||
$function$;
|
||||
|
||||
grant execute on function medreport.get_medipost_dispatch_tries(text) to service_role;
|
||||
grant select, insert, update, delete on table audit.medipost_dispatch to service_role;
|
||||
@@ -0,0 +1,42 @@
|
||||
create extension if not exists pg_net;
|
||||
|
||||
create or replace function medreport.medipost_retry_dispatch(
|
||||
order_id text
|
||||
)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
as $function$
|
||||
declare
|
||||
response_result record;
|
||||
begin
|
||||
select into response_result
|
||||
net.http_post(
|
||||
url := 'https://test.medreport.ee/api/job/medipost-retry-dispatch',
|
||||
headers := jsonb_build_object(
|
||||
'Content-Type', 'application/json',
|
||||
'x-jobs-api-key', 'fd26ec26-70ed-11f0-9e95-431ac3b15a84'
|
||||
),
|
||||
body := jsonb_build_object(
|
||||
'medusaOrderId', order_id
|
||||
)::text
|
||||
) as request_id;
|
||||
|
||||
return jsonb_build_object(
|
||||
'success', true
|
||||
);
|
||||
|
||||
exception
|
||||
when others then
|
||||
return jsonb_build_object(
|
||||
'success', false
|
||||
);
|
||||
end;
|
||||
$function$;
|
||||
|
||||
grant execute on function medreport.medipost_retry_dispatch(text) to service_role;
|
||||
|
||||
comment on function medreport.medipost_retry_dispatch(text) is
|
||||
'Manually trigger a medipost retry dispatch for a specific order ID.
|
||||
Parameters:
|
||||
- order_id: The medusa order ID to retry dispatch for
|
||||
Returns: JSONB with success status and request details';
|
||||
@@ -0,0 +1,25 @@
|
||||
CREATE OR REPLACE FUNCTION medreport.get_order_possible_actions(p_medusa_order_id text)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
order_status text;
|
||||
is_queued boolean;
|
||||
BEGIN
|
||||
-- Get the analysis order status
|
||||
SELECT status INTO order_status
|
||||
FROM medreport.analysis_orders
|
||||
WHERE medusa_order_id = p_medusa_order_id;
|
||||
|
||||
-- Check if status is QUEUED
|
||||
is_queued := (order_status = 'QUEUED');
|
||||
|
||||
-- Return JSON object with actions and their allowed status
|
||||
RETURN jsonb_build_object(
|
||||
'retry_dispatch', is_queued,
|
||||
'mark_as_not_received_by_synlab', is_queued
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
grant execute on function medreport.get_order_possible_actions(text) to service_role;
|
||||
@@ -1,4 +1,8 @@
|
||||
export const getAnalysisElementMedusaProductIds = (products: ({ metadata?: { analysisElementMedusaProductIds?: string } | null } | null)[]) => {
|
||||
export const getAnalysisElementMedusaProductIds = (products: ({
|
||||
metadata?: {
|
||||
analysisElementMedusaProductIds?: string;
|
||||
} | null;
|
||||
} | null)[]) => {
|
||||
if (!products) {
|
||||
return [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user