feat(bot): cut over multi-household member flows

This commit is contained in:
2026-03-09 06:14:57 +04:00
parent de86706f4f
commit 7c602900ee
20 changed files with 1068 additions and 163 deletions

View File

@@ -1,4 +1,5 @@
import type { FinanceCommandService } from '@household/application'
import type { HouseholdConfigurationRepository } from '@household/ports'
import type { Bot, Context } from 'grammy'
function commandArgs(ctx: Context): string[] {
@@ -10,9 +11,39 @@ function commandArgs(ctx: Context): string[] {
return raw.split(/\s+/).filter(Boolean)
}
export function createFinanceCommandsService(financeService: FinanceCommandService): {
function isGroupChat(ctx: Context): boolean {
return ctx.chat?.type === 'group' || ctx.chat?.type === 'supergroup'
}
export function createFinanceCommandsService(options: {
householdConfigurationRepository: HouseholdConfigurationRepository
financeServiceForHousehold: (householdId: string) => FinanceCommandService
}): {
register: (bot: Bot) => void
} {
async function resolveGroupFinanceService(ctx: Context): Promise<{
service: FinanceCommandService
householdId: string
} | null> {
if (!isGroupChat(ctx)) {
await ctx.reply('Use this command inside a household group.')
return null
}
const household = await options.householdConfigurationRepository.getTelegramHouseholdChat(
ctx.chat!.id.toString()
)
if (!household) {
await ctx.reply('Household is not configured for this chat yet. Run /setup first.')
return null
}
return {
service: options.financeServiceForHousehold(household.householdId),
householdId: household.householdId
}
}
async function requireMember(ctx: Context) {
const telegramUserId = ctx.from?.id?.toString()
if (!telegramUserId) {
@@ -20,33 +51,42 @@ export function createFinanceCommandsService(financeService: FinanceCommandServi
return null
}
const member = await financeService.getMemberByTelegramUserId(telegramUserId)
const scoped = await resolveGroupFinanceService(ctx)
if (!scoped) {
return null
}
const member = await scoped.service.getMemberByTelegramUserId(telegramUserId)
if (!member) {
await ctx.reply('You are not a member of this household.')
return null
}
return member
return {
member,
service: scoped.service,
householdId: scoped.householdId
}
}
async function requireAdmin(ctx: Context) {
const member = await requireMember(ctx)
if (!member) {
const resolved = await requireMember(ctx)
if (!resolved) {
return null
}
if (!member.isAdmin) {
if (!resolved.member.isAdmin) {
await ctx.reply('Only household admins can use this command.')
return null
}
return member
return resolved
}
function register(bot: Bot): void {
bot.command('cycle_open', async (ctx) => {
const admin = await requireAdmin(ctx)
if (!admin) {
const resolved = await requireAdmin(ctx)
if (!resolved) {
return
}
@@ -57,7 +97,7 @@ export function createFinanceCommandsService(financeService: FinanceCommandServi
}
try {
const cycle = await financeService.openCycle(args[0]!, args[1])
const cycle = await resolved.service.openCycle(args[0]!, args[1])
await ctx.reply(`Cycle opened: ${cycle.period} (${cycle.currency})`)
} catch (error) {
await ctx.reply(`Failed to open cycle: ${(error as Error).message}`)
@@ -65,13 +105,13 @@ export function createFinanceCommandsService(financeService: FinanceCommandServi
})
bot.command('cycle_close', async (ctx) => {
const admin = await requireAdmin(ctx)
if (!admin) {
const resolved = await requireAdmin(ctx)
if (!resolved) {
return
}
try {
const cycle = await financeService.closeCycle(commandArgs(ctx)[0])
const cycle = await resolved.service.closeCycle(commandArgs(ctx)[0])
if (!cycle) {
await ctx.reply('No cycle found to close.')
return
@@ -84,8 +124,8 @@ export function createFinanceCommandsService(financeService: FinanceCommandServi
})
bot.command('rent_set', async (ctx) => {
const admin = await requireAdmin(ctx)
if (!admin) {
const resolved = await requireAdmin(ctx)
if (!resolved) {
return
}
@@ -96,7 +136,7 @@ export function createFinanceCommandsService(financeService: FinanceCommandServi
}
try {
const result = await financeService.setRent(args[0]!, args[1], args[2])
const result = await resolved.service.setRent(args[0]!, args[1], args[2])
if (!result) {
await ctx.reply('No period provided and no open cycle found.')
return
@@ -111,8 +151,8 @@ export function createFinanceCommandsService(financeService: FinanceCommandServi
})
bot.command('utility_add', async (ctx) => {
const admin = await requireAdmin(ctx)
if (!admin) {
const resolved = await requireAdmin(ctx)
if (!resolved) {
return
}
@@ -123,7 +163,12 @@ export function createFinanceCommandsService(financeService: FinanceCommandServi
}
try {
const result = await financeService.addUtilityBill(args[0]!, args[1]!, admin.id, args[2])
const result = await resolved.service.addUtilityBill(
args[0]!,
args[1]!,
resolved.member.id,
args[2]
)
if (!result) {
await ctx.reply('No open cycle found. Use /cycle_open first.')
return
@@ -138,13 +183,13 @@ export function createFinanceCommandsService(financeService: FinanceCommandServi
})
bot.command('statement', async (ctx) => {
const member = await requireMember(ctx)
if (!member) {
const resolved = await requireMember(ctx)
if (!resolved) {
return
}
try {
const statement = await financeService.generateStatement(commandArgs(ctx)[0])
const statement = await resolved.service.generateStatement(commandArgs(ctx)[0])
if (!statement) {
await ctx.reply('No cycle found for statement.')
return