Add personal goal automation alerts
This commit is contained in:
parent
5448dbb7eb
commit
0156d562ea
@ -1,8 +1,10 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
import { NotificationsModule } from "../notifications/notifications.module";
|
||||||
import { PlanningController } from "./planning.controller";
|
import { PlanningController } from "./planning.controller";
|
||||||
import { PlanningService } from "./planning.service";
|
import { PlanningService } from "./planning.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [NotificationsModule],
|
||||||
controllers: [PlanningController],
|
controllers: [PlanningController],
|
||||||
providers: [PlanningService],
|
providers: [PlanningService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { NotificationsService } from "../notifications/notifications.service";
|
||||||
import { PrismaService } from "../prisma/prisma.service";
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
import {
|
import {
|
||||||
CreateHouseholdBudgetDto,
|
CreateHouseholdBudgetDto,
|
||||||
@ -12,7 +13,10 @@ import {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PlanningService {
|
export class PlanningService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly notifications: NotificationsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async listBudgets(userId: string, householdId?: string) {
|
async listBudgets(userId: string, householdId?: string) {
|
||||||
const where: any = householdId
|
const where: any = householdId
|
||||||
@ -87,6 +91,7 @@ export class PlanningService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
await this.audit(userId, "planning.personal_goal.create", { goalId: goal.id });
|
await this.audit(userId, "planning.personal_goal.create", { goalId: goal.id });
|
||||||
|
await this.notifyGoalAutomation(userId, goal, null, "created");
|
||||||
return this.withGoalProgress(goal);
|
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.audit(userId, "planning.personal_goal.update", { goalId, fields: Object.keys(dto) });
|
||||||
|
await this.notifyGoalAutomation(userId, goal, existing, "updated");
|
||||||
return this.withGoalProgress(goal);
|
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)) };
|
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[]) {
|
private inferCadence(dates: Date[]) {
|
||||||
const gaps = dates.slice(1).map((date, index) => Math.round((date.getTime() - dates[index].getTime()) / 86400000));
|
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;
|
const average = gaps.reduce((sum, gap) => sum + gap, 0) / gaps.length;
|
||||||
|
|||||||
@ -20,8 +20,9 @@ const createService = () => {
|
|||||||
transactionRaw: model(),
|
transactionRaw: model(),
|
||||||
auditLog: { create: jest.fn() },
|
auditLog: { create: jest.fn() },
|
||||||
};
|
};
|
||||||
const service = new PlanningService(prisma as any);
|
const notifications = { notifyUser: jest.fn() };
|
||||||
return { service, prisma };
|
const service = new PlanningService(prisma as any, notifications as any);
|
||||||
|
return { service, prisma, notifications };
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("PlanningService", () => {
|
describe("PlanningService", () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user