From 7b71e29d39c40e5055a7d277a7ef9ed72800d501 Mon Sep 17 00:00:00 2001 From: Justice Date: Sun, 31 May 2026 00:32:43 +0100 Subject: [PATCH] refactor(notifications): extract notification creation helper --- .../src/insurance/insurance.service.ts | 32 ++--- .../notifications/notification.helper.spec.ts | 87 ++++++++++++++ .../src/notifications/notification.helper.ts | 111 ++++++++++++++++++ .../notifications/notifications.service.ts | 5 +- .../backend/src/rewards/rewards.service.ts | 17 +-- .../backend/src/vaults/vaults.service.ts | 40 ++++--- 6 files changed, 247 insertions(+), 45 deletions(-) create mode 100644 harvest-finance/backend/src/notifications/notification.helper.spec.ts create mode 100644 harvest-finance/backend/src/notifications/notification.helper.ts diff --git a/harvest-finance/backend/src/insurance/insurance.service.ts b/harvest-finance/backend/src/insurance/insurance.service.ts index 7ef2beb85..005db2586 100644 --- a/harvest-finance/backend/src/insurance/insurance.service.ts +++ b/harvest-finance/backend/src/insurance/insurance.service.ts @@ -16,7 +16,7 @@ import { } from '../database/entities/insurance-subscription.entity'; import { FarmVault } from '../database/entities/farm-vault.entity'; import { NotificationsService } from '../notifications/notifications.service'; -import { NotificationType } from '../database/entities/notification.entity'; +import { NotificationHelper } from '../notifications/notification.helper'; import { RiskAssessmentDto, SubscribeInsuranceDto } from './dto/insurance.dto'; /** ---------- Types returned to controllers ---------- */ @@ -283,13 +283,15 @@ export class InsuranceService { const saved = await this.subscriptionRepo.save(subscription); - // Notify the farmer - await this.notificationsService.create({ - userId, - title: 'Insurance Plan Activated', - message: `Your "${plan.name}" crop insurance is now active. Monthly premium: $${monthlyPremium.toFixed(2)}. Coverage period: ${coverageStart.toLocaleDateString()} – ${coverageEnd.toLocaleDateString()}.`, - type: NotificationType.INSURANCE, - }); + await this.notificationsService.create( + NotificationHelper.insuranceActivated({ + userId, + planName: plan.name, + monthlyPremium, + coverageStart, + coverageEnd, + }), + ); return saved; } @@ -326,12 +328,14 @@ export class InsuranceService { .getMany(); for (const sub of expiring) { - await this.notificationsService.create({ - userId: sub.userId, - title: 'Insurance Renewal Reminder', - message: `Your "${sub.plan.name}" insurance coverage expires on ${new Date(sub.coverageEnd).toLocaleDateString()}. Renew now to maintain protection for your ${sub.cropType} crop.`, - type: NotificationType.INSURANCE, - }); + await this.notificationsService.create( + NotificationHelper.insuranceRenewalReminder({ + userId: sub.userId, + planName: sub.plan.name, + coverageEnd: sub.coverageEnd, + cropType: sub.cropType, + }), + ); } return expiring.length; diff --git a/harvest-finance/backend/src/notifications/notification.helper.spec.ts b/harvest-finance/backend/src/notifications/notification.helper.spec.ts new file mode 100644 index 000000000..307056831 --- /dev/null +++ b/harvest-finance/backend/src/notifications/notification.helper.spec.ts @@ -0,0 +1,87 @@ +import { NotificationType } from '../database/entities/notification.entity'; +import { NotificationHelper } from './notification.helper'; + +describe('NotificationHelper', () => { + it('builds large deposit admin alerts', () => { + expect( + NotificationHelper.largeDepositAlert({ + amount: 12000, + vaultName: 'Maize Vault', + }), + ).toEqual({ + title: 'Large Deposit Alert', + message: + 'A large deposit of 12000 has been initiated for vault Maize Vault.', + type: NotificationType.LARGE_TRANSACTION, + adminOnly: true, + }); + }); + + it('builds user deposit confirmation notifications', () => { + expect( + NotificationHelper.depositConfirmed({ + userId: 'user-1', + amount: '500.25', + vaultId: 'vault-1', + }), + ).toEqual({ + userId: 'user-1', + title: 'Deposit Confirmed', + message: 'Your deposit of 500.25 into vault vault-1 has been confirmed.', + type: NotificationType.DEPOSIT, + }); + }); + + it('builds user withdrawal confirmation notifications', () => { + expect( + NotificationHelper.withdrawalConfirmed({ + userId: 'user-1', + amount: 250, + vaultName: 'Cassava Vault', + }), + ).toEqual({ + userId: 'user-1', + title: 'Withdrawal Confirmed', + message: + 'Your withdrawal of 250 from vault Cassava Vault has been confirmed.', + type: NotificationType.WITHDRAWAL, + }); + }); + + it('builds insurance activation notifications', () => { + const coverageStart = new Date('2026-01-01T00:00:00.000Z'); + const coverageEnd = new Date('2026-12-31T00:00:00.000Z'); + + expect( + NotificationHelper.insuranceActivated({ + userId: 'user-1', + planName: 'Basic Cover', + monthlyPremium: 18.5, + coverageStart, + coverageEnd, + }), + ).toEqual({ + userId: 'user-1', + title: 'Insurance Plan Activated', + message: `Your "Basic Cover" crop insurance is now active. Monthly premium: $18.50. Coverage period: ${coverageStart.toLocaleDateString()} - ${coverageEnd.toLocaleDateString()}.`, + type: NotificationType.INSURANCE, + }); + }); + + it('builds reward claim notifications', () => { + expect( + NotificationHelper.rewardsClaimed({ + userId: 'user-1', + claimedAmount: 1.234567891, + vaultId: 'vault-1', + vaultName: 'Rice Vault', + }), + ).toEqual({ + userId: 'user-1', + title: 'Rewards Claimed', + message: + 'You have successfully claimed 1.23456789 in rewards from vault Rice Vault.', + type: NotificationType.REWARD, + }); + }); +}); diff --git a/harvest-finance/backend/src/notifications/notification.helper.ts b/harvest-finance/backend/src/notifications/notification.helper.ts new file mode 100644 index 000000000..2b29f46bd --- /dev/null +++ b/harvest-finance/backend/src/notifications/notification.helper.ts @@ -0,0 +1,111 @@ +import { NotificationType } from '../database/entities/notification.entity'; +import { CreateNotificationDto } from './dto/create-notification.dto'; + +export interface LargeDepositNotificationParams { + amount: number; + vaultName: string; +} + +export interface DepositConfirmedNotificationParams { + userId: string; + amount: number | string; + vaultId: string; +} + +export interface WithdrawalConfirmedNotificationParams { + userId: string; + amount: number; + vaultName: string; +} + +export interface InsuranceActivatedNotificationParams { + userId: string; + planName: string; + monthlyPremium: number; + coverageStart: Date; + coverageEnd: Date; +} + +export interface InsuranceRenewalNotificationParams { + userId: string; + planName: string; + coverageEnd: Date | string; + cropType: string; +} + +export interface RewardsClaimedNotificationParams { + userId: string; + claimedAmount: number; + vaultId?: string; + vaultName: string; +} + +export class NotificationHelper { + static largeDepositAlert( + params: LargeDepositNotificationParams, + ): CreateNotificationDto { + return { + title: 'Large Deposit Alert', + message: `A large deposit of ${params.amount} has been initiated for vault ${params.vaultName}.`, + type: NotificationType.LARGE_TRANSACTION, + adminOnly: true, + }; + } + + static depositConfirmed( + params: DepositConfirmedNotificationParams, + ): CreateNotificationDto { + return { + userId: params.userId, + title: 'Deposit Confirmed', + message: `Your deposit of ${params.amount} into vault ${params.vaultId} has been confirmed.`, + type: NotificationType.DEPOSIT, + }; + } + + static withdrawalConfirmed( + params: WithdrawalConfirmedNotificationParams, + ): CreateNotificationDto { + return { + userId: params.userId, + title: 'Withdrawal Confirmed', + message: `Your withdrawal of ${params.amount} from vault ${params.vaultName} has been confirmed.`, + type: NotificationType.WITHDRAWAL, + }; + } + + static insuranceActivated( + params: InsuranceActivatedNotificationParams, + ): CreateNotificationDto { + return { + userId: params.userId, + title: 'Insurance Plan Activated', + message: `Your "${params.planName}" crop insurance is now active. Monthly premium: $${params.monthlyPremium.toFixed(2)}. Coverage period: ${params.coverageStart.toLocaleDateString()} - ${params.coverageEnd.toLocaleDateString()}.`, + type: NotificationType.INSURANCE, + }; + } + + static insuranceRenewalReminder( + params: InsuranceRenewalNotificationParams, + ): CreateNotificationDto { + return { + userId: params.userId, + title: 'Insurance Renewal Reminder', + message: `Your "${params.planName}" insurance coverage expires on ${new Date(params.coverageEnd).toLocaleDateString()}. Renew now to maintain protection for your ${params.cropType} crop.`, + type: NotificationType.INSURANCE, + }; + } + + static rewardsClaimed( + params: RewardsClaimedNotificationParams, + ): CreateNotificationDto { + const source = params.vaultId ? `vault ${params.vaultName}` : 'all vaults'; + + return { + userId: params.userId, + title: 'Rewards Claimed', + message: `You have successfully claimed ${params.claimedAmount.toFixed(8)} in rewards from ${source}.`, + type: NotificationType.REWARD, + }; + } +} diff --git a/harvest-finance/backend/src/notifications/notifications.service.ts b/harvest-finance/backend/src/notifications/notifications.service.ts index 2ac99f05e..1525a4f5a 100644 --- a/harvest-finance/backend/src/notifications/notifications.service.ts +++ b/harvest-finance/backend/src/notifications/notifications.service.ts @@ -1,10 +1,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { - Notification, - NotificationType, -} from '../database/entities/notification.entity'; +import { Notification } from '../database/entities/notification.entity'; import { CreateNotificationDto } from './dto/create-notification.dto'; import { NotificationResponseDto } from './dto/notification-response.dto'; diff --git a/harvest-finance/backend/src/rewards/rewards.service.ts b/harvest-finance/backend/src/rewards/rewards.service.ts index 02f36f14a..87db09bb8 100644 --- a/harvest-finance/backend/src/rewards/rewards.service.ts +++ b/harvest-finance/backend/src/rewards/rewards.service.ts @@ -11,7 +11,7 @@ import { ClaimRewardsResponseDto, } from './dto/reward-response.dto'; import { NotificationsService } from '../notifications/notifications.service'; -import { NotificationType } from '../database/entities/notification.entity'; +import { NotificationHelper } from '../notifications/notification.helper'; import { VaultGateway } from '../realtime/vault.gateway'; @Injectable() @@ -113,13 +113,14 @@ export class RewardsService { }); await this.rewardRepo.save(claimRecord); - // Trigger notification for user - await this.notificationsService.create({ - userId, - title: 'Rewards Claimed', - message: `You have successfully claimed ${claimedAmount.toFixed(8)} in rewards from ${vaultId ? 'vault ' + vaultName : 'all vaults'}.`, - type: NotificationType.REWARD, - }); + await this.notificationsService.create( + NotificationHelper.rewardsClaimed({ + userId, + claimedAmount, + vaultId, + vaultName, + }), + ); // Emit real-time event this.vaultGateway.emitHarvest({ diff --git a/harvest-finance/backend/src/vaults/vaults.service.ts b/harvest-finance/backend/src/vaults/vaults.service.ts index ee21c4760..ca744e4ef 100644 --- a/harvest-finance/backend/src/vaults/vaults.service.ts +++ b/harvest-finance/backend/src/vaults/vaults.service.ts @@ -18,7 +18,7 @@ import { DepositResponseDto, } from './dto/vault-response.dto'; import { NotificationsService } from '../notifications/notifications.service'; -import { NotificationType } from '../database/entities/notification.entity'; +import { NotificationHelper } from '../notifications/notification.helper'; import { CustomLoggerService } from '../logger/custom-logger.service'; import { VaultGateway } from '../realtime/vault.gateway'; import { ContractCacheService } from '../common/cache/contract-cache.service'; @@ -150,12 +150,12 @@ export class VaultsService { }); if (amount >= LARGE_DEPOSIT_THRESHOLD) { - await this.notificationsService.create({ - title: 'Large Deposit Alert', - message: `A large deposit of ${amount} has been initiated for vault ${vault.vaultName}.`, - type: NotificationType.LARGE_TRANSACTION, - adminOnly: true, - }); + await this.notificationsService.create( + NotificationHelper.largeDepositAlert({ + amount, + vaultName: vault.vaultName, + }), + ); } const confirmedDeposit = await this.confirmDeposit(result.deposit.id); @@ -209,12 +209,13 @@ export class VaultsService { throw new NotFoundException('Deposit not found after confirmation'); } - await this.notificationsService.create({ - userId: updatedDeposit.userId, - title: 'Deposit Confirmed', - message: `Your deposit of ${updatedDeposit.amount} into vault ${updatedDeposit.vaultId} has been confirmed.`, - type: NotificationType.DEPOSIT, - }); + await this.notificationsService.create( + NotificationHelper.depositConfirmed({ + userId: updatedDeposit.userId, + amount: updatedDeposit.amount, + vaultId: updatedDeposit.vaultId, + }), + ); return updatedDeposit; } @@ -350,12 +351,13 @@ export class VaultsService { throw new NotFoundException('Withdrawal not found after confirmation'); } - await this.notificationsService.create({ - userId, - title: 'Withdrawal Confirmed', - message: `Your withdrawal of ${amount} from vault ${vault.vaultName} has been confirmed.`, - type: NotificationType.DEPOSIT, - }); + await this.notificationsService.create( + NotificationHelper.withdrawalConfirmed({ + userId, + amount, + vaultName: vault.vaultName, + }), + ); this.logger.log( `Withdrawal of ${amount} confirmed from vault ${vaultId} by user ${userId}`,