Add personal goal automation alerts
This commit is contained in:
parent
5448dbb7eb
commit
0156d562ea
@ -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],
|
||||
})
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user