mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
fix: ( notification ) add notification templet and fix issues
This commit is contained in:
@@ -141,6 +141,9 @@ ALTER TABLE "SeatClass" ALTER COLUMN "baseFareMinor" SET DEFAULT 0;
|
||||
ALTER TABLE "Ticket" ALTER COLUMN "status" SET DEFAULT 'ACTIVE';
|
||||
|
||||
-- AlterTable
|
||||
-- gender is created here on a clean migration history (no prior migration adds it);
|
||||
-- on an already-drifted DB where it exists as varchar, normalize it to TEXT.
|
||||
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "gender" TEXT;
|
||||
ALTER TABLE "User" ALTER COLUMN "gender" SET DATA TYPE TEXT;
|
||||
|
||||
-- CreateIndex
|
||||
|
||||
@@ -550,18 +550,22 @@ async function seedSegmentFares() {
|
||||
|
||||
async function seedNotificationTemplates() {
|
||||
console.log('\n🔔 Seeding notification templates...');
|
||||
// NOTE: `code` must match the templateKey passed by NotificationsService.send(...).
|
||||
// The event-driven handlers use the dotted event names (booking.created, payment.succeeded).
|
||||
const templates = [
|
||||
{ id: uuidv4(), code: 'BOOKING_CONFIRMED', channel: 'EMAIL', subject: 'Booking Confirmed', bodyTemplate: 'Your booking {{bookingRef}} is confirmed for {{date}}' },
|
||||
{ id: uuidv4(), code: 'PAYMENT_RECEIVED', channel: 'SMS', bodyTemplate: 'Payment ETB {{amount}} received for {{bookingRef}}' },
|
||||
{ id: uuidv4(), code: 'TRIP_DEPARTURE', channel: 'PUSH', 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' },
|
||||
{ id: uuidv4(), code: 'PROMOTION', channel: 'PUSH', bodyTemplate: 'Get {{percentOff}}% off on {{route}}' },
|
||||
{ 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}}.' },
|
||||
// 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' },
|
||||
{ id: uuidv4(), code: 'promotion.offer', channel: 'PUSH', subject: 'Special Offer', bodyTemplate: 'Get {{percentOff}}% off on {{route}}' },
|
||||
];
|
||||
|
||||
for (const t of templates) {
|
||||
await prisma.notificationTemplate.upsert({
|
||||
where: { code: t.code },
|
||||
update: {},
|
||||
// Refresh the editable fields on re-seed so template tweaks actually take effect.
|
||||
update: { channel: t.channel, subject: t.subject ?? null, bodyTemplate: t.bodyTemplate, active: true },
|
||||
create: t,
|
||||
});
|
||||
}
|
||||
@@ -684,7 +688,8 @@ async function main() {
|
||||
['system users', seedSystemUsers],
|
||||
['fare rules', seedFareRules],
|
||||
['segment fares', seedSegmentFares],
|
||||
['currency', seedCurrency]
|
||||
['currency', seedCurrency],
|
||||
['notification templates', seedNotificationTemplates]
|
||||
];
|
||||
|
||||
let failed = 0;
|
||||
|
||||
@@ -40,18 +40,5 @@ export class SendEmail {
|
||||
@IsOptional()
|
||||
context?: Record<string, any>;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
templateName?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
from?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
replyTo?: string;
|
||||
}
|
||||
|
||||
@@ -29,11 +29,17 @@ export class EmailClientService implements OnApplicationBootstrap {
|
||||
}
|
||||
|
||||
async sendEmail(dto: SendEmail) {
|
||||
if (!this.enabled) return {};
|
||||
if (!this.enabled) {
|
||||
this.logger.warn(`RABBITMQ disabled — skipped EMAIL to ${dto.to}`);
|
||||
return {};
|
||||
}
|
||||
this.emailServiceClient.emit("send-email", {
|
||||
...dto,
|
||||
appKey: "IFHCRS-LICENSE-MANAGEMENT",
|
||||
});
|
||||
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 ?? ""}"`,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,12 @@ export class NotificationsService {
|
||||
private async getUserPreferredChannels(recipient: string): Promise<NotificationChannelType[]> {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ id: recipient }, { email: recipient }, { phone: recipient }],
|
||||
OR: [
|
||||
{ id: recipient },
|
||||
{ email: recipient },
|
||||
{ phone: recipient },
|
||||
{ passenger: { id: recipient } },
|
||||
],
|
||||
},
|
||||
include: { preferences: true },
|
||||
});
|
||||
@@ -192,7 +197,12 @@ export class NotificationsService {
|
||||
): Promise<string | null> {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ id: recipient }, { email: recipient }, { phone: recipient }],
|
||||
OR: [
|
||||
{ id: recipient },
|
||||
{ email: recipient },
|
||||
{ phone: recipient },
|
||||
{ passenger: { id: recipient } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -239,27 +249,46 @@ export class NotificationsService {
|
||||
|
||||
@OnEvent('booking.created')
|
||||
async onBookingCreated(payload: any) {
|
||||
const booking = payload.booking;
|
||||
await this.send(
|
||||
'booking.created',
|
||||
payload.booking.passengerId,
|
||||
booking.passengerId,
|
||||
{
|
||||
bookingRef: payload.booking.bookingRef,
|
||||
bookingRef: booking.bookingRef,
|
||||
amount: this.formatAmount(booking),
|
||||
currency: booking.displayCurrency ?? 'ETB',
|
||||
category: 'BOOKING',
|
||||
deepLink: `edr://bookings/${payload.booking.bookingRef}`,
|
||||
deepLink: `edr://bookings/${booking.bookingRef}`,
|
||||
},
|
||||
// For now, always notify the travelling passenger on every channel.
|
||||
['IN_APP', 'EMAIL', 'SMS'],
|
||||
);
|
||||
}
|
||||
|
||||
@OnEvent('payment.succeeded')
|
||||
async onPaymentSucceeded(payload: any) {
|
||||
const booking = payload.booking;
|
||||
await this.send(
|
||||
'payment.succeeded',
|
||||
payload.booking.passengerId,
|
||||
booking.passengerId,
|
||||
{
|
||||
bookingRef: payload.booking.bookingRef,
|
||||
bookingRef: booking.bookingRef,
|
||||
amount: this.formatAmount(booking),
|
||||
currency: booking.displayCurrency ?? 'ETB',
|
||||
category: 'PAYMENT',
|
||||
deepLink: `edr://tickets/${payload.booking.bookingRef}`,
|
||||
deepLink: `edr://tickets/${booking.bookingRef}`,
|
||||
},
|
||||
['IN_APP', 'EMAIL', 'SMS'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a booking's payable amount from minor units into a major-unit string.
|
||||
* Money is stored as integer minor units (e.g. 59600 santim) to avoid floating-point
|
||||
* drift; we divide by 100 only here, at the display edge. e.g. 59600 -> "596.00".
|
||||
*/
|
||||
private formatAmount(booking: any): string {
|
||||
const minor = booking.displayTotalMinor ?? booking.totalMinor ?? 0;
|
||||
return (minor / 100).toFixed(2);
|
||||
}
|
||||
}
|
||||
@@ -31,20 +31,34 @@ export class SmsClientService implements OnApplicationBootstrap {
|
||||
}
|
||||
|
||||
async sendSms(dto: SingleMessageDto) {
|
||||
if (!this.enabled) return {};
|
||||
if (!this.enabled) {
|
||||
this.logger.warn(`RABBITMQ disabled — skipped SMS to ${dto.to}`);
|
||||
return {};
|
||||
}
|
||||
this.smsClient.emit("send-sms", {
|
||||
...dto,
|
||||
appKey: "IFHCRS-LICENSE-MANAGEMENT",
|
||||
});
|
||||
this.logger.log(
|
||||
`SMS emitted to RabbitMQ [${process.env.SMS_QUEUE ?? "sms_queue"}] pattern='send-sms' to=${dto.to} message="${dto.message}"`,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
async sendBulkMessages(dto: BulkMessagesDto) {
|
||||
if (!this.enabled) return {};
|
||||
if (!this.enabled) {
|
||||
this.logger.warn(`RABBITMQ disabled — skipped BULK SMS (${dto.messages?.length ?? 0} messages)`);
|
||||
return {};
|
||||
}
|
||||
this.smsClient.emit("ozeking-bulk-sms", {
|
||||
...dto,
|
||||
appKey: "IFHCRS-LICENSE-MANAGEMENT",
|
||||
});
|
||||
this.logger.log(
|
||||
`BULK SMS emitted to RabbitMQ [${process.env.SMS_QUEUE ?? "sms_queue"}] pattern='ozeking-bulk-sms' count=${dto.messages?.length ?? 0} messages=${JSON.stringify(
|
||||
(dto.messages ?? []).map((m) => ({ to: m.to, message: m.message })),
|
||||
)}`,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user