feat(member): add household lifecycle states

This commit is contained in:
2026-03-11 13:44:38 +04:00
parent 015298281c
commit 773abf2531
32 changed files with 3671 additions and 38 deletions

View File

@@ -28,6 +28,7 @@ function createRepositoryStub() {
householdId: household.householdId,
telegramUserId: '1',
displayName: 'Stan',
status: 'active',
preferredLocale: null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
@@ -94,6 +95,7 @@ function createRepositoryStub() {
householdId: input.householdId,
telegramUserId: input.telegramUserId,
displayName: input.displayName,
status: input.status ?? 'active',
preferredLocale: input.preferredLocale ?? null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
@@ -121,6 +123,7 @@ function createRepositoryStub() {
householdId: pending.householdId,
telegramUserId: pending.telegramUserId,
displayName: pending.displayName,
status: 'active',
preferredLocale: null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
@@ -174,7 +177,8 @@ function createRepositoryStub() {
isActive: input.isActive
}),
promoteHouseholdAdmin: async () => null,
updateHouseholdMemberRentShareWeight: async () => null
updateHouseholdMemberRentShareWeight: async () => null,
updateHouseholdMemberStatus: async () => null
}
return {
@@ -243,6 +247,7 @@ describe('createHouseholdAdminService', () => {
householdId: 'household-1',
telegramUserId: '2',
displayName: 'Alice',
status: 'active',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,

View File

@@ -99,6 +99,7 @@ function createRepositoryStub() {
householdId: input.householdId,
telegramUserId: input.telegramUserId,
displayName: input.displayName,
status: input.status ?? 'active',
preferredLocale: input.preferredLocale ?? null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
@@ -133,6 +134,7 @@ function createRepositoryStub() {
householdId: pending.householdId,
telegramUserId: pending.telegramUserId,
displayName: pending.displayName,
status: 'active',
preferredLocale: null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
@@ -198,6 +200,9 @@ function createRepositoryStub() {
},
async updateHouseholdMemberRentShareWeight() {
return null
},
async updateHouseholdMemberStatus() {
return null
}
}
@@ -325,6 +330,7 @@ describe('createHouseholdOnboardingService', () => {
id: 'member-42',
householdId: 'household-1',
displayName: 'Stan',
status: 'active',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
@@ -340,6 +346,7 @@ describe('createHouseholdOnboardingService', () => {
householdId: 'household-1',
telegramUserId: '42',
displayName: 'Stan',
status: 'active',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
@@ -358,6 +365,7 @@ describe('createHouseholdOnboardingService', () => {
householdId: 'household-2',
telegramUserId: '42',
displayName: 'Stan elsewhere',
status: 'active',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,

View File

@@ -17,6 +17,7 @@ export type HouseholdMiniAppAccess =
id: string
householdId: string
displayName: string
status: HouseholdMemberRecord['status']
isAdmin: boolean
preferredLocale: SupportedLocale | null
householdDefaultLocale: SupportedLocale
@@ -68,6 +69,7 @@ export interface HouseholdOnboardingService {
id: string
householdId: string
displayName: string
status: HouseholdMemberRecord['status']
isAdmin: boolean
preferredLocale: SupportedLocale | null
householdDefaultLocale: SupportedLocale
@@ -84,6 +86,7 @@ function toMember(member: HouseholdMemberRecord): {
id: string
householdId: string
displayName: string
status: HouseholdMemberRecord['status']
isAdmin: boolean
preferredLocale: SupportedLocale | null
householdDefaultLocale: SupportedLocale
@@ -93,6 +96,7 @@ function toMember(member: HouseholdMemberRecord): {
id: member.id,
householdId: member.householdId,
displayName: member.displayName,
status: member.status,
isAdmin: member.isAdmin,
preferredLocale: member.preferredLocale,
householdDefaultLocale: member.householdDefaultLocale,

View File

@@ -174,6 +174,7 @@ function createRepositoryStub() {
householdId: input.householdId,
telegramUserId: input.telegramUserId,
displayName: input.displayName,
status: input.status ?? existing?.status ?? 'active',
preferredLocale: input.preferredLocale ?? existing?.preferredLocale ?? null,
householdDefaultLocale:
[...households.values()].find((household) => household.householdId === input.householdId)
@@ -215,6 +216,7 @@ function createRepositoryStub() {
householdId: pending.householdId,
telegramUserId: pending.telegramUserId,
displayName: pending.displayName,
status: 'active',
preferredLocale: null,
householdDefaultLocale:
[...households.values()].find(
@@ -319,6 +321,22 @@ function createRepositoryStub() {
}
members.set(`${householdId}:${member.telegramUserId}`, next)
return next
},
async updateHouseholdMemberStatus(householdId, memberId, status) {
const member = [...members.values()].find(
(entry) => entry.householdId === householdId && entry.id === memberId
)
if (!member) {
return null
}
const next = {
...member,
status
}
members.set(`${householdId}:${member.telegramUserId}`, next)
return next
}
}
@@ -353,6 +371,7 @@ describe('createHouseholdSetupService', () => {
householdId: result.household.householdId,
telegramUserId: '42',
displayName: 'Stan',
status: 'active',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
@@ -391,6 +410,7 @@ describe('createHouseholdSetupService', () => {
householdId: result.household.householdId,
telegramUserId: '77',
displayName: 'Mia',
status: 'active',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,

View File

@@ -19,6 +19,7 @@ function createRepository(): HouseholdConfigurationRepository {
householdId: 'household-1',
telegramUserId: '123456',
displayName: 'Stan',
status: 'active',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
@@ -109,7 +110,8 @@ function createRepository(): HouseholdConfigurationRepository {
isActive: input.isActive
}),
promoteHouseholdAdmin: async () => null,
updateHouseholdMemberRentShareWeight: async () => null
updateHouseholdMemberRentShareWeight: async () => null,
updateHouseholdMemberStatus: async () => null
}
}

View File

@@ -61,6 +61,7 @@ function repository(): HouseholdConfigurationRepository {
householdId: input.householdId,
telegramUserId: input.telegramUserId,
displayName: input.displayName,
status: input.status ?? 'active',
preferredLocale: input.preferredLocale ?? null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
@@ -87,6 +88,7 @@ function repository(): HouseholdConfigurationRepository {
householdId: input.householdId,
telegramUserId: input.telegramUserId,
displayName: 'Stan',
status: 'active',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
@@ -108,6 +110,7 @@ function repository(): HouseholdConfigurationRepository {
householdId: 'household-1',
telegramUserId,
displayName: 'Stan',
status: 'active',
preferredLocale: locale,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
@@ -152,6 +155,7 @@ function repository(): HouseholdConfigurationRepository {
householdId,
telegramUserId: '123456',
displayName: 'Stan',
status: 'active',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
@@ -165,11 +169,26 @@ function repository(): HouseholdConfigurationRepository {
householdId: 'household-1',
telegramUserId: '123456',
displayName: 'Stan',
status: 'active',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight,
isAdmin: false
}
: null,
updateHouseholdMemberStatus: async (_householdId, memberId, status) =>
memberId === 'member-123456'
? {
id: memberId,
householdId: 'household-1',
telegramUserId: '123456',
displayName: 'Stan',
status,
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: false
}
: null
}
}
@@ -318,6 +337,7 @@ describe('createMiniAppAdminService', () => {
householdId: 'household-1',
telegramUserId: '123456',
displayName: 'Stan',
status: 'active',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
@@ -342,6 +362,7 @@ describe('createMiniAppAdminService', () => {
householdId: 'household-1',
telegramUserId: '123456',
displayName: 'Stan',
status: 'active',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
@@ -349,4 +370,30 @@ describe('createMiniAppAdminService', () => {
}
})
})
test('updates a household member lifecycle status for admins', async () => {
const service = createMiniAppAdminService(repository())
const result = await service.updateMemberStatus({
householdId: 'household-1',
actorIsAdmin: true,
memberId: 'member-123456',
status: 'away'
})
expect(result).toEqual({
status: 'ok',
member: {
id: 'member-123456',
householdId: 'household-1',
telegramUserId: '123456',
displayName: 'Stan',
status: 'away',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: false
}
})
})
})

View File

@@ -1,6 +1,7 @@
import type {
HouseholdBillingSettingsRecord,
HouseholdConfigurationRepository,
HouseholdMemberLifecycleStatus,
HouseholdMemberRecord,
HouseholdPendingMemberRecord,
HouseholdTopicBindingRecord,
@@ -126,6 +127,21 @@ export interface MiniAppAdminService {
reason: 'not_admin' | 'invalid_weight' | 'member_not_found'
}
>
updateMemberStatus(input: {
householdId: string
actorIsAdmin: boolean
memberId: string
status: HouseholdMemberLifecycleStatus
}): Promise<
| {
status: 'ok'
member: HouseholdMemberRecord
}
| {
status: 'rejected'
reason: 'not_admin' | 'member_not_found'
}
>
}
export function createMiniAppAdminService(
@@ -349,6 +365,32 @@ export function createMiniAppAdminService(
}
}
return {
status: 'ok',
member
}
},
async updateMemberStatus(input) {
if (!input.actorIsAdmin) {
return {
status: 'rejected',
reason: 'not_admin'
}
}
const member = await repository.updateHouseholdMemberStatus(
input.householdId,
input.memberId,
input.status
)
if (!member) {
return {
status: 'rejected',
reason: 'member_not_found'
}
}
return {
status: 'ok',
member