mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 15:44:02 +00:00
feat(bot): add multi-household reminder delivery
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { and, asc, eq } from 'drizzle-orm'
|
||||
|
||||
import { createDbClient, schema } from '@household/db'
|
||||
import { instantToDate, normalizeSupportedLocale, nowInstant } from '@household/domain'
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type HouseholdTelegramChatRecord,
|
||||
type HouseholdTopicBindingRecord,
|
||||
type HouseholdTopicRole,
|
||||
type ReminderTarget,
|
||||
type RegisterTelegramHouseholdChatResult
|
||||
} from '@household/ports'
|
||||
|
||||
@@ -125,6 +126,27 @@ function toHouseholdMemberRecord(row: {
|
||||
}
|
||||
}
|
||||
|
||||
function toReminderTarget(row: {
|
||||
householdId: string
|
||||
householdName: string
|
||||
telegramChatId: string
|
||||
reminderThreadId: string | null
|
||||
defaultLocale: string
|
||||
}): ReminderTarget {
|
||||
const locale = normalizeSupportedLocale(row.defaultLocale)
|
||||
if (!locale) {
|
||||
throw new Error(`Unsupported household default locale: ${row.defaultLocale}`)
|
||||
}
|
||||
|
||||
return {
|
||||
householdId: row.householdId,
|
||||
householdName: row.householdName,
|
||||
telegramChatId: row.telegramChatId,
|
||||
telegramThreadId: row.reminderThreadId,
|
||||
locale
|
||||
}
|
||||
}
|
||||
|
||||
export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
||||
repository: HouseholdConfigurationRepository
|
||||
close: () => Promise<void>
|
||||
@@ -364,6 +386,35 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
||||
return rows.map(toHouseholdTopicBindingRecord)
|
||||
},
|
||||
|
||||
async listReminderTargets() {
|
||||
const rows = await db
|
||||
.select({
|
||||
householdId: schema.householdTelegramChats.householdId,
|
||||
householdName: schema.households.name,
|
||||
telegramChatId: schema.householdTelegramChats.telegramChatId,
|
||||
reminderThreadId: schema.householdTopicBindings.telegramThreadId,
|
||||
defaultLocale: schema.households.defaultLocale
|
||||
})
|
||||
.from(schema.householdTelegramChats)
|
||||
.innerJoin(
|
||||
schema.households,
|
||||
eq(schema.householdTelegramChats.householdId, schema.households.id)
|
||||
)
|
||||
.leftJoin(
|
||||
schema.householdTopicBindings,
|
||||
and(
|
||||
eq(
|
||||
schema.householdTopicBindings.householdId,
|
||||
schema.householdTelegramChats.householdId
|
||||
),
|
||||
eq(schema.householdTopicBindings.role, 'reminders')
|
||||
)
|
||||
)
|
||||
.orderBy(asc(schema.householdTelegramChats.telegramChatId), asc(schema.households.name))
|
||||
|
||||
return rows.map(toReminderTarget)
|
||||
},
|
||||
|
||||
async upsertHouseholdJoinToken(input) {
|
||||
const rows = await db
|
||||
.insert(schema.householdJoinTokens)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
|
||||
import { createDbClient, schema } from '@household/db'
|
||||
import type { ReminderDispatchRepository } from '@household/ports'
|
||||
|
||||
@@ -34,6 +36,20 @@ export function createDbReminderDispatchRepository(databaseUrl: string): {
|
||||
dedupeKey,
|
||||
claimed: rows.length > 0
|
||||
}
|
||||
},
|
||||
|
||||
async releaseReminderDispatch(input) {
|
||||
const dedupeKey = `${input.period}:${input.reminderType}`
|
||||
|
||||
await db
|
||||
.delete(schema.processedBotMessages)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.processedBotMessages.householdId, input.householdId),
|
||||
eq(schema.processedBotMessages.source, 'scheduler-reminder'),
|
||||
eq(schema.processedBotMessages.sourceMessageKey, dedupeKey)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ function createRepositoryStub() {
|
||||
getHouseholdTopicBinding: async () => null,
|
||||
findHouseholdTopicByTelegramContext: async () => null,
|
||||
listHouseholdTopicBindings: async () => [],
|
||||
listReminderTargets: async () => [],
|
||||
upsertHouseholdJoinToken: async (input) =>
|
||||
({
|
||||
householdId: household.householdId,
|
||||
|
||||
@@ -55,6 +55,9 @@ function createRepositoryStub() {
|
||||
async listHouseholdTopicBindings() {
|
||||
return []
|
||||
},
|
||||
async listReminderTargets() {
|
||||
return []
|
||||
},
|
||||
async upsertHouseholdJoinToken(input) {
|
||||
joinToken = {
|
||||
householdId: household.householdId,
|
||||
|
||||
@@ -93,6 +93,9 @@ function createRepositoryStub() {
|
||||
async listHouseholdTopicBindings(householdId) {
|
||||
return bindings.get(householdId) ?? []
|
||||
},
|
||||
async listReminderTargets() {
|
||||
return []
|
||||
},
|
||||
|
||||
async upsertHouseholdJoinToken(input) {
|
||||
const household = [...households.values()].find(
|
||||
|
||||
@@ -37,6 +37,7 @@ function createRepository(): HouseholdConfigurationRepository {
|
||||
getHouseholdTopicBinding: async () => null,
|
||||
findHouseholdTopicByTelegramContext: async () => null,
|
||||
listHouseholdTopicBindings: async () => [],
|
||||
listReminderTargets: async () => [],
|
||||
upsertHouseholdJoinToken: async () => ({
|
||||
householdId: household.householdId,
|
||||
householdName: household.householdName,
|
||||
|
||||
@@ -28,6 +28,7 @@ function repository(): HouseholdConfigurationRepository {
|
||||
getHouseholdTopicBinding: async () => null,
|
||||
findHouseholdTopicByTelegramContext: async () => null,
|
||||
listHouseholdTopicBindings: async () => [],
|
||||
listReminderTargets: async () => [],
|
||||
upsertHouseholdJoinToken: async () => ({
|
||||
householdId: 'household-1',
|
||||
householdName: 'Kojori House',
|
||||
|
||||
@@ -22,6 +22,8 @@ class ReminderDispatchRepositoryStub implements ReminderDispatchRepository {
|
||||
this.lastClaim = input
|
||||
return this.nextResult
|
||||
}
|
||||
|
||||
async releaseReminderDispatch(): Promise<void> {}
|
||||
}
|
||||
|
||||
describe('createReminderJobService', () => {
|
||||
|
||||
@@ -19,7 +19,7 @@ const server = {
|
||||
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
||||
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
|
||||
DATABASE_URL: z.string().url(),
|
||||
HOUSEHOLD_ID: z.string().uuid(),
|
||||
HOUSEHOLD_ID: z.string().uuid().optional(),
|
||||
SUPABASE_URL: z.string().url().optional(),
|
||||
SUPABASE_PUBLISHABLE_KEY: z.string().min(1).optional(),
|
||||
SUPABASE_SERVICE_ROLE_KEY: z.string().min(1).optional(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SupportedLocale } from '@household/domain'
|
||||
import type { ReminderTarget } from './reminders'
|
||||
|
||||
export const HOUSEHOLD_TOPIC_ROLES = ['purchase', 'feedback', 'reminders'] as const
|
||||
|
||||
@@ -80,6 +81,7 @@ export interface HouseholdConfigurationRepository {
|
||||
telegramThreadId: string
|
||||
}): Promise<HouseholdTopicBindingRecord | null>
|
||||
listHouseholdTopicBindings(householdId: string): Promise<readonly HouseholdTopicBindingRecord[]>
|
||||
listReminderTargets(): Promise<readonly ReminderTarget[]>
|
||||
upsertHouseholdJoinToken(input: {
|
||||
householdId: string
|
||||
token: string
|
||||
|
||||
@@ -3,6 +3,7 @@ export {
|
||||
type ClaimReminderDispatchInput,
|
||||
type ClaimReminderDispatchResult,
|
||||
type ReminderDispatchRepository,
|
||||
type ReminderTarget,
|
||||
type ReminderType
|
||||
} from './reminders'
|
||||
export {
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import type { SupportedLocale } from '@household/domain'
|
||||
|
||||
export const REMINDER_TYPES = ['utilities', 'rent-warning', 'rent-due'] as const
|
||||
|
||||
export type ReminderType = (typeof REMINDER_TYPES)[number]
|
||||
|
||||
export interface ReminderTarget {
|
||||
householdId: string
|
||||
householdName: string
|
||||
telegramChatId: string
|
||||
telegramThreadId: string | null
|
||||
locale: SupportedLocale
|
||||
}
|
||||
|
||||
export interface ClaimReminderDispatchInput {
|
||||
householdId: string
|
||||
period: string
|
||||
@@ -16,4 +26,9 @@ export interface ClaimReminderDispatchResult {
|
||||
|
||||
export interface ReminderDispatchRepository {
|
||||
claimReminderDispatch(input: ClaimReminderDispatchInput): Promise<ClaimReminderDispatchResult>
|
||||
releaseReminderDispatch(input: {
|
||||
householdId: string
|
||||
period: string
|
||||
reminderType: ReminderType
|
||||
}): Promise<void>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user