diff --git a/apps/bot/src/anonymous-feedback.test.ts b/apps/bot/src/anonymous-feedback.test.ts index 6e96194..71ec8b5 100644 --- a/apps/bot/src/anonymous-feedback.test.ts +++ b/apps/bot/src/anonymous-feedback.test.ts @@ -148,6 +148,7 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit isAdmin: input.isAdmin === true }), getHouseholdMember: async () => null, + listHouseholdMembers: async () => [], listHouseholdMembersByTelegramUserId: async () => [ { id: 'member-123456', diff --git a/apps/bot/src/miniapp-admin.test.ts b/apps/bot/src/miniapp-admin.test.ts index e2a5cc9..a5fd324 100644 --- a/apps/bot/src/miniapp-admin.test.ts +++ b/apps/bot/src/miniapp-admin.test.ts @@ -64,6 +64,7 @@ function onboardingRepository(): HouseholdConfigurationRepository { isAdmin: input.isAdmin === true }), getHouseholdMember: async () => null, + listHouseholdMembers: async () => [], listHouseholdMembersByTelegramUserId: async () => [], listPendingHouseholdMembers: async () => [ { diff --git a/apps/bot/src/miniapp-auth.test.ts b/apps/bot/src/miniapp-auth.test.ts index 0672c42..34c88e7 100644 --- a/apps/bot/src/miniapp-auth.test.ts +++ b/apps/bot/src/miniapp-auth.test.ts @@ -95,6 +95,8 @@ function onboardingRepository(): HouseholdConfigurationRepository { return member }, getHouseholdMember: async (_householdId, telegramUserId) => members.get(telegramUserId) ?? null, + listHouseholdMembers: async (householdId) => + [...members.values()].filter((member) => member.householdId === householdId), listHouseholdMembersByTelegramUserId: async (telegramUserId) => { const member = members.get(telegramUserId) return member ? [member] : [] diff --git a/apps/bot/src/miniapp-dashboard.test.ts b/apps/bot/src/miniapp-dashboard.test.ts index d5d9195..ed2c8d7 100644 --- a/apps/bot/src/miniapp-dashboard.test.ts +++ b/apps/bot/src/miniapp-dashboard.test.ts @@ -121,6 +121,7 @@ function onboardingRepository(): HouseholdConfigurationRepository { isAdmin: input.isAdmin === true }), getHouseholdMember: async () => null, + listHouseholdMembers: async () => [], listHouseholdMembersByTelegramUserId: async () => [], listPendingHouseholdMembers: async () => [], approvePendingHouseholdMember: async () => null diff --git a/packages/adapters-db/src/household-config-repository.ts b/packages/adapters-db/src/household-config-repository.ts index 15184d4..85dc20d 100644 --- a/packages/adapters-db/src/household-config-repository.ts +++ b/packages/adapters-db/src/household-config-repository.ts @@ -595,6 +595,22 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): { return row ? toHouseholdMemberRecord(row) : null }, + async listHouseholdMembers(householdId) { + const rows = await db + .select({ + id: schema.members.id, + householdId: schema.members.householdId, + telegramUserId: schema.members.telegramUserId, + displayName: schema.members.displayName, + isAdmin: schema.members.isAdmin + }) + .from(schema.members) + .where(eq(schema.members.householdId, householdId)) + .orderBy(schema.members.displayName, schema.members.telegramUserId) + + return rows.map(toHouseholdMemberRecord) + }, + async listHouseholdMembersByTelegramUserId(telegramUserId) { const rows = await db .select({ diff --git a/packages/application/src/household-admin-service.test.ts b/packages/application/src/household-admin-service.test.ts index d0d38e2..3545b41 100644 --- a/packages/application/src/household-admin-service.test.ts +++ b/packages/application/src/household-admin-service.test.ts @@ -92,6 +92,8 @@ function createRepositoryStub() { return record }, getHouseholdMember: async (_householdId, telegramUserId) => members.get(telegramUserId) ?? null, + listHouseholdMembers: async (householdId) => + [...members.values()].filter((member) => member.householdId === householdId), listHouseholdMembersByTelegramUserId: async (telegramUserId) => [...members.values()].filter((member) => member.telegramUserId === telegramUserId), listPendingHouseholdMembers: async () => [...pendingMembers.values()], diff --git a/packages/application/src/household-onboarding-service.test.ts b/packages/application/src/household-onboarding-service.test.ts index 80610e2..74027a1 100644 --- a/packages/application/src/household-onboarding-service.test.ts +++ b/packages/application/src/household-onboarding-service.test.ts @@ -101,6 +101,9 @@ function createRepositoryStub() { async getHouseholdMember(_householdId, telegramUserId) { return members.get(telegramUserId) ?? null }, + async listHouseholdMembers(householdId) { + return [...members.values()].filter((member) => member.householdId === householdId) + }, async listHouseholdMembersByTelegramUserId(telegramUserId) { const member = members.get(telegramUserId) return member ? [member] : [] diff --git a/packages/application/src/household-setup-service.test.ts b/packages/application/src/household-setup-service.test.ts index 3780fe7..6ab3cdb 100644 --- a/packages/application/src/household-setup-service.test.ts +++ b/packages/application/src/household-setup-service.test.ts @@ -176,6 +176,10 @@ function createRepositoryStub() { return members.get(`${householdId}:${telegramUserId}`) ?? null }, + async listHouseholdMembers(householdId) { + return [...members.values()].filter((member) => member.householdId === householdId) + }, + async listHouseholdMembersByTelegramUserId(telegramUserId) { return [...members.values()].filter((member) => member.telegramUserId === telegramUserId) }, @@ -240,6 +244,77 @@ describe('createHouseholdSetupService', () => { }) }) + test('ensures the actor becomes admin when rerunning setup for an existing household with no admins', async () => { + const { repository } = createRepositoryStub() + const service = createHouseholdSetupService(repository) + + await service.setupGroupChat({ + actorIsAdmin: true, + telegramChatId: '-100123', + telegramChatType: 'supergroup', + title: 'Kojori House' + }) + + const result = await service.setupGroupChat({ + actorIsAdmin: true, + actorTelegramUserId: '77', + actorDisplayName: 'Mia', + telegramChatId: '-100123', + telegramChatType: 'supergroup', + title: 'Kojori House' + }) + + expect(result.status).toBe('existing') + if (result.status !== 'existing') { + return + } + + const admin = await repository.getHouseholdMember(result.household.householdId, '77') + expect(admin).toEqual({ + id: 'member-77', + householdId: result.household.householdId, + telegramUserId: '77', + displayName: 'Mia', + isAdmin: true + }) + }) + + test('does not grant admin when rerunning setup for an existing household that already has admins', async () => { + const { repository } = createRepositoryStub() + const service = createHouseholdSetupService(repository) + + const created = await service.setupGroupChat({ + actorIsAdmin: true, + actorTelegramUserId: '42', + actorDisplayName: 'Stan', + telegramChatId: '-100123', + telegramChatType: 'supergroup', + title: 'Kojori House' + }) + + expect(created.status).toBe('created') + if (created.status !== 'created') { + return + } + + const result = await service.setupGroupChat({ + actorIsAdmin: true, + actorTelegramUserId: '77', + actorDisplayName: 'Mia', + telegramChatId: '-100123', + telegramChatType: 'supergroup', + title: 'Kojori House' + }) + + expect(result.status).toBe('existing') + if (result.status !== 'existing') { + return + } + + const admin = await repository.getHouseholdMember(result.household.householdId, '77') + expect(admin).toBeNull() + }) + test('rejects setup when the actor is not a group admin', async () => { const { repository } = createRepositoryStub() const service = createHouseholdSetupService(repository) diff --git a/packages/application/src/household-setup-service.ts b/packages/application/src/household-setup-service.ts index bf67fd5..1e4c3cb 100644 --- a/packages/application/src/household-setup-service.ts +++ b/packages/application/src/household-setup-service.ts @@ -85,13 +85,20 @@ export function createHouseholdSetupService( : {}) }) - if (registered.status === 'created' && input.actorTelegramUserId && input.actorDisplayName) { - await repository.ensureHouseholdMember({ - householdId: registered.household.householdId, - telegramUserId: input.actorTelegramUserId, - displayName: input.actorDisplayName, - isAdmin: true - }) + if (input.actorTelegramUserId && input.actorDisplayName) { + const existingMembers = await repository.listHouseholdMembers( + registered.household.householdId + ) + const hasAdmin = existingMembers.some((member) => member.isAdmin) + + if (registered.status === 'created' || !hasAdmin) { + await repository.ensureHouseholdMember({ + householdId: registered.household.householdId, + telegramUserId: input.actorTelegramUserId, + displayName: input.actorDisplayName, + isAdmin: true + }) + } } return { diff --git a/packages/application/src/miniapp-admin-service.test.ts b/packages/application/src/miniapp-admin-service.test.ts index 05dd285..e29da2f 100644 --- a/packages/application/src/miniapp-admin-service.test.ts +++ b/packages/application/src/miniapp-admin-service.test.ts @@ -53,6 +53,7 @@ function repository(): HouseholdConfigurationRepository { isAdmin: input.isAdmin === true }), getHouseholdMember: async () => null, + listHouseholdMembers: async () => [], listHouseholdMembersByTelegramUserId: async () => [], listPendingHouseholdMembers: async () => [ { diff --git a/packages/ports/src/household-config.ts b/packages/ports/src/household-config.ts index a059257..3c97675 100644 --- a/packages/ports/src/household-config.ts +++ b/packages/ports/src/household-config.ts @@ -105,6 +105,7 @@ export interface HouseholdConfigurationRepository { householdId: string, telegramUserId: string ): Promise + listHouseholdMembers(householdId: string): Promise listHouseholdMembersByTelegramUserId( telegramUserId: string ): Promise