api: wire BullMQ sync into medicine regimen CUD
Add medicineReminder.sync.ts and dailyReminder.sync.ts that manage repeatable BullMQ jobs keyed by regimen/user ID. Call syncMedicineReminderJobs after create, update, and deactivate. Update scheduledAt schema fields from z.string() to z.array(z.string()).
Parents:
ed130655 file(s) changed
- api/src/schemas/careAction.schema.ts +1 -1
- api/src/schemas/medicineRegimen.schema.ts +2 -2
- api/src/services/dailyReminder.sync.ts +41 -0
- api/src/services/medicineRegimen.service.ts +10 -3
- api/src/services/medicineReminder.sync.ts +60 -0
api/src/schemas/careAction.schema.ts
1 1
import { z } from 'zod';
2 2
import {
3 3
CareActionType,
4 4
AppointmentType,
5 5
ActivityType,
▸ 2 unchanged lines
8 8
9 9
const MedicineInlineSchema = z.object({
10 10
name: z.string().min(1),
11 11
dosage: z.string().min(1),
12 12
periodicity: z.nativeEnum(MedicinePeriodicity),
13 -
scheduledAt: z.string().optional(),
13 +
scheduledAt: z.array(z.string()).optional().default([]),
14 14
});
15 15
16 16
export const CreateCareActionSchema = z.object({
17 17
type: z.nativeEnum(CareActionType),
18 18
moment: z.coerce.date(),
▸ 28 unchanged lines
47 47
triggerId: z.number().int().positive().optional(),
48 48
moodId: z.number().int().positive().optional(),
49 49
}).strict();
50 50
51 51
export type PatchCareActionInput = z.infer<typeof PatchCareActionSchema>;
api/src/schemas/medicineRegimen.schema.ts
1 1
import { z } from 'zod';
2 2
import { MedicinePeriodicity } from '@prisma/client';
3 3
4 4
export const CreateMedicineRegimenSchema = z.object({
5 5
name: z.string().min(1),
6 6
dosage: z.string().min(1),
7 7
periodicity: z.nativeEnum(MedicinePeriodicity),
8 -
scheduledAt: z.string().optional(),
8 +
scheduledAt: z.array(z.string()).optional().default([]),
9 9
});
10 10
11 11
export type CreateMedicineRegimenInput = z.infer<typeof CreateMedicineRegimenSchema>;
12 12
13 13
export const UpdateMedicineRegimenSchema = z.object({
14 14
name: z.string().min(1).optional(),
15 15
dosage: z.string().min(1).optional(),
16 16
periodicity: z.nativeEnum(MedicinePeriodicity).optional(),
17 -
scheduledAt: z.string().optional(),
17 +
scheduledAt: z.array(z.string()).optional().default([]),
18 18
active: z.boolean().optional(),
19 19
});
20 20
21 21
export type UpdateMedicineRegimenInput = z.infer<typeof UpdateMedicineRegimenSchema>;
api/src/services/dailyReminder.sync.ts
@@ -0,0 +1,41 @@
1 + import { User } from "@prisma/client";
2 + import { getQueue } from "@app/lib/queue/QueueRegistry";
3 + import { DailyReminderJobName } from "@app/lib/queue/types";
4 +
5 + // XXX: Might be moved to the utilities?
6 + function timeToCron(time: string): string {
7 + const [hour, minute] = time.split(":").map(Number);
8 + return `${minute} ${hour} * * *`;
9 + }
10 +
11 + export async function syncDailyReminderJob(user: User): Promise<void> {
12 + console.log(`[daily-sync] syncing user ${user.id}`);
13 +
14 + const queue = getQueue("daily-reminders");
15 +
16 + const existing = await queue.getRepeatableJobs();
17 + for (const job of existing) {
18 + if (job.id?.includes(`daily-reminder-user-${user.id}`)) {
19 + await queue.removeRepeatableByKey(job.key);
20 + }
21 + }
22 +
23 + if (!user.notificationsEnabled || !user.dailyReminderTime) {
24 + console.log(`[daily-sync] user ${user.id} skipped (enabled=${user.notificationsEnabled}, time=${user.dailyReminderTime})`);
25 +
26 + return;
27 + }
28 +
29 + const cron = timeToCron(user.dailyReminderTime);
30 +
31 + await queue.add(
32 + DailyReminderJobName.Send,
33 + { userId: user.id },
34 + {
35 + repeat: { pattern: cron, tz: "America/Sao_Paulo" },
36 + jobId: `daily-reminder-user-${user.id}`,
37 + }
38 + );
39 +
40 + console.log(`[daily-sync] scheduled daily reminder for user ${user.id} at ${user.dailyReminderTime} => cron ${cron}`);
41 + }
api/src/services/medicineRegimen.service.ts
1 1
import { prisma } from '@app/lib/prisma';
2 2
import { MedicineRegimen } from '@prisma/client';
3 3
import { CreateMedicineRegimenInput, UpdateMedicineRegimenInput } from '@app/schemas';
4 +
import { syncMedicineReminderJobs } from '@app/services/medicineReminder.sync';
4 5
5 6
export const getMedicineRegimensByUserId = async (
6 7
userId: number,
7 8
): Promise<MedicineRegimen[]> => {
8 9
return prisma.medicineRegimen.findMany({
▸ 4 unchanged lines
13 14
14 15
export const createMedicineRegimen = async (
15 16
userId: number,
16 17
input: CreateMedicineRegimenInput,
17 18
): Promise<MedicineRegimen> => {
18 -
return prisma.medicineRegimen.create({
19 +
const regimen = await prisma.medicineRegimen.create({
19 20
data: { ...input, userId },
20 21
});
22 +
await syncMedicineReminderJobs(regimen);
23 +
return regimen;
21 24
};
22 25
23 26
export const updateMedicineRegimen = async (
24 27
userId: number,
25 28
id: number,
26 29
input: UpdateMedicineRegimenInput,
27 30
): Promise<MedicineRegimen> => {
28 -
return prisma.medicineRegimen.update({
31 +
const regimen = await prisma.medicineRegimen.update({
29 32
where: { id, userId },
30 33
data: input,
31 34
});
35 +
await syncMedicineReminderJobs(regimen);
36 +
return regimen;
32 37
};
33 38
34 39
export const deactivateMedicineRegimen = async (
35 40
userId: number,
36 41
id: number,
37 42
): Promise<MedicineRegimen> => {
38 -
return prisma.medicineRegimen.update({
43 +
const regimen = await prisma.medicineRegimen.update({
39 44
where: { id, userId },
40 45
data: { active: false },
41 46
});
47 +
await syncMedicineReminderJobs(regimen);
48 +
return regimen;
42 49
};
api/src/services/medicineReminder.sync.ts
@@ -0,0 +1,60 @@
1 + import { MedicineRegimen } from "@prisma/client";
2 + import { getQueue } from "@app/lib/queue/QueueRegistry";
3 + import { MedicineReminderJobName } from "@app/lib/queue/types";
4 +
5 + function timeToCron(time: string): string {
6 + const [hour, minute] = time.split(":").map(Number);
7 + return `${minute} ${hour} * * *`;
8 + }
9 +
10 + export async function syncMedicineReminderJobs(
11 + regimen: MedicineRegimen
12 + ): Promise<void> {
13 + console.log(`[medicine-sync] syncing regimen ${regimen.id} (active=${regimen.active}, times=[${regimen.scheduledAt}])`);
14 +
15 + const queue = getQueue("medicine-reminders");
16 +
17 + const existing = await queue.getRepeatableJobs();
18 + let removedCount = 0;
19 +
20 + for (const job of existing) {
21 + if (job.id?.startsWith(`regimen-${regimen.id}-`)) {
22 + await queue.removeRepeatableByKey(job.key);
23 + removedCount++;
24 + }
25 + }
26 +
27 + console.log(`[medicine-sync] removed ${removedCount} existing jobs for regimen ${regimen.id}`);
28 +
29 + if (!regimen.active) {
30 + console.log(`[medicine-sync] regimen ${regimen.id} inactive, no jobs scheduled`);
31 +
32 + return;
33 + }
34 +
35 + if (regimen.scheduledAt.length === 0) {
36 + console.log(`[medicine-sync] regimen ${regimen.id} has no scheduledAt times, skipping`);
37 +
38 + return;
39 + }
40 +
41 + for (const time of regimen.scheduledAt) {
42 + const cron = timeToCron(time);
43 + await queue.add(
44 + MedicineReminderJobName.Send,
45 + {
46 + userId: regimen.userId,
47 + regimenId: regimen.id,
48 + medicineName: regimen.name,
49 + dosage: regimen.dosage,
50 + scheduledTime: time,
51 + },
52 + {
53 + repeat: { pattern: cron, tz: "America/Sao_Paulo" },
54 + jobId: `regimen-${regimen.id}-${time}`,
55 + }
56 + );
57 +
58 + console.log(`[medicine-sync] scheduled job for regimen ${regimen.id} at ${time} => cron ${cron}`);
59 + }
60 + }