feat(bot): add self-hosted scheduled dispatch support

Co-authored-by: claw <stanislavkalishin+claw@gmail.com>
This commit is contained in:
2026-03-30 15:27:15 +02:00
parent 94c1f48794
commit 575a68b3bb
13 changed files with 331 additions and 40 deletions

View File

@@ -1,4 +1,4 @@
import { and, asc, eq } from 'drizzle-orm'
import { and, asc, eq, lte } from 'drizzle-orm'
import { createDbClient, schema } from '@household/db'
import { instantFromDatabaseValue, instantToDate, nowInstant } from '@household/domain'
@@ -129,6 +129,26 @@ export function createDbScheduledDispatchRepository(databaseUrl: string): {
return rows.map(mapScheduledDispatch)
},
async listDueScheduledDispatches(input) {
const filters = [
eq(schema.scheduledDispatches.status, 'scheduled'),
lte(schema.scheduledDispatches.dueAt, instantToDate(input.dueBefore))
]
if (input.provider) {
filters.push(eq(schema.scheduledDispatches.provider, input.provider))
}
const rows = await db
.select(scheduledDispatchSelect())
.from(schema.scheduledDispatches)
.where(and(...filters))
.orderBy(asc(schema.scheduledDispatches.dueAt), asc(schema.scheduledDispatches.createdAt))
.limit(input.limit)
return rows.map(mapScheduledDispatch)
},
async updateScheduledDispatch(input) {
const updates: Record<string, unknown> = {
updatedAt: instantToDate(input.updatedAt)

View File

@@ -68,6 +68,19 @@ class ScheduledDispatchRepositoryStub implements ScheduledDispatchRepository {
return [...this.dispatches.values()].filter((dispatch) => dispatch.householdId === householdId)
}
async listDueScheduledDispatches(input: {
dueBefore: Temporal.Instant
provider?: ScheduledDispatchRecord['provider']
limit: number
}): Promise<readonly ScheduledDispatchRecord[]> {
return [...this.dispatches.values()]
.filter((dispatch) => dispatch.status === 'scheduled')
.filter((dispatch) => dispatch.dueAt.epochMilliseconds <= input.dueBefore.epochMilliseconds)
.filter((dispatch) => (input.provider ? dispatch.provider === input.provider : true))
.sort((left, right) => left.dueAt.epochMilliseconds - right.dueAt.epochMilliseconds)
.slice(0, input.limit)
}
async updateScheduledDispatch(input: {
dispatchId: string
dueAt?: Temporal.Instant

View File

@@ -86,6 +86,7 @@ export interface ScheduledDispatchService {
cancelAdHocNotification(notificationId: string, cancelledAt?: Instant): Promise<void>
reconcileHouseholdBuiltInDispatches(householdId: string, asOf?: Instant): Promise<void>
reconcileAllBuiltInDispatches(asOf?: Instant): Promise<void>
listDueDispatches(input?: { asOf?: Instant; limit?: number }): Promise<readonly ScheduledDispatchRecord[]>
getDispatchById(dispatchId: string): Promise<ScheduledDispatchRecord | null>
claimDispatch(dispatchId: string): Promise<boolean>
releaseDispatch(dispatchId: string): Promise<void>
@@ -307,6 +308,14 @@ export function createScheduledDispatchService(input: {
}
},
listDueDispatches(inputValue) {
return input.repository.listDueScheduledDispatches({
dueBefore: inputValue?.asOf ?? nowInstant(),
provider: input.scheduler.provider,
limit: inputValue?.limit ?? 25
})
},
getDispatchById(dispatchId) {
return input.repository.getScheduledDispatchById(dispatchId)
},

View File

@@ -7,7 +7,11 @@ export const SCHEDULED_DISPATCH_KINDS = [
'rent_due'
] as const
export const SCHEDULED_DISPATCH_STATUSES = ['scheduled', 'sent', 'cancelled'] as const
export const SCHEDULED_DISPATCH_PROVIDERS = ['gcp-cloud-tasks', 'aws-eventbridge'] as const
export const SCHEDULED_DISPATCH_PROVIDERS = [
'gcp-cloud-tasks',
'aws-eventbridge',
'self-hosted'
] as const
export type ScheduledDispatchKind = (typeof SCHEDULED_DISPATCH_KINDS)[number]
export type ScheduledDispatchStatus = (typeof SCHEDULED_DISPATCH_STATUSES)[number]
@@ -64,6 +68,11 @@ export interface ScheduledDispatchRepository {
listScheduledDispatchesForHousehold(
householdId: string
): Promise<readonly ScheduledDispatchRecord[]>
listDueScheduledDispatches(input: {
dueBefore: Instant
provider?: ScheduledDispatchProvider
limit: number
}): Promise<readonly ScheduledDispatchRecord[]>
updateScheduledDispatch(
input: UpdateScheduledDispatchInput
): Promise<ScheduledDispatchRecord | null>