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:
Ho Ngoc Hai
2026-04-11 20:04:42 +07:00
parent 7008230424
commit 18e50a9649
51 changed files with 1998 additions and 1499 deletions

View File

@@ -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');
}
}
}