feat: create email template for TTO reservation confirmation

feat: implement order notifications service with TTO reservation confirmation handling

feat: create migration for TTO booking email webhook trigger
This commit is contained in:
Danel Kungla
2025-09-30 16:05:43 +03:00
parent 4003284f3a
commit 72f6f2b716
56 changed files with 3692 additions and 294 deletions

View File

@@ -1,8 +1,13 @@
'use server';
import { AccountBalanceService, AccountBalanceSummary } from '@kit/accounts/services/account-balance.service';
import {
AccountBalanceService,
AccountBalanceSummary,
} from '@kit/accounts/services/account-balance.service';
export async function getAccountBalanceSummary(accountId: string): Promise<AccountBalanceSummary | null> {
export async function getAccountBalanceSummary(
accountId: string,
): Promise<AccountBalanceSummary | null> {
try {
const service = new AccountBalanceService();
return await service.getBalanceSummary(accountId);

View File

@@ -1,25 +1,29 @@
'use server';
import { z } from 'zod';
import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts';
import { createNotificationsApi } from '@/packages/features/notifications/src/server/api';
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
import { listProductTypes } from '@lib/data';
import { initiateMultiPaymentSession, placeOrder } from '@lib/data/cart';
import type { StoreCart, StoreOrder } from '@medusajs/types';
import jwt from 'jsonwebtoken';
import { z } from 'zod';
import type { StoreCart, StoreOrder } from "@medusajs/types";
import type { AccountBalanceSummary } from '@kit/accounts/services/account-balance.service';
import { initiateMultiPaymentSession, placeOrder } from "@lib/data/cart";
import type { AccountBalanceSummary } from "@kit/accounts/services/account-balance.service";
import { handleNavigateToPayment } from "~/lib/services/medusaCart.service";
import { loadCurrentUserAccount } from "./load-user-account";
import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service";
import { createAnalysisOrder, getAnalysisOrder } from "~/lib/services/order.service";
import { listProductTypes } from "@lib/data";
import { sendOrderToMedipost } from "~/lib/services/medipost/medipostPrivateMessage.service";
import { AccountWithParams } from "@/packages/features/accounts/src/types/accounts";
import { createI18nServerInstance } from "~/lib/i18n/i18n.server";
import { getSupabaseServerAdminClient } from "@/packages/supabase/src/clients/server-admin-client";
import { createNotificationsApi } from "@/packages/features/notifications/src/server/api";
import { FailureReason } from '~/lib/types/connected-online';
import { getOrderedTtoServices } from '~/lib/services/reservation.service';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { bookAppointment } from '~/lib/services/connected-online.service';
import { sendOrderToMedipost } from '~/lib/services/medipost/medipostPrivateMessage.service';
import { handleNavigateToPayment } from '~/lib/services/medusaCart.service';
import { getOrderedAnalysisIds } from '~/lib/services/medusaOrder.service';
import {
createAnalysisOrder,
getAnalysisOrder,
} from '~/lib/services/order.service';
import { getOrderedTtoServices } from '~/lib/services/reservation.service';
import { FailureReason } from '~/lib/types/connected-online';
import { loadCurrentUserAccount } from './load-user-account';
const ANALYSIS_PACKAGES_TYPE_HANDLE = 'analysis-packages';
const ANALYSIS_TYPE_HANDLE = 'synlab-analysis';
@@ -40,12 +44,16 @@ const env = () =>
isEnabledDispatchOnMontonioCallback: z.boolean({
error: 'MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK is required',
}),
medusaBackendPublicUrl: z.string({
error: 'MEDUSA_BACKEND_PUBLIC_URL is required',
}).min(1),
companyBenefitsPaymentSecretKey: z.string({
error: 'COMPANY_BENEFITS_PAYMENT_SECRET_KEY is required',
}).min(1),
medusaBackendPublicUrl: z
.string({
error: 'MEDUSA_BACKEND_PUBLIC_URL is required',
})
.min(1),
companyBenefitsPaymentSecretKey: z
.string({
error: 'COMPANY_BENEFITS_PAYMENT_SECRET_KEY is required',
})
.min(1),
})
.parse({
emailSender: process.env.EMAIL_SENDER,
@@ -53,7 +61,8 @@ const env = () =>
isEnabledDispatchOnMontonioCallback:
process.env.MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK === 'true',
medusaBackendPublicUrl: process.env.MEDUSA_BACKEND_PUBLIC_URL!,
companyBenefitsPaymentSecretKey: process.env.COMPANY_BENEFITS_PAYMENT_SECRET_KEY!,
companyBenefitsPaymentSecretKey:
process.env.COMPANY_BENEFITS_PAYMENT_SECRET_KEY!,
});
export const initiatePayment = async ({
@@ -92,23 +101,30 @@ export const initiatePayment = async ({
// place order if all paid already
const { orderId } = await handlePlaceOrder({ cart });
const companyBenefitsOrderToken = jwt.sign({
accountId,
companyBenefitsPaymentSessionId,
orderId,
totalByBenefits,
}, env().companyBenefitsPaymentSecretKey, {
algorithm: 'HS256',
});
const webhookResponse = await fetch(`${env().medusaBackendPublicUrl}/hooks/payment/company-benefits_company-benefits`, {
method: 'POST',
body: JSON.stringify({
orderToken: companyBenefitsOrderToken,
}),
headers: {
'Content-Type': 'application/json',
const companyBenefitsOrderToken = jwt.sign(
{
accountId,
companyBenefitsPaymentSessionId,
orderId,
totalByBenefits,
},
});
env().companyBenefitsPaymentSecretKey,
{
algorithm: 'HS256',
},
);
const webhookResponse = await fetch(
`${env().medusaBackendPublicUrl}/hooks/payment/company-benefits_company-benefits`,
{
method: 'POST',
body: JSON.stringify({
orderToken: companyBenefitsOrderToken,
}),
headers: {
'Content-Type': 'application/json',
},
},
);
if (!webhookResponse.ok) {
throw new Error('Failed to send company benefits webhook');
}
@@ -118,14 +134,15 @@ export const initiatePayment = async ({
console.error('Error initiating payment', error);
}
return { url: null, isFullyPaidByBenefits: false, orderId: null, unavailableLineItemIds: [] };
}
return {
url: null,
isFullyPaidByBenefits: false,
orderId: null,
unavailableLineItemIds: [],
};
};
export async function handlePlaceOrder({
cart,
}: {
cart: StoreCart;
}) {
export async function handlePlaceOrder({ cart }: { cart: StoreCart }) {
const { account } = await loadCurrentUserAccount();
if (!account) {
throw new Error('Account not found in context');
@@ -184,6 +201,7 @@ export async function handlePlaceOrder({
);
bookServiceResults = await Promise.all(bookingPromises);
}
// TODO: SEND EMAIL
if (email) {
if (analysisPackageOrder) {

View File

@@ -6,7 +6,7 @@ export const isValidOpenAiEnv = async () => {
await client.models.list();
return true;
} catch (e) {
console.log('No openAI env');
console.log('AI not enabled');
return false;
}
};