eletrotupi / tcc/ commit / 6c0f1d0

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()).
Pedro Lucas Porcellis porcellis@eletrotupi.com · 4 months ago 6c0f1d02fc725cde9d020ca4c6f2675f6c3698f4 Browse files
Parents ed13065
Changed files
5
Additions
+114
Deletions
-6
api/src/schemas/careAction.schema.ts
1 1
import { z } from 'zod';
2 2
import {
3 3
  CareActionType,
4 4
  AppointmentType,
5 5
  ActivityType,
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(),
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>;
/dev/null → api/src/services/dailyReminder.sync.ts
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({
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
};
/dev/null → api/src/services/medicineReminder.sync.ts
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 +
}