mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat: ( notifications ) add payment/cancellation events and ticket confirmation
This commit is contained in:
@@ -555,6 +555,8 @@ async function seedNotificationTemplates() {
|
||||
const templates = [
|
||||
{ id: uuidv4(), code: 'booking.created', channel: 'EMAIL', subject: 'Booking Confirmed', bodyTemplate: 'Your booking {{bookingRef}} is confirmed. Total: {{amount}} {{currency}}.' },
|
||||
{ id: uuidv4(), code: 'payment.succeeded', channel: 'SMS', subject: 'Payment Received', bodyTemplate: 'Payment of {{amount}} {{currency}} received for booking {{bookingRef}}.' },
|
||||
{ id: uuidv4(), code: 'payment.failed', channel: 'SMS', subject: 'Payment Failed', bodyTemplate: 'Payment for booking {{bookingRef}} could not be completed. Please try again.' },
|
||||
{ id: uuidv4(), code: 'booking.cancelled', channel: 'EMAIL', subject: 'Booking Cancelled', bodyTemplate: 'Your booking {{bookingRef}} has been cancelled. Refund: {{refundAmount}} {{currency}}.' },
|
||||
// Templates below are not wired to handlers yet (Phase 2 — full event coverage).
|
||||
{ id: uuidv4(), code: 'trip.departure', channel: 'PUSH', subject: 'Trip Departing Soon', bodyTemplate: 'Your trip {{route}} departs in {{minutes}} minutes' },
|
||||
{ id: uuidv4(), code: 'trip.delay', channel: 'EMAIL', subject: 'Trip Delayed', bodyTemplate: 'Your trip {{route}} is delayed by {{delayMinutes}} minutes' },
|
||||
|
||||
@@ -1007,6 +1007,7 @@ export class BookingsService {
|
||||
await this.prisma.bookingCancellation.create({ data: { bookingId: booking.id, cancelledBy: booking.passengerId, reason, refundAmount, refundMethod: booking.paymentIntent?.method ?? 'ORIGINAL', refundStatus: 'PENDING' } });
|
||||
await this.seatsService.releaseSeats(booking.seats.map((s) => s.seatId));
|
||||
await this.prisma.booking.update({ where: { bookingRef }, data: { status: 'CANCELLED' } });
|
||||
this.eventEmitter.emit('booking.cancelled', { booking, refundAmount });
|
||||
return { cancelled: true, refundAmount: refundAmount / 100, currency: 'ETB' };
|
||||
}
|
||||
|
||||
|
||||
@@ -28,18 +28,23 @@ export class EmailClientService implements OnApplicationBootstrap {
|
||||
);
|
||||
}
|
||||
|
||||
async sendEmail(dto: SendEmail) {
|
||||
async sendEmail(dto: SendEmail): Promise<{ queued: boolean }> {
|
||||
if (!this.enabled) {
|
||||
this.logger.warn(`RABBITMQ disabled — skipped EMAIL to ${dto.to}`);
|
||||
return {};
|
||||
this.logger.warn(`RABBITMQ disabled — skipped EMAIL`);
|
||||
return { queued: false };
|
||||
}
|
||||
this.emailServiceClient.emit("send-email", {
|
||||
...dto,
|
||||
appKey: "IFHCRS-LICENSE-MANAGEMENT",
|
||||
});
|
||||
// Fire-and-forget enqueue: this confirms the message was handed to RabbitMQ, NOT delivered.
|
||||
this.logger.log(
|
||||
`EMAIL emitted to RabbitMQ [${process.env.EMAIL_QUEUE ?? "email_queue"}] pattern='send-email' to=${dto.to} subject="${dto.subject ?? ""}" body="${dto.text ?? dto.body ?? dto.html ?? ""}"`,
|
||||
`EMAIL queued to RabbitMQ [${process.env.EMAIL_QUEUE ?? "email_queue"}] pattern='send-email'`,
|
||||
);
|
||||
return {};
|
||||
// Recipient + content are PII — keep them at debug level only.
|
||||
this.logger.debug(
|
||||
`EMAIL payload to=${dto.to} subject="${dto.subject ?? ""}" body="${dto.text ?? dto.body ?? dto.html ?? ""}"`,
|
||||
);
|
||||
return { queued: true };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { ClientsModule, Transport } from '@nestjs/microservices';
|
||||
import { NotificationsController } from './notifications.controller';
|
||||
import { NotificationsService } from './notifications.service';
|
||||
@@ -8,6 +9,8 @@ import { SmsClientService } from './sms-client.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// Required by IamGuard (injects HttpService) used in NotificationsController.
|
||||
HttpModule.register({ timeout: 10_000 }),
|
||||
ClientsModule.register([
|
||||
{
|
||||
name: 'EMAIL_SERVICE',
|
||||
|
||||
@@ -19,8 +19,8 @@ export class NotificationsService {
|
||||
private pushAdapter: PushAdapter,
|
||||
) {
|
||||
this.channels = new Map<NotificationChannelType, NotificationChannel>([
|
||||
['EMAIL', { send: (to, subject, body) => this.emailClient.sendEmail({ to, subject, text: body }).then(() => true) }],
|
||||
['SMS', { send: (to, _subject, body) => this.smsClient.sendSms({ to, message: body }).then(() => true) }],
|
||||
['EMAIL', { send: (to, subject, body) => this.emailClient.sendEmail({ to, subject, text: body }).then((r) => r.queued) }],
|
||||
['SMS', { send: (to, _subject, body) => this.smsClient.sendSms({ to, message: body }).then((r) => r.queued) }],
|
||||
['PUSH', this.pushAdapter as NotificationChannel],
|
||||
]);
|
||||
}
|
||||
@@ -37,27 +37,38 @@ export class NotificationsService {
|
||||
recipient: string,
|
||||
context: Record<string, unknown>,
|
||||
channels?: NotificationChannelType[],
|
||||
): Promise<{ sent: boolean; channels: string[] }> {
|
||||
): Promise<{ queued: boolean; channels: string[] }> {
|
||||
const template = await this.prisma.notificationTemplate.findUnique({
|
||||
where: { code: templateKey },
|
||||
});
|
||||
|
||||
if (!template || !template.active) {
|
||||
this.logger.warn(`Template ${templateKey} not found or inactive`);
|
||||
return { sent: false, channels: [] };
|
||||
return { queued: false, channels: [] };
|
||||
}
|
||||
|
||||
const { subject, body } = this.interpolate(template, context);
|
||||
const targetChannels = channels || await this.getUserPreferredChannels(recipient);
|
||||
const sentChannels: string[] = [];
|
||||
|
||||
// Always create in-app notification
|
||||
if (targetChannels.includes('IN_APP')) {
|
||||
await this.createInAppNotification(recipient, subject, body, context);
|
||||
sentChannels.push('IN_APP');
|
||||
// Channel resolution: explicit argument wins; otherwise honor the template's declared
|
||||
// channel(s); otherwise fall back to the recipient's preferences.
|
||||
let targetChannels: NotificationChannelType[];
|
||||
if (channels) {
|
||||
targetChannels = channels;
|
||||
} else if (template.channel) {
|
||||
targetChannels = this.parseTemplateChannels(template.channel);
|
||||
} else {
|
||||
targetChannels = await this.getUserPreferredChannels(recipient);
|
||||
}
|
||||
|
||||
// Channels successfully handed off (in-app persisted / email+SMS enqueued to RabbitMQ).
|
||||
// NOTE: enqueue is fire-and-forget — this is NOT a delivery confirmation.
|
||||
const queuedChannels: string[] = [];
|
||||
|
||||
if (targetChannels.includes('IN_APP')) {
|
||||
await this.createInAppNotification(recipient, subject, body, context);
|
||||
queuedChannels.push('IN_APP');
|
||||
}
|
||||
|
||||
// Send via other channels
|
||||
for (const channelType of targetChannels) {
|
||||
if (channelType === 'IN_APP') continue;
|
||||
|
||||
@@ -73,13 +84,26 @@ export class NotificationsService {
|
||||
continue;
|
||||
}
|
||||
|
||||
const success = await adapter.send(recipientAddress, subject, body, context);
|
||||
if (success) {
|
||||
sentChannels.push(channelType);
|
||||
const queued = await adapter.send(recipientAddress, subject, body, context);
|
||||
if (queued) {
|
||||
queuedChannels.push(channelType);
|
||||
}
|
||||
}
|
||||
|
||||
return { sent: sentChannels.length > 0, channels: sentChannels };
|
||||
return { queued: queuedChannels.length > 0, channels: queuedChannels };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a template's `channel` column (e.g. "EMAIL" or "EMAIL,SMS") into valid channel
|
||||
* types, always including IN_APP so an in-app record is created.
|
||||
*/
|
||||
private parseTemplateChannels(channel: string): NotificationChannelType[] {
|
||||
const valid: NotificationChannelType[] = ['EMAIL', 'SMS', 'PUSH', 'IN_APP'];
|
||||
const parsed = channel
|
||||
.split(',')
|
||||
.map((c) => c.trim().toUpperCase())
|
||||
.filter((c): c is NotificationChannelType => valid.includes(c as NotificationChannelType));
|
||||
return Array.from(new Set<NotificationChannelType>(['IN_APP', ...parsed]));
|
||||
}
|
||||
|
||||
private async createInAppNotification(
|
||||
@@ -122,16 +146,20 @@ export class NotificationsService {
|
||||
template: { subject?: string | null; bodyTemplate: string },
|
||||
context: Record<string, unknown>,
|
||||
): { subject: string; body: string } {
|
||||
const subject = template.subject || 'Notification';
|
||||
let body = template.bodyTemplate;
|
||||
return {
|
||||
subject: this.applyVars(template.subject || 'Notification', context),
|
||||
body: this.applyVars(template.bodyTemplate, context),
|
||||
};
|
||||
}
|
||||
|
||||
// Simple template interpolation: {{variable}}
|
||||
/** Replaces {{variable}} placeholders in a string with values from the context. */
|
||||
private applyVars(text: string, context: Record<string, unknown>): string {
|
||||
let out = text;
|
||||
for (const [key, value] of Object.entries(context)) {
|
||||
const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
|
||||
body = body.replace(regex, String(value));
|
||||
out = out.replace(regex, String(value));
|
||||
}
|
||||
|
||||
return { subject, body };
|
||||
return out;
|
||||
}
|
||||
|
||||
private async getUserPreferredChannels(recipient: string): Promise<NotificationChannelType[]> {
|
||||
@@ -227,18 +255,210 @@ export class NotificationsService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment succeeded → one combined "payment successful, here is your ticket" notification.
|
||||
* Email carries the full ticket (HTML + QR); SMS is a short pointer to view it. The shallow
|
||||
* event payload is re-fetched with the relations needed to render the ticket.
|
||||
*/
|
||||
@OnEvent('payment.succeeded')
|
||||
async onPaymentSucceeded(payload: any) {
|
||||
const passengerId = payload.booking.passengerId;
|
||||
const bookingId = payload.booking.id;
|
||||
|
||||
const booking = await this.prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
include: {
|
||||
schedule: { include: { originStation: true, destinationStation: true, train: true } },
|
||||
seats: { include: { seat: { include: { coach: { include: { coachType: true } } } } } },
|
||||
},
|
||||
});
|
||||
const ticket = await this.prisma.ticket.findUnique({ where: { bookingId } });
|
||||
|
||||
const ref = booking?.bookingRef ?? payload.booking.bookingRef;
|
||||
const amount = this.formatAmount(booking ?? payload.booking);
|
||||
const currency = (booking ?? payload.booking).displayCurrency ?? 'ETB';
|
||||
const ticketUrl = `${process.env.PORTAL_URL ?? 'http://localhost:5174'}/booking/confirmation?ref=${ref}`;
|
||||
|
||||
// IN_APP — always created.
|
||||
await this.createInAppNotification(
|
||||
passengerId,
|
||||
'Payment successful',
|
||||
`Your payment of ${amount} ${currency} for booking ${ref} was successful. Your ticket is ready.`,
|
||||
{ category: 'PAYMENT', deepLink: `edr://tickets/${ref}` },
|
||||
);
|
||||
|
||||
// Ticket not ready (generation failed/raced) — fall back to a payment-only confirmation.
|
||||
if (!ticket || !booking) {
|
||||
this.logger.warn(`payment.succeeded: ticket not ready for booking ${ref}; sending payment-only confirmation`);
|
||||
const text = `EDR: Payment of ${amount} ${currency} received for booking ${ref}. Your ticket is being prepared.`;
|
||||
await this.deliverEmail(passengerId, `Payment received — ${ref}`, text);
|
||||
await this.deliverSms(passengerId, text);
|
||||
return;
|
||||
}
|
||||
|
||||
// SMS — short pointer (no HTML/QR over SMS).
|
||||
await this.deliverSms(
|
||||
passengerId,
|
||||
`EDR: Booking ${ref} confirmed, ${amount} ${currency} paid. Show ref ${ref} at the gate or view your ticket: ${ticketUrl}`,
|
||||
);
|
||||
|
||||
// EMAIL — rich HTML ticket with plain-text fallback.
|
||||
await this.deliverEmail(
|
||||
passengerId,
|
||||
`Your EDR ticket — ${ref}`,
|
||||
this.buildTicketEmailText(booking, amount, currency, ticketUrl),
|
||||
this.buildTicketEmailHtml(booking, ticket, amount, currency, ticketUrl),
|
||||
);
|
||||
}
|
||||
|
||||
private async deliverEmail(recipient: string, subject: string, text: string, html?: string): Promise<void> {
|
||||
const to = await this.getRecipientAddress(recipient, 'EMAIL');
|
||||
if (!to) {
|
||||
this.logger.warn(`No EMAIL address for recipient: ${recipient}`);
|
||||
return;
|
||||
}
|
||||
await this.emailClient.sendEmail({ to, subject, text, html });
|
||||
}
|
||||
|
||||
private async deliverSms(recipient: string, message: string): Promise<void> {
|
||||
const to = await this.getRecipientAddress(recipient, 'SMS');
|
||||
if (!to) {
|
||||
this.logger.warn(`No SMS address for recipient: ${recipient}`);
|
||||
return;
|
||||
}
|
||||
await this.smsClient.sendSms({ to, message });
|
||||
}
|
||||
|
||||
private buildTicketEmailText(booking: any, amount: string, currency: string, url: string): string {
|
||||
const s = booking.schedule ?? {};
|
||||
const dep = s.departureAt ? new Date(s.departureAt).toLocaleString('en-GB') : 'TBD';
|
||||
const passengers = (booking.seats ?? []).map((bs: any) => bs.passengerName).filter(Boolean).join(', ');
|
||||
return [
|
||||
`Booking ${booking.bookingRef} confirmed.`,
|
||||
`${s.originStation?.name ?? ''} -> ${s.destinationStation?.name ?? ''}`,
|
||||
`Train: ${s.train?.name ?? s.train?.number ?? ''}`,
|
||||
`Departs: ${dep}`,
|
||||
passengers ? `Passengers: ${passengers}` : '',
|
||||
`Total paid: ${amount} ${currency}`,
|
||||
`View your ticket: ${url}`,
|
||||
].filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
private buildTicketEmailHtml(booking: any, ticket: any, amount: string, currency: string, url: string): string {
|
||||
const s = booking.schedule ?? {};
|
||||
const fmt = (d: any) =>
|
||||
d ? new Date(d).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' }) : 'TBD';
|
||||
const seatRows = (booking.seats ?? [])
|
||||
.map((bs: any) => {
|
||||
const coach = bs.seat?.coach?.number ?? '-';
|
||||
const seatNo = bs.seat?.seatNumber ?? '-';
|
||||
const cls = bs.seat?.coach?.coachType?.name ?? '-';
|
||||
return `<tr>
|
||||
<td style="padding:8px;border-bottom:1px solid #eee;">${bs.passengerName ?? ''}</td>
|
||||
<td style="padding:8px;border-bottom:1px solid #eee;">${coach}</td>
|
||||
<td style="padding:8px;border-bottom:1px solid #eee;">${seatNo}</td>
|
||||
<td style="padding:8px;border-bottom:1px solid #eee;">${cls}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
|
||||
<body style="margin:0;font-family:Arial,Helvetica,sans-serif;color:#333;background:#f4f4f4;">
|
||||
<div style="max-width:600px;margin:0 auto;background:#fff;">
|
||||
<div style="background:#0066cc;color:#fff;padding:24px;text-align:center;">
|
||||
<h2 style="margin:0;">Ethio-Djibouti Railway</h2>
|
||||
<p style="margin:8px 0 0;">Payment successful — your ticket is ready</p>
|
||||
</div>
|
||||
<div style="padding:24px;">
|
||||
<p>Booking reference: <strong>${booking.bookingRef}</strong></p>
|
||||
<table style="width:100%;border-collapse:collapse;margin:16px 0;">
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#666;">From</td>
|
||||
<td style="padding:8px 0;text-align:right;"><strong>${s.originStation?.name ?? ''}</strong> (${s.originStation?.code ?? ''})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#666;">To</td>
|
||||
<td style="padding:8px 0;text-align:right;"><strong>${s.destinationStation?.name ?? ''}</strong> (${s.destinationStation?.code ?? ''})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#666;">Train</td>
|
||||
<td style="padding:8px 0;text-align:right;">${s.train?.name ?? s.train?.number ?? ''}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#666;">Departs</td>
|
||||
<td style="padding:8px 0;text-align:right;">${fmt(s.departureAt)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#666;">Arrives</td>
|
||||
<td style="padding:8px 0;text-align:right;">${fmt(s.arrivalAt)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3 style="margin:16px 0 8px;">Passengers</h3>
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<tr style="text-align:left;color:#666;">
|
||||
<th style="padding:8px;border-bottom:2px solid #eee;">Name</th>
|
||||
<th style="padding:8px;border-bottom:2px solid #eee;">Coach</th>
|
||||
<th style="padding:8px;border-bottom:2px solid #eee;">Seat</th>
|
||||
<th style="padding:8px;border-bottom:2px solid #eee;">Class</th>
|
||||
</tr>
|
||||
${seatRows}
|
||||
</table>
|
||||
|
||||
<div style="text-align:center;margin:24px 0;">
|
||||
<p style="color:#666;margin:0 0 8px;">Show this QR code at the gate</p>
|
||||
<img src="${ticket.qrPayload}" alt="Ticket QR code" width="180" height="180" style="border:1px solid #eee;padding:8px;background:#fff;" />
|
||||
</div>
|
||||
|
||||
<table style="width:100%;border-collapse:collapse;border-top:2px solid #eee;margin-top:16px;">
|
||||
<tr>
|
||||
<td style="padding:12px 0;font-size:16px;"><strong>Total paid</strong></td>
|
||||
<td style="padding:12px 0;font-size:16px;text-align:right;"><strong>${amount} ${currency}</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div style="text-align:center;margin:24px 0;">
|
||||
<a href="${url}" style="background:#0066cc;color:#fff;text-decoration:none;padding:12px 28px;border-radius:4px;display:inline-block;">View ticket</a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:center;padding:20px;color:#999;font-size:12px;">
|
||||
<p style="margin:0;">© Ethio-Djibouti Railway. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
@OnEvent('payment.failed')
|
||||
async onPaymentFailed(payload: any) {
|
||||
const booking = payload.booking;
|
||||
await this.send(
|
||||
'payment.succeeded',
|
||||
'payment.failed',
|
||||
booking.passengerId,
|
||||
{
|
||||
bookingRef: booking.bookingRef,
|
||||
amount: this.formatAmount(booking),
|
||||
currency: booking.displayCurrency ?? 'ETB',
|
||||
category: 'PAYMENT',
|
||||
deepLink: `edr://tickets/${booking.bookingRef}`,
|
||||
deepLink: `edr://bookings/${booking.bookingRef}`,
|
||||
},
|
||||
['IN_APP', 'EMAIL', 'SMS'],
|
||||
);
|
||||
}
|
||||
|
||||
@OnEvent('booking.cancelled')
|
||||
async onBookingCancelled(payload: any) {
|
||||
const booking = payload.booking;
|
||||
await this.send(
|
||||
'booking.cancelled',
|
||||
booking.passengerId,
|
||||
{
|
||||
bookingRef: booking.bookingRef,
|
||||
// refundAmount is computed in ETB minor units in BookingsService.cancel().
|
||||
refundAmount: ((payload.refundAmount ?? 0) / 100).toFixed(2),
|
||||
currency: 'ETB',
|
||||
category: 'BOOKING',
|
||||
deepLink: `edr://bookings/${booking.bookingRef}`,
|
||||
},
|
||||
['IN_APP', 'EMAIL', 'SMS'],
|
||||
);
|
||||
|
||||
@@ -30,26 +30,29 @@ export class SmsClientService implements OnApplicationBootstrap {
|
||||
});
|
||||
}
|
||||
|
||||
async sendSms(dto: SingleMessageDto) {
|
||||
async sendSms(dto: SingleMessageDto): Promise<{ queued: boolean }> {
|
||||
if (!this.enabled) {
|
||||
this.logger.warn(`RABBITMQ disabled — skipped SMS to ${dto.to}`);
|
||||
return {};
|
||||
this.logger.warn(`RABBITMQ disabled — skipped SMS`);
|
||||
return { queued: false };
|
||||
}
|
||||
this.smsClient.emit("send-sms", {
|
||||
to: dto.to,
|
||||
text: dto.message,
|
||||
appKey: "IFHCRS-LICENSE-MANAGEMENT",
|
||||
});
|
||||
// Fire-and-forget enqueue: confirms hand-off to RabbitMQ, NOT delivery.
|
||||
this.logger.log(
|
||||
`SMS emitted to RabbitMQ [${process.env.SMS_QUEUE ?? "sms_queue"}] pattern='send-sms' to=${dto.to} text="${dto.message}"`,
|
||||
`SMS queued to RabbitMQ [${process.env.SMS_QUEUE ?? "sms_queue"}] pattern='send-sms'`,
|
||||
);
|
||||
return {};
|
||||
// Recipient + content are PII — debug only.
|
||||
this.logger.debug(`SMS payload to=${dto.to} text="${dto.message}"`);
|
||||
return { queued: true };
|
||||
}
|
||||
|
||||
async sendBulkMessages(dto: BulkMessagesDto) {
|
||||
async sendBulkMessages(dto: BulkMessagesDto): Promise<{ queued: boolean }> {
|
||||
if (!this.enabled) {
|
||||
this.logger.warn(`RABBITMQ disabled — skipped BULK SMS (${dto.messages?.length ?? 0} messages)`);
|
||||
return {};
|
||||
return { queued: false };
|
||||
}
|
||||
const messages = (dto.messages ?? []).map((m) => ({ to: m.to, text: m.message, from: m.from }));
|
||||
this.smsClient.emit("ozeking-bulk-sms", {
|
||||
@@ -57,8 +60,9 @@ export class SmsClientService implements OnApplicationBootstrap {
|
||||
appKey: "IFHCRS-LICENSE-MANAGEMENT",
|
||||
});
|
||||
this.logger.log(
|
||||
`BULK SMS emitted to RabbitMQ [${process.env.SMS_QUEUE ?? "sms_queue"}] pattern='ozeking-bulk-sms' count=${messages.length} messages=${JSON.stringify(messages)}`,
|
||||
`BULK SMS queued to RabbitMQ [${process.env.SMS_QUEUE ?? "sms_queue"}] pattern='ozeking-bulk-sms' count=${messages.length}`,
|
||||
);
|
||||
return {};
|
||||
this.logger.debug(`BULK SMS payload messages=${JSON.stringify(messages)}`);
|
||||
return { queued: true };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,6 +622,12 @@ export class PaymentsService {
|
||||
failureMessage: event.failureMessage,
|
||||
});
|
||||
}
|
||||
const failedBooking = await this.prisma.booking.findUnique({
|
||||
where: { id: event.referenceId },
|
||||
});
|
||||
if (failedBooking) {
|
||||
this.eventEmitter.emit("payment.failed", { booking: failedBooking });
|
||||
}
|
||||
return { processed: true };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user