mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
feat(jobs): daily medication-inventory expire-stale queue (03:00 cron)
This commit is contained in:
1 parent
a0b9c11e68
commit
7f133a1aa7
3 files changed
+98
No files matched your search
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* v1.4.25 W19b — module-shape contract for the
|
||||
* medication-inventory-expire job. The actual sweep logic lives in
|
||||
* `expireStaleInUseItems` (covered by
|
||||
* `src/lib/medications/inventory/__tests__/service.test.ts`); this
|
||||
* test pins the queue name + cron schedule so a typo in the worker
|
||||
* registration is caught at the unit-test level.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
MEDICATION_INVENTORY_EXPIRE_QUEUE,
|
||||
MEDICATION_INVENTORY_EXPIRE_CRON,
|
||||
} from "@/lib/jobs/medication-inventory-expire";
|
||||
|
||||
describe("medication-inventory-expire module", () => {
|
||||
it("exports the canonical queue name", () => {
|
||||
expect(MEDICATION_INVENTORY_EXPIRE_QUEUE).toBe("medication-inventory-expire");
|
||||
});
|
||||
|
||||
it("schedules at 03:30 Europe/Berlin", () => {
|
||||
expect(MEDICATION_INVENTORY_EXPIRE_CRON).toBe("30 3 * * *");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* v1.4.25 W19b — pg-boss helper for the daily
|
||||
* `medication-inventory-expire` queue. The cron at 03:00 Europe/Berlin
|
||||
* (interleaved with the other 03:xx cleanup crons) sweeps every user
|
||||
* for IN_USE pens whose 30-day in-use window has lapsed and flips
|
||||
* their state to EXPIRED.
|
||||
*
|
||||
* The expire pass is idempotent — re-running it within the same day
|
||||
* only updates rows that the previous pass missed (because they hit
|
||||
* 30 days between the two runs). EXPIRED is a terminal-ish state in
|
||||
* the state machine; once flipped, a row stays EXPIRED until the
|
||||
* user explicitly deletes it or marks it USED_UP.
|
||||
*
|
||||
* Notification surface: opt-in only. The W19b scope deliberately
|
||||
* leaves the push notification disabled by default — Marc directive
|
||||
* "notifications saturate the lock screen on first sync" applies
|
||||
* here too. Users opt in via Settings → Notifications (a future
|
||||
* preference toggle); until then the job is silent and the user
|
||||
* sees the EXPIRED badge in the UI on next page-load.
|
||||
*/
|
||||
|
||||
export const MEDICATION_INVENTORY_EXPIRE_QUEUE = "medication-inventory-expire";
|
||||
|
||||
/**
|
||||
* Cron schedule (Europe/Berlin) for the daily expire pass. 03:00
|
||||
* matches the idempotency-cleanup slot and lands inside the existing
|
||||
* 02:xx–03:xx maintenance window. Co-locating with the other cleanup
|
||||
* crons keeps the late-night CPU profile coherent.
|
||||
*/
|
||||
export const MEDICATION_INVENTORY_EXPIRE_CRON = "30 3 * * *";
|
||||
|
||||
export interface MedicationInventoryExpirePayload {
|
||||
triggeredAt: string;
|
||||
}
|
||||
@@ -40,6 +40,12 @@ import {
|
||||
type PrDetectionPayload,
|
||||
} from "@/lib/jobs/pr-detection";
|
||||
import { detectPersonalRecordsForUser } from "@/lib/personal-records/pr-detection-worker";
|
||||
import {
|
||||
MEDICATION_INVENTORY_EXPIRE_QUEUE,
|
||||
MEDICATION_INVENTORY_EXPIRE_CRON,
|
||||
type MedicationInventoryExpirePayload,
|
||||
} from "@/lib/jobs/medication-inventory-expire";
|
||||
import { expireStaleInUseItems } from "@/lib/medications/inventory/service";
|
||||
import { rotateLegacyMoodLogSecrets } from "@/lib/moodlog-secret";
|
||||
import { deleteMessage } from "@/lib/telegram";
|
||||
import { decrypt, encrypt } from "@/lib/crypto";
|
||||
@@ -1171,6 +1177,29 @@ async function handlePrDetection(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.4.25 W19b — daily expire-stale pass for `MedicationInventoryItem`
|
||||
* rows. Flips IN_USE pens whose 30-day window has lapsed to EXPIRED
|
||||
* via the pure state-machine evaluator.
|
||||
*/
|
||||
async function handleMedicationInventoryExpire(
|
||||
jobs: Job<MedicationInventoryExpirePayload>[],
|
||||
) {
|
||||
void jobs;
|
||||
await withBackgroundEvent("job.medication_inventory_expire", async (evt) => {
|
||||
try {
|
||||
const count = await expireStaleInUseItems({ nowMs: Date.now() });
|
||||
evt.addMeta("inventory_expired_count", count);
|
||||
} catch (err) {
|
||||
evt.addWarning(
|
||||
`medication-inventory-expire failed: ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function handleOffhostBackup(jobs: Job<OffhostBackupPayload>[]) {
|
||||
void jobs;
|
||||
await withBackgroundEvent("job.offhost_backup", async (evt) => {
|
||||
@@ -1421,6 +1450,7 @@ export async function startReminderWorker() {
|
||||
HOST_METRIC_QUEUE,
|
||||
FEEDBACK_AGGREGATOR_QUEUE,
|
||||
PR_DETECTION_QUEUE,
|
||||
MEDICATION_INVENTORY_EXPIRE_QUEUE,
|
||||
];
|
||||
|
||||
for (const q of allQueues) {
|
||||
@@ -1453,6 +1483,11 @@ export async function startReminderWorker() {
|
||||
// every user; per-user push-suppression cannot apply on the cron
|
||||
// path (the silent flag is set by the ingest hooks).
|
||||
[PR_DETECTION_QUEUE, PR_DETECTION_FALLBACK_CRON],
|
||||
// v1.4.25 W19b — daily expire-stale pass for the per-pen inventory
|
||||
// entities. Flips IN_USE rows whose 30-day clock has blown to
|
||||
// EXPIRED at 03:30 Europe/Berlin (in the existing 02:xx–03:xx
|
||||
// maintenance window, right after idempotency-cleanup).
|
||||
[MEDICATION_INVENTORY_EXPIRE_QUEUE, MEDICATION_INVENTORY_EXPIRE_CRON],
|
||||
];
|
||||
|
||||
for (const [name, cron] of schedules) {
|
||||
@@ -1560,6 +1595,11 @@ export async function startReminderWorker() {
|
||||
{ localConcurrency: PR_DETECTION_CONCURRENCY },
|
||||
handlePrDetection,
|
||||
);
|
||||
await boss.work<MedicationInventoryExpirePayload>(
|
||||
MEDICATION_INVENTORY_EXPIRE_QUEUE,
|
||||
{ localConcurrency: 1 },
|
||||
handleMedicationInventoryExpire,
|
||||
);
|
||||
|
||||
return boss;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user