diff --git a/src/planning/planning.module.ts b/src/planning/planning.module.ts index c582b2e..cf92106 100644 --- a/src/planning/planning.module.ts +++ b/src/planning/planning.module.ts @@ -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], }) diff --git a/src/planning/planning.service.ts b/src/planning/planning.service.ts index 6e49c84..b60eeff 100644 --- a/src/planning/planning.service.ts +++ b/src/planning/planning.service.ts @@ -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; diff --git a/test/planning.service.spec.ts b/test/planning.service.spec.ts index 5c2dfe1..88350a6 100644 --- a/test/planning.service.spec.ts +++ b/test/planning.service.spec.ts @@ -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", () => {