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:
@@ -48,6 +48,12 @@ class DatabaseWebhookRouterService {
|
||||
return this.handleAnalysisOrdersWebhook(payload);
|
||||
}
|
||||
|
||||
case 'connected_online_reservation': {
|
||||
const payload = body as RecordChange<typeof body.table>;
|
||||
|
||||
return this.handleTtoOrdersWebhook(payload);
|
||||
}
|
||||
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
@@ -100,13 +106,27 @@ class DatabaseWebhookRouterService {
|
||||
return;
|
||||
}
|
||||
|
||||
const { createAnalysisOrderWebhooksService } = await import(
|
||||
'@kit/notifications/webhooks/analysis-order-notifications.service'
|
||||
const { createOrderWebhooksService } = await import(
|
||||
'@kit/notifications/webhooks/order-notifications.service'
|
||||
);
|
||||
|
||||
const service = createAnalysisOrderWebhooksService();
|
||||
const service = createOrderWebhooksService();
|
||||
|
||||
return service.handleStatusChangeWebhook(record);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleTtoOrdersWebhook(
|
||||
body: RecordChange<'connected_online_reservation'>,
|
||||
) {
|
||||
if (body.type === 'UPDATE' && body.record) {
|
||||
const { createOrderWebhooksService } = await import(
|
||||
'@kit/notifications/webhooks/order-notifications.service'
|
||||
);
|
||||
|
||||
const service = createOrderWebhooksService();
|
||||
|
||||
return service.handleTtoReservationConfirmationWebhook(body.record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,20 @@
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .turbo node_modules",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"email:dev": "email dev --dir src/emails --port 3001"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "0.0.41"
|
||||
"@react-email/components": "0.0.41",
|
||||
"react-email": "4.2.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/i18n": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*"
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@react-email/preview-server": "4.2.12"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
|
||||
@@ -6,22 +6,28 @@ 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 }}
|
||||
/>
|
||||
))}
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:footer.title`)}
|
||||
</Text>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:footer.emailField`)}{' '}
|
||||
<a href={`mailto:${t(`${namespace}:footer.title`)}`}>
|
||||
{t(`${namespace}:footer.email`)}
|
||||
</a>
|
||||
</Text>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:footer.phoneField`)}{' '}
|
||||
<a href={`tel:${t(`${namespace}:footer.phone`)}`}>
|
||||
{t(`${namespace}:footer.phone`)}
|
||||
</a>
|
||||
</Text>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
<a href={`https://${t(`${namespace}:footer.website`)}`}>
|
||||
{t(`${namespace}:footer.website`)}
|
||||
</a>
|
||||
</Text>
|
||||
</EmailFooter>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Container, Text } from '@react-email/components';
|
||||
import { Container, Section } from '@react-email/components';
|
||||
|
||||
export function EmailFooter(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<Container className="mt-[24px]">
|
||||
<Text className="px-4 text-[12px] leading-[20px] text-gray-300">
|
||||
<Section className="px-4 text-[12px] leading-[20px] text-gray-300">
|
||||
{props.children}
|
||||
</Text>
|
||||
</Section>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
8
packages/email-templates/src/emails/email.tsx
Normal file
8
packages/email-templates/src/emails/email.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
const Email = () => {
|
||||
return <div>Email</div>;
|
||||
};
|
||||
|
||||
export default Email;
|
||||
Email.PreviewProps = {};
|
||||
@@ -0,0 +1,137 @@
|
||||
import {
|
||||
Body,
|
||||
Head,
|
||||
Html,
|
||||
Link,
|
||||
Preview,
|
||||
Tailwind,
|
||||
Text,
|
||||
render,
|
||||
} from '@react-email/components';
|
||||
|
||||
import { BodyStyle } from '../components/body-style';
|
||||
import CommonFooter from '../components/common-footer';
|
||||
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';
|
||||
|
||||
export async function renderTtoReservationConfirmationEmail({
|
||||
language,
|
||||
recipientName,
|
||||
startTime,
|
||||
orderName,
|
||||
locationName,
|
||||
locationAddress,
|
||||
orderId,
|
||||
serviceProviderName,
|
||||
serviceProviderEmail,
|
||||
serviceProviderPhone,
|
||||
}: {
|
||||
language: string;
|
||||
recipientName: string;
|
||||
startTime: string;
|
||||
orderName: string;
|
||||
locationName?: string;
|
||||
locationAddress?: string | null;
|
||||
orderId: string;
|
||||
serviceProviderName?: string;
|
||||
serviceProviderEmail?: string | null;
|
||||
serviceProviderPhone?: string | null;
|
||||
}) {
|
||||
const namespace = 'tto-reservation-confirmation-email';
|
||||
|
||||
const { t } = await initializeEmailI18n({
|
||||
language,
|
||||
namespace: [namespace, 'common'],
|
||||
});
|
||||
|
||||
const previewText = t(`${namespace}:previewText`, {
|
||||
reservation: orderName,
|
||||
});
|
||||
|
||||
const subject = t(`${namespace}:subject`, {
|
||||
reservation: orderName,
|
||||
});
|
||||
|
||||
const email = (
|
||||
<Html>
|
||||
<Head>
|
||||
<BodyStyle />
|
||||
</Head>
|
||||
|
||||
<Preview>{previewText}</Preview>
|
||||
|
||||
<Tailwind>
|
||||
<Body>
|
||||
<EmailWrapper>
|
||||
<EmailContent>
|
||||
<EmailHeader>
|
||||
<EmailHeading>{previewText}</EmailHeading>
|
||||
</EmailHeader>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:helloName`, { name: recipientName })}
|
||||
</Text>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:thankYou`)}
|
||||
</Text>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{orderName}, {new Date(startTime).toLocaleString()},{' '}
|
||||
{locationAddress}, {locationName}
|
||||
</Text>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
<Link
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/home/order/${orderId}`}
|
||||
>
|
||||
{t(`${namespace}:viewOrder`)}
|
||||
</Link>
|
||||
</Text>
|
||||
<Text className="mt-10 text-[16px] leading-[24px] text-[#242424]">
|
||||
{serviceProviderName}
|
||||
</Text>
|
||||
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||
{t(`${namespace}:customerSupport`)}
|
||||
<Link href={`tel:${serviceProviderPhone})}`}>
|
||||
{serviceProviderPhone}
|
||||
</Link>
|
||||
</Text>
|
||||
<Link href={`mailto:${serviceProviderEmail}`}>
|
||||
{serviceProviderEmail}
|
||||
</Link>
|
||||
|
||||
<CommonFooter t={t} />
|
||||
</EmailContent>
|
||||
</EmailWrapper>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
|
||||
const html = await render(email);
|
||||
|
||||
return {
|
||||
html,
|
||||
subject,
|
||||
email,
|
||||
};
|
||||
}
|
||||
|
||||
const PreviewEmail = async () => {
|
||||
const { email } = await renderTtoReservationConfirmationEmail({
|
||||
language: 'et',
|
||||
recipientName: 'John Doe',
|
||||
startTime: '2025-09-27 05:45:00+00',
|
||||
orderName: 'Hambaarst',
|
||||
locationName: 'Tallinn',
|
||||
locationAddress: 'Põhja puiestee, 2/3A',
|
||||
orderId: '123123',
|
||||
serviceProviderName: 'Dentas OÜ',
|
||||
serviceProviderEmail: 'email@example.ee',
|
||||
serviceProviderPhone: '+372111111',
|
||||
});
|
||||
return email;
|
||||
};
|
||||
|
||||
export default PreviewEmail;
|
||||
PreviewEmail.PreviewProps = {};
|
||||
@@ -11,3 +11,4 @@ export * from './emails/order-processing.email';
|
||||
export * from './emails/patient-first-results-received.email';
|
||||
export * from './emails/patient-full-results-received.email';
|
||||
export * from './emails/book-time-failed.email';
|
||||
export * from './emails/tto-reservation-confirmation.email';
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"previewText": "Your booking is confirmed! - {{reservation}}",
|
||||
"subject": "Your booking is confirmed! - {{reservation}}",
|
||||
"helloName": "Hello, {{name}}!",
|
||||
"thankYou": "Thank you for your order!",
|
||||
"viewOrder": "See booking",
|
||||
"customerSupport": "Customer support: "
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"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>"
|
||||
"title": "MedReport",
|
||||
"emailField": "E-mail:",
|
||||
"email": "info@medreport.ee",
|
||||
"phoneField": "Klienditugi:",
|
||||
"phone": "+372 5887 1517",
|
||||
"website": "www.medreport.ee"
|
||||
},
|
||||
"helloName": "Tere, {{name}}",
|
||||
"hello": "Tere"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"previewText": "Teie broneering on kinnitatud! - {{reservation}}",
|
||||
"subject": "Teie broneering on kinnitatud! - {{reservation}}",
|
||||
"helloName": "Tere, {{name}}!",
|
||||
"thankYou": "Täname tellimuse eest!",
|
||||
"viewOrder": "Vaata broneeringut",
|
||||
"customerSupport": "Klienditugi: "
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"previewText": "Your booking is confirmed! - {{reservation}}",
|
||||
"subject": "Your booking is confirmed! - {{reservation}}",
|
||||
"helloName": "Hello, {{name}}!",
|
||||
"thankYou": "Thank you for your order!",
|
||||
"viewOrder": "See booking",
|
||||
"customerSupport": "Customer support: "
|
||||
}
|
||||
@@ -43,12 +43,17 @@ export class AccountBalanceService {
|
||||
offset?: number;
|
||||
entryType?: string;
|
||||
includeInactive?: boolean;
|
||||
} = {}
|
||||
} = {},
|
||||
): Promise<{
|
||||
entries: AccountBalanceEntry[];
|
||||
total: number;
|
||||
}> {
|
||||
const { limit = 50, offset = 0, entryType, includeInactive = false } = options;
|
||||
const {
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
entryType,
|
||||
includeInactive = false,
|
||||
} = options;
|
||||
|
||||
let query = this.supabase
|
||||
.schema('medreport')
|
||||
@@ -105,7 +110,8 @@ export class AccountBalanceService {
|
||||
console.error('Error getting expiring balance:', expiringError);
|
||||
}
|
||||
|
||||
const expiringSoon = expiringData?.reduce((sum, entry) => sum + (entry.amount || 0), 0) || 0;
|
||||
const expiringSoon =
|
||||
expiringData?.reduce((sum, entry) => sum + (entry.amount || 0), 0) || 0;
|
||||
|
||||
return {
|
||||
totalBalance: balance,
|
||||
@@ -116,12 +122,13 @@ export class AccountBalanceService {
|
||||
|
||||
async processPeriodicBenefitDistributions(): Promise<void> {
|
||||
console.info('Processing periodic benefit distributions...');
|
||||
const { error } = await this.supabase.schema('medreport').rpc('process_periodic_benefit_distributions');
|
||||
const { error } = await this.supabase
|
||||
.schema('medreport')
|
||||
.rpc('process_periodic_benefit_distributions');
|
||||
if (error) {
|
||||
console.error('Error processing periodic benefit distributions:', error);
|
||||
throw new Error('Failed to process periodic benefit distributions');
|
||||
}
|
||||
console.info('Periodic benefit distributions processed successfully');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Database } from '@kit/supabase/database';
|
||||
|
||||
export type AccountBalanceEntry = Database['medreport']['Tables']['account_balance_entries']['Row'];
|
||||
export type AccountBalanceEntry =
|
||||
Database['medreport']['Tables']['account_balance_entries']['Row'];
|
||||
|
||||
@@ -22,4 +22,5 @@ export type AccountWithParams = Account & {
|
||||
export type CompanyParams =
|
||||
Database['medreport']['Tables']['company_params']['Row'];
|
||||
|
||||
export type BmiThresholds = Database['medreport']['Tables']['bmi_thresholds']['Row'];
|
||||
export type BmiThresholds =
|
||||
Database['medreport']['Tables']['bmi_thresholds']['Row'];
|
||||
|
||||
@@ -2,8 +2,8 @@ import 'server-only';
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import type { ApplicationRole } from '@kit/accounts/types/accounts';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
export function createAdminAccountsService(client: SupabaseClient<Database>) {
|
||||
return new AdminAccountsService(client);
|
||||
@@ -24,10 +24,7 @@ class AdminAccountsService {
|
||||
}
|
||||
}
|
||||
|
||||
async updateRole(
|
||||
accountId: string,
|
||||
role: ApplicationRole,
|
||||
) {
|
||||
async updateRole(accountId: string, role: ApplicationRole) {
|
||||
const { error } = await this.adminClient
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
|
||||
@@ -5,7 +5,7 @@ import { redirect } from 'next/navigation';
|
||||
|
||||
import { sdk } from '@lib/config';
|
||||
import medusaError from '@lib/util/medusa-error';
|
||||
import { HttpTypes } from '@medusajs/types';
|
||||
import { HttpTypes, StoreCart } from '@medusajs/types';
|
||||
|
||||
import {
|
||||
getAuthHeaders,
|
||||
@@ -259,18 +259,19 @@ export async function setShippingMethod({
|
||||
}
|
||||
|
||||
export async function initiateMultiPaymentSession(
|
||||
cart: HttpTypes.StoreCart,
|
||||
cart: StoreCart,
|
||||
benefitsAmount: number,
|
||||
) {
|
||||
const headers = {
|
||||
...(await getAuthHeaders()),
|
||||
};
|
||||
|
||||
return sdk.client.fetch<unknown>(`/store/multi-payment`, {
|
||||
method: 'POST',
|
||||
body: { cartId: cart.id, benefitsAmount },
|
||||
headers,
|
||||
})
|
||||
return sdk.client
|
||||
.fetch<unknown>(`/store/multi-payment`, {
|
||||
method: 'POST',
|
||||
body: { cartId: cart.id, benefitsAmount },
|
||||
headers,
|
||||
})
|
||||
.then(async (response) => {
|
||||
console.info('Payment session initiated:', response);
|
||||
const cartCacheTag = await getCacheTag('carts');
|
||||
@@ -284,7 +285,11 @@ export async function initiateMultiPaymentSession(
|
||||
};
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Error initiating payment session:', e, JSON.stringify(Object.keys(e)));
|
||||
console.error(
|
||||
'Error initiating payment session:',
|
||||
e,
|
||||
JSON.stringify(Object.keys(e)),
|
||||
);
|
||||
return medusaError(e);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export const listCollections = async (
|
||||
|
||||
queryParams.limit = queryParams.limit || '100';
|
||||
queryParams.offset = queryParams.offset || '0';
|
||||
console.log('SDK_CONFIG: ', SDK_CONFIG.baseUrl);
|
||||
|
||||
return sdk.client
|
||||
.fetch<{ collections: HttpTypes.StoreCollection[]; count: number }>(
|
||||
'/store/collections',
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { sdk } from '@lib/config';
|
||||
|
||||
import {
|
||||
renderAllResultsReceivedEmail,
|
||||
renderFirstResultsReceivedEmail,
|
||||
renderOrderProcessingEmail,
|
||||
renderPatientFirstResultsReceivedEmail,
|
||||
renderPatientFullResultsReceivedEmail,
|
||||
renderTtoReservationConfirmationEmail,
|
||||
} from '@kit/email-templates';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { getFullName } from '@kit/shared/utils';
|
||||
@@ -25,13 +28,15 @@ import {
|
||||
} from '~/lib/services/mailer.service';
|
||||
|
||||
type AnalysisOrder = Database['medreport']['Tables']['analysis_orders']['Row'];
|
||||
type TtoReservation =
|
||||
Database['medreport']['Tables']['connected_online_reservation']['Row'];
|
||||
|
||||
export function createAnalysisOrderWebhooksService() {
|
||||
return new AnalysisOrderWebhooksService();
|
||||
export function createOrderWebhooksService() {
|
||||
return new OrderWebhooksService();
|
||||
}
|
||||
|
||||
class AnalysisOrderWebhooksService {
|
||||
private readonly namespace = 'analysis_orders.webhooks';
|
||||
class OrderWebhooksService {
|
||||
private readonly namespace = 'orders.webhooks';
|
||||
|
||||
async handleStatusChangeWebhook(analysisOrder: AnalysisOrder) {
|
||||
const logger = await getLogger();
|
||||
@@ -90,6 +95,128 @@ class AnalysisOrderWebhooksService {
|
||||
}
|
||||
}
|
||||
|
||||
async handleTtoReservationConfirmationWebhook(
|
||||
ttoReservation: TtoReservation,
|
||||
) {
|
||||
const logger = await getLogger();
|
||||
const ctx = {
|
||||
ttoReservationId: ttoReservation.id,
|
||||
namespace: this.namespace,
|
||||
};
|
||||
|
||||
try {
|
||||
const userContact = await getUserContactAdmin(ttoReservation.user_id);
|
||||
if (!userContact.email) {
|
||||
await createNotificationLog({
|
||||
action: NotificationAction.TTO_ORDER_CONFIRMATION,
|
||||
status: 'FAIL',
|
||||
comment: 'No email found for ' + ttoReservation.user_id,
|
||||
relatedRecordId: ttoReservation.id,
|
||||
});
|
||||
logger.warn(ctx, 'No email found ');
|
||||
return;
|
||||
}
|
||||
const supabaseClient = getSupabaseServerAdminClient();
|
||||
if (!ttoReservation.medusa_cart_line_item_id) {
|
||||
await createNotificationLog({
|
||||
action: NotificationAction.TTO_ORDER_CONFIRMATION,
|
||||
status: 'FAIL',
|
||||
comment: 'No cart item id found for ' + ttoReservation.user_id,
|
||||
relatedRecordId: ttoReservation.id,
|
||||
});
|
||||
logger.warn(ctx, 'No cart item id found ');
|
||||
return;
|
||||
}
|
||||
const [
|
||||
{ data: cartItem },
|
||||
{ data: location },
|
||||
{ data: serviceProvider },
|
||||
] = await Promise.all([
|
||||
supabaseClient
|
||||
.from('cart_line_item')
|
||||
.select('cart_id,title')
|
||||
.eq('id', ttoReservation.medusa_cart_line_item_id)
|
||||
.single(),
|
||||
supabaseClient
|
||||
.schema('medreport')
|
||||
.from('connected_online_locations')
|
||||
.select('name,address')
|
||||
.eq('sync_id', ttoReservation.location_sync_id || 0)
|
||||
.single(),
|
||||
supabaseClient
|
||||
.schema('medreport')
|
||||
.from('connected_online_providers')
|
||||
.select('email,phone_number,name')
|
||||
.eq('id', ttoReservation.clinic_id)
|
||||
.single(),
|
||||
]);
|
||||
|
||||
if (!cartItem) {
|
||||
await createNotificationLog({
|
||||
action: NotificationAction.TTO_ORDER_CONFIRMATION,
|
||||
status: 'FAIL',
|
||||
comment: 'No medusa cart item found for ' + ttoReservation.user_id,
|
||||
relatedRecordId: ttoReservation.medusa_cart_line_item_id,
|
||||
});
|
||||
logger.warn(ctx, 'No cart item found ');
|
||||
return;
|
||||
}
|
||||
|
||||
const [{ data: orderCart }] = await Promise.all([
|
||||
supabaseClient
|
||||
.from('order_cart')
|
||||
.select('order_id')
|
||||
.eq('cart_id', cartItem.cart_id)
|
||||
.single(),
|
||||
]);
|
||||
|
||||
if (!orderCart) {
|
||||
await createNotificationLog({
|
||||
action: NotificationAction.TTO_ORDER_CONFIRMATION,
|
||||
status: 'FAIL',
|
||||
comment: 'No medusa order cart found for ' + ttoReservation.user_id,
|
||||
relatedRecordId: cartItem.cart_id,
|
||||
});
|
||||
logger.warn(ctx, 'No order cart found ');
|
||||
return;
|
||||
}
|
||||
|
||||
await sendEmailFromTemplate(
|
||||
renderTtoReservationConfirmationEmail,
|
||||
{
|
||||
language: userContact.preferred_locale ?? 'et',
|
||||
recipientName: getFullName(userContact.name, userContact.last_name),
|
||||
startTime: ttoReservation.start_time,
|
||||
orderName: cartItem.title,
|
||||
locationName: location?.name,
|
||||
locationAddress: location?.address,
|
||||
orderId: orderCart.order_id,
|
||||
serviceProviderName: serviceProvider?.name,
|
||||
serviceProviderEmail: serviceProvider?.email,
|
||||
serviceProviderPhone: serviceProvider?.phone_number,
|
||||
},
|
||||
userContact.email,
|
||||
);
|
||||
|
||||
return createNotificationLog({
|
||||
action: NotificationAction.TTO_ORDER_CONFIRMATION,
|
||||
status: 'SUCCESS',
|
||||
relatedRecordId: orderCart.order_id,
|
||||
});
|
||||
} catch (e: any) {
|
||||
createNotificationLog({
|
||||
action: NotificationAction.TTO_ORDER_CONFIRMATION,
|
||||
status: 'FAIL',
|
||||
comment: e?.message,
|
||||
relatedRecordId: ttoReservation.id,
|
||||
});
|
||||
logger.error(
|
||||
ctx,
|
||||
`Error while processing tto reservation: ${JSON.stringify(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async sendProcessingNotification(analysisOrder: AnalysisOrder) {
|
||||
const logger = await getLogger();
|
||||
const supabase = getSupabaseServerAdminClient();
|
||||
@@ -6,6 +6,7 @@ import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Ellipsis } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { formatCurrency } from '@kit/shared/utils';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -20,7 +21,6 @@ import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { formatCurrency } from '@kit/shared/utils';
|
||||
|
||||
import { RemoveMemberDialog } from './remove-member-dialog';
|
||||
import { RoleBadge } from './role-badge';
|
||||
@@ -135,7 +135,10 @@ function useGetColumns(
|
||||
}[];
|
||||
},
|
||||
): ColumnDef<Members[0]>[] {
|
||||
const { t, i18n: { language } } = useTranslation('teams');
|
||||
const {
|
||||
t,
|
||||
i18n: { language },
|
||||
} = useTranslation('teams');
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
@@ -183,7 +186,7 @@ function useGetColumns(
|
||||
header: t('distributedBenefitsAmount'),
|
||||
cell: ({ row }) => {
|
||||
const benefitAmount = params.membersBenefitsUsage.find(
|
||||
(usage) => usage.personal_account_id === row.original.id
|
||||
(usage) => usage.personal_account_id === row.original.id,
|
||||
)?.benefit_amount;
|
||||
if (typeof benefitAmount !== 'number') {
|
||||
return '-';
|
||||
@@ -203,7 +206,11 @@ function useGetColumns(
|
||||
const isPrimaryOwner = primary_owner_user_id === user_id;
|
||||
|
||||
return (
|
||||
<span className={'flex items-center space-x-1 flex-wrap space-y-1 sm:space-y-0 sm:flex-nowrap'}>
|
||||
<span
|
||||
className={
|
||||
'flex flex-wrap items-center space-y-1 space-x-1 sm:flex-nowrap sm:space-y-0'
|
||||
}
|
||||
>
|
||||
<RoleBadge role={role} />
|
||||
|
||||
<If condition={isPrimaryOwner}>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { redirect } from 'next/navigation';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AccountBalanceService } from '@kit/accounts/services/account-balance.service';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { createNotificationsApi } from '@kit/notifications/api';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
@@ -18,7 +19,6 @@ import { RenewInvitationSchema } from '../../schema/renew-invitation.schema';
|
||||
import { UpdateInvitationSchema } from '../../schema/update-invitation.schema';
|
||||
import { createAccountInvitationsService } from '../services/account-invitations.service';
|
||||
import { createAccountPerSeatBillingService } from '../services/account-per-seat-billing.service';
|
||||
import { AccountBalanceService } from '@kit/accounts/services/account-balance.service';
|
||||
|
||||
/**
|
||||
* @name createInvitationsAction
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import type { Account } from '@kit/accounts/types/accounts';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
|
||||
export function createAccountWebhooksService() {
|
||||
return new AccountWebhooksService();
|
||||
|
||||
@@ -4,7 +4,10 @@ import type { UuringuVastus } from '@kit/shared/types/medipost-analysis';
|
||||
import { toArray } from '@kit/shared/utils';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
import type { AnalysisOrder, AnalysisOrderStatus } from '../types/analysis-orders';
|
||||
import type {
|
||||
AnalysisOrder,
|
||||
AnalysisOrderStatus,
|
||||
} from '../types/analysis-orders';
|
||||
import type {
|
||||
AnalysisResultDetailsElement,
|
||||
AnalysisResultDetailsMapped,
|
||||
@@ -462,8 +465,10 @@ class UserAnalysesApi {
|
||||
}) {
|
||||
const orderIdParam = orderId;
|
||||
const medusaOrderIdParam = medusaOrderId;
|
||||
|
||||
console.info(`Updating order id=${orderId} medusaOrderId=${medusaOrderId} status=${orderStatus}`);
|
||||
|
||||
console.info(
|
||||
`Updating order id=${orderId} medusaOrderId=${medusaOrderId} status=${orderStatus}`,
|
||||
);
|
||||
if (!orderIdParam && !medusaOrderIdParam) {
|
||||
throw new Error('Either orderId or medusaOrderId must be provided');
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Euro, LayoutDashboard, Settings, Users } from 'lucide-react';
|
||||
|
||||
import { NavigationConfigSchema } from '@kit/ui/navigation-schema';
|
||||
|
||||
import pathsConfig from './paths.config';
|
||||
import featureFlagsConfig from './feature-flags.config';
|
||||
import pathsConfig from './paths.config';
|
||||
|
||||
const iconClasses = 'w-4';
|
||||
|
||||
@@ -23,10 +23,10 @@ const getRoutes = (account: string) => [
|
||||
},
|
||||
featureFlagsConfig.enableTeamAccountBilling
|
||||
? {
|
||||
label: 'common:routes.billing',
|
||||
path: createPath(pathsConfig.app.accountBilling, account),
|
||||
Icon: <Euro className={iconClasses} />,
|
||||
}
|
||||
label: 'common:routes.billing',
|
||||
path: createPath(pathsConfig.app.accountBilling, account),
|
||||
Icon: <Euro className={iconClasses} />,
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
label: 'common:routes.companySettings',
|
||||
|
||||
Reference in New Issue
Block a user