feat(member): improve assistant roster awareness

This commit is contained in:
2026-03-11 15:10:20 +04:00
parent 79f96ba45b
commit 0787847c19
27 changed files with 1429 additions and 3 deletions

View File

@@ -1297,6 +1297,40 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
})
},
async updateHouseholdMemberDisplayName(householdId, memberId, displayName) {
const rows = await db
.update(schema.members)
.set({
displayName
})
.where(and(eq(schema.members.householdId, householdId), eq(schema.members.id, memberId)))
.returning({
id: schema.members.id,
householdId: schema.members.householdId,
telegramUserId: schema.members.telegramUserId,
displayName: schema.members.displayName,
lifecycleStatus: schema.members.lifecycleStatus,
preferredLocale: schema.members.preferredLocale,
rentShareWeight: schema.members.rentShareWeight,
isAdmin: schema.members.isAdmin
})
const row = rows[0]
if (!row) {
return null
}
const household = await this.getHouseholdChatByHouseholdId(householdId)
if (!household) {
throw new Error('Failed to resolve household chat after member display name update')
}
return toHouseholdMemberRecord({
...row,
defaultLocale: household.defaultLocale
})
},
async promoteHouseholdAdmin(householdId, memberId) {
const rows = await db
.update(schema.members)

View File

@@ -145,6 +145,7 @@ function createRepositoryStub() {
}
: null
},
updateHouseholdMemberDisplayName: async () => null,
getHouseholdBillingSettings: async (householdId) => ({
householdId,
settlementCurrency: 'GEL',

View File

@@ -156,6 +156,9 @@ function createRepositoryStub() {
}
: null
},
async updateHouseholdMemberDisplayName() {
return null
},
async getHouseholdBillingSettings(householdId) {
return {
householdId,

View File

@@ -252,6 +252,9 @@ function createRepositoryStub() {
}
: null
},
async updateHouseholdMemberDisplayName() {
return null
},
async getHouseholdBillingSettings(householdId) {
return {
householdId,

View File

@@ -78,6 +78,7 @@ function createRepository(): HouseholdConfigurationRepository {
householdDefaultLocale: 'ru'
}
: null,
updateHouseholdMemberDisplayName: async () => null,
getHouseholdBillingSettings: async (householdId) => ({
householdId,
settlementCurrency: 'GEL',

View File

@@ -136,6 +136,20 @@ function repository(): HouseholdConfigurationRepository {
isAdmin: false
}
: null,
updateHouseholdMemberDisplayName: async (_householdId, memberId, displayName) =>
memberId === 'member-123456'
? {
id: memberId,
householdId: 'household-1',
telegramUserId: '123456',
displayName,
status: 'active',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: false
}
: null,
getHouseholdBillingSettings: async (householdId) => ({
householdId,
settlementCurrency: 'GEL',
@@ -445,6 +459,57 @@ describe('createMiniAppAdminService', () => {
})
})
test('updates the acting member display name', async () => {
const service = createMiniAppAdminService(repository())
const result = await service.updateOwnDisplayName({
householdId: 'household-1',
actorMemberId: 'member-123456',
displayName: 'Stan Cozy'
})
expect(result).toEqual({
status: 'ok',
member: {
id: 'member-123456',
householdId: 'household-1',
telegramUserId: '123456',
displayName: 'Stan Cozy',
status: 'active',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: false
}
})
})
test('updates another member display name for admins', async () => {
const service = createMiniAppAdminService(repository())
const result = await service.updateMemberDisplayName({
householdId: 'household-1',
actorIsAdmin: true,
memberId: 'member-123456',
displayName: 'Stan Cozy'
})
expect(result).toEqual({
status: 'ok',
member: {
id: 'member-123456',
householdId: 'household-1',
telegramUserId: '123456',
displayName: 'Stan Cozy',
status: 'active',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: false
}
})
})
test('updates a household member lifecycle status for admins', async () => {
const service = createMiniAppAdminService(repository())

View File

@@ -146,6 +146,35 @@ export interface MiniAppAdminService {
reason: 'not_admin' | 'member_not_found'
}
>
updateOwnDisplayName(input: {
householdId: string
actorMemberId: string
displayName: string
}): Promise<
| {
status: 'ok'
member: HouseholdMemberRecord
}
| {
status: 'rejected'
reason: 'invalid_display_name' | 'member_not_found'
}
>
updateMemberDisplayName(input: {
householdId: string
actorIsAdmin: boolean
memberId: string
displayName: string
}): Promise<
| {
status: 'ok'
member: HouseholdMemberRecord
}
| {
status: 'rejected'
reason: 'not_admin' | 'invalid_display_name' | 'member_not_found'
}
>
updateMemberAbsencePolicy(input: {
householdId: string
actorIsAdmin: boolean
@@ -171,6 +200,16 @@ function periodFromLocalDate(localDate: Temporal.PlainDate): string {
return `${localDate.year}-${String(localDate.month).padStart(2, '0')}`
}
function normalizeDisplayName(raw: string): string | null {
const trimmed = raw.trim()
if (trimmed.length < 2 || trimmed.length > 80) {
return null
}
return trimmed.replace(/\s+/g, ' ')
}
export function createMiniAppAdminService(
repository: HouseholdConfigurationRepository
): MiniAppAdminService {
@@ -445,6 +484,69 @@ export function createMiniAppAdminService(
}
},
async updateOwnDisplayName(input) {
const displayName = normalizeDisplayName(input.displayName)
if (!displayName) {
return {
status: 'rejected',
reason: 'invalid_display_name'
}
}
const member = await repository.updateHouseholdMemberDisplayName(
input.householdId,
input.actorMemberId,
displayName
)
if (!member) {
return {
status: 'rejected',
reason: 'member_not_found'
}
}
return {
status: 'ok',
member
}
},
async updateMemberDisplayName(input) {
if (!input.actorIsAdmin) {
return {
status: 'rejected',
reason: 'not_admin'
}
}
const displayName = normalizeDisplayName(input.displayName)
if (!displayName) {
return {
status: 'rejected',
reason: 'invalid_display_name'
}
}
const member = await repository.updateHouseholdMemberDisplayName(
input.householdId,
input.memberId,
displayName
)
if (!member) {
return {
status: 'rejected',
reason: 'member_not_found'
}
}
return {
status: 'ok',
member
}
},
async updateMemberAbsencePolicy(input) {
if (!input.actorIsAdmin) {
return {

View File

@@ -206,6 +206,11 @@ export interface HouseholdConfigurationRepository {
telegramUserId: string,
locale: SupportedLocale
): Promise<HouseholdMemberRecord | null>
updateHouseholdMemberDisplayName(
householdId: string,
memberId: string,
displayName: string
): Promise<HouseholdMemberRecord | null>
promoteHouseholdAdmin(
householdId: string,
memberId: string