From 090a8414559025837a33a7f58b8df69d58a8bd5b Mon Sep 17 00:00:00 2001 From: portable Date: Sat, 27 Jun 2026 08:35:22 +0100 Subject: [PATCH] perf: parallelize notification delivery loops (#765 #766) perf: eliminate redundant DB reads in transaction-documents (#768) --- src/notifications/notifications.service.ts | 101 ++++++++---------- src/properties/property-expiry.service.ts | 70 ++++++------ .../transaction-documents.service.ts | 16 ++- 3 files changed, 93 insertions(+), 94 deletions(-) diff --git a/src/notifications/notifications.service.ts b/src/notifications/notifications.service.ts index 157ef18..4bf95eb 100644 --- a/src/notifications/notifications.service.ts +++ b/src/notifications/notifications.service.ts @@ -35,63 +35,50 @@ export class NotificationsService { { user: transaction.seller, role: 'Seller' }, ]; - for (const party of parties) { - const { user, role } = party; - const preferences = user.preferences; - - const title = `Transaction ${transaction.status}`; - const message = `Your transaction for property "${transaction.property.title}" has been updated to ${transaction.status}.`; - - // 1. In-App Notification - const canInApp = await this.userPreferencesService.shouldDeliverNotification( - user.id, - 'TRANSACTION_UPDATE', - 'inApp', - ); - if (canInApp) { - await this.sendNotification(user.id, title, message, 'TRANSACTION_UPDATE', { - transactionId: transaction.id, - status: transaction.status, - }); - } - - // 2. Email Notification with template - const canEmail = await this.userPreferencesService.shouldDeliverNotification( - user.id, - 'TRANSACTION_UPDATE', - 'email', - ); - if (canEmail) { - await this.emailService.sendTransactionStatusEmail(user.email, transaction.status, { - transactionId: transaction.id, - propertyTitle: transaction.property.title, - propertyAddress: `${transaction.property.address}, ${transaction.property.city}, ${transaction.property.state} ${transaction.property.zipCode}`, - buyerName: transaction.buyer.firstName - ? `${transaction.buyer.firstName} ${transaction.buyer.lastName || ''}` - : transaction.buyer.email, - sellerName: transaction.seller.firstName - ? `${transaction.seller.firstName} ${transaction.seller.lastName || ''}` - : transaction.seller.email, - amount: `$${Number(transaction.amount || 0).toLocaleString()}`, - completionDate: - transaction.status === 'COMPLETED' ? new Date().toLocaleDateString() : undefined, - blockchainTxHash: transaction.blockchainTxHash || undefined, - cancellationReason: transaction.cancellationReason || undefined, - cancelledDate: - transaction.status === 'CANCELLED' ? new Date().toLocaleDateString() : undefined, - }); - } - - // 3. SMS Notification - const canSms = await this.userPreferencesService.shouldDeliverNotification( - user.id, - 'TRANSACTION_UPDATE', - 'sms', - ); - if (canSms && user.phone) { - await this.smsService.sendSms(user.phone, message); - } - } + await Promise.all( + parties.map(async ({ user, role }) => { + const title = `Transaction ${transaction.status}`; + const message = `Your transaction for property "${transaction.property.title}" has been updated to ${transaction.status}.`; + + const [canInApp, canEmail, canSms] = await Promise.all([ + this.userPreferencesService.shouldDeliverNotification(user.id, 'TRANSACTION_UPDATE', 'inApp'), + this.userPreferencesService.shouldDeliverNotification(user.id, 'TRANSACTION_UPDATE', 'email'), + this.userPreferencesService.shouldDeliverNotification(user.id, 'TRANSACTION_UPDATE', 'sms'), + ]); + + await Promise.all([ + canInApp + ? this.sendNotification(user.id, title, message, 'TRANSACTION_UPDATE', { + transactionId: transaction.id, + status: transaction.status, + }) + : Promise.resolve(), + canEmail + ? this.emailService.sendTransactionStatusEmail(user.email, transaction.status, { + transactionId: transaction.id, + propertyTitle: transaction.property.title, + propertyAddress: `${transaction.property.address}, ${transaction.property.city}, ${transaction.property.state} ${transaction.property.zipCode}`, + buyerName: transaction.buyer.firstName + ? `${transaction.buyer.firstName} ${transaction.buyer.lastName || ''}` + : transaction.buyer.email, + sellerName: transaction.seller.firstName + ? `${transaction.seller.firstName} ${transaction.seller.lastName || ''}` + : transaction.seller.email, + amount: `$${Number(transaction.amount || 0).toLocaleString()}`, + completionDate: + transaction.status === 'COMPLETED' ? new Date().toLocaleDateString() : undefined, + blockchainTxHash: transaction.blockchainTxHash || undefined, + cancellationReason: transaction.cancellationReason || undefined, + cancelledDate: + transaction.status === 'CANCELLED' ? new Date().toLocaleDateString() : undefined, + }) + : Promise.resolve(), + canSms && user.phone + ? this.smsService.sendSms(user.phone, message) + : Promise.resolve(), + ]); + }), + ); } async sendNotification( diff --git a/src/properties/property-expiry.service.ts b/src/properties/property-expiry.service.ts index 2b99cd9..fb7e0e0 100644 --- a/src/properties/property-expiry.service.ts +++ b/src/properties/property-expiry.service.ts @@ -75,23 +75,24 @@ export class PropertyExpiryService { }, }); - for (const property of properties) { - const title = `Property Listing Expiring Soon`; - const message = `Your property "${property.title}" is scheduled to expire in 7 days. Consider renewing it to keep it active.`; - - // Send notification to property owner - await this.notificationsService.sendNotification( - property.ownerId, - title, - message, - 'PROPERTY_EXPIRY_WARNING', - { - propertyId: property.id, - propertyTitle: property.title, - expiryDate: property.expiryDate, - }, - ); - } + await Promise.all( + properties.map((property) => { + const title = `Property Listing Expiring Soon`; + const message = `Your property "${property.title}" is scheduled to expire in 7 days. Consider renewing it to keep it active.`; + + return this.notificationsService.sendNotification( + property.ownerId, + title, + message, + 'PROPERTY_EXPIRY_WARNING', + { + propertyId: property.id, + propertyTitle: property.title, + expiryDate: property.expiryDate, + }, + ); + }), + ); } /** @@ -120,23 +121,24 @@ export class PropertyExpiryService { }, }); - for (const property of properties) { - const title = `Property Listing Expired`; - const message = `Your property "${property.title}" has expired due to reaching its expiry date. You can renew it to make it active again.`; - - // Send notification to property owner - await this.notificationsService.sendNotification( - property.ownerId, - title, - message, - 'PROPERTY_EXPIRED', - { - propertyId: property.id, - propertyTitle: property.title, - expiryDate: property.expiryDate, - }, - ); - } + await Promise.all( + properties.map((property) => { + const title = `Property Listing Expired`; + const message = `Your property "${property.title}" has expired due to reaching its expiry date. You can renew it to make it active again.`; + + return this.notificationsService.sendNotification( + property.ownerId, + title, + message, + 'PROPERTY_EXPIRED', + { + propertyId: property.id, + propertyTitle: property.title, + expiryDate: property.expiryDate, + }, + ); + }), + ); } /** diff --git a/src/transactions/transaction-documents.service.ts b/src/transactions/transaction-documents.service.ts index 65d3211..b223ad0 100644 --- a/src/transactions/transaction-documents.service.ts +++ b/src/transactions/transaction-documents.service.ts @@ -80,7 +80,11 @@ export class TransactionDocumentsService { const tx = await this.ensureTransactionExists(transactionId); this.assertAccess(tx, userId, userRole); - const doc = await this.findOne(transactionId, documentId, userId, userRole); + const doc = await this.prisma.document.findFirst({ + where: { id: documentId, transactionId }, + include: { versions: { orderBy: { versionNumber: 'asc' } } }, + }); + if (!doc) throw new NotFoundException('Document not found for this transaction'); const nextVersion = (doc.versions?.length ?? 0) + 1; @@ -111,7 +115,10 @@ export class TransactionDocumentsService { const tx = await this.ensureTransactionExists(transactionId); this.assertAccess(tx, userId, userRole); - await this.findOne(transactionId, documentId, userId, userRole); + const doc = await this.prisma.document.findFirst({ + where: { id: documentId, transactionId }, + }); + if (!doc) throw new NotFoundException('Document not found for this transaction'); return this.prisma.documentVersion.findMany({ where: { documentId }, orderBy: { versionNumber: 'asc' }, @@ -126,7 +133,10 @@ export class TransactionDocumentsService { const tx = await this.ensureTransactionExists(transactionId); this.assertAccess(tx, userId, userRole); - await this.findOne(transactionId, documentId, userId, userRole); + const doc = await this.prisma.document.findFirst({ + where: { id: documentId, transactionId }, + }); + if (!doc) throw new NotFoundException('Document not found for this transaction'); return this.prisma.document.delete({ where: { id: documentId } }); }