Merge branch 'develop' into feature/MED-129

This commit is contained in:
Danel Kungla
2025-09-24 15:00:27 +03:00
622 changed files with 19603 additions and 10824 deletions

View File

@@ -1,6 +1,5 @@
import { cache } from 'react';
import { createAccountsApi } from '@kit/accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';

View File

@@ -1,7 +1,7 @@
import { cache } from 'react';
import { getProductCategories } from '@lib/data/categories';
import { listProducts, listProductTypes } from '@lib/data/products';
import { listProductTypes, listProducts } from '@lib/data/products';
import { listRegions } from '@lib/data/regions';
import { OrderAnalysisCard } from '../../_components/order-analyses-cards';
@@ -40,29 +40,32 @@ async function analysesLoader() {
);
const categoryProducts = category
? await listProducts({
countryCode,
queryParams: { limit: 100, category_id: category.id, order: 'title' },
})
countryCode,
queryParams: { limit: 100, category_id: category.id, order: 'title' },
})
: null;
return {
analyses:
categoryProducts?.response.products
.filter(({ status, metadata }) => status === 'published' && !!metadata?.analysisIdOriginal)
.map<OrderAnalysisCard>(
({ title, description, subtitle, variants }) => {
const variant = variants![0]!;
return {
title,
description,
subtitle,
variant: {
id: variant.id,
},
price: variant.calculated_price?.calculated_amount ?? null,
};
},
) ?? [],
.filter(
({ status, metadata }) =>
status === 'published' && !!metadata?.analysisIdOriginal,
)
.map<OrderAnalysisCard>(
({ title, description, subtitle, variants }) => {
const variant = variants![0]!;
return {
title,
description,
subtitle,
variant: {
id: variant.id,
},
price: variant.calculated_price?.calculated_amount ?? null,
};
},
) ?? [],
countryCode,
};
}

View File

@@ -1,14 +1,17 @@
import { cache } from 'react';
import { listProductTypes, listProducts } from "@lib/data/products";
import { listRegions } from '@lib/data/regions';
import { getAnalysisElementMedusaProductIds } from '@/utils/medusa-product';
import { listProductTypes, listProducts } from '@lib/data/products';
import { listRegions } from '@lib/data/regions';
import type { StoreProduct } from '@medusajs/types';
import { loadCurrentUserAccount } from './load-user-account';
import type { AccountWithParams } from '@kit/accounts/types/accounts';
import type { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
import PersonalCode from '~/lib/utils';
import { loadCurrentUserAccount } from './load-user-account';
async function countryCodesLoader() {
const countryCodes = await listRegions().then((regions) =>
regions?.map((r) => r.countries?.map((c) => c.iso_2)).flat(),
@@ -33,24 +36,25 @@ function userSpecificVariantLoader({
throw new Error('Personal code not found');
}
const { ageRange, gender: { value: gender } } = PersonalCode.parsePersonalCode(personalCode);
const {
ageRange,
gender: { value: gender },
} = PersonalCode.parsePersonalCode(personalCode);
return ({
product,
}: {
product: StoreProduct;
}) => {
return ({ product }: { product: StoreProduct }) => {
const variants = product.variants;
if (!variants) {
return null;
}
const variant = variants.find((v) => v.options?.every((o) => [ageRange, gender].includes(o.value)));
const variant = variants.find((v) =>
v.options?.every((o) => [ageRange, gender].includes(o.value)),
);
if (!variant) {
return null;
}
return variant;
}
};
}
async function analysisPackageElementsLoader({
@@ -60,30 +64,46 @@ async function analysisPackageElementsLoader({
analysisPackagesWithVariant: AnalysisPackageWithVariant[];
countryCode: string;
}) {
const analysisElementMedusaProductIds = getAnalysisElementMedusaProductIds(analysisPackagesWithVariant);
const analysisElementMedusaProductIds = getAnalysisElementMedusaProductIds(
analysisPackagesWithVariant,
);
if (analysisElementMedusaProductIds.length === 0) {
return [];
}
const { response: { products } } = await listProducts({
const {
response: { products },
} = await listProducts({
countryCode,
queryParams: {
id: analysisElementMedusaProductIds,
limit: 100,
order: "title",
order: 'title',
},
});
const standardPackage = analysisPackagesWithVariant.find(({ isStandard }) => isStandard);
const standardPlusPackage = analysisPackagesWithVariant.find(({ isStandardPlus }) => isStandardPlus);
const premiumPackage = analysisPackagesWithVariant.find(({ isPremium }) => isPremium);
const standardPackage = analysisPackagesWithVariant.find(
({ isStandard }) => isStandard,
);
const standardPlusPackage = analysisPackagesWithVariant.find(
({ isStandardPlus }) => isStandardPlus,
);
const premiumPackage = analysisPackagesWithVariant.find(
({ isPremium }) => isPremium,
);
if (!standardPackage || !standardPlusPackage || !premiumPackage) {
return [];
}
const standardPackageAnalyses = getAnalysisElementMedusaProductIds([standardPackage]);
const standardPlusPackageAnalyses = getAnalysisElementMedusaProductIds([standardPlusPackage]);
const premiumPackageAnalyses = getAnalysisElementMedusaProductIds([premiumPackage]);
const standardPackageAnalyses = getAnalysisElementMedusaProductIds([
standardPackage,
]);
const standardPlusPackageAnalyses = getAnalysisElementMedusaProductIds([
standardPlusPackage,
]);
const premiumPackageAnalyses = getAnalysisElementMedusaProductIds([
premiumPackage,
]);
return products.map(({ id, title, description }) => ({
id,
@@ -103,18 +123,20 @@ async function analysisPackagesWithVariantLoader({
countryCode: string;
}) {
const productTypes = await loadProductTypes();
const productType = productTypes.find(({ metadata }) => metadata?.handle === 'analysis-packages');
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 },
queryParams: { limit: 100, 'type_id[0]': productType.id },
});
const getVariant = userSpecificVariantLoader({ account });
const analysisPackagesWithVariant = analysisPackagesResponse.response.products
.reduce((acc, product) => {
const analysisPackagesWithVariant =
analysisPackagesResponse.response.products.reduce((acc, product) => {
const variant = getVariant({ product });
if (!variant) {
return acc;
@@ -124,14 +146,17 @@ async function analysisPackagesWithVariantLoader({
{
variant,
variantId: variant.id,
nrOfAnalyses: getAnalysisElementMedusaProductIds([{ ...product, variant }]).length,
nrOfAnalyses: getAnalysisElementMedusaProductIds([
{ ...product, variant },
]).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',
isStandardPlus:
product.metadata?.analysisPackageTier === 'standard-plus',
isPremium: product.metadata?.analysisPackageTier === 'premium',
},
];
@@ -149,13 +174,23 @@ async function analysisPackagesLoader() {
const countryCodes = await loadCountryCodes();
const countryCode = countryCodes[0]!;
const analysisPackagesWithVariant = await analysisPackagesWithVariantLoader({ account, countryCode });
const analysisPackagesWithVariant = await analysisPackagesWithVariantLoader({
account,
countryCode,
});
if (!analysisPackagesWithVariant) {
return { analysisPackageElements: [], analysisPackages: [], countryCode };
}
const analysisPackageElements = await analysisPackageElementsLoader({ analysisPackagesWithVariant, countryCode });
const analysisPackageElements = await analysisPackageElementsLoader({
analysisPackagesWithVariant,
countryCode,
});
return { analysisPackageElements, analysisPackages: analysisPackagesWithVariant, countryCode };
return {
analysisPackageElements,
analysisPackages: analysisPackagesWithVariant,
countryCode,
};
}
export const loadAnalysisPackages = cache(analysisPackagesLoader);

View File

@@ -0,0 +1,140 @@
import { cache } from 'react';
import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts';
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
import { Database } from '@/packages/supabase/src/database.types';
import OpenAI from 'openai';
import PersonalCode from '~/lib/utils';
import { OrderAnalysisCard } from '../../_components/order-analyses-cards';
export const loadRecommendations = cache(recommendationsLoader);
type AnalysisResponses =
Database['medreport']['Functions']['get_latest_analysis_response_elements_for_current_user']['Returns'];
const getLatestResponseTime = (items: AnalysisResponses) => {
if (!items?.length) return null;
let latest = null;
for (const it of items) {
const d = new Date(it.response_time);
const t = d.getTime();
if (!Number.isNaN(t) && (latest === null || t > latest.getTime())) {
latest = d;
}
}
return latest;
};
async function recommendationsLoader(
analyses: OrderAnalysisCard[],
account: AccountWithParams | null,
): Promise<string[]> {
if (!process.env.OPENAI_API_KEY) {
return [];
}
if (!account?.personal_code) {
return [];
}
const supabaseClient = getSupabaseServerClient();
const userAnalysesApi = createUserAnalysesApi(supabaseClient);
const analysisResponses = await userAnalysesApi.getAllUserAnalysisResponses();
const analysesRecommendationsPromptId =
process.env.PROMPT_ID_ANALYSIS_RECOMMENDATIONS;
const latestResponseTime = getLatestResponseTime(analysisResponses);
const latestISO = latestResponseTime
? new Date(latestResponseTime).toISOString()
: new Date('2025').toISOString();
if (!analysesRecommendationsPromptId) {
console.error('No prompt ID for analysis recommendations');
return [];
}
const previouslyRecommended = await supabaseClient
.schema('medreport')
.from('ai_responses')
.select('*')
.eq('account_id', account.id)
.eq('prompt_id', analysesRecommendationsPromptId)
.eq('latest_data_change', latestISO);
if (previouslyRecommended.data?.[0]?.response) {
return JSON.parse(previouslyRecommended.data[0].response as string)
.recommended;
}
const openAIClient = new OpenAI();
const { gender, age } = PersonalCode.parsePersonalCode(account.personal_code);
const weight = account.accountParams?.weight || 'unknown';
const formattedAnalysisResponses = analysisResponses.map(
({
analysis_name_lab,
response_value,
norm_upper,
norm_lower,
norm_status,
}) => ({
name: analysis_name_lab,
value: response_value,
normUpper: norm_upper,
normLower: norm_lower,
normStatus: norm_status,
}),
);
const formattedAnalyses = analyses.map(({ description, title }) => ({
description,
title,
}));
let response;
try {
response = await openAIClient.responses.create({
store: false,
prompt: {
id: analysesRecommendationsPromptId,
variables: {
analyses: JSON.stringify(formattedAnalyses),
results: JSON.stringify(formattedAnalysisResponses),
gender: gender.value,
age: age.toString(),
weight: weight.toString(),
},
},
});
} catch (error) {
console.error('Error calling OpenAI: ', error);
return [];
}
const json = JSON.parse(response.output_text);
try {
await supabaseClient
.schema('medreport')
.from('ai_responses')
.insert({
account_id: account.id,
prompt_name: 'Analysis Recommendations',
prompt_id: analysesRecommendationsPromptId,
input: JSON.stringify({
analyses: formattedAnalyses,
results: formattedAnalysisResponses,
gender,
age,
weight,
}),
latest_data_change: latestISO,
response: response.output_text,
});
} catch (error) {
console.error('Error saving AI response: ', error);
}
return json.recommended;
}

View File

@@ -1,8 +1,9 @@
import { cache } from 'react';
import { requireUserInServerComponent } from '@/lib/server/require-user-in-server-component';
import { createAccountsApi } from '@kit/accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { requireUserInServerComponent } from '@/lib/server/require-user-in-server-component';
export type UserAccount = Awaited<ReturnType<typeof loadUserAccount>>;

View File

@@ -1,8 +1,9 @@
import { cache } from 'react';
import type { AnalysisResultDetailsMapped } from '@/packages/features/user-analyses/src/types/analysis-results';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
import type { AnalysisResultDetailsMapped } from '@/packages/features/user-analyses/src/types/analysis-results';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export type UserAnalyses = Awaited<ReturnType<typeof loadUserAnalysis>>;

View File

@@ -1,6 +1,6 @@
"use server";
'use server';
import { retrieveCart, updateCart, updateLineItem } from "@lib/data/cart";
import { retrieveCart, updateCart, updateLineItem } from '@lib/data/cart';
export const updateCartPartnerLocation = async ({
cartId,
@@ -15,7 +15,7 @@ export const updateCartPartnerLocation = async ({
}) => {
const cart = await retrieveCart(cartId);
if (!cart) {
throw new Error("Cart not found");
throw new Error('Cart not found');
}
for (const lineItemId of lineIds) {
@@ -35,4 +35,4 @@ export const updateCartPartnerLocation = async ({
partner_location_id: partnerLocationId,
},
});
}
};