mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 21:24:04 +00:00
feat(bot): add secure reminder job runtime
This commit is contained in:
111
apps/bot/src/reminder-jobs.ts
Normal file
111
apps/bot/src/reminder-jobs.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user