fix(api): add error handling to remaining 51 CQRS handlers across 8 modules
Wraps every handler's execute() method in a try-catch block that: - Re-throws DomainExceptions to preserve structured error responses - Logs unexpected infrastructure errors with full context - Throws InternalServerErrorException with Vietnamese user message Modules updated: - auth (11 handlers: register, refresh-token, verify-kyc, deletions, profile queries) - listings (7 handlers: create, moderate, upload, status, search, queries) - payments (5 handlers: create, callback, refund, status, transactions) - subscriptions (7 handlers: create, cancel, upgrade, meter, quota, billing, plans) - analytics (8 handlers: reports, events, market-index, district, heatmap, trends, valuation) - search (9 handlers: saved-search CRUD, reindex, sync, geo-search, properties) - notifications (1 handler: send-notification) - agents (3 handlers: quality-score, dashboard, public-profile) Combined with the previous commit (29 handlers in admin, inquiries, leads, reviews), all 80+ CQRS handlers now have comprehensive error handling. Verification: - pnpm typecheck: 0 errors - pnpm test: 1387 tests passed (228 files) Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { type EventBusService, type LoggerService } from '@modules/shared';
|
||||
import { DomainException, type EventBusService, type LoggerService } from '@modules/shared';
|
||||
import { NotificationSentEvent } from '../../../domain/events/notification-sent.event';
|
||||
import {
|
||||
NOTIFICATION_PREFERENCE_REPOSITORY,
|
||||
@@ -30,69 +30,79 @@ export class SendNotificationHandler implements ICommandHandler<SendNotification
|
||||
) {}
|
||||
|
||||
async execute(command: SendNotificationCommand): Promise<void> {
|
||||
const { userId, channel, templateKey, templateData, recipientAddress } = command;
|
||||
|
||||
// Check user preference
|
||||
const isEnabled = await this.preferenceRepo.isEnabled(userId, channel, templateKey);
|
||||
if (!isEnabled) {
|
||||
this.logger.log(
|
||||
`Notification skipped: user ${userId} disabled ${channel}/${templateKey}`,
|
||||
'SendNotificationHandler',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Render template
|
||||
const rendered = this.templateService.render(templateKey, templateData);
|
||||
|
||||
// Persist notification log
|
||||
const notification = await this.notificationRepo.create({
|
||||
userId,
|
||||
channel,
|
||||
templateKey,
|
||||
subject: rendered.subject,
|
||||
body: rendered.body,
|
||||
metadata: templateData,
|
||||
});
|
||||
|
||||
try {
|
||||
switch (channel) {
|
||||
case 'EMAIL':
|
||||
await this.emailService.send({
|
||||
to: recipientAddress,
|
||||
subject: rendered.subject,
|
||||
html: rendered.body,
|
||||
});
|
||||
break;
|
||||
const { userId, channel, templateKey, templateData, recipientAddress } = command;
|
||||
|
||||
case 'PUSH':
|
||||
await this.fcmService.send({
|
||||
token: recipientAddress,
|
||||
title: rendered.subject,
|
||||
body: rendered.body.replace(/<[^>]*>/g, ''), // Strip HTML for push
|
||||
});
|
||||
break;
|
||||
|
||||
case 'SMS':
|
||||
case 'ZALO_OA':
|
||||
// Placeholder — these channels will be implemented when providers are integrated
|
||||
this.logger.warn(
|
||||
`Channel ${channel} not yet implemented — notification logged but not sent`,
|
||||
'SendNotificationHandler',
|
||||
);
|
||||
await this.notificationRepo.updateStatus(notification.id, 'PENDING');
|
||||
return;
|
||||
// Check user preference
|
||||
const isEnabled = await this.preferenceRepo.isEnabled(userId, channel, templateKey);
|
||||
if (!isEnabled) {
|
||||
this.logger.log(
|
||||
`Notification skipped: user ${userId} disabled ${channel}/${templateKey}`,
|
||||
'SendNotificationHandler',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.notificationRepo.updateStatus(notification.id, 'SENT');
|
||||
this.eventBus.publish(new NotificationSentEvent(notification.id, userId, channel, templateKey));
|
||||
// Render template
|
||||
const rendered = this.templateService.render(templateKey, templateData);
|
||||
|
||||
// Persist notification log
|
||||
const notification = await this.notificationRepo.create({
|
||||
userId,
|
||||
channel,
|
||||
templateKey,
|
||||
subject: rendered.subject,
|
||||
body: rendered.body,
|
||||
metadata: templateData,
|
||||
});
|
||||
|
||||
try {
|
||||
switch (channel) {
|
||||
case 'EMAIL':
|
||||
await this.emailService.send({
|
||||
to: recipientAddress,
|
||||
subject: rendered.subject,
|
||||
html: rendered.body,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'PUSH':
|
||||
await this.fcmService.send({
|
||||
token: recipientAddress,
|
||||
title: rendered.subject,
|
||||
body: rendered.body.replace(/<[^>]*>/g, ''), // Strip HTML for push
|
||||
});
|
||||
break;
|
||||
|
||||
case 'SMS':
|
||||
case 'ZALO_OA':
|
||||
// Placeholder — these channels will be implemented when providers are integrated
|
||||
this.logger.warn(
|
||||
`Channel ${channel} not yet implemented — notification logged but not sent`,
|
||||
'SendNotificationHandler',
|
||||
);
|
||||
await this.notificationRepo.updateStatus(notification.id, 'PENDING');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.notificationRepo.updateStatus(notification.id, 'SENT');
|
||||
this.eventBus.publish(new NotificationSentEvent(notification.id, userId, channel, templateKey));
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
await this.notificationRepo.updateStatus(notification.id, 'FAILED', errorMsg);
|
||||
this.logger.error(
|
||||
`Notification ${notification.id} failed on ${channel}: ${errorMsg}`,
|
||||
'SendNotificationHandler',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
await this.notificationRepo.updateStatus(notification.id, 'FAILED', errorMsg);
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Notification ${notification.id} failed on ${channel}: ${errorMsg}`,
|
||||
'SendNotificationHandler',
|
||||
`Failed to send notification: ${error instanceof Error ? error.message : error}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
this.constructor.name,
|
||||
);
|
||||
throw new InternalServerErrorException('Không thể gửi thông báo');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user