feat: add customer email functionality for booking confirmations

- Implemented email confirmation for admin and public bookings.
- Added customerEmail field to booking schemas and services.
- Created email templates for booking confirmation, cancellation, and no-show notifications.
- Updated booking handlers to send emails upon booking creation and status updates.
- Enhanced frontend forms to capture customer email during booking creation.
This commit is contained in:
Jose Selesan
2026-06-02 19:21:04 -03:00
parent 50fa4ed9a5
commit 85f234b05e
22 changed files with 610 additions and 1536 deletions

View File

@@ -3,6 +3,7 @@ import {
PublicBookingServiceError,
createPublicBooking,
} from '@/modules/public-booking/services/public-booking.service';
import { sendBookingConfirmation } from '@/services/booking-email.service';
import type { AppContext } from '@/types/hono';
import type { CreatePublicBookingInput } from '@repo/api-contract';
@@ -30,6 +31,19 @@ export async function createPublicBookingHandler(c: AppContext) {
})
);
void sendBookingConfirmation({
bookingCode: booking.bookingCode,
complexName: booking.complexName,
date: booking.date,
startTime: booking.startTime,
endTime: booking.endTime,
courtName: booking.courtName,
sportName: booking.sport.name,
customerName: booking.customerName,
customerEmail: booking.customerEmail,
price: booking.price,
});
return c.json(booking, 201);
} catch (error) {
if (error instanceof PublicBookingServiceError) {

View File

@@ -276,6 +276,8 @@ function mapBookingResponse(input: {
endTime: string;
customerName: string;
customerPhone: string;
customerEmail: string | null;
price: number;
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
court: {
id: string;
@@ -304,7 +306,9 @@ function mapBookingResponse(input: {
endTime: input.endTime,
customerName: input.customerName,
customerPhone: input.customerPhone,
customerEmail: input.customerEmail ?? undefined,
status: input.status,
price: input.price,
createdAt: input.createdAt.toISOString(),
};
}
@@ -325,6 +329,7 @@ export async function getPublicBookingConfirmation(complexSlug: string, bookingC
bookingDate: true,
startTime: true,
endTime: true,
customerEmail: true,
status: true,
createdAt: true,
court: {
@@ -381,6 +386,7 @@ export async function getPublicBookingConfirmation(complexSlug: string, bookingC
slug: booking.court.sport.slug,
},
status: booking.status,
customerEmail: booking.customerEmail ?? undefined,
createdAt: booking.createdAt.toISOString(),
};
}
@@ -625,6 +631,7 @@ export async function createPublicBooking(complexSlug: string, input: CreatePubl
endTime: selectedSlot.endTime,
customerName: input.customerName.trim(),
customerPhone: input.customerPhone.trim(),
customerEmail: input.customerEmail?.trim() || null,
status: 'CONFIRMED',
},
select: {
@@ -636,11 +643,14 @@ export async function createPublicBooking(complexSlug: string, input: CreatePubl
endTime: true,
customerName: true,
customerPhone: true,
customerEmail: true,
status: true,
},
});
});
const price = resolveSlotPrice(selectedCourt, dayOfWeek, selectedSlot);
return mapBookingResponse({
bookingId: booking.id,
bookingCode: booking.bookingCode,
@@ -650,6 +660,8 @@ export async function createPublicBooking(complexSlug: string, input: CreatePubl
endTime: booking.endTime,
customerName: booking.customerName,
customerPhone: booking.customerPhone,
customerEmail: booking.customerEmail,
price,
status: booking.status,
court: {
id: selectedCourt.id,