Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions harvest-finance/backend/src/insurance/insurance.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------- */
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
});
});
});
111 changes: 111 additions & 0 deletions harvest-finance/backend/src/notifications/notification.helper.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
}
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
17 changes: 9 additions & 8 deletions harvest-finance/backend/src/rewards/rewards.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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({
Expand Down
40 changes: 21 additions & 19 deletions harvest-finance/backend/src/vaults/vaults.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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}`,
Expand Down