diff --git a/apps/backend/src/modules/public-booking/services/public-booking.service.ts b/apps/backend/src/modules/public-booking/services/public-booking.service.ts index 41bdd7b..5c9361b 100644 --- a/apps/backend/src/modules/public-booking/services/public-booking.service.ts +++ b/apps/backend/src/modules/public-booking/services/public-booking.service.ts @@ -13,6 +13,16 @@ type Slot = { endTime: string; }; +type PriceableCourt = { + basePrice: unknown; + priceRules: Array<{ + dayOfWeek: DayOfWeek | null; + startTime: string | null; + endTime: string | null; + price: unknown; + }>; +}; + type ComplexWithPublicBookingData = Awaited>; const DAY_OF_WEEK_BY_INDEX: DayOfWeek[] = [ @@ -230,11 +240,7 @@ async function getComplexWithBookingData(complexSlug: string) { return complex; } -function resolveSlotPrice( - court: ComplexWithPublicBookingData['courts'][number], - dayOfWeek: DayOfWeek, - slot: Slot -): number { +function resolveSlotPrice(court: PriceableCourt, dayOfWeek: DayOfWeek, slot: Slot): number { const slotStart = toMinutes(slot.startTime); const slotEnd = toMinutes(slot.endTime); const matchingRules = court.priceRules @@ -251,9 +257,9 @@ function resolveSlotPrice( }) .sort((first, second) => { const firstSpecificity = - (first.dayOfWeek ? 1 : 0) + (first.startTime && first.endTime ? 1 : 0); + (first.dayOfWeek ? 2 : 0) + (first.startTime && first.endTime ? 1 : 0); const secondSpecificity = - (second.dayOfWeek ? 1 : 0) + (second.startTime && second.endTime ? 1 : 0); + (second.dayOfWeek ? 2 : 0) + (second.startTime && second.endTime ? 1 : 0); return secondSpecificity - firstSpecificity; }); @@ -324,6 +330,11 @@ export async function getPublicBookingConfirmation(complexSlug: string, bookingC court: { select: { name: true, + basePrice: true, + priceRules: { + where: { isActive: true }, + orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }], + }, sport: { select: { id: true, @@ -334,6 +345,7 @@ export async function getPublicBookingConfirmation(complexSlug: string, bookingC complex: { select: { complexName: true, + physicalAddress: true, complexSlug: true, }, }, @@ -346,13 +358,22 @@ export async function getPublicBookingConfirmation(complexSlug: string, bookingC throw new PublicBookingServiceError('Reserva no encontrada.', 404); } + const date = formatIsoDate(booking.bookingDate); + const { dayOfWeek } = parseIsoDate(date); + const price = resolveSlotPrice(booking.court, dayOfWeek, { + startTime: booking.startTime, + endTime: booking.endTime, + }); + return { bookingCode: booking.bookingCode, complexName: booking.court.complex.complexName, + complexAddress: booking.court.complex.physicalAddress ?? undefined, complexSlug: booking.court.complex.complexSlug, - date: formatIsoDate(booking.bookingDate), + date, startTime: booking.startTime, endTime: booking.endTime, + price, courtName: booking.court.name, sport: { id: booking.court.sport.id, diff --git a/apps/frontend/src/features/public-booking/components/share-whatsapp-button.tsx b/apps/frontend/src/features/public-booking/components/share-whatsapp-button.tsx index d0a9261..46f7261 100644 --- a/apps/frontend/src/features/public-booking/components/share-whatsapp-button.tsx +++ b/apps/frontend/src/features/public-booking/components/share-whatsapp-button.tsx @@ -18,16 +18,35 @@ function formatDateLabel(isoDate: string) { }).format(date); } +function formatBookingPrice(price: number): string { + if (price === 0) { + return 'Sin cargo'; + } + + return new Intl.NumberFormat('es-AR', { + style: 'currency', + currency: 'ARS', + maximumFractionDigits: Number.isInteger(price) ? 0 : 2, + }).format(price); +} + function createWhatsappMessage(confirmation: PublicBookingConfirmation) { const dateLabel = formatDateLabel(confirmation.date); - return [ - 'Ya reservamos cancha para jugar.', - `Complejo: ${confirmation.complexName}`, + const lines = ['Ya reservamos cancha para jugar.', `Complejo: ${confirmation.complexName}`]; + + if (confirmation.complexAddress) { + lines.push(`Direccion: ${confirmation.complexAddress}`); + } + + lines.push( `Cancha: ${confirmation.courtName} (${confirmation.sport.name})`, `Fecha: ${dateLabel}`, `Horario: ${confirmation.startTime} - ${confirmation.endTime}`, - `Codigo de reserva: ${confirmation.bookingCode}`, - ].join('\n'); + `Precio: ${formatBookingPrice(confirmation.price)}`, + `Codigo de reserva: ${confirmation.bookingCode}` + ); + + return lines.join('\n'); } export function ShareWhatsappButton({ confirmation }: ShareWhatsappButtonProps) { @@ -36,7 +55,12 @@ export function ShareWhatsappButton({ confirmation }: ShareWhatsappButtonProps) )}`; return ( - diff --git a/packages/api-contract/src/public-booking.ts b/packages/api-contract/src/public-booking.ts index 5cb2aa9..5d75f3f 100644 --- a/packages/api-contract/src/public-booking.ts +++ b/packages/api-contract/src/public-booking.ts @@ -80,10 +80,12 @@ export const publicBookingSchema = z.object({ export const publicBookingConfirmationSchema = z.object({ bookingCode: z.string().regex(BOOKING_CODE_REGEX), complexName: z.string(), + complexAddress: z.string().optional(), complexSlug: z.string(), date: z.string().regex(ISO_DATE_REGEX), startTime: z.string().regex(TIME_REGEX), endTime: z.string().regex(TIME_REGEX), + price: z.number().nonnegative(), courtName: z.string(), sport: publicBookingSportSchema, status: z.enum(['CONFIRMED', 'CANCELLED', 'COMPLETED']),