feat(bot): add ad hoc reminder notifications

This commit is contained in:
2026-03-24 01:28:26 +04:00
parent dc499214d9
commit 7218b55b1f
21 changed files with 6746 additions and 8 deletions

View File

@@ -0,0 +1,114 @@
import { describe, expect, test } from 'bun:test'
import { Temporal } from '@household/domain'
import type { AdHocNotificationService, DeliverableAdHocNotification } from '@household/application'
import { createAdHocNotificationJobsHandler } from './ad-hoc-notification-jobs'
function dueNotification(
input: Partial<DeliverableAdHocNotification['notification']> = {}
): DeliverableAdHocNotification {
return {
notification: {
id: input.id ?? 'notif-1',
householdId: input.householdId ?? 'household-1',
creatorMemberId: input.creatorMemberId ?? 'creator',
assigneeMemberId: input.assigneeMemberId ?? 'assignee',
originalRequestText: 'raw',
notificationText: input.notificationText ?? 'Ping Georgiy',
timezone: input.timezone ?? 'Asia/Tbilisi',
scheduledFor: input.scheduledFor ?? Temporal.Instant.from('2026-03-23T09:00:00Z'),
timePrecision: input.timePrecision ?? 'exact',
deliveryMode: input.deliveryMode ?? 'topic',
dmRecipientMemberIds: input.dmRecipientMemberIds ?? [],
friendlyTagAssignee: input.friendlyTagAssignee ?? true,
status: input.status ?? 'scheduled',
sourceTelegramChatId: null,
sourceTelegramThreadId: null,
sentAt: null,
cancelledAt: null,
cancelledByMemberId: null,
createdAt: Temporal.Instant.from('2026-03-22T09:00:00Z'),
updatedAt: Temporal.Instant.from('2026-03-22T09:00:00Z')
},
creator: {
memberId: 'creator',
telegramUserId: '111',
displayName: 'Dima'
},
assignee: {
memberId: 'assignee',
telegramUserId: '222',
displayName: 'Georgiy'
},
dmRecipients: [
{
memberId: 'recipient',
telegramUserId: '333',
displayName: 'Alice'
}
]
}
}
describe('createAdHocNotificationJobsHandler', () => {
test('delivers topic notifications and marks them sent', async () => {
const sentTopicMessages: string[] = []
const sentNotifications: string[] = []
const service: AdHocNotificationService = {
scheduleNotification: async () => {
throw new Error('not used')
},
listUpcomingNotifications: async () => [],
cancelNotification: async () => ({ status: 'not_found' }),
listDueNotifications: async () => [dueNotification()],
claimDueNotification: async () => true,
releaseDueNotification: async () => {},
markNotificationSent: async (notificationId) => {
sentNotifications.push(notificationId)
return null
}
}
const handler = createAdHocNotificationJobsHandler({
notificationService: service,
householdConfigurationRepository: {
async getHouseholdChatByHouseholdId() {
return {
householdId: 'household-1',
householdName: 'Kojori',
telegramChatId: '777',
telegramChatType: 'supergroup',
title: 'Kojori',
defaultLocale: 'ru'
}
},
async getHouseholdTopicBinding() {
return {
householdId: 'household-1',
role: 'reminders',
telegramThreadId: '103',
topicName: 'Reminders'
}
}
},
sendTopicMessage: async (input) => {
sentTopicMessages.push(`${input.chatId}:${input.threadId}:${input.text}`)
},
sendDirectMessage: async () => {}
})
const response = await handler.handle(
new Request('http://localhost/jobs/notifications/due', {
method: 'POST'
})
)
const payload = (await response.json()) as { ok: boolean; notifications: { outcome: string }[] }
expect(payload.ok).toBe(true)
expect(payload.notifications[0]?.outcome).toBe('sent')
expect(sentTopicMessages[0]).toContain('tg://user?id=222')
expect(sentNotifications).toEqual(['notif-1'])
})
})