Add personal goal automation alerts

This commit is contained in:
MOHAN 2026-07-17 23:45:07 +05:30
parent 5448dbb7eb
commit 0156d562ea
3 changed files with 56 additions and 3 deletions

View File

@ -1,8 +1,10 @@
import { Module } from "@nestjs/common";
import { NotificationsModule } from "../notifications/notifications.module";
import { PlanningController } from "./planning.controller";
import { PlanningService } from "./planning.service";
@Module({
imports: [NotificationsModule],
controllers: [PlanningController],
providers: [PlanningService],
})

View File

@ -1,5 +1,6 @@
import { BadRequestException, Injectable } from "@nestjs/common";
import { Prisma } from "@prisma/client";
import { NotificationsService } from "../notifications/notifications.service";
import { PrismaService } from "../prisma/prisma.service";
import {
CreateHouseholdBudgetDto,
@ -12,7 +13,10 @@ import {
@Injectable()
export class PlanningService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly notifications: NotificationsService,
) {}
async listBudgets(userId: string, householdId?: string) {
const where: any = householdId
@ -87,6 +91,7 @@ export class PlanningService {
},
});
await this.audit(userId, "planning.personal_goal.create", { goalId: goal.id });
await this.notifyGoalAutomation(userId, goal, null, "created");
return this.withGoalProgress(goal);
}
@ -103,6 +108,7 @@ export class PlanningService {
},
});
await this.audit(userId, "planning.personal_goal.update", { goalId, fields: Object.keys(dto) });
await this.notifyGoalAutomation(userId, goal, existing, "updated");
return this.withGoalProgress(goal);
}
@ -260,6 +266,50 @@ export class PlanningService {
return { ...goal, progressPercent: target > 0 ? Math.min(100, Math.round((current / target) * 100)) : 0, remainingAmount: this.round(Math.max(0, target - current)) };
}
private async notifyGoalAutomation(userId: string, goal: any, previous: any | null, reason: "created" | "updated") {
const current = this.toNumber(goal.currentAmount);
const target = this.toNumber(goal.targetAmount);
const progress = target > 0 ? Math.min(100, Math.round((current / target) * 100)) : 0;
const previousProgress = previous
? Math.min(100, Math.round((this.toNumber(previous.currentAmount) / Math.max(this.toNumber(previous.targetAmount), 1)) * 100))
: 0;
if (progress >= 100 && previousProgress < 100) {
await this.notifications.notifyUser(userId, {
type: "goal.completed",
severity: "info",
title: `Goal completed: ${goal.name}`,
body: `You reached ${goal.isoCurrencyCode} ${this.round(target).toFixed(2)}.`,
metadata: { goalId: goal.id, progress },
});
return;
}
const crossedMilestone = [25, 50, 75].find((milestone) => progress >= milestone && previousProgress < milestone);
if (crossedMilestone) {
await this.notifications.notifyUser(userId, {
type: "goal.progress",
severity: "info",
title: `Goal progress: ${goal.name}`,
body: `You reached ${crossedMilestone}% of this goal.`,
metadata: { goalId: goal.id, progress, milestone: crossedMilestone },
});
}
if (reason === "created" && goal.targetDate) {
const daysUntilTarget = Math.ceil((new Date(goal.targetDate).getTime() - Date.now()) / 86400000);
if (daysUntilTarget >= 0 && daysUntilTarget <= 30) {
await this.notifications.notifyUser(userId, {
type: "goal.target_date",
severity: "warning",
title: `Goal due soon: ${goal.name}`,
body: `Target date is in ${daysUntilTarget} day(s).`,
metadata: { goalId: goal.id, daysUntilTarget },
});
}
}
}
private inferCadence(dates: Date[]) {
const gaps = dates.slice(1).map((date, index) => Math.round((date.getTime() - dates[index].getTime()) / 86400000));
const average = gaps.reduce((sum, gap) => sum + gap, 0) / gaps.length;

View File

@ -20,8 +20,9 @@ const createService = () => {
transactionRaw: model(),
auditLog: { create: jest.fn() },
};
const service = new PlanningService(prisma as any);
return { service, prisma };
const notifications = { notifyUser: jest.fn() };
const service = new PlanningService(prisma as any, notifications as any);
return { service, prisma, notifications };
};
describe("PlanningService", () => {