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:
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user