feat(bot): add secure reminder job runtime

This commit is contained in:
2026-03-08 22:15:01 +04:00
parent f6d1f34acf
commit 6c0dbfc48e
14 changed files with 670 additions and 4 deletions

View File

@@ -0,0 +1,111 @@
import { BillingPeriod } from '@household/domain'
import type { ReminderJobService } from '@household/application'
const REMINDER_TYPES = ['utilities', 'rent-warning', 'rent-due'] as const
type ReminderType = (typeof REMINDER_TYPES)[number]
interface ReminderJobRequestBody {
period?: string
jobId?: string
dryRun?: boolean
}
function json(body: object, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: {
'content-type': 'application/json; charset=utf-8'
}
})
}
function parseReminderType(raw: string): ReminderType | null {
if ((REMINDER_TYPES as readonly string[]).includes(raw)) {
return raw as ReminderType
}
return null
}
function currentPeriod(): string {
const now = new Date()
const year = now.getUTCFullYear()
const month = `${now.getUTCMonth() + 1}`.padStart(2, '0')
return `${year}-${month}`
}
async function readBody(request: Request): Promise<ReminderJobRequestBody> {
const text = await request.text()
if (text.trim().length === 0) {
return {}
}
const parsed = JSON.parse(text) as ReminderJobRequestBody
return parsed
}
export function createReminderJobsHandler(options: {
householdId: string
reminderService: ReminderJobService
forceDryRun?: boolean
}): {
handle: (request: Request, rawReminderType: string) => Promise<Response>
} {
return {
handle: async (request, rawReminderType) => {
const reminderType = parseReminderType(rawReminderType)
if (!reminderType) {
return json({ ok: false, error: 'Invalid reminder type' }, 400)
}
try {
const body = await readBody(request)
const period = BillingPeriod.fromString(body.period ?? currentPeriod()).toString()
const dryRun = options.forceDryRun === true || body.dryRun === true
const result = await options.reminderService.handleJob({
householdId: options.householdId,
period,
reminderType,
dryRun
})
const logPayload = {
event: 'scheduler.reminder.dispatch',
reminderType,
period,
jobId: body.jobId ?? null,
dedupeKey: result.dedupeKey,
outcome: result.status,
dryRun
}
console.log(JSON.stringify(logPayload))
return json({
ok: true,
jobId: body.jobId ?? null,
reminderType,
period,
dedupeKey: result.dedupeKey,
outcome: result.status,
dryRun,
messageText: result.messageText
})
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown reminder job error'
console.error(
JSON.stringify({
event: 'scheduler.reminder.dispatch_failed',
reminderType: rawReminderType,
error: message
})
)
return json({ ok: false, error: message }, 400)
}
}
}
}