IAM, package, luggage, app health, rate limit, and more

This commit is contained in:
Stephanos A
2026-06-24 14:02:51 +03:00
parent e10b013b62
commit 86760933e8
63 changed files with 3475 additions and 280 deletions

View File

@@ -439,6 +439,126 @@ export class NotificationsService {
</html>`;
}
async sendBoardingPassNotification(params: {
passengerId: string | null;
contactEmail: string | null;
contactPhone: string | null;
bookingRef: string;
leg: string | null;
booking: any;
ticket: any;
}): Promise<void> {
const { passengerId, contactEmail, contactPhone, bookingRef, leg, booking, ticket } = params;
// Resolve contact — prefer IAM user record, fall back to booking contact fields
let email: string | null = contactEmail ?? null;
let phone: string | null = contactPhone ?? null;
if (passengerId) {
const resolved = await this.getRecipientAddress(passengerId, 'EMAIL').catch(() => null);
const resolvedPhone = await this.getRecipientAddress(passengerId, 'SMS').catch(() => null);
if (resolved) email = resolved;
if (resolvedPhone) phone = resolvedPhone;
}
const s = booking.schedule ?? {};
const fmt = (d: any) =>
d ? new Date(d).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' }) : 'TBD';
const legLabel = leg ? ` (${leg.replace(/_/g, ' ')})` : '';
const origin = s.originStation?.name ?? '';
const dest = s.destinationStation?.name ?? '';
const train = s.train?.name ?? s.train?.number ?? '';
const dep = fmt(s.departureAt);
const arr = fmt(s.arrivalAt);
const seats: { name: string; coach: string; seat: string; cls: string }[] = (booking.seats ?? []).map((bs: any) => ({
name: bs.passengerName ?? '',
coach: bs.seat?.coach?.number ?? '-',
seat: bs.seat?.seatNumber ?? '-',
cls: bs.seat?.coach?.coachType?.name ?? '-',
}));
const seatLines = seats.map(s => ` ${s.name} — Coach ${s.coach}, Seat ${s.seat} (${s.cls})`).join('\n');
const smsText =
`EDR Boarding Pass${legLabel}\n` +
`Ref: ${bookingRef}\n` +
`${origin}${dest}\n` +
`Train: ${train} | Dep: ${dep}\n` +
(seatLines ? `${seatLines}\n` : '') +
`Barcode: ${ticket.barcodePayload}`;
if (phone) {
await this.smsClient.sendSms({ to: phone, message: smsText }).catch((e) =>
this.logger.error(`Boarding pass SMS failed for ${bookingRef}: ${e?.message}`),
);
}
if (email) {
const seatRows = seats
.map(
(s) =>
`<tr>
<td style="padding:8px;border-bottom:1px solid #eee;">${s.name}</td>
<td style="padding:8px;border-bottom:1px solid #eee;">${s.coach}</td>
<td style="padding:8px;border-bottom:1px solid #eee;">${s.seat}</td>
<td style="padding:8px;border-bottom:1px solid #eee;">${s.cls}</td>
</tr>`,
)
.join('');
const html = `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></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;">Boarding Pass${legLabel}</p>
</div>
<div style="padding:24px;">
<p>Booking reference: <strong>${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="text-align:right;"><strong>${origin}</strong></td></tr>
<tr><td style="padding:8px 0;color:#666;">To</td><td style="text-align:right;"><strong>${dest}</strong></td></tr>
<tr><td style="padding:8px 0;color:#666;">Train</td><td style="text-align:right;">${train}</td></tr>
<tr><td style="padding:8px 0;color:#666;">Departs</td><td style="text-align:right;">${dep}</td></tr>
<tr><td style="padding:8px 0;color:#666;">Arrives</td><td style="text-align:right;">${arr}</td></tr>
</table>
<h3 style="margin:16px 0 8px;">Passengers</h3>
<table style="width:100%;border-collapse:collapse;">
<tr style="color:#666;text-align:left;">
<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;">QR code for gate scanning</p>
<img src="${ticket.qrPayload}" alt="Boarding pass QR" width="180" height="180"
style="border:1px solid #eee;padding:8px;background:#fff;" />
<p style="color:#666;font-size:12px;margin:8px 0 0;">Barcode: <strong>${ticket.barcodePayload}</strong></p>
</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>`;
const textFallback =
`EDR Boarding Pass${legLabel}\nRef: ${bookingRef}\n${origin}${dest}\n` +
`Train: ${train} | Departs: ${dep} | Arrives: ${arr}\n${seatLines}\n` +
`Barcode: ${ticket.barcodePayload}`;
await this.emailClient
.sendEmail({ to: email, subject: `EDR Boarding Pass — ${bookingRef}${legLabel}`, text: textFallback, html })
.catch((e) => this.logger.error(`Boarding pass email failed for ${bookingRef}: ${e?.message}`));
}
}
@OnEvent('payment.failed')
async onPaymentFailed(payload: any) {
const booking = payload.booking;