Merge pull request #38 from MR-medreport/MED-48
feat(MED-48 MED-100): update products -> cart -> montonio -> orders flow, send email
This commit is contained in:
@@ -30,7 +30,7 @@ export class MontonioOrderHandlerService {
|
||||
locale: string;
|
||||
merchantReference: string;
|
||||
}) {
|
||||
const token = jwt.sign({
|
||||
const params = {
|
||||
accessKey,
|
||||
description,
|
||||
currency,
|
||||
@@ -38,16 +38,17 @@ export class MontonioOrderHandlerService {
|
||||
locale,
|
||||
// 15 minutes
|
||||
expiresAt: new Date(Date.now() + 15 * 60 * 1000).toISOString(),
|
||||
notificationUrl,
|
||||
returnUrl,
|
||||
notificationUrl: notificationUrl.replace("localhost", "webhook.site"),
|
||||
returnUrl: returnUrl.replace("localhost", "webhook.site"),
|
||||
askAdditionalInfo: false,
|
||||
merchantReference,
|
||||
type: "one_time",
|
||||
}, secretKey, {
|
||||
};
|
||||
const token = jwt.sign(params, secretKey, {
|
||||
algorithm: "HS256",
|
||||
expiresIn: "10m",
|
||||
});
|
||||
|
||||
|
||||
try {
|
||||
const { data } = await axios.post(`${apiUrl}/api/payment-links`, { data: token });
|
||||
return data.url;
|
||||
|
||||
22
packages/email-templates/src/components/common-footer.tsx
Normal file
22
packages/email-templates/src/components/common-footer.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { TFunction } from "i18next";
|
||||
import { Text } from "@react-email/components";
|
||||
import { EmailFooter } from "./footer";
|
||||
|
||||
export default function CommonFooter({ t }: { t: TFunction }) {
|
||||
const namespace = 'common';
|
||||
|
||||
const lines = [
|
||||
t(`${namespace}:footer.lines1`),
|
||||
t(`${namespace}:footer.lines2`),
|
||||
t(`${namespace}:footer.lines3`),
|
||||
t(`${namespace}:footer.lines4`),
|
||||
];
|
||||
|
||||
return (
|
||||
<EmailFooter>
|
||||
{lines.map((line, index) => (
|
||||
<Text key={index} className="text-[16px] leading-[24px] text-[#242424]" dangerouslySetInnerHTML={{ __html: line }} />
|
||||
))}
|
||||
</EmailFooter>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Container, Text } from '@react-email/components';
|
||||
|
||||
export function EmailFooter(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<Container>
|
||||
<Container className="mt-[24px]">
|
||||
<Text className="px-4 text-[12px] leading-[20px] text-gray-300">
|
||||
{props.children}
|
||||
</Text>
|
||||
|
||||
99
packages/email-templates/src/emails/synlab.email.tsx
Normal file
99
packages/email-templates/src/emails/synlab.email.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
Body,
|
||||
Head,
|
||||
Html,
|
||||
Preview,
|
||||
Tailwind,
|
||||
Text,
|
||||
render,
|
||||
} from '@react-email/components';
|
||||
|
||||
import { BodyStyle } from '../components/body-style';
|
||||
import { EmailContent } from '../components/content';
|
||||
import { EmailHeader } from '../components/header';
|
||||
import { EmailHeading } from '../components/heading';
|
||||
import { EmailWrapper } from '../components/wrapper';
|
||||
import { initializeEmailI18n } from '../lib/i18n';
|
||||
import CommonFooter from '../components/common-footer';
|
||||
|
||||
interface Props {
|
||||
analysisPackageName: string;
|
||||
language?: string;
|
||||
personName: string;
|
||||
partnerLocationName: string;
|
||||
}
|
||||
|
||||
export async function renderSynlabAnalysisPackageEmail(props: Props) {
|
||||
const namespace = 'synlab-email';
|
||||
|
||||
const { t } = await initializeEmailI18n({
|
||||
language: props.language,
|
||||
namespace: [namespace, 'common'],
|
||||
});
|
||||
|
||||
const previewText = t(`${namespace}:previewText`);
|
||||
const subject = t(`${namespace}:subject`, {
|
||||
analysisPackageName: props.analysisPackageName,
|
||||
});
|
||||
|
||||
const heading = t(`${namespace}:heading`, {
|
||||
analysisPackageName: props.analysisPackageName,
|
||||
});
|
||||
|
||||
const hello = t(`${namespace}:hello`, {
|
||||
personName: props.personName,
|
||||
});
|
||||
|
||||
const lines = [
|
||||
t(`${namespace}:lines1`, {
|
||||
analysisPackageName: props.analysisPackageName,
|
||||
partnerLocationName: props.partnerLocationName,
|
||||
}),
|
||||
t(`${namespace}:lines2`),
|
||||
t(`${namespace}:lines3`),
|
||||
t(`${namespace}:lines4`),
|
||||
t(`${namespace}:lines5`),
|
||||
t(`${namespace}:lines6`),
|
||||
];
|
||||
|
||||
const html = await render(
|
||||
<Html>
|
||||
<Head>
|
||||
<BodyStyle />
|
||||
</Head>
|
||||
|
||||
<Preview>{previewText}</Preview>
|
||||
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{heading}</EmailHeading>
|
||||
</EmailHeader>
|
||||
|
||||
<EmailContent>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{hello}
|
||||
</Text>
|
||||
|
||||
{lines.map((line, index) => (
|
||||
<Text
|
||||
key={index}
|
||||
className="text-[16px] leading-[24px] text-[#242424]"
|
||||
dangerouslySetInnerHTML={{ __html: line }}
|
||||
/>
|
||||
))}
|
||||
|
||||
<CommonFooter t={t} />
|
||||
</EmailContent>
|
||||
</EmailWrapper>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>,
|
||||
);
|
||||
|
||||
return {
|
||||
html,
|
||||
subject,
|
||||
};
|
||||
}
|
||||
@@ -2,3 +2,4 @@ export * from './emails/invite.email';
|
||||
export * from './emails/account-delete.email';
|
||||
export * from './emails/otp.email';
|
||||
export * from './emails/company-offer.email';
|
||||
export * from './emails/synlab.email';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { initializeServerI18n } from '@kit/i18n/server';
|
||||
|
||||
export function initializeEmailI18n(params: {
|
||||
language: string | undefined;
|
||||
namespace: string;
|
||||
namespace: string | string[];
|
||||
}) {
|
||||
const language = params.language ?? 'en';
|
||||
|
||||
|
||||
8
packages/email-templates/src/locales/en/common.json
Normal file
8
packages/email-templates/src/locales/en/common.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"footer": {
|
||||
"lines1": "MedReport",
|
||||
"lines2": "E-mail: <a href=\"mailto:info@medreport.ee\">info@medreport.ee</a>",
|
||||
"lines3": "Customer service: <a href=\"tel:+37258871517\">+372 5887 1517</a>",
|
||||
"lines4": "<a href=\"https://www.medreport.ee\">www.medreport.ee</a>"
|
||||
}
|
||||
}
|
||||
12
packages/email-templates/src/locales/en/synlab-email.json
Normal file
12
packages/email-templates/src/locales/en/synlab-email.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"subject": "Your Synlab order has been placed - {{analysisPackageName}}",
|
||||
"previewText": "Your Synlab order has been placed - {{analysisPackageName}}",
|
||||
"heading": "Your Synlab order has been placed - {{analysisPackageName}}",
|
||||
"hello": "Hello {{personName}},",
|
||||
"lines1": "The order for {{analysisPackageName}} analysis package has been sent to the lab. Please go to the lab to collect the sample: SYNLAB - {{partnerLocationName}}",
|
||||
"lines2": "<i>If you are unable to go to the lab to collect the sample, you can go to any other suitable collection point - <a href=\"https://medreport.ee/et/verevotupunktid\">view locations and opening hours</a>.</i>",
|
||||
"lines3": "It is recommended to collect the sample in the morning (before 12:00) and not to eat or drink (water can be drunk).",
|
||||
"lines4": "At the collection point, select the order from the queue: the order from the doctor.",
|
||||
"lines5": "If you have any questions, please contact us.",
|
||||
"lines6": "SYNLAB customer service phone: <a href=\"tel:+37217123\">17123</a>"
|
||||
}
|
||||
8
packages/email-templates/src/locales/et/common.json
Normal file
8
packages/email-templates/src/locales/et/common.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"footer": {
|
||||
"lines1": "MedReport",
|
||||
"lines2": "E-mail: <a href=\"mailto:info@medreport.ee\">info@medreport.ee</a>",
|
||||
"lines3": "Klienditugi: <a href=\"tel:+37258871517\">+372 5887 1517</a>",
|
||||
"lines4": "<a href=\"https://www.medreport.ee\">www.medreport.ee</a>"
|
||||
}
|
||||
}
|
||||
12
packages/email-templates/src/locales/et/synlab-email.json
Normal file
12
packages/email-templates/src/locales/et/synlab-email.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"subject": "Teie SYNLAB tellimus on kinnitatud - {{analysisPackageName}}",
|
||||
"previewText": "Teie SYNLAB tellimus on kinnitatud - {{analysisPackageName}}",
|
||||
"heading": "Teie SYNLAB tellimus on kinnitatud - {{analysisPackageName}}",
|
||||
"hello": "Tere {{personName}},",
|
||||
"lines1": "Saatekiri {{analysisPackageName}} analüüsi uuringuteks on saadetud laborisse digitaalselt. Palun mine proove andma: SYNLAB - {{partnerLocationName}}",
|
||||
"lines2": "<i>Kui Teil ei ole võimalik valitud asukohta minna proove andma, siis võite minna endale sobivasse proovivõtupunkti - <a href=\"https://medreport.ee/et/verevotupunktid\">vaata asukohti ja lahtiolekuaegasid</a>.</i>",
|
||||
"lines3": "Soovituslik on proove anda pigem hommikul (enne 12:00) ning söömata ja joomata (vett võib juua).",
|
||||
"lines4": "Proovivõtupunktis valige järjekorrasüsteemis: <strong>saatekirjad</strong> alt <strong>eriarsti saatekiri</strong>.",
|
||||
"lines5": "Juhul kui tekkis lisaküsimusi, siis võtke julgelt ühendust.",
|
||||
"lines6": "SYNLAB klienditoe telefon: <a href=\"tel:+37217123\">17123</a>"
|
||||
}
|
||||
@@ -4,6 +4,10 @@ import { Database } from '@kit/supabase/database';
|
||||
|
||||
import { UserAnalysis } from '../types/accounts';
|
||||
|
||||
export type AccountWithParams = Database['medreport']['Tables']['accounts']['Row'] & {
|
||||
account_params: Pick<Database['medreport']['Tables']['account_params']['Row'], 'weight' | 'height'> | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Class representing an API for interacting with user accounts.
|
||||
* @constructor
|
||||
@@ -17,11 +21,11 @@ class AccountsApi {
|
||||
* @description Get the account data for the given ID.
|
||||
* @param id
|
||||
*/
|
||||
async getAccount(id: string) {
|
||||
async getAccount(id: string): Promise<AccountWithParams> {
|
||||
const { data, error } = await this.client
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.select('*')
|
||||
.select('*, account_params: account_params (weight, height)')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "./cookies";
|
||||
import { getRegion } from "./regions";
|
||||
import { sdk } from "@lib/config";
|
||||
import { retrieveOrder } from "./orders";
|
||||
|
||||
/**
|
||||
* Retrieves a cart by its ID. If no ID is provided, it will use the cart ID from the cookies.
|
||||
@@ -161,9 +162,11 @@ export async function addToCart({
|
||||
export async function updateLineItem({
|
||||
lineId,
|
||||
quantity,
|
||||
metadata,
|
||||
}: {
|
||||
lineId: string;
|
||||
quantity: number;
|
||||
metadata?: Record<string, any>;
|
||||
}) {
|
||||
if (!lineId) {
|
||||
throw new Error("Missing lineItem ID when updating line item");
|
||||
@@ -180,7 +183,7 @@ export async function updateLineItem({
|
||||
};
|
||||
|
||||
await sdk.store.cart
|
||||
.updateLineItem(cartId, lineId, { quantity }, {}, headers)
|
||||
.updateLineItem(cartId, lineId, { quantity, metadata }, {}, headers)
|
||||
.then(async () => {
|
||||
const cartCacheTag = await getCacheTag("carts");
|
||||
revalidateTag(cartCacheTag);
|
||||
@@ -392,7 +395,7 @@ export async function setAddresses(currentState: unknown, formData: FormData) {
|
||||
* @param cartId - optional - The ID of the cart to place an order for.
|
||||
* @returns The cart object if the order was successful, or null if not.
|
||||
*/
|
||||
export async function placeOrder(cartId?: string) {
|
||||
export async function placeOrder(cartId?: string, options: { revalidateCacheTags: boolean } = { revalidateCacheTags: true }) {
|
||||
const id = cartId || (await getCartId());
|
||||
|
||||
if (!id) {
|
||||
@@ -406,21 +409,26 @@ export async function placeOrder(cartId?: string) {
|
||||
const cartRes = await sdk.store.cart
|
||||
.complete(id, {}, headers)
|
||||
.then(async (cartRes) => {
|
||||
const cartCacheTag = await getCacheTag("carts");
|
||||
revalidateTag(cartCacheTag);
|
||||
if (options?.revalidateCacheTags) {
|
||||
const cartCacheTag = await getCacheTag("carts");
|
||||
revalidateTag(cartCacheTag);
|
||||
}
|
||||
return cartRes;
|
||||
})
|
||||
.catch(medusaError);
|
||||
|
||||
if (cartRes?.type === "order") {
|
||||
const orderCacheTag = await getCacheTag("orders");
|
||||
revalidateTag(orderCacheTag);
|
||||
if (options?.revalidateCacheTags) {
|
||||
const orderCacheTag = await getCacheTag("orders");
|
||||
revalidateTag(orderCacheTag);
|
||||
}
|
||||
|
||||
removeCartId();
|
||||
redirect(`/home/order/${cartRes?.order.id}/confirmed`);
|
||||
} else {
|
||||
throw new Error("Cart is not an order");
|
||||
}
|
||||
|
||||
return retrieveOrder(cartRes.order.id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,7 @@ export const listProducts = async ({
|
||||
regionId,
|
||||
}: {
|
||||
pageParam?: number
|
||||
queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams & { collection_id?: string }
|
||||
queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams & { "type_id[0]"?: string }
|
||||
countryCode?: string
|
||||
regionId?: string
|
||||
}): Promise<{
|
||||
@@ -134,3 +134,24 @@ export const listProductsWithSort = async ({
|
||||
queryParams,
|
||||
}
|
||||
}
|
||||
|
||||
export const listProductTypes = async (): Promise<{ productTypes: HttpTypes.StoreProductType[]; count: number }> => {
|
||||
const next = {
|
||||
...(await getCacheOptions("productTypes")),
|
||||
};
|
||||
|
||||
return sdk.client
|
||||
.fetch<{ product_types: HttpTypes.StoreProductType[]; count: number }>(
|
||||
"/store/product-types",
|
||||
{
|
||||
next,
|
||||
cache: "force-cache",
|
||||
query: {
|
||||
fields: "id,value,metadata",
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(({ product_types, count }) => {
|
||||
return { productTypes: product_types, count };
|
||||
});
|
||||
};
|
||||
|
||||
@@ -214,7 +214,15 @@ export type Database = {
|
||||
recorded_at?: string
|
||||
weight?: number | null
|
||||
}
|
||||
Relationships: []
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "account_params_account_id_fkey"
|
||||
columns: ["account_id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "accounts"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
accounts: {
|
||||
Row: {
|
||||
@@ -308,13 +316,7 @@ export type Database = {
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "accounts_memberships_account_id_fkey"
|
||||
columns: ["account_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "accounts"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
|
||||
{
|
||||
foreignKeyName: "accounts_memberships_account_id_fkey"
|
||||
columns: ["account_id"]
|
||||
|
||||
Reference in New Issue
Block a user