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';
|
ALTER TABLE "Ticket" ALTER COLUMN "status" SET DEFAULT 'ACTIVE';
|
||||||
|
|
||||||
-- AlterTable
|
-- 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;
|
ALTER TABLE "User" ALTER COLUMN "gender" SET DATA TYPE TEXT;
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
|
|||||||
@@ -550,18 +550,22 @@ async function seedSegmentFares() {
|
|||||||
|
|
||||||
async function seedNotificationTemplates() {
|
async function seedNotificationTemplates() {
|
||||||
console.log('\n🔔 Seeding notification templates...');
|
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 = [
|
const templates = [
|
||||||
{ id: uuidv4(), code: 'BOOKING_CONFIRMED', channel: 'EMAIL', subject: 'Booking Confirmed', bodyTemplate: 'Your booking {{bookingRef}} is confirmed for {{date}}' },
|
{ id: uuidv4(), code: 'booking.created', channel: 'EMAIL', subject: 'Booking Confirmed', bodyTemplate: 'Your booking {{bookingRef}} is confirmed. Total: {{amount}} {{currency}}.' },
|
||||||
{ id: uuidv4(), code: 'PAYMENT_RECEIVED', channel: 'SMS', bodyTemplate: 'Payment ETB {{amount}} received for {{bookingRef}}' },
|
{ id: uuidv4(), code: 'payment.succeeded', channel: 'SMS', subject: 'Payment Received', bodyTemplate: 'Payment of {{amount}} {{currency}} received for booking {{bookingRef}}.' },
|
||||||
{ id: uuidv4(), code: 'TRIP_DEPARTURE', channel: 'PUSH', bodyTemplate: 'Your trip {{route}} departs in {{minutes}} minutes' },
|
// Templates below are not wired to handlers yet (Phase 2 — full event coverage).
|
||||||
{ id: uuidv4(), code: 'TRIP_DELAY', channel: 'EMAIL', subject: 'Trip Delayed', bodyTemplate: 'Your trip {{route}} is delayed by {{delayMinutes}} minutes' },
|
{ id: uuidv4(), code: 'trip.departure', channel: 'PUSH', subject: 'Trip Departing Soon', bodyTemplate: 'Your trip {{route}} departs in {{minutes}} minutes' },
|
||||||
{ id: uuidv4(), code: 'PROMOTION', channel: 'PUSH', bodyTemplate: 'Get {{percentOff}}% off on {{route}}' },
|
{ 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) {
|
for (const t of templates) {
|
||||||
await prisma.notificationTemplate.upsert({
|
await prisma.notificationTemplate.upsert({
|
||||||
where: { code: t.code },
|
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,
|
create: t,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -684,7 +688,8 @@ async function main() {
|
|||||||
['system users', seedSystemUsers],
|
['system users', seedSystemUsers],
|
||||||
['fare rules', seedFareRules],
|
['fare rules', seedFareRules],
|
||||||
['segment fares', seedSegmentFares],
|
['segment fares', seedSegmentFares],
|
||||||
['currency', seedCurrency]
|
['currency', seedCurrency],
|
||||||
|
['notification templates', seedNotificationTemplates]
|
||||||
];
|
];
|
||||||
|
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
|
|||||||
@@ -40,18 +40,5 @@ export class SendEmail {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
context?: Record<string, any>;
|
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) {
|
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", {
|
this.emailServiceClient.emit("send-email", {
|
||||||
...dto,
|
...dto,
|
||||||
appKey: "IFHCRS-LICENSE-MANAGEMENT",
|
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 {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,7 +169,12 @@ export class NotificationsService {
|
|||||||
private async getUserPreferredChannels(recipient: string): Promise<NotificationChannelType[]> {
|
private async getUserPreferredChannels(recipient: string): Promise<NotificationChannelType[]> {
|
||||||
const user = await this.prisma.user.findFirst({
|
const user = await this.prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
OR: [{ id: recipient }, { email: recipient }, { phone: recipient }],
|
OR: [
|
||||||
|
{ id: recipient },
|
||||||
|
{ email: recipient },
|
||||||
|
{ phone: recipient },
|
||||||
|
{ passenger: { id: recipient } },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
include: { preferences: true },
|
include: { preferences: true },
|
||||||
});
|
});
|
||||||
@@ -192,7 +197,12 @@ export class NotificationsService {
|
|||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const user = await this.prisma.user.findFirst({
|
const user = await this.prisma.user.findFirst({
|
||||||
where: {
|
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')
|
@OnEvent('booking.created')
|
||||||
async onBookingCreated(payload: any) {
|
async onBookingCreated(payload: any) {
|
||||||
|
const booking = payload.booking;
|
||||||
await this.send(
|
await this.send(
|
||||||
'booking.created',
|
'booking.created',
|
||||||
payload.booking.passengerId,
|
booking.passengerId,
|
||||||
{
|
{
|
||||||
bookingRef: payload.booking.bookingRef,
|
bookingRef: booking.bookingRef,
|
||||||
|
amount: this.formatAmount(booking),
|
||||||
|
currency: booking.displayCurrency ?? 'ETB',
|
||||||
category: 'BOOKING',
|
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')
|
@OnEvent('payment.succeeded')
|
||||||
async onPaymentSucceeded(payload: any) {
|
async onPaymentSucceeded(payload: any) {
|
||||||
|
const booking = payload.booking;
|
||||||
await this.send(
|
await this.send(
|
||||||
'payment.succeeded',
|
'payment.succeeded',
|
||||||
payload.booking.passengerId,
|
booking.passengerId,
|
||||||
{
|
{
|
||||||
bookingRef: payload.booking.bookingRef,
|
bookingRef: booking.bookingRef,
|
||||||
|
amount: this.formatAmount(booking),
|
||||||
|
currency: booking.displayCurrency ?? 'ETB',
|
||||||
category: 'PAYMENT',
|
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) {
|
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", {
|
this.smsClient.emit("send-sms", {
|
||||||
...dto,
|
...dto,
|
||||||
appKey: "IFHCRS-LICENSE-MANAGEMENT",
|
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 {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendBulkMessages(dto: BulkMessagesDto) {
|
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", {
|
this.smsClient.emit("ozeking-bulk-sms", {
|
||||||
...dto,
|
...dto,
|
||||||
appKey: "IFHCRS-LICENSE-MANAGEMENT",
|
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 {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user