api: introduce daily energy insight
Pedro Lucas Porcellis porcellis@eletrotupi.com · 4 months ago
58fc6edcba4ecd00dfece8d7b69e529d18da9ae5 Browse filesParents
a2d0b31Changed files
6
Additions
+115
Deletions
-1
/dev/null → api/prisma/migrations/20260614055540_add_daily_energy_insight_type/migration.sql
1 +
-- AlterEnum
2 +
ALTER TYPE "InsightType" ADD VALUE 'DAILY_ENERGY';
api/prisma/schema.prisma
1 1
generator client {
2 2
provider = "prisma-client-js"
3 3
//output = "../src/generated/prisma"
4 4
}
5 5
136 136
MOOD_TREND
137 137
ENERGY_SLEEP_CORRELATION
138 138
TRIGGER_PATTERN
139 139
WEEKLY_SUMMARY
140 140
STREAK
141 +
DAILY_ENERGY
141 142
}
142 143
143 144
enum InsightPeriod {
144 145
DAILY
145 146
WEEKLY
186 187
ANXIETY
187 188
GRATITUDE
188 189
FOCUS
189 190
TIREDNESS
190 191
}
/dev/null → api/src/lib/queue/processors/insights/dailyEnergy.ts
1 +
import { prisma } from "@app/lib/prisma";
2 +
import { InsightType, InsightPeriod } from "@prisma/client";
3 +
import { InsightPeriodPayload } from "@app/lib/queue/types";
4 +
import { avg } from "@app/utils/calc";
5 +
6 +
export async function processDailyEnergy({
7 +
userId,
8 +
periodStart,
9 +
periodEnd,
10 +
}: InsightPeriodPayload): Promise<void> {
11 +
const start = new Date(periodStart);
12 +
const end = new Date(periodEnd);
13 +
14 +
const moods = await prisma.mood.findMany({
15 +
where: { userId, moment: { gte: start, lte: end } },
16 +
orderBy: { moment: "asc" },
17 +
select: {
18 +
energyLevel: true,
19 +
stressLevel: true,
20 +
anxietyLevel: true,
21 +
selectedMood: true,
22 +
moment: true,
23 +
},
24 +
});
25 +
26 +
if (moods.length === 0) return;
27 +
28 +
const avgEnergy = parseFloat(avg(moods.map((m) => m.energyLevel)).toFixed(2));
29 +
const avgStress = parseFloat(avg(moods.map((m) => m.stressLevel)).toFixed(2));
30 +
const avgAnxiety = parseFloat(
31 +
avg(moods.map((m) => m.anxietyLevel)).toFixed(2),
32 +
);
33 +
const peak = Math.max(...moods.map((m) => m.energyLevel));
34 +
const trough = Math.min(...moods.map((m) => m.energyLevel));
35 +
36 +
// Trend across the day: compare first half vs second half entries
37 +
// Not that great, but it works
38 +
const mid = Math.floor(moods.length / 2);
39 +
const morningAvg =
40 +
moods.length > 1
41 +
? avg(moods.slice(0, mid).map((m) => m.energyLevel))
42 +
: null;
43 +
44 +
const eveningAvg =
45 +
moods.length > 1 ? avg(moods.slice(mid).map((m) => m.energyLevel)) : null;
46 +
47 +
const dayTrend =
48 +
morningAvg && eveningAvg
49 +
? parseFloat((((eveningAvg - morningAvg) / morningAvg) * 100).toFixed(1))
50 +
: null;
51 +
52 +
await prisma.insight.upsert({
53 +
where: {
54 +
userId_type_periodStart: {
55 +
userId,
56 +
type: InsightType.DAILY_ENERGY,
57 +
periodStart: start,
58 +
},
59 +
},
60 +
update: {
61 +
body: buildBody(avgEnergy, dayTrend),
62 +
metadata: {
63 +
avgEnergy,
64 +
avgStress,
65 +
avgAnxiety,
66 +
peak,
67 +
trough,
68 +
dayTrend,
69 +
entryCount: moods.length,
70 +
},
71 +
generatedAt: new Date(),
72 +
},
73 +
create: {
74 +
userId,
75 +
type: InsightType.DAILY_ENERGY,
76 +
period: InsightPeriod.DAILY,
77 +
title: "Energia do Dia",
78 +
body: buildBody(avgEnergy, dayTrend),
79 +
metadata: {
80 +
avgEnergy,
81 +
avgStress,
82 +
avgAnxiety,
83 +
peak,
84 +
trough,
85 +
dayTrend,
86 +
entryCount: moods.length,
87 +
},
88 +
periodStart: start,
89 +
periodEnd: end,
90 +
},
91 +
});
92 +
}
93 +
94 +
function buildBody(avgEnergy: number, dayTrend: number | null): string {
95 +
const level = avgEnergy >= 7 ? "alta" : avgEnergy >= 4 ? "moderada" : "baixa";
96 +
if (dayTrend === null) return `Energia ${level} hoje.`;
97 +
if (dayTrend > 10)
98 +
return `Energia ${level} hoje, com melhora ao longo do dia.`;
99 +
if (dayTrend < -10)
100 +
return `Energia ${level} hoje, com queda ao longo do dia.`;
101 +
return `Energia ${level} e estável ao longo do dia.`;
102 +
}
api/src/lib/queue/processors/insights/index.ts
1 1
import { Job } from "bullmq";
2 2
import {
3 3
InsightJobName,
4 4
InsightJobData,
5 5
InsightPeriodPayload,
7 7
import { getQueue } from "@app/lib/queue/QueueRegistry";
8 8
import { prisma } from "@app/lib/prisma";
9 9
import { processMoodTrend } from "@app/lib/queue/processors/insights/moodTrend";
10 10
import { processEnergySleep } from "@app/lib/queue/processors/insights/energySleep";
11 11
import { processTriggerPattern } from "@app/lib/queue/processors/insights/triggerPattern";
12 +
import { processDailyEnergy } from "@app/lib/queue/processors/insights/dailyEnergy";
12 13
13 14
export async function insightProcessor(
14 15
job: Job<InsightJobData["data"]>,
15 16
): Promise<void> {
16 17
console.log(`Insight job ${job.name} started`);
25 26
case InsightJobName.EnergySleep:
26 27
return processEnergySleep(job.data as InsightPeriodPayload);
27 28
28 29
case InsightJobName.TriggerPattern:
29 30
return processTriggerPattern(job.data as InsightPeriodPayload);
31 +
32 +
case InsightJobName.DailyEnergy:
33 +
return processDailyEnergy(job.data as InsightPeriodPayload);
30 34
31 35
default:
32 36
throw new Error(`Unknown insight job: ${job.name}`);
33 37
}
34 38
}
58 62
data: { userId: u.id, ...payload },
59 63
},
60 64
]),
61 65
);
62 66
}
api/src/lib/queue/types.ts
1 1
// Mail queue
2 2
export enum MailJobName {
3 3
WelcomeEmail = "mail:welcome",
4 4
PasswordReset = "mail:password-reset",
5 5
ActivateAccountEmail = "mail:activate-account",
21 21
export enum InsightJobName {
22 22
FanOut = "insight:fan-out",
23 23
MoodTrend = "insight:mood-trend",
24 24
EnergySleep = "insight:energy-sleep",
25 25
TriggerPattern = "insight:trigger-pattern",
26 +
DailyEnergy = "insight:daily-energy"
26 27
}
27 28
28 29
export type InsightPeriodPayload = {
29 30
userId: number;
30 31
periodStart: string;
35 36
36 37
export type InsightJobData =
37 38
| { name: InsightJobName.FanOut; data: InsightFanOutPayload }
38 39
| { name: InsightJobName.MoodTrend; data: InsightPeriodPayload }
39 40
| { name: InsightJobName.EnergySleep; data: InsightPeriodPayload }
40 -
| { name: InsightJobName.TriggerPattern; data: InsightPeriodPayload };
41 +
| { name: InsightJobName.TriggerPattern; data: InsightPeriodPayload }
42 +
| { name: InsightJobName.DailyEnergy; data: InsightPeriodPayload };
/dev/null → api/src/utils/calc.ts
1 +
// TODO: Move other calculations here
2 +
export const avg = (values: number[]) =>
3 +
values.reduce((a, b) => a + b, 0) / values.length;