mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 22:44:03 +00:00
feat(locale): persist household and member preferences
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
|
|
||||||
import { createDbClient, schema } from '@household/db'
|
import { createDbClient, schema } from '@household/db'
|
||||||
import { instantToDate, nowInstant } from '@household/domain'
|
import { instantToDate, normalizeSupportedLocale, nowInstant } from '@household/domain'
|
||||||
import {
|
import {
|
||||||
HOUSEHOLD_TOPIC_ROLES,
|
HOUSEHOLD_TOPIC_ROLES,
|
||||||
type HouseholdConfigurationRepository,
|
type HouseholdConfigurationRepository,
|
||||||
@@ -30,13 +30,20 @@ function toHouseholdTelegramChatRecord(row: {
|
|||||||
telegramChatId: string
|
telegramChatId: string
|
||||||
telegramChatType: string
|
telegramChatType: string
|
||||||
title: string | null
|
title: string | null
|
||||||
|
defaultLocale: string
|
||||||
}): HouseholdTelegramChatRecord {
|
}): HouseholdTelegramChatRecord {
|
||||||
|
const defaultLocale = normalizeSupportedLocale(row.defaultLocale)
|
||||||
|
if (!defaultLocale) {
|
||||||
|
throw new Error(`Unsupported household default locale: ${row.defaultLocale}`)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
householdId: row.householdId,
|
householdId: row.householdId,
|
||||||
householdName: row.householdName,
|
householdName: row.householdName,
|
||||||
telegramChatId: row.telegramChatId,
|
telegramChatId: row.telegramChatId,
|
||||||
telegramChatType: row.telegramChatType,
|
telegramChatType: row.telegramChatType,
|
||||||
title: row.title
|
title: row.title,
|
||||||
|
defaultLocale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,14 +82,21 @@ function toHouseholdPendingMemberRecord(row: {
|
|||||||
displayName: string
|
displayName: string
|
||||||
username: string | null
|
username: string | null
|
||||||
languageCode: string | null
|
languageCode: string | null
|
||||||
|
defaultLocale: string
|
||||||
}): HouseholdPendingMemberRecord {
|
}): HouseholdPendingMemberRecord {
|
||||||
|
const householdDefaultLocale = normalizeSupportedLocale(row.defaultLocale)
|
||||||
|
if (!householdDefaultLocale) {
|
||||||
|
throw new Error(`Unsupported household default locale: ${row.defaultLocale}`)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
householdId: row.householdId,
|
householdId: row.householdId,
|
||||||
householdName: row.householdName,
|
householdName: row.householdName,
|
||||||
telegramUserId: row.telegramUserId,
|
telegramUserId: row.telegramUserId,
|
||||||
displayName: row.displayName,
|
displayName: row.displayName,
|
||||||
username: row.username,
|
username: row.username,
|
||||||
languageCode: row.languageCode
|
languageCode: row.languageCode,
|
||||||
|
householdDefaultLocale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,13 +105,22 @@ function toHouseholdMemberRecord(row: {
|
|||||||
householdId: string
|
householdId: string
|
||||||
telegramUserId: string
|
telegramUserId: string
|
||||||
displayName: string
|
displayName: string
|
||||||
|
preferredLocale: string | null
|
||||||
|
defaultLocale: string
|
||||||
isAdmin: number
|
isAdmin: number
|
||||||
}): HouseholdMemberRecord {
|
}): HouseholdMemberRecord {
|
||||||
|
const householdDefaultLocale = normalizeSupportedLocale(row.defaultLocale)
|
||||||
|
if (!householdDefaultLocale) {
|
||||||
|
throw new Error(`Unsupported household default locale: ${row.defaultLocale}`)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
householdId: row.householdId,
|
householdId: row.householdId,
|
||||||
telegramUserId: row.telegramUserId,
|
telegramUserId: row.telegramUserId,
|
||||||
displayName: row.displayName,
|
displayName: row.displayName,
|
||||||
|
preferredLocale: normalizeSupportedLocale(row.preferredLocale),
|
||||||
|
householdDefaultLocale,
|
||||||
isAdmin: row.isAdmin === 1
|
isAdmin: row.isAdmin === 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,7 +143,8 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
householdName: schema.households.name,
|
householdName: schema.households.name,
|
||||||
telegramChatId: schema.householdTelegramChats.telegramChatId,
|
telegramChatId: schema.householdTelegramChats.telegramChatId,
|
||||||
telegramChatType: schema.householdTelegramChats.telegramChatType,
|
telegramChatType: schema.householdTelegramChats.telegramChatType,
|
||||||
title: schema.householdTelegramChats.title
|
title: schema.householdTelegramChats.title,
|
||||||
|
defaultLocale: schema.households.defaultLocale
|
||||||
})
|
})
|
||||||
.from(schema.householdTelegramChats)
|
.from(schema.householdTelegramChats)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
@@ -160,7 +184,8 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
})
|
})
|
||||||
.returning({
|
.returning({
|
||||||
id: schema.households.id,
|
id: schema.households.id,
|
||||||
name: schema.households.name
|
name: schema.households.name,
|
||||||
|
defaultLocale: schema.households.defaultLocale
|
||||||
})
|
})
|
||||||
|
|
||||||
const household = insertedHouseholds[0]
|
const household = insertedHouseholds[0]
|
||||||
@@ -195,7 +220,8 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
householdName: household.name,
|
householdName: household.name,
|
||||||
telegramChatId: chat.telegramChatId,
|
telegramChatId: chat.telegramChatId,
|
||||||
telegramChatType: chat.telegramChatType,
|
telegramChatType: chat.telegramChatType,
|
||||||
title: chat.title
|
title: chat.title,
|
||||||
|
defaultLocale: household.defaultLocale
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -208,7 +234,8 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
householdName: schema.households.name,
|
householdName: schema.households.name,
|
||||||
telegramChatId: schema.householdTelegramChats.telegramChatId,
|
telegramChatId: schema.householdTelegramChats.telegramChatId,
|
||||||
telegramChatType: schema.householdTelegramChats.telegramChatType,
|
telegramChatType: schema.householdTelegramChats.telegramChatType,
|
||||||
title: schema.householdTelegramChats.title
|
title: schema.householdTelegramChats.title,
|
||||||
|
defaultLocale: schema.households.defaultLocale
|
||||||
})
|
})
|
||||||
.from(schema.householdTelegramChats)
|
.from(schema.householdTelegramChats)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
@@ -229,7 +256,8 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
householdName: schema.households.name,
|
householdName: schema.households.name,
|
||||||
telegramChatId: schema.householdTelegramChats.telegramChatId,
|
telegramChatId: schema.householdTelegramChats.telegramChatId,
|
||||||
telegramChatType: schema.householdTelegramChats.telegramChatType,
|
telegramChatType: schema.householdTelegramChats.telegramChatType,
|
||||||
title: schema.householdTelegramChats.title
|
title: schema.householdTelegramChats.title,
|
||||||
|
defaultLocale: schema.households.defaultLocale
|
||||||
})
|
})
|
||||||
.from(schema.householdTelegramChats)
|
.from(schema.householdTelegramChats)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
@@ -412,7 +440,8 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
householdName: schema.households.name,
|
householdName: schema.households.name,
|
||||||
telegramChatId: schema.householdTelegramChats.telegramChatId,
|
telegramChatId: schema.householdTelegramChats.telegramChatId,
|
||||||
telegramChatType: schema.householdTelegramChats.telegramChatType,
|
telegramChatType: schema.householdTelegramChats.telegramChatType,
|
||||||
title: schema.householdTelegramChats.title
|
title: schema.householdTelegramChats.title,
|
||||||
|
defaultLocale: schema.households.defaultLocale
|
||||||
})
|
})
|
||||||
.from(schema.householdJoinTokens)
|
.from(schema.householdJoinTokens)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
@@ -468,7 +497,8 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
const householdRows = await db
|
const householdRows = await db
|
||||||
.select({
|
.select({
|
||||||
householdId: schema.households.id,
|
householdId: schema.households.id,
|
||||||
householdName: schema.households.name
|
householdName: schema.households.name,
|
||||||
|
defaultLocale: schema.households.defaultLocale
|
||||||
})
|
})
|
||||||
.from(schema.households)
|
.from(schema.households)
|
||||||
.where(eq(schema.households.id, row.householdId))
|
.where(eq(schema.households.id, row.householdId))
|
||||||
@@ -485,7 +515,8 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
telegramUserId: row.telegramUserId,
|
telegramUserId: row.telegramUserId,
|
||||||
displayName: row.displayName,
|
displayName: row.displayName,
|
||||||
username: row.username,
|
username: row.username,
|
||||||
languageCode: row.languageCode
|
languageCode: row.languageCode,
|
||||||
|
defaultLocale: household.defaultLocale
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -497,7 +528,8 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
telegramUserId: schema.householdPendingMembers.telegramUserId,
|
telegramUserId: schema.householdPendingMembers.telegramUserId,
|
||||||
displayName: schema.householdPendingMembers.displayName,
|
displayName: schema.householdPendingMembers.displayName,
|
||||||
username: schema.householdPendingMembers.username,
|
username: schema.householdPendingMembers.username,
|
||||||
languageCode: schema.householdPendingMembers.languageCode
|
languageCode: schema.householdPendingMembers.languageCode,
|
||||||
|
defaultLocale: schema.households.defaultLocale
|
||||||
})
|
})
|
||||||
.from(schema.householdPendingMembers)
|
.from(schema.householdPendingMembers)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
@@ -524,7 +556,8 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
telegramUserId: schema.householdPendingMembers.telegramUserId,
|
telegramUserId: schema.householdPendingMembers.telegramUserId,
|
||||||
displayName: schema.householdPendingMembers.displayName,
|
displayName: schema.householdPendingMembers.displayName,
|
||||||
username: schema.householdPendingMembers.username,
|
username: schema.householdPendingMembers.username,
|
||||||
languageCode: schema.householdPendingMembers.languageCode
|
languageCode: schema.householdPendingMembers.languageCode,
|
||||||
|
defaultLocale: schema.households.defaultLocale
|
||||||
})
|
})
|
||||||
.from(schema.householdPendingMembers)
|
.from(schema.householdPendingMembers)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
@@ -545,12 +578,14 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
householdId: input.householdId,
|
householdId: input.householdId,
|
||||||
telegramUserId: input.telegramUserId,
|
telegramUserId: input.telegramUserId,
|
||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
|
preferredLocale: input.preferredLocale ?? null,
|
||||||
isAdmin: input.isAdmin ? 1 : 0
|
isAdmin: input.isAdmin ? 1 : 0
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: [schema.members.householdId, schema.members.telegramUserId],
|
target: [schema.members.householdId, schema.members.telegramUserId],
|
||||||
set: {
|
set: {
|
||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
|
preferredLocale: input.preferredLocale ?? schema.members.preferredLocale,
|
||||||
...(input.isAdmin
|
...(input.isAdmin
|
||||||
? {
|
? {
|
||||||
isAdmin: 1
|
isAdmin: 1
|
||||||
@@ -563,6 +598,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
householdId: schema.members.householdId,
|
householdId: schema.members.householdId,
|
||||||
telegramUserId: schema.members.telegramUserId,
|
telegramUserId: schema.members.telegramUserId,
|
||||||
displayName: schema.members.displayName,
|
displayName: schema.members.displayName,
|
||||||
|
preferredLocale: schema.members.preferredLocale,
|
||||||
isAdmin: schema.members.isAdmin
|
isAdmin: schema.members.isAdmin
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -571,7 +607,15 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
throw new Error('Failed to ensure household member')
|
throw new Error('Failed to ensure household member')
|
||||||
}
|
}
|
||||||
|
|
||||||
return toHouseholdMemberRecord(row)
|
const household = await this.getHouseholdChatByHouseholdId(row.householdId)
|
||||||
|
if (!household) {
|
||||||
|
throw new Error('Failed to resolve household for member')
|
||||||
|
}
|
||||||
|
|
||||||
|
return toHouseholdMemberRecord({
|
||||||
|
...row,
|
||||||
|
defaultLocale: household.defaultLocale
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async getHouseholdMember(householdId, telegramUserId) {
|
async getHouseholdMember(householdId, telegramUserId) {
|
||||||
@@ -581,9 +625,12 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
householdId: schema.members.householdId,
|
householdId: schema.members.householdId,
|
||||||
telegramUserId: schema.members.telegramUserId,
|
telegramUserId: schema.members.telegramUserId,
|
||||||
displayName: schema.members.displayName,
|
displayName: schema.members.displayName,
|
||||||
|
preferredLocale: schema.members.preferredLocale,
|
||||||
|
defaultLocale: schema.households.defaultLocale,
|
||||||
isAdmin: schema.members.isAdmin
|
isAdmin: schema.members.isAdmin
|
||||||
})
|
})
|
||||||
.from(schema.members)
|
.from(schema.members)
|
||||||
|
.innerJoin(schema.households, eq(schema.members.householdId, schema.households.id))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(schema.members.householdId, householdId),
|
eq(schema.members.householdId, householdId),
|
||||||
@@ -603,9 +650,12 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
householdId: schema.members.householdId,
|
householdId: schema.members.householdId,
|
||||||
telegramUserId: schema.members.telegramUserId,
|
telegramUserId: schema.members.telegramUserId,
|
||||||
displayName: schema.members.displayName,
|
displayName: schema.members.displayName,
|
||||||
|
preferredLocale: schema.members.preferredLocale,
|
||||||
|
defaultLocale: schema.households.defaultLocale,
|
||||||
isAdmin: schema.members.isAdmin
|
isAdmin: schema.members.isAdmin
|
||||||
})
|
})
|
||||||
.from(schema.members)
|
.from(schema.members)
|
||||||
|
.innerJoin(schema.households, eq(schema.members.householdId, schema.households.id))
|
||||||
.where(eq(schema.members.householdId, householdId))
|
.where(eq(schema.members.householdId, householdId))
|
||||||
.orderBy(schema.members.displayName, schema.members.telegramUserId)
|
.orderBy(schema.members.displayName, schema.members.telegramUserId)
|
||||||
|
|
||||||
@@ -619,9 +669,12 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
householdId: schema.members.householdId,
|
householdId: schema.members.householdId,
|
||||||
telegramUserId: schema.members.telegramUserId,
|
telegramUserId: schema.members.telegramUserId,
|
||||||
displayName: schema.members.displayName,
|
displayName: schema.members.displayName,
|
||||||
|
preferredLocale: schema.members.preferredLocale,
|
||||||
|
defaultLocale: schema.households.defaultLocale,
|
||||||
isAdmin: schema.members.isAdmin
|
isAdmin: schema.members.isAdmin
|
||||||
})
|
})
|
||||||
.from(schema.members)
|
.from(schema.members)
|
||||||
|
.innerJoin(schema.households, eq(schema.members.householdId, schema.households.id))
|
||||||
.where(eq(schema.members.telegramUserId, telegramUserId))
|
.where(eq(schema.members.telegramUserId, telegramUserId))
|
||||||
.orderBy(schema.members.householdId, schema.members.displayName)
|
.orderBy(schema.members.householdId, schema.members.displayName)
|
||||||
|
|
||||||
@@ -636,7 +689,8 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
telegramUserId: schema.householdPendingMembers.telegramUserId,
|
telegramUserId: schema.householdPendingMembers.telegramUserId,
|
||||||
displayName: schema.householdPendingMembers.displayName,
|
displayName: schema.householdPendingMembers.displayName,
|
||||||
username: schema.householdPendingMembers.username,
|
username: schema.householdPendingMembers.username,
|
||||||
languageCode: schema.householdPendingMembers.languageCode
|
languageCode: schema.householdPendingMembers.languageCode,
|
||||||
|
defaultLocale: schema.households.defaultLocale
|
||||||
})
|
})
|
||||||
.from(schema.householdPendingMembers)
|
.from(schema.householdPendingMembers)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
@@ -658,7 +712,8 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
telegramUserId: schema.householdPendingMembers.telegramUserId,
|
telegramUserId: schema.householdPendingMembers.telegramUserId,
|
||||||
displayName: schema.householdPendingMembers.displayName,
|
displayName: schema.householdPendingMembers.displayName,
|
||||||
username: schema.householdPendingMembers.username,
|
username: schema.householdPendingMembers.username,
|
||||||
languageCode: schema.householdPendingMembers.languageCode
|
languageCode: schema.householdPendingMembers.languageCode,
|
||||||
|
defaultLocale: schema.households.defaultLocale
|
||||||
})
|
})
|
||||||
.from(schema.householdPendingMembers)
|
.from(schema.householdPendingMembers)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
@@ -684,12 +739,15 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
householdId: pending.householdId,
|
householdId: pending.householdId,
|
||||||
telegramUserId: pending.telegramUserId,
|
telegramUserId: pending.telegramUserId,
|
||||||
displayName: pending.displayName,
|
displayName: pending.displayName,
|
||||||
|
preferredLocale: normalizeSupportedLocale(pending.languageCode),
|
||||||
isAdmin: input.isAdmin ? 1 : 0
|
isAdmin: input.isAdmin ? 1 : 0
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: [schema.members.householdId, schema.members.telegramUserId],
|
target: [schema.members.householdId, schema.members.telegramUserId],
|
||||||
set: {
|
set: {
|
||||||
displayName: pending.displayName,
|
displayName: pending.displayName,
|
||||||
|
preferredLocale:
|
||||||
|
normalizeSupportedLocale(pending.languageCode) ?? schema.members.preferredLocale,
|
||||||
...(input.isAdmin
|
...(input.isAdmin
|
||||||
? {
|
? {
|
||||||
isAdmin: 1
|
isAdmin: 1
|
||||||
@@ -702,6 +760,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
householdId: schema.members.householdId,
|
householdId: schema.members.householdId,
|
||||||
telegramUserId: schema.members.telegramUserId,
|
telegramUserId: schema.members.telegramUserId,
|
||||||
displayName: schema.members.displayName,
|
displayName: schema.members.displayName,
|
||||||
|
preferredLocale: schema.members.preferredLocale,
|
||||||
isAdmin: schema.members.isAdmin
|
isAdmin: schema.members.isAdmin
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -719,7 +778,76 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
throw new Error('Failed to approve pending household member')
|
throw new Error('Failed to approve pending household member')
|
||||||
}
|
}
|
||||||
|
|
||||||
return toHouseholdMemberRecord(member)
|
return toHouseholdMemberRecord({
|
||||||
|
...member,
|
||||||
|
defaultLocale: pending.defaultLocale
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateHouseholdDefaultLocale(householdId, locale) {
|
||||||
|
const updatedHouseholds = await db
|
||||||
|
.update(schema.households)
|
||||||
|
.set({
|
||||||
|
defaultLocale: locale
|
||||||
|
})
|
||||||
|
.where(eq(schema.households.id, householdId))
|
||||||
|
.returning({
|
||||||
|
id: schema.households.id,
|
||||||
|
name: schema.households.name,
|
||||||
|
defaultLocale: schema.households.defaultLocale
|
||||||
|
})
|
||||||
|
|
||||||
|
const household = updatedHouseholds[0]
|
||||||
|
if (!household) {
|
||||||
|
throw new Error('Failed to update household default locale')
|
||||||
|
}
|
||||||
|
|
||||||
|
const chat = await this.getHouseholdChatByHouseholdId(householdId)
|
||||||
|
if (!chat) {
|
||||||
|
throw new Error('Failed to resolve household chat after locale update')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...chat,
|
||||||
|
defaultLocale: normalizeSupportedLocale(household.defaultLocale) ?? chat.defaultLocale
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateMemberPreferredLocale(householdId, telegramUserId, locale) {
|
||||||
|
const rows = await db
|
||||||
|
.update(schema.members)
|
||||||
|
.set({
|
||||||
|
preferredLocale: locale
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.members.householdId, householdId),
|
||||||
|
eq(schema.members.telegramUserId, telegramUserId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returning({
|
||||||
|
id: schema.members.id,
|
||||||
|
householdId: schema.members.householdId,
|
||||||
|
telegramUserId: schema.members.telegramUserId,
|
||||||
|
displayName: schema.members.displayName,
|
||||||
|
preferredLocale: schema.members.preferredLocale,
|
||||||
|
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 locale update')
|
||||||
|
}
|
||||||
|
|
||||||
|
return toHouseholdMemberRecord({
|
||||||
|
...row,
|
||||||
|
defaultLocale: household.defaultLocale
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ function createRepositoryStub() {
|
|||||||
householdName: 'Kojori House',
|
householdName: 'Kojori House',
|
||||||
telegramChatId: '-100123',
|
telegramChatId: '-100123',
|
||||||
telegramChatType: 'supergroup',
|
telegramChatType: 'supergroup',
|
||||||
title: 'Kojori House'
|
title: 'Kojori House',
|
||||||
|
defaultLocale: 'ru'
|
||||||
}
|
}
|
||||||
const members = new Map<string, HouseholdMemberRecord>()
|
const members = new Map<string, HouseholdMemberRecord>()
|
||||||
const pendingMembers = new Map<string, HouseholdPendingMemberRecord>()
|
const pendingMembers = new Map<string, HouseholdPendingMemberRecord>()
|
||||||
@@ -27,6 +28,8 @@ function createRepositoryStub() {
|
|||||||
householdId: household.householdId,
|
householdId: household.householdId,
|
||||||
telegramUserId: '1',
|
telegramUserId: '1',
|
||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
|
preferredLocale: null,
|
||||||
|
householdDefaultLocale: household.defaultLocale,
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
})
|
})
|
||||||
pendingMembers.set('2', {
|
pendingMembers.set('2', {
|
||||||
@@ -35,7 +38,8 @@ function createRepositoryStub() {
|
|||||||
telegramUserId: '2',
|
telegramUserId: '2',
|
||||||
displayName: 'Alice',
|
displayName: 'Alice',
|
||||||
username: 'alice',
|
username: 'alice',
|
||||||
languageCode: 'en'
|
languageCode: 'en',
|
||||||
|
householdDefaultLocale: household.defaultLocale
|
||||||
})
|
})
|
||||||
|
|
||||||
const repository: HouseholdConfigurationRepository = {
|
const repository: HouseholdConfigurationRepository = {
|
||||||
@@ -71,7 +75,8 @@ function createRepositoryStub() {
|
|||||||
telegramUserId: input.telegramUserId,
|
telegramUserId: input.telegramUserId,
|
||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
username: input.username?.trim() || null,
|
username: input.username?.trim() || null,
|
||||||
languageCode: input.languageCode?.trim() || null
|
languageCode: input.languageCode?.trim() || null,
|
||||||
|
householdDefaultLocale: household.defaultLocale
|
||||||
}
|
}
|
||||||
pendingMembers.set(input.telegramUserId, record)
|
pendingMembers.set(input.telegramUserId, record)
|
||||||
return record
|
return record
|
||||||
@@ -86,6 +91,8 @@ function createRepositoryStub() {
|
|||||||
householdId: input.householdId,
|
householdId: input.householdId,
|
||||||
telegramUserId: input.telegramUserId,
|
telegramUserId: input.telegramUserId,
|
||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
|
preferredLocale: input.preferredLocale ?? null,
|
||||||
|
householdDefaultLocale: household.defaultLocale,
|
||||||
isAdmin: input.isAdmin === true
|
isAdmin: input.isAdmin === true
|
||||||
}
|
}
|
||||||
members.set(input.telegramUserId, record)
|
members.set(input.telegramUserId, record)
|
||||||
@@ -110,10 +117,25 @@ function createRepositoryStub() {
|
|||||||
householdId: pending.householdId,
|
householdId: pending.householdId,
|
||||||
telegramUserId: pending.telegramUserId,
|
telegramUserId: pending.telegramUserId,
|
||||||
displayName: pending.displayName,
|
displayName: pending.displayName,
|
||||||
|
preferredLocale: null,
|
||||||
|
householdDefaultLocale: household.defaultLocale,
|
||||||
isAdmin: input.isAdmin === true
|
isAdmin: input.isAdmin === true
|
||||||
}
|
}
|
||||||
members.set(member.telegramUserId, member)
|
members.set(member.telegramUserId, member)
|
||||||
return member
|
return member
|
||||||
|
},
|
||||||
|
updateHouseholdDefaultLocale: async (_householdId, locale) => ({
|
||||||
|
...household,
|
||||||
|
defaultLocale: locale
|
||||||
|
}),
|
||||||
|
updateMemberPreferredLocale: async (_householdId, telegramUserId, locale) => {
|
||||||
|
const member = members.get(telegramUserId)
|
||||||
|
return member
|
||||||
|
? {
|
||||||
|
...member,
|
||||||
|
preferredLocale: locale
|
||||||
|
}
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +166,8 @@ describe('createHouseholdAdminService', () => {
|
|||||||
telegramUserId: '2',
|
telegramUserId: '2',
|
||||||
displayName: 'Alice',
|
displayName: 'Alice',
|
||||||
username: 'alice',
|
username: 'alice',
|
||||||
languageCode: 'en'
|
languageCode: 'en',
|
||||||
|
householdDefaultLocale: 'ru'
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -182,6 +205,8 @@ describe('createHouseholdAdminService', () => {
|
|||||||
householdId: 'household-1',
|
householdId: 'household-1',
|
||||||
telegramUserId: '2',
|
telegramUserId: '2',
|
||||||
displayName: 'Alice',
|
displayName: 'Alice',
|
||||||
|
preferredLocale: null,
|
||||||
|
householdDefaultLocale: 'ru',
|
||||||
isAdmin: false
|
isAdmin: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ function createRepositoryStub() {
|
|||||||
householdName: 'Kojori House',
|
householdName: 'Kojori House',
|
||||||
telegramChatId: '-100123',
|
telegramChatId: '-100123',
|
||||||
telegramChatType: 'supergroup',
|
telegramChatType: 'supergroup',
|
||||||
title: 'Kojori House'
|
title: 'Kojori House',
|
||||||
|
defaultLocale: 'ru'
|
||||||
}
|
}
|
||||||
let joinToken: HouseholdJoinTokenRecord | null = null
|
let joinToken: HouseholdJoinTokenRecord | null = null
|
||||||
const pendingMembers = new Map<string, HouseholdPendingMemberRecord>()
|
const pendingMembers = new Map<string, HouseholdPendingMemberRecord>()
|
||||||
@@ -76,7 +77,8 @@ function createRepositoryStub() {
|
|||||||
telegramUserId: input.telegramUserId,
|
telegramUserId: input.telegramUserId,
|
||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
username: input.username?.trim() || null,
|
username: input.username?.trim() || null,
|
||||||
languageCode: input.languageCode?.trim() || null
|
languageCode: input.languageCode?.trim() || null,
|
||||||
|
householdDefaultLocale: household.defaultLocale
|
||||||
}
|
}
|
||||||
pendingMembers.set(input.telegramUserId, record)
|
pendingMembers.set(input.telegramUserId, record)
|
||||||
return record
|
return record
|
||||||
@@ -93,6 +95,8 @@ function createRepositoryStub() {
|
|||||||
householdId: input.householdId,
|
householdId: input.householdId,
|
||||||
telegramUserId: input.telegramUserId,
|
telegramUserId: input.telegramUserId,
|
||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
|
preferredLocale: input.preferredLocale ?? null,
|
||||||
|
householdDefaultLocale: household.defaultLocale,
|
||||||
isAdmin: input.isAdmin === true
|
isAdmin: input.isAdmin === true
|
||||||
}
|
}
|
||||||
members.set(input.telegramUserId, member)
|
members.set(input.telegramUserId, member)
|
||||||
@@ -124,8 +128,25 @@ function createRepositoryStub() {
|
|||||||
householdId: pending.householdId,
|
householdId: pending.householdId,
|
||||||
telegramUserId: pending.telegramUserId,
|
telegramUserId: pending.telegramUserId,
|
||||||
displayName: pending.displayName,
|
displayName: pending.displayName,
|
||||||
|
preferredLocale: null,
|
||||||
|
householdDefaultLocale: household.defaultLocale,
|
||||||
isAdmin: input.isAdmin === true
|
isAdmin: input.isAdmin === true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async updateHouseholdDefaultLocale(_householdId, locale) {
|
||||||
|
return {
|
||||||
|
...household,
|
||||||
|
defaultLocale: locale
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async updateMemberPreferredLocale(_householdId, telegramUserId, locale) {
|
||||||
|
const member = members.get(telegramUserId)
|
||||||
|
return member
|
||||||
|
? {
|
||||||
|
...member,
|
||||||
|
preferredLocale: locale
|
||||||
|
}
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +197,8 @@ describe('createHouseholdOnboardingService', () => {
|
|||||||
status: 'join_required',
|
status: 'join_required',
|
||||||
household: {
|
household: {
|
||||||
id: 'household-1',
|
id: 'household-1',
|
||||||
name: 'Kojori House'
|
name: 'Kojori House',
|
||||||
|
defaultLocale: 'ru'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -204,7 +226,8 @@ describe('createHouseholdOnboardingService', () => {
|
|||||||
status: 'pending',
|
status: 'pending',
|
||||||
household: {
|
household: {
|
||||||
id: 'household-1',
|
id: 'household-1',
|
||||||
name: 'Kojori House'
|
name: 'Kojori House',
|
||||||
|
defaultLocale: 'ru'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -219,7 +242,8 @@ describe('createHouseholdOnboardingService', () => {
|
|||||||
status: 'pending',
|
status: 'pending',
|
||||||
household: {
|
household: {
|
||||||
id: 'household-1',
|
id: 'household-1',
|
||||||
name: 'Kojori House'
|
name: 'Kojori House',
|
||||||
|
defaultLocale: 'ru'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -250,6 +274,8 @@ describe('createHouseholdOnboardingService', () => {
|
|||||||
id: 'member-42',
|
id: 'member-42',
|
||||||
householdId: 'household-1',
|
householdId: 'household-1',
|
||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
|
preferredLocale: null,
|
||||||
|
householdDefaultLocale: 'ru',
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -262,6 +288,8 @@ describe('createHouseholdOnboardingService', () => {
|
|||||||
householdId: 'household-1',
|
householdId: 'household-1',
|
||||||
telegramUserId: '42',
|
telegramUserId: '42',
|
||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
|
preferredLocale: null,
|
||||||
|
householdDefaultLocale: 'ru',
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
}
|
}
|
||||||
const service = createHouseholdOnboardingService({ repository })
|
const service = createHouseholdOnboardingService({ repository })
|
||||||
@@ -277,6 +305,8 @@ describe('createHouseholdOnboardingService', () => {
|
|||||||
householdId: 'household-2',
|
householdId: 'household-2',
|
||||||
telegramUserId: '42',
|
telegramUserId: '42',
|
||||||
displayName: 'Stan elsewhere',
|
displayName: 'Stan elsewhere',
|
||||||
|
preferredLocale: null,
|
||||||
|
householdDefaultLocale: 'ru',
|
||||||
isAdmin: false
|
isAdmin: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { randomBytes } from 'node:crypto'
|
import { randomBytes } from 'node:crypto'
|
||||||
|
|
||||||
|
import type { SupportedLocale } from '@household/domain'
|
||||||
import type { HouseholdConfigurationRepository, HouseholdMemberRecord } from '@household/ports'
|
import type { HouseholdConfigurationRepository, HouseholdMemberRecord } from '@household/ports'
|
||||||
|
|
||||||
export interface HouseholdOnboardingIdentity {
|
export interface HouseholdOnboardingIdentity {
|
||||||
@@ -17,6 +18,8 @@ export type HouseholdMiniAppAccess =
|
|||||||
householdId: string
|
householdId: string
|
||||||
displayName: string
|
displayName: string
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
|
preferredLocale: SupportedLocale | null
|
||||||
|
householdDefaultLocale: SupportedLocale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -24,6 +27,7 @@ export type HouseholdMiniAppAccess =
|
|||||||
household: {
|
household: {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
defaultLocale: SupportedLocale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -31,6 +35,7 @@ export type HouseholdMiniAppAccess =
|
|||||||
household: {
|
household: {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
defaultLocale: SupportedLocale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -53,6 +58,7 @@ export interface HouseholdOnboardingService {
|
|||||||
household: {
|
household: {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
defaultLocale: SupportedLocale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -62,6 +68,8 @@ export interface HouseholdOnboardingService {
|
|||||||
householdId: string
|
householdId: string
|
||||||
displayName: string
|
displayName: string
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
|
preferredLocale: SupportedLocale | null
|
||||||
|
householdDefaultLocale: SupportedLocale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -75,12 +83,16 @@ function toMember(member: HouseholdMemberRecord): {
|
|||||||
householdId: string
|
householdId: string
|
||||||
displayName: string
|
displayName: string
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
|
preferredLocale: SupportedLocale | null
|
||||||
|
householdDefaultLocale: SupportedLocale
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
id: member.id,
|
id: member.id,
|
||||||
householdId: member.householdId,
|
householdId: member.householdId,
|
||||||
displayName: member.displayName,
|
displayName: member.displayName,
|
||||||
isAdmin: member.isAdmin
|
isAdmin: member.isAdmin,
|
||||||
|
preferredLocale: member.preferredLocale,
|
||||||
|
householdDefaultLocale: member.householdDefaultLocale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +167,8 @@ export function createHouseholdOnboardingService(options: {
|
|||||||
status: 'pending',
|
status: 'pending',
|
||||||
household: {
|
household: {
|
||||||
id: existingPending.householdId,
|
id: existingPending.householdId,
|
||||||
name: existingPending.householdName
|
name: existingPending.householdName,
|
||||||
|
defaultLocale: existingPending.householdDefaultLocale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,7 +195,8 @@ export function createHouseholdOnboardingService(options: {
|
|||||||
status: 'pending',
|
status: 'pending',
|
||||||
household: {
|
household: {
|
||||||
id: pending.householdId,
|
id: pending.householdId,
|
||||||
name: pending.householdName
|
name: pending.householdName,
|
||||||
|
defaultLocale: pending.householdDefaultLocale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,7 +205,8 @@ export function createHouseholdOnboardingService(options: {
|
|||||||
status: 'join_required',
|
status: 'join_required',
|
||||||
household: {
|
household: {
|
||||||
id: household.householdId,
|
id: household.householdId,
|
||||||
name: household.householdName
|
name: household.householdName,
|
||||||
|
defaultLocale: household.defaultLocale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -235,7 +250,8 @@ export function createHouseholdOnboardingService(options: {
|
|||||||
status: 'pending',
|
status: 'pending',
|
||||||
household: {
|
household: {
|
||||||
id: pending.householdId,
|
id: pending.householdId,
|
||||||
name: pending.householdName
|
name: pending.householdName,
|
||||||
|
defaultLocale: pending.householdDefaultLocale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ function createRepositoryStub() {
|
|||||||
householdName: input.householdName,
|
householdName: input.householdName,
|
||||||
telegramChatId: input.telegramChatId,
|
telegramChatId: input.telegramChatId,
|
||||||
telegramChatType: input.telegramChatType,
|
telegramChatType: input.telegramChatType,
|
||||||
title: input.title?.trim() || null
|
title: input.title?.trim() || null,
|
||||||
|
defaultLocale: 'ru'
|
||||||
}
|
}
|
||||||
households.set(input.telegramChatId, created)
|
households.set(input.telegramChatId, created)
|
||||||
|
|
||||||
@@ -141,7 +142,8 @@ function createRepositoryStub() {
|
|||||||
telegramUserId: input.telegramUserId,
|
telegramUserId: input.telegramUserId,
|
||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
username: input.username?.trim() || null,
|
username: input.username?.trim() || null,
|
||||||
languageCode: input.languageCode?.trim() || null
|
languageCode: input.languageCode?.trim() || null,
|
||||||
|
householdDefaultLocale: household.defaultLocale
|
||||||
}
|
}
|
||||||
pendingMembers.set(key, record)
|
pendingMembers.set(key, record)
|
||||||
return record
|
return record
|
||||||
@@ -166,6 +168,10 @@ function createRepositoryStub() {
|
|||||||
householdId: input.householdId,
|
householdId: input.householdId,
|
||||||
telegramUserId: input.telegramUserId,
|
telegramUserId: input.telegramUserId,
|
||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
|
preferredLocale: input.preferredLocale ?? existing?.preferredLocale ?? null,
|
||||||
|
householdDefaultLocale:
|
||||||
|
[...households.values()].find((household) => household.householdId === input.householdId)
|
||||||
|
?.defaultLocale ?? 'ru',
|
||||||
isAdmin: input.isAdmin === true || existing?.isAdmin === true
|
isAdmin: input.isAdmin === true || existing?.isAdmin === true
|
||||||
}
|
}
|
||||||
members.set(key, next)
|
members.set(key, next)
|
||||||
@@ -202,10 +208,39 @@ function createRepositoryStub() {
|
|||||||
householdId: pending.householdId,
|
householdId: pending.householdId,
|
||||||
telegramUserId: pending.telegramUserId,
|
telegramUserId: pending.telegramUserId,
|
||||||
displayName: pending.displayName,
|
displayName: pending.displayName,
|
||||||
|
preferredLocale: null,
|
||||||
|
householdDefaultLocale:
|
||||||
|
[...households.values()].find(
|
||||||
|
(household) => household.householdId === pending.householdId
|
||||||
|
)?.defaultLocale ?? 'ru',
|
||||||
isAdmin: input.isAdmin === true
|
isAdmin: input.isAdmin === true
|
||||||
}
|
}
|
||||||
members.set(key, member)
|
members.set(key, member)
|
||||||
return member
|
return member
|
||||||
|
},
|
||||||
|
async updateHouseholdDefaultLocale(householdId, locale) {
|
||||||
|
const household =
|
||||||
|
[...households.values()].find((entry) => entry.householdId === householdId) ?? null
|
||||||
|
if (!household) {
|
||||||
|
throw new Error('Missing household')
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = {
|
||||||
|
...household,
|
||||||
|
defaultLocale: locale
|
||||||
|
}
|
||||||
|
households.set(next.telegramChatId, next)
|
||||||
|
return next
|
||||||
|
},
|
||||||
|
async updateMemberPreferredLocale(householdId, telegramUserId, locale) {
|
||||||
|
const key = `${householdId}:${telegramUserId}`
|
||||||
|
const member = members.get(key)
|
||||||
|
return member
|
||||||
|
? {
|
||||||
|
...member,
|
||||||
|
preferredLocale: locale
|
||||||
|
}
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,6 +275,8 @@ describe('createHouseholdSetupService', () => {
|
|||||||
householdId: result.household.householdId,
|
householdId: result.household.householdId,
|
||||||
telegramUserId: '42',
|
telegramUserId: '42',
|
||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
|
preferredLocale: null,
|
||||||
|
householdDefaultLocale: 'ru',
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -275,6 +312,8 @@ describe('createHouseholdSetupService', () => {
|
|||||||
householdId: result.household.householdId,
|
householdId: result.household.householdId,
|
||||||
telegramUserId: '77',
|
telegramUserId: '77',
|
||||||
displayName: 'Mia',
|
displayName: 'Mia',
|
||||||
|
preferredLocale: null,
|
||||||
|
householdDefaultLocale: 'ru',
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ export {
|
|||||||
type ReminderJobResult,
|
type ReminderJobResult,
|
||||||
type ReminderJobService
|
type ReminderJobService
|
||||||
} from './reminder-job-service'
|
} from './reminder-job-service'
|
||||||
|
export {
|
||||||
|
createLocalePreferenceService,
|
||||||
|
type LocalePreferenceService
|
||||||
|
} from './locale-preference-service'
|
||||||
export {
|
export {
|
||||||
parsePurchaseMessage,
|
parsePurchaseMessage,
|
||||||
type ParsedPurchaseResult,
|
type ParsedPurchaseResult,
|
||||||
|
|||||||
115
packages/application/src/locale-preference-service.test.ts
Normal file
115
packages/application/src/locale-preference-service.test.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { describe, expect, test } from 'bun:test'
|
||||||
|
|
||||||
|
import type { HouseholdConfigurationRepository, HouseholdMemberRecord } from '@household/ports'
|
||||||
|
|
||||||
|
import { createLocalePreferenceService } from './locale-preference-service'
|
||||||
|
|
||||||
|
function createRepository(): HouseholdConfigurationRepository {
|
||||||
|
const household = {
|
||||||
|
householdId: 'household-1',
|
||||||
|
householdName: 'Kojori House',
|
||||||
|
telegramChatId: '-100123',
|
||||||
|
telegramChatType: 'supergroup',
|
||||||
|
title: 'Kojori House',
|
||||||
|
defaultLocale: 'ru' as const
|
||||||
|
}
|
||||||
|
|
||||||
|
const member: HouseholdMemberRecord = {
|
||||||
|
id: 'member-1',
|
||||||
|
householdId: 'household-1',
|
||||||
|
telegramUserId: '123456',
|
||||||
|
displayName: 'Stan',
|
||||||
|
preferredLocale: null,
|
||||||
|
householdDefaultLocale: 'ru',
|
||||||
|
isAdmin: true
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
registerTelegramHouseholdChat: async () => ({ status: 'existing', household }),
|
||||||
|
getTelegramHouseholdChat: async () => household,
|
||||||
|
getHouseholdChatByHouseholdId: async () => household,
|
||||||
|
bindHouseholdTopic: async (input) => ({
|
||||||
|
householdId: input.householdId,
|
||||||
|
role: input.role,
|
||||||
|
telegramThreadId: input.telegramThreadId,
|
||||||
|
topicName: input.topicName?.trim() || null
|
||||||
|
}),
|
||||||
|
getHouseholdTopicBinding: async () => null,
|
||||||
|
findHouseholdTopicByTelegramContext: async () => null,
|
||||||
|
listHouseholdTopicBindings: async () => [],
|
||||||
|
upsertHouseholdJoinToken: async () => ({
|
||||||
|
householdId: household.householdId,
|
||||||
|
householdName: household.householdName,
|
||||||
|
token: 'join-token',
|
||||||
|
createdByTelegramUserId: null
|
||||||
|
}),
|
||||||
|
getHouseholdJoinToken: async () => null,
|
||||||
|
getHouseholdByJoinToken: async () => household,
|
||||||
|
upsertPendingHouseholdMember: async (input) => ({
|
||||||
|
householdId: input.householdId,
|
||||||
|
householdName: household.householdName,
|
||||||
|
telegramUserId: input.telegramUserId,
|
||||||
|
displayName: input.displayName,
|
||||||
|
username: input.username?.trim() || null,
|
||||||
|
languageCode: input.languageCode?.trim() || null,
|
||||||
|
householdDefaultLocale: 'ru'
|
||||||
|
}),
|
||||||
|
getPendingHouseholdMember: async () => null,
|
||||||
|
findPendingHouseholdMemberByTelegramUserId: async () => null,
|
||||||
|
ensureHouseholdMember: async () => member,
|
||||||
|
getHouseholdMember: async () => member,
|
||||||
|
listHouseholdMembers: async () => [member],
|
||||||
|
listHouseholdMembersByTelegramUserId: async () => [member],
|
||||||
|
listPendingHouseholdMembers: async () => [],
|
||||||
|
approvePendingHouseholdMember: async () => member,
|
||||||
|
updateHouseholdDefaultLocale: async (_householdId, locale) => ({
|
||||||
|
...household,
|
||||||
|
defaultLocale: locale
|
||||||
|
}),
|
||||||
|
updateMemberPreferredLocale: async (_householdId, telegramUserId, locale) =>
|
||||||
|
telegramUserId === member.telegramUserId
|
||||||
|
? {
|
||||||
|
...member,
|
||||||
|
preferredLocale: locale,
|
||||||
|
householdDefaultLocale: 'ru'
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('createLocalePreferenceService', () => {
|
||||||
|
test('updates member locale preference', async () => {
|
||||||
|
const service = createLocalePreferenceService(createRepository())
|
||||||
|
|
||||||
|
const result = await service.updateMemberLocale({
|
||||||
|
householdId: 'household-1',
|
||||||
|
telegramUserId: '123456',
|
||||||
|
locale: 'en'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
status: 'updated',
|
||||||
|
member: {
|
||||||
|
householdId: 'household-1',
|
||||||
|
telegramUserId: '123456',
|
||||||
|
preferredLocale: 'en',
|
||||||
|
householdDefaultLocale: 'ru'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('rejects household locale update for non-admin actors', async () => {
|
||||||
|
const service = createLocalePreferenceService(createRepository())
|
||||||
|
|
||||||
|
const result = await service.updateHouseholdLocale({
|
||||||
|
householdId: 'household-1',
|
||||||
|
actorIsAdmin: false,
|
||||||
|
locale: 'en'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
status: 'rejected',
|
||||||
|
reason: 'not_admin'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
94
packages/application/src/locale-preference-service.ts
Normal file
94
packages/application/src/locale-preference-service.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import type { SupportedLocale } from '@household/domain'
|
||||||
|
import type { HouseholdConfigurationRepository } from '@household/ports'
|
||||||
|
|
||||||
|
export interface LocalePreferenceService {
|
||||||
|
updateMemberLocale(input: {
|
||||||
|
householdId: string
|
||||||
|
telegramUserId: string
|
||||||
|
locale: SupportedLocale
|
||||||
|
}): Promise<
|
||||||
|
| {
|
||||||
|
status: 'updated'
|
||||||
|
member: {
|
||||||
|
householdId: string
|
||||||
|
telegramUserId: string
|
||||||
|
preferredLocale: SupportedLocale | null
|
||||||
|
householdDefaultLocale: SupportedLocale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: 'rejected'
|
||||||
|
reason: 'member_not_found'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
updateHouseholdLocale(input: {
|
||||||
|
householdId: string
|
||||||
|
actorIsAdmin: boolean
|
||||||
|
locale: SupportedLocale
|
||||||
|
}): Promise<
|
||||||
|
| {
|
||||||
|
status: 'updated'
|
||||||
|
household: {
|
||||||
|
householdId: string
|
||||||
|
defaultLocale: SupportedLocale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: 'rejected'
|
||||||
|
reason: 'not_admin'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLocalePreferenceService(
|
||||||
|
repository: HouseholdConfigurationRepository
|
||||||
|
): LocalePreferenceService {
|
||||||
|
return {
|
||||||
|
async updateMemberLocale(input) {
|
||||||
|
const member = await repository.updateMemberPreferredLocale(
|
||||||
|
input.householdId,
|
||||||
|
input.telegramUserId,
|
||||||
|
input.locale
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
return {
|
||||||
|
status: 'rejected',
|
||||||
|
reason: 'member_not_found' as const
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'updated',
|
||||||
|
member: {
|
||||||
|
householdId: member.householdId,
|
||||||
|
telegramUserId: member.telegramUserId,
|
||||||
|
preferredLocale: member.preferredLocale,
|
||||||
|
householdDefaultLocale: member.householdDefaultLocale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateHouseholdLocale(input) {
|
||||||
|
if (!input.actorIsAdmin) {
|
||||||
|
return {
|
||||||
|
status: 'rejected',
|
||||||
|
reason: 'not_admin' as const
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const household = await repository.updateHouseholdDefaultLocale(
|
||||||
|
input.householdId,
|
||||||
|
input.locale
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'updated',
|
||||||
|
household: {
|
||||||
|
householdId: household.householdId,
|
||||||
|
defaultLocale: household.defaultLocale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,8 @@ function repository(): HouseholdConfigurationRepository {
|
|||||||
householdName: 'Kojori House',
|
householdName: 'Kojori House',
|
||||||
telegramChatId: '-100123',
|
telegramChatId: '-100123',
|
||||||
telegramChatType: 'supergroup',
|
telegramChatType: 'supergroup',
|
||||||
title: 'Kojori House'
|
title: 'Kojori House',
|
||||||
|
defaultLocale: 'ru'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
getTelegramHouseholdChat: async () => null,
|
getTelegramHouseholdChat: async () => null,
|
||||||
@@ -41,7 +42,8 @@ function repository(): HouseholdConfigurationRepository {
|
|||||||
telegramUserId: input.telegramUserId,
|
telegramUserId: input.telegramUserId,
|
||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
username: input.username?.trim() || null,
|
username: input.username?.trim() || null,
|
||||||
languageCode: input.languageCode?.trim() || null
|
languageCode: input.languageCode?.trim() || null,
|
||||||
|
householdDefaultLocale: 'ru'
|
||||||
}),
|
}),
|
||||||
getPendingHouseholdMember: async () => null,
|
getPendingHouseholdMember: async () => null,
|
||||||
findPendingHouseholdMemberByTelegramUserId: async () => null,
|
findPendingHouseholdMemberByTelegramUserId: async () => null,
|
||||||
@@ -50,6 +52,8 @@ function repository(): HouseholdConfigurationRepository {
|
|||||||
householdId: input.householdId,
|
householdId: input.householdId,
|
||||||
telegramUserId: input.telegramUserId,
|
telegramUserId: input.telegramUserId,
|
||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
|
preferredLocale: input.preferredLocale ?? null,
|
||||||
|
householdDefaultLocale: 'ru',
|
||||||
isAdmin: input.isAdmin === true
|
isAdmin: input.isAdmin === true
|
||||||
}),
|
}),
|
||||||
getHouseholdMember: async () => null,
|
getHouseholdMember: async () => null,
|
||||||
@@ -62,7 +66,8 @@ function repository(): HouseholdConfigurationRepository {
|
|||||||
telegramUserId: '123456',
|
telegramUserId: '123456',
|
||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
username: 'stan',
|
username: 'stan',
|
||||||
languageCode: 'ru'
|
languageCode: 'ru',
|
||||||
|
householdDefaultLocale: 'ru'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
approvePendingHouseholdMember: async (input) =>
|
approvePendingHouseholdMember: async (input) =>
|
||||||
@@ -72,6 +77,28 @@ function repository(): HouseholdConfigurationRepository {
|
|||||||
householdId: input.householdId,
|
householdId: input.householdId,
|
||||||
telegramUserId: input.telegramUserId,
|
telegramUserId: input.telegramUserId,
|
||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
|
preferredLocale: null,
|
||||||
|
householdDefaultLocale: 'ru',
|
||||||
|
isAdmin: false
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
updateHouseholdDefaultLocale: async (_householdId, locale) => ({
|
||||||
|
householdId: 'household-1',
|
||||||
|
householdName: 'Kojori House',
|
||||||
|
telegramChatId: '-100123',
|
||||||
|
telegramChatType: 'supergroup',
|
||||||
|
title: 'Kojori House',
|
||||||
|
defaultLocale: locale
|
||||||
|
}),
|
||||||
|
updateMemberPreferredLocale: async (_householdId, telegramUserId, locale) =>
|
||||||
|
telegramUserId === '123456'
|
||||||
|
? {
|
||||||
|
id: 'member-123456',
|
||||||
|
householdId: 'household-1',
|
||||||
|
telegramUserId,
|
||||||
|
displayName: 'Stan',
|
||||||
|
preferredLocale: locale,
|
||||||
|
householdDefaultLocale: 'ru',
|
||||||
isAdmin: false
|
isAdmin: false
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
@@ -96,7 +123,8 @@ describe('createMiniAppAdminService', () => {
|
|||||||
telegramUserId: '123456',
|
telegramUserId: '123456',
|
||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
username: 'stan',
|
username: 'stan',
|
||||||
languageCode: 'ru'
|
languageCode: 'ru',
|
||||||
|
householdDefaultLocale: 'ru'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
@@ -132,6 +160,8 @@ describe('createMiniAppAdminService', () => {
|
|||||||
householdId: 'household-1',
|
householdId: 'household-1',
|
||||||
telegramUserId: '123456',
|
telegramUserId: '123456',
|
||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
|
preferredLocale: null,
|
||||||
|
householdDefaultLocale: 'ru',
|
||||||
isAdmin: false
|
isAdmin: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
2
packages/db/drizzle/0008_lowly_spiral.sql
Normal file
2
packages/db/drizzle/0008_lowly_spiral.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "households" ADD COLUMN "default_locale" text DEFAULT 'ru' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "members" ADD COLUMN "preferred_locale" text;--> statement-breakpoint
|
||||||
2143
packages/db/drizzle/meta/0008_snapshot.json
Normal file
2143
packages/db/drizzle/meta/0008_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -57,6 +57,13 @@
|
|||||||
"when": 1773051000000,
|
"when": 1773051000000,
|
||||||
"tag": "0007_sudden_murmur",
|
"tag": "0007_sudden_murmur",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773047624171,
|
||||||
|
"tag": "0008_lowly_spiral",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
export const households = pgTable('households', {
|
export const households = pgTable('households', {
|
||||||
id: uuid('id').defaultRandom().primaryKey(),
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
|
defaultLocale: text('default_locale').default('ru').notNull(),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -142,6 +143,7 @@ export const members = pgTable(
|
|||||||
.references(() => households.id, { onDelete: 'cascade' }),
|
.references(() => households.id, { onDelete: 'cascade' }),
|
||||||
telegramUserId: text('telegram_user_id').notNull(),
|
telegramUserId: text('telegram_user_id').notNull(),
|
||||||
displayName: text('display_name').notNull(),
|
displayName: text('display_name').notNull(),
|
||||||
|
preferredLocale: text('preferred_locale'),
|
||||||
isAdmin: integer('is_admin').default(0).notNull(),
|
isAdmin: integer('is_admin').default(0).notNull(),
|
||||||
joinedAt: timestamp('joined_at', { withTimezone: true }).defaultNow().notNull()
|
joinedAt: timestamp('joined_at', { withTimezone: true }).defaultNow().notNull()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export { BillingPeriod } from './billing-period'
|
|||||||
export { DOMAIN_ERROR_CODE, DomainError } from './errors'
|
export { DOMAIN_ERROR_CODE, DomainError } from './errors'
|
||||||
export { BillingCycleId, HouseholdId, MemberId, PurchaseEntryId } from './ids'
|
export { BillingCycleId, HouseholdId, MemberId, PurchaseEntryId } from './ids'
|
||||||
export { CURRENCIES, Money } from './money'
|
export { CURRENCIES, Money } from './money'
|
||||||
|
export { normalizeSupportedLocale, SUPPORTED_LOCALES } from './locale'
|
||||||
export {
|
export {
|
||||||
Temporal,
|
Temporal,
|
||||||
instantFromDatabaseValue,
|
instantFromDatabaseValue,
|
||||||
@@ -13,6 +14,7 @@ export {
|
|||||||
nowInstant
|
nowInstant
|
||||||
} from './time'
|
} from './time'
|
||||||
export type { CurrencyCode } from './money'
|
export type { CurrencyCode } from './money'
|
||||||
|
export type { SupportedLocale } from './locale'
|
||||||
export type { Instant } from './time'
|
export type { Instant } from './time'
|
||||||
export type {
|
export type {
|
||||||
SettlementInput,
|
SettlementInput,
|
||||||
|
|||||||
14
packages/domain/src/locale.ts
Normal file
14
packages/domain/src/locale.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export const SUPPORTED_LOCALES = ['en', 'ru'] as const
|
||||||
|
|
||||||
|
export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number]
|
||||||
|
|
||||||
|
export function normalizeSupportedLocale(value?: string | null): SupportedLocale | null {
|
||||||
|
const normalized = value?.trim().toLowerCase()
|
||||||
|
if (!normalized) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (SUPPORTED_LOCALES as readonly string[]).includes(normalized)
|
||||||
|
? (normalized as SupportedLocale)
|
||||||
|
: null
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { SupportedLocale } from '@household/domain'
|
||||||
|
|
||||||
export const HOUSEHOLD_TOPIC_ROLES = ['purchase', 'feedback', 'reminders'] as const
|
export const HOUSEHOLD_TOPIC_ROLES = ['purchase', 'feedback', 'reminders'] as const
|
||||||
|
|
||||||
export type HouseholdTopicRole = (typeof HOUSEHOLD_TOPIC_ROLES)[number]
|
export type HouseholdTopicRole = (typeof HOUSEHOLD_TOPIC_ROLES)[number]
|
||||||
@@ -8,6 +10,7 @@ export interface HouseholdTelegramChatRecord {
|
|||||||
telegramChatId: string
|
telegramChatId: string
|
||||||
telegramChatType: string
|
telegramChatType: string
|
||||||
title: string | null
|
title: string | null
|
||||||
|
defaultLocale: SupportedLocale
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HouseholdTopicBindingRecord {
|
export interface HouseholdTopicBindingRecord {
|
||||||
@@ -31,6 +34,7 @@ export interface HouseholdPendingMemberRecord {
|
|||||||
displayName: string
|
displayName: string
|
||||||
username: string | null
|
username: string | null
|
||||||
languageCode: string | null
|
languageCode: string | null
|
||||||
|
householdDefaultLocale: SupportedLocale
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HouseholdMemberRecord {
|
export interface HouseholdMemberRecord {
|
||||||
@@ -38,6 +42,8 @@ export interface HouseholdMemberRecord {
|
|||||||
householdId: string
|
householdId: string
|
||||||
telegramUserId: string
|
telegramUserId: string
|
||||||
displayName: string
|
displayName: string
|
||||||
|
preferredLocale: SupportedLocale | null
|
||||||
|
householdDefaultLocale: SupportedLocale
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +105,7 @@ export interface HouseholdConfigurationRepository {
|
|||||||
householdId: string
|
householdId: string
|
||||||
telegramUserId: string
|
telegramUserId: string
|
||||||
displayName: string
|
displayName: string
|
||||||
|
preferredLocale?: SupportedLocale | null
|
||||||
isAdmin?: boolean
|
isAdmin?: boolean
|
||||||
}): Promise<HouseholdMemberRecord>
|
}): Promise<HouseholdMemberRecord>
|
||||||
getHouseholdMember(
|
getHouseholdMember(
|
||||||
@@ -115,4 +122,13 @@ export interface HouseholdConfigurationRepository {
|
|||||||
telegramUserId: string
|
telegramUserId: string
|
||||||
isAdmin?: boolean
|
isAdmin?: boolean
|
||||||
}): Promise<HouseholdMemberRecord | null>
|
}): Promise<HouseholdMemberRecord | null>
|
||||||
|
updateHouseholdDefaultLocale(
|
||||||
|
householdId: string,
|
||||||
|
locale: SupportedLocale
|
||||||
|
): Promise<HouseholdTelegramChatRecord>
|
||||||
|
updateMemberPreferredLocale(
|
||||||
|
householdId: string,
|
||||||
|
telegramUserId: string,
|
||||||
|
locale: SupportedLocale
|
||||||
|
): Promise<HouseholdMemberRecord | null>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user