feat(miniapp): add household general settings

This commit is contained in:
2026-03-12 11:30:11 +04:00
parent 8160f644cc
commit 4c19ee798d
14 changed files with 268 additions and 43 deletions

View File

@@ -469,6 +469,7 @@ describe('createMiniAppSettingsHandler', () => {
expect(await response.json()).toEqual({ expect(await response.json()).toEqual({
ok: true, ok: true,
authorized: true, authorized: true,
householdName: 'Kojori House',
settings: { settings: {
householdId: 'household-1', householdId: 'household-1',
settlementCurrency: 'GEL', settlementCurrency: 'GEL',
@@ -570,6 +571,7 @@ describe('createMiniAppUpdateSettingsHandler', () => {
expect(await response.json()).toEqual({ expect(await response.json()).toEqual({
ok: true, ok: true,
authorized: true, authorized: true,
householdName: 'Kojori House',
settings: { settings: {
householdId: 'household-1', householdId: 'household-1',
settlementCurrency: 'GEL', settlementCurrency: 'GEL',

View File

@@ -49,6 +49,7 @@ async function readApprovalPayload(request: Request): Promise<{
async function readSettingsUpdatePayload(request: Request): Promise<{ async function readSettingsUpdatePayload(request: Request): Promise<{
initData: string initData: string
householdName?: string
settlementCurrency?: string settlementCurrency?: string
paymentBalanceAdjustmentPolicy?: string paymentBalanceAdjustmentPolicy?: string
rentAmountMajor?: string rentAmountMajor?: string
@@ -69,6 +70,7 @@ async function readSettingsUpdatePayload(request: Request): Promise<{
const text = await clonedRequest.text() const text = await clonedRequest.text()
let parsed: { let parsed: {
householdName?: string
settlementCurrency?: string settlementCurrency?: string
paymentBalanceAdjustmentPolicy?: string paymentBalanceAdjustmentPolicy?: string
rentAmountMajor?: string rentAmountMajor?: string
@@ -99,6 +101,11 @@ async function readSettingsUpdatePayload(request: Request): Promise<{
return { return {
initData: payload.initData, initData: payload.initData,
...(typeof parsed.householdName === 'string'
? {
householdName: parsed.householdName
}
: {}),
...(typeof parsed.rentAmountMajor === 'string' ...(typeof parsed.rentAmountMajor === 'string'
? { ? {
rentAmountMajor: parsed.rentAmountMajor rentAmountMajor: parsed.rentAmountMajor
@@ -545,6 +552,7 @@ export function createMiniAppSettingsHandler(options: {
{ {
ok: true, ok: true,
authorized: true, authorized: true,
householdName: result.householdName,
settings: serializeBillingSettings(result.settings), settings: serializeBillingSettings(result.settings),
assistantConfig: serializeAssistantConfig(result.assistantConfig), assistantConfig: serializeAssistantConfig(result.assistantConfig),
topics: result.topics, topics: result.topics,
@@ -620,6 +628,11 @@ export function createMiniAppUpdateSettingsHandler(options: {
const result = await options.miniAppAdminService.updateSettings({ const result = await options.miniAppAdminService.updateSettings({
householdId: session.member.householdId, householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin, actorIsAdmin: session.member.isAdmin,
...(payload.householdName !== undefined
? {
householdName: payload.householdName
}
: {}),
...(payload.settlementCurrency ...(payload.settlementCurrency
? { ? {
settlementCurrency: payload.settlementCurrency settlementCurrency: payload.settlementCurrency
@@ -675,6 +688,7 @@ export function createMiniAppUpdateSettingsHandler(options: {
{ {
ok: true, ok: true,
authorized: true, authorized: true,
householdName: result.householdName,
settings: serializeBillingSettings(result.settings), settings: serializeBillingSettings(result.settings),
assistantConfig: serializeAssistantConfig(result.assistantConfig) assistantConfig: serializeAssistantConfig(result.assistantConfig)
}, },

View File

@@ -100,6 +100,7 @@ export interface MiniAppSessionResult {
member?: { member?: {
id: string id: string
householdId: string householdId: string
householdName: string
displayName: string displayName: string
status: 'active' | 'away' | 'left' status: 'active' | 'away' | 'left'
isAdmin: boolean isAdmin: boolean

View File

@@ -90,6 +90,7 @@ type SessionState =
mode: 'live' | 'demo' mode: 'live' | 'demo'
member: { member: {
id: string id: string
householdName: string
displayName: string displayName: string
status: 'active' | 'away' | 'left' status: 'active' | 'away' | 'left'
isAdmin: boolean isAdmin: boolean
@@ -348,6 +349,7 @@ function App() {
const [testingRolePreview, setTestingRolePreview] = createSignal<TestingRolePreview | null>(null) const [testingRolePreview, setTestingRolePreview] = createSignal<TestingRolePreview | null>(null)
const [addingPayment, setAddingPayment] = createSignal(false) const [addingPayment, setAddingPayment] = createSignal(false)
const [billingForm, setBillingForm] = createSignal({ const [billingForm, setBillingForm] = createSignal({
householdName: '',
settlementCurrency: 'GEL' as 'USD' | 'GEL', settlementCurrency: 'GEL' as 'USD' | 'GEL',
paymentBalanceAdjustmentPolicy: 'utilities' as 'utilities' | 'rent' | 'separate', paymentBalanceAdjustmentPolicy: 'utilities' as 'utilities' | 'rent' | 'separate',
rentAmountMajor: '', rentAmountMajor: '',
@@ -762,6 +764,7 @@ function App() {
'' ''
})) }))
setBillingForm({ setBillingForm({
householdName: payload.householdName,
settlementCurrency: payload.settings.settlementCurrency, settlementCurrency: payload.settings.settlementCurrency,
paymentBalanceAdjustmentPolicy: payload.settings.paymentBalanceAdjustmentPolicy, paymentBalanceAdjustmentPolicy: payload.settings.paymentBalanceAdjustmentPolicy,
rentAmountMajor: payload.settings.rentAmountMinor rentAmountMajor: payload.settings.rentAmountMinor
@@ -883,6 +886,7 @@ function App() {
) )
) )
setBillingForm({ setBillingForm({
householdName: demoAdminSettings.householdName,
settlementCurrency: demoAdminSettings.settings.settlementCurrency, settlementCurrency: demoAdminSettings.settings.settlementCurrency,
paymentBalanceAdjustmentPolicy: demoAdminSettings.settings.paymentBalanceAdjustmentPolicy, paymentBalanceAdjustmentPolicy: demoAdminSettings.settings.paymentBalanceAdjustmentPolicy,
rentAmountMajor: demoAdminSettings.settings.rentAmountMinor rentAmountMajor: demoAdminSettings.settings.rentAmountMinor
@@ -1200,7 +1204,7 @@ function App() {
setSavingBillingSettings(true) setSavingBillingSettings(true)
try { try {
const { settings, assistantConfig } = await updateMiniAppBillingSettings( const { householdName, settings, assistantConfig } = await updateMiniAppBillingSettings(
initData, initData,
billingForm() billingForm()
) )
@@ -1208,11 +1212,27 @@ function App() {
current current
? { ? {
...current, ...current,
householdName,
settings, settings,
assistantConfig assistantConfig
} }
: current : current
) )
setBillingForm((current) => ({
...current,
householdName
}))
setSession((current) =>
current.status === 'ready'
? {
...current,
member: {
...current.member,
householdName
}
}
: current
)
setCycleForm((current) => ({ setCycleForm((current) => ({
...current, ...current,
rentCurrency: settings.rentCurrency, rentCurrency: settings.rentCurrency,
@@ -1992,6 +2012,8 @@ function App() {
locale={locale()} locale={locale()}
readyIsAdmin={effectiveIsAdmin()} readyIsAdmin={effectiveIsAdmin()}
householdDefaultLocale={readySession()?.member.householdDefaultLocale ?? 'en'} householdDefaultLocale={readySession()?.member.householdDefaultLocale ?? 'en'}
householdName={readySession()?.member.householdName ?? billingForm().householdName}
profileDisplayName={readySession()?.member.displayName ?? displayNameDraft()}
dashboard={dashboard()} dashboard={dashboard()}
adminSettings={adminSettings()} adminSettings={adminSettings()}
cycleState={cycleState()} cycleState={cycleState()}
@@ -2030,6 +2052,7 @@ function App() {
resolvedMemberAbsencePolicy(memberId, status) resolvedMemberAbsencePolicy(memberId, status)
} }
onChangeHouseholdLocale={handleHouseholdLocaleChange} onChangeHouseholdLocale={handleHouseholdLocaleChange}
onOpenProfileEditor={() => setProfileEditorOpen(true)}
onOpenCycleModal={() => setCycleRentOpen(true)} onOpenCycleModal={() => setCycleRentOpen(true)}
onCloseCycleModal={() => setCycleRentOpen(false)} onCloseCycleModal={() => setCycleRentOpen(false)}
onSaveCycleRent={handleSaveCycleRent} onSaveCycleRent={handleSaveCycleRent}
@@ -2061,6 +2084,12 @@ function App() {
settlementCurrency: value settlementCurrency: value
})) }))
} }
onBillingHouseholdNameChange={(value) =>
setBillingForm((current) => ({
...current,
householdName: value
}))
}
onBillingAdjustmentPolicyChange={(value) => onBillingAdjustmentPolicyChange={(value) =>
setBillingForm((current) => ({ setBillingForm((current) => ({
...current, ...current,
@@ -2290,7 +2319,11 @@ function App() {
<TopBar <TopBar
subtitle={copy().appSubtitle} subtitle={copy().appSubtitle}
title={copy().appTitle} title={
readySession()?.member.householdName ??
onboardingSession()?.householdName ??
copy().appTitle
}
languageLabel={copy().language} languageLabel={copy().language}
locale={locale()} locale={locale()}
saving={savingMemberLocale()} saving={savingMemberLocale()}
@@ -2391,15 +2424,6 @@ function App() {
)} )}
</Show> </Show>
</div> </div>
<Show when={readySession()?.mode === 'live'}>
<Button
variant="secondary"
class="app-context-row__action"
onClick={() => setProfileEditorOpen(true)}
>
{copy().manageProfileAction}
</Button>
</Show>
</section> </section>
<section class="content-stack">{panel()}</section> <section class="content-stack">{panel()}</section>

View File

@@ -9,6 +9,7 @@ import type {
export const demoMember: NonNullable<MiniAppSession['member']> = { export const demoMember: NonNullable<MiniAppSession['member']> = {
id: 'demo-member', id: 'demo-member',
householdId: 'demo-household', householdId: 'demo-household',
householdName: 'Kojori House',
displayName: 'Stas', displayName: 'Stas',
status: 'active', status: 'active',
isAdmin: true, isAdmin: true,
@@ -191,6 +192,7 @@ export const demoPendingMembers: readonly MiniAppPendingMember[] = [
] ]
export const demoAdminSettings: MiniAppAdminSettingsPayload = { export const demoAdminSettings: MiniAppAdminSettingsPayload = {
householdName: 'Kojori House',
settings: { settings: {
householdId: 'demo-household', householdId: 'demo-household',
settlementCurrency: 'GEL', settlementCurrency: 'GEL',

View File

@@ -30,6 +30,10 @@ export const dictionary = {
reload: 'Retry', reload: 'Retry',
language: 'Language', language: 'Language',
householdLanguage: 'Household language', householdLanguage: 'Household language',
generalSettingsBody:
'Household identity, default language, and personal profile controls live here.',
householdNameLabel: 'Household name',
householdNameHint: 'This appears in the mini app, join flow, and bot responses.',
savingLanguage: 'Saving…', savingLanguage: 'Saving…',
onLabel: 'On', onLabel: 'On',
offLabel: 'Off', offLabel: 'Off',
@@ -37,6 +41,7 @@ export const dictionary = {
balances: 'Balances', balances: 'Balances',
ledger: 'Ledger', ledger: 'Ledger',
house: 'House', house: 'House',
houseSectionGeneral: 'General',
houseSectionBilling: 'Billing', houseSectionBilling: 'Billing',
houseSectionUtilities: 'Utilities', houseSectionUtilities: 'Utilities',
houseSectionMembers: 'Members', houseSectionMembers: 'Members',
@@ -292,6 +297,9 @@ export const dictionary = {
reload: 'Повторить', reload: 'Повторить',
language: 'Язык', language: 'Язык',
householdLanguage: 'Язык дома', householdLanguage: 'Язык дома',
generalSettingsBody: 'Здесь живут имя дома, язык по умолчанию и доступ к личному профилю.',
householdNameLabel: 'Название дома',
householdNameHint: 'Показывается в mini app, при вступлении и в ответах бота.',
savingLanguage: 'Сохраняем…', savingLanguage: 'Сохраняем…',
onLabel: 'Вкл', onLabel: 'Вкл',
offLabel: 'Выкл', offLabel: 'Выкл',
@@ -299,6 +307,7 @@ export const dictionary = {
balances: 'Баланс', balances: 'Баланс',
ledger: 'Леджер', ledger: 'Леджер',
house: 'Дом', house: 'Дом',
houseSectionGeneral: 'Общее',
houseSectionBilling: 'Биллинг', houseSectionBilling: 'Биллинг',
houseSectionUtilities: 'Коммуналка', houseSectionUtilities: 'Коммуналка',
houseSectionMembers: 'Участники', houseSectionMembers: 'Участники',

View File

@@ -5,6 +5,7 @@ export interface MiniAppSession {
member?: { member?: {
id: string id: string
householdId: string householdId: string
householdName: string
displayName: string displayName: string
status: 'active' | 'away' | 'left' status: 'active' | 'away' | 'left'
isAdmin: boolean isAdmin: boolean
@@ -138,6 +139,7 @@ export interface MiniAppDashboard {
} }
export interface MiniAppAdminSettingsPayload { export interface MiniAppAdminSettingsPayload {
householdName: string
settings: MiniAppBillingSettings settings: MiniAppBillingSettings
assistantConfig: MiniAppAssistantConfig assistantConfig: MiniAppAssistantConfig
topics: readonly MiniAppTopicBinding[] topics: readonly MiniAppTopicBinding[]
@@ -386,6 +388,7 @@ export async function fetchMiniAppAdminSettings(
const payload = (await response.json()) as { const payload = (await response.json()) as {
ok: boolean ok: boolean
authorized?: boolean authorized?: boolean
householdName?: string
settings?: MiniAppBillingSettings settings?: MiniAppBillingSettings
assistantConfig?: MiniAppAssistantConfig assistantConfig?: MiniAppAssistantConfig
topics?: MiniAppTopicBinding[] topics?: MiniAppTopicBinding[]
@@ -398,6 +401,7 @@ export async function fetchMiniAppAdminSettings(
if ( if (
!response.ok || !response.ok ||
!payload.authorized || !payload.authorized ||
!payload.householdName ||
!payload.settings || !payload.settings ||
!payload.assistantConfig || !payload.assistantConfig ||
!payload.topics || !payload.topics ||
@@ -409,6 +413,7 @@ export async function fetchMiniAppAdminSettings(
} }
return { return {
householdName: payload.householdName,
settings: payload.settings, settings: payload.settings,
assistantConfig: payload.assistantConfig, assistantConfig: payload.assistantConfig,
topics: payload.topics, topics: payload.topics,
@@ -423,6 +428,7 @@ export async function updateMiniAppBillingSettings(
input: { input: {
settlementCurrency?: 'USD' | 'GEL' settlementCurrency?: 'USD' | 'GEL'
paymentBalanceAdjustmentPolicy?: 'utilities' | 'rent' | 'separate' paymentBalanceAdjustmentPolicy?: 'utilities' | 'rent' | 'separate'
householdName?: string
rentAmountMajor?: string rentAmountMajor?: string
rentCurrency: 'USD' | 'GEL' rentCurrency: 'USD' | 'GEL'
rentDueDay: number rentDueDay: number
@@ -434,6 +440,7 @@ export async function updateMiniAppBillingSettings(
assistantTone?: string assistantTone?: string
} }
): Promise<{ ): Promise<{
householdName: string
settings: MiniAppBillingSettings settings: MiniAppBillingSettings
assistantConfig: MiniAppAssistantConfig assistantConfig: MiniAppAssistantConfig
}> { }> {
@@ -451,16 +458,24 @@ export async function updateMiniAppBillingSettings(
const payload = (await response.json()) as { const payload = (await response.json()) as {
ok: boolean ok: boolean
authorized?: boolean authorized?: boolean
householdName?: string
settings?: MiniAppBillingSettings settings?: MiniAppBillingSettings
assistantConfig?: MiniAppAssistantConfig assistantConfig?: MiniAppAssistantConfig
error?: string error?: string
} }
if (!response.ok || !payload.authorized || !payload.settings || !payload.assistantConfig) { if (
!response.ok ||
!payload.authorized ||
!payload.householdName ||
!payload.settings ||
!payload.assistantConfig
) {
throw new Error(payload.error ?? 'Failed to update billing settings') throw new Error(payload.error ?? 'Failed to update billing settings')
} }
return { return {
householdName: payload.householdName,
settings: payload.settings, settings: payload.settings,
assistantConfig: payload.assistantConfig assistantConfig: payload.assistantConfig
} }

View File

@@ -28,6 +28,7 @@ type UtilityBillDraft = {
} }
type BillingForm = { type BillingForm = {
householdName: string
settlementCurrency: 'USD' | 'GEL' settlementCurrency: 'USD' | 'GEL'
paymentBalanceAdjustmentPolicy: 'utilities' | 'rent' | 'separate' paymentBalanceAdjustmentPolicy: 'utilities' | 'rent' | 'separate'
rentAmountMajor: string rentAmountMajor: string
@@ -55,6 +56,8 @@ type Props = {
locale: 'en' | 'ru' locale: 'en' | 'ru'
readyIsAdmin: boolean readyIsAdmin: boolean
householdDefaultLocale: 'en' | 'ru' householdDefaultLocale: 'en' | 'ru'
householdName: string
profileDisplayName: string
dashboard: MiniAppDashboard | null dashboard: MiniAppDashboard | null
adminSettings: MiniAppAdminSettingsPayload | null adminSettings: MiniAppAdminSettingsPayload | null
cycleState: MiniAppAdminCycleState | null cycleState: MiniAppAdminCycleState | null
@@ -101,6 +104,7 @@ type Props = {
effectiveFromPeriod: string | null effectiveFromPeriod: string | null
} }
onChangeHouseholdLocale: (locale: 'en' | 'ru') => Promise<void> onChangeHouseholdLocale: (locale: 'en' | 'ru') => Promise<void>
onOpenProfileEditor: () => void
onOpenCycleModal: () => void onOpenCycleModal: () => void
onCloseCycleModal: () => void onCloseCycleModal: () => void
onSaveCycleRent: () => Promise<void> onSaveCycleRent: () => Promise<void>
@@ -111,6 +115,7 @@ type Props = {
onOpenBillingSettingsModal: () => void onOpenBillingSettingsModal: () => void
onCloseBillingSettingsModal: () => void onCloseBillingSettingsModal: () => void
onSaveBillingSettings: () => Promise<void> onSaveBillingSettings: () => Promise<void>
onBillingHouseholdNameChange: (value: string) => void
onBillingSettlementCurrencyChange: (value: 'USD' | 'GEL') => void onBillingSettlementCurrencyChange: (value: 'USD' | 'GEL') => void
onBillingAdjustmentPolicyChange: (value: 'utilities' | 'rent' | 'separate') => void onBillingAdjustmentPolicyChange: (value: 'utilities' | 'rent' | 'separate') => void
onBillingRentAmountChange: (value: string) => void onBillingRentAmountChange: (value: string) => void
@@ -213,15 +218,83 @@ export function HouseScreen(props: Props) {
<strong>{props.copy.residentHouseTitle ?? ''}</strong> <strong>{props.copy.residentHouseTitle ?? ''}</strong>
</header> </header>
<p>{props.copy.residentHouseBody ?? ''}</p> <p>{props.copy.residentHouseBody ?? ''}</p>
<div class="panel-toolbar">
<Button variant="secondary" onClick={props.onOpenProfileEditor}>
<PencilIcon />
{props.copy.manageProfileAction ?? ''}
</Button>
</div>
</article> </article>
</div> </div>
} }
> >
<div class="admin-layout"> <div class="admin-layout">
<HouseSection
title={props.copy.houseSectionGeneral ?? ''}
body={props.copy.generalSettingsBody}
defaultOpen
>
<section class="admin-section">
<div class="admin-grid">
<article class="balance-item">
<header>
<strong>{props.copy.householdNameLabel ?? ''}</strong>
<span>{props.householdName}</span>
</header>
<p>{props.copy.householdNameHint ?? ''}</p>
<div class="panel-toolbar">
<Button variant="secondary" onClick={props.onOpenBillingSettingsModal}>
<SettingsIcon />
{props.copy.manageSettingsAction ?? ''}
</Button>
</div>
</article>
<article class="balance-item">
<header>
<strong>{props.copy.householdLanguage ?? ''}</strong>
<span>{props.householdDefaultLocale.toUpperCase()}</span>
</header>
<div class="locale-switch__buttons locale-switch__buttons--inline">
<button
classList={{ 'is-active': props.householdDefaultLocale === 'en' }}
type="button"
disabled={props.savingHouseholdLocale}
onClick={() => void props.onChangeHouseholdLocale('en')}
>
EN
</button>
<button
classList={{ 'is-active': props.householdDefaultLocale === 'ru' }}
type="button"
disabled={props.savingHouseholdLocale}
onClick={() => void props.onChangeHouseholdLocale('ru')}
>
RU
</button>
</div>
</article>
<article class="balance-item">
<header>
<strong>{props.copy.manageProfileAction ?? ''}</strong>
<span>{props.profileDisplayName}</span>
</header>
<p>{props.copy.profileEditorBody ?? ''}</p>
<div class="panel-toolbar">
<Button variant="secondary" onClick={props.onOpenProfileEditor}>
<PencilIcon />
{props.copy.manageProfileAction ?? ''}
</Button>
</div>
</article>
</div>
</section>
</HouseSection>
<HouseSection <HouseSection
title={props.copy.houseSectionBilling ?? ''} title={props.copy.houseSectionBilling ?? ''}
body={props.copy.billingSettingsEditorBody} body={props.copy.billingSettingsEditorBody}
defaultOpen
> >
<section class="admin-section"> <section class="admin-section">
<div class="admin-grid"> <div class="admin-grid">
@@ -290,31 +363,6 @@ export function HouseScreen(props: Props) {
</Button> </Button>
</div> </div>
</article> </article>
<article class="balance-item">
<header>
<strong>{props.copy.householdLanguage ?? ''}</strong>
<span>{props.householdDefaultLocale.toUpperCase()}</span>
</header>
<div class="locale-switch__buttons locale-switch__buttons--inline">
<button
classList={{ 'is-active': props.householdDefaultLocale === 'en' }}
type="button"
disabled={props.savingHouseholdLocale}
onClick={() => void props.onChangeHouseholdLocale('en')}
>
EN
</button>
<button
classList={{ 'is-active': props.householdDefaultLocale === 'ru' }}
type="button"
disabled={props.savingHouseholdLocale}
onClick={() => void props.onChangeHouseholdLocale('ru')}
>
RU
</button>
</div>
</article>
</div> </div>
<Modal <Modal
open={props.cycleRentOpen} open={props.cycleRentOpen}
@@ -414,6 +462,18 @@ export function HouseScreen(props: Props) {
} }
> >
<div class="editor-grid"> <div class="editor-grid">
<Field
label={props.copy.householdNameLabel ?? ''}
hint={props.copy.householdNameHint ?? ''}
wide
>
<input
value={props.billingForm.householdName}
onInput={(event) =>
props.onBillingHouseholdNameChange(event.currentTarget.value)
}
/>
</Field>
<Field label={props.copy.settlementCurrency ?? ''}> <Field label={props.copy.settlementCurrency ?? ''}>
<select <select
value={props.billingForm.settlementCurrency} value={props.billingForm.settlementCurrency}

View File

@@ -1320,6 +1320,35 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
} }
}, },
async updateHouseholdName(householdId, householdName) {
const updatedHouseholds = await db
.update(schema.households)
.set({
name: householdName
})
.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 name')
}
const chat = await this.getHouseholdChatByHouseholdId(householdId)
if (!chat) {
throw new Error('Failed to resolve household chat after name update')
}
return {
...chat,
householdName: household.name
}
},
async updateMemberPreferredLocale(householdId, telegramUserId, locale) { async updateMemberPreferredLocale(householdId, telegramUserId, locale) {
const rows = await db const rows = await db
.update(schema.members) .update(schema.members)

View File

@@ -338,6 +338,7 @@ describe('createHouseholdOnboardingService', () => {
member: { member: {
id: 'member-42', id: 'member-42',
householdId: 'household-1', householdId: 'household-1',
householdName: 'Kojori House',
displayName: 'Stan', displayName: 'Stan',
status: 'active', status: 'active',
preferredLocale: null, preferredLocale: null,

View File

@@ -16,6 +16,7 @@ export type HouseholdMiniAppAccess =
member: { member: {
id: string id: string
householdId: string householdId: string
householdName: string
displayName: string displayName: string
status: HouseholdMemberRecord['status'] status: HouseholdMemberRecord['status']
isAdmin: boolean isAdmin: boolean
@@ -68,6 +69,7 @@ export interface HouseholdOnboardingService {
member: { member: {
id: string id: string
householdId: string householdId: string
householdName: string
displayName: string displayName: string
status: HouseholdMemberRecord['status'] status: HouseholdMemberRecord['status']
isAdmin: boolean isAdmin: boolean
@@ -161,9 +163,19 @@ export function createHouseholdOnboardingService(options: {
) ?? null) ) ?? null)
if (matchingActiveMember) { if (matchingActiveMember) {
const household = await options.repository.getHouseholdChatByHouseholdId(
matchingActiveMember.householdId
)
if (!household) {
throw new Error('Failed to resolve household for active mini app member')
}
return { return {
status: 'active', status: 'active',
member: toMember(matchingActiveMember) member: {
...toMember(matchingActiveMember),
householdName: household.householdName
}
} }
} }
@@ -232,9 +244,19 @@ export function createHouseholdOnboardingService(options: {
).find((member) => member.householdId === household.householdId) ).find((member) => member.householdId === household.householdId)
if (activeMember) { if (activeMember) {
const householdRecord = await options.repository.getHouseholdChatByHouseholdId(
activeMember.householdId
)
if (!householdRecord) {
throw new Error('Failed to resolve household after mini app join')
}
return { return {
status: 'active', status: 'active',
member: toMember(activeMember) member: {
...toMember(activeMember),
householdName: householdRecord.householdName
}
} }
} }

View File

@@ -25,7 +25,14 @@ function repository(): HouseholdConfigurationRepository {
} }
}), }),
getTelegramHouseholdChat: async () => null, getTelegramHouseholdChat: async () => null,
getHouseholdChatByHouseholdId: async () => null, getHouseholdChatByHouseholdId: async () => ({
householdId: 'household-1',
householdName: 'Kojori House',
telegramChatId: '-100123',
telegramChatType: 'supergroup',
title: 'Kojori House',
defaultLocale: 'ru'
}),
bindHouseholdTopic: async (input) => ({ bindHouseholdTopic: async (input) => ({
householdId: input.householdId, householdId: input.householdId,
role: input.role, role: input.role,
@@ -268,6 +275,7 @@ describe('createMiniAppAdminService', () => {
expect(result).toEqual({ expect(result).toEqual({
status: 'ok', status: 'ok',
householdName: 'Kojori House',
settings: { settings: {
householdId: 'household-1', householdId: 'household-1',
settlementCurrency: 'GEL', settlementCurrency: 'GEL',
@@ -327,6 +335,7 @@ describe('createMiniAppAdminService', () => {
expect(result).toEqual({ expect(result).toEqual({
status: 'ok', status: 'ok',
householdName: 'Kojori House',
settings: { settings: {
householdId: 'household-1', householdId: 'household-1',
settlementCurrency: 'GEL', settlementCurrency: 'GEL',

View File

@@ -29,6 +29,7 @@ export interface MiniAppAdminService {
getSettings(input: { householdId: string; actorIsAdmin: boolean }): Promise< getSettings(input: { householdId: string; actorIsAdmin: boolean }): Promise<
| { | {
status: 'ok' status: 'ok'
householdName: string
settings: HouseholdBillingSettingsRecord settings: HouseholdBillingSettingsRecord
assistantConfig: HouseholdAssistantConfigRecord assistantConfig: HouseholdAssistantConfigRecord
categories: readonly HouseholdUtilityCategoryRecord[] categories: readonly HouseholdUtilityCategoryRecord[]
@@ -44,6 +45,7 @@ export interface MiniAppAdminService {
updateSettings(input: { updateSettings(input: {
householdId: string householdId: string
actorIsAdmin: boolean actorIsAdmin: boolean
householdName?: string
settlementCurrency?: string settlementCurrency?: string
paymentBalanceAdjustmentPolicy?: string paymentBalanceAdjustmentPolicy?: string
rentAmountMajor?: string rentAmountMajor?: string
@@ -58,6 +60,7 @@ export interface MiniAppAdminService {
}): Promise< }): Promise<
| { | {
status: 'ok' status: 'ok'
householdName: string
settings: HouseholdBillingSettingsRecord settings: HouseholdBillingSettingsRecord
assistantConfig: HouseholdAssistantConfigRecord assistantConfig: HouseholdAssistantConfigRecord
} }
@@ -215,6 +218,19 @@ function normalizeDisplayName(raw: string): string | null {
return trimmed.replace(/\s+/g, ' ') return trimmed.replace(/\s+/g, ' ')
} }
function normalizeHouseholdName(raw: string | undefined): string | null | undefined {
if (raw === undefined) {
return undefined
}
const trimmed = raw.trim()
if (trimmed.length < 2 || trimmed.length > 120) {
return null
}
return trimmed.replace(/\s+/g, ' ')
}
function defaultAssistantConfig(householdId: string): HouseholdAssistantConfigRecord { function defaultAssistantConfig(householdId: string): HouseholdAssistantConfigRecord {
return { return {
householdId, householdId,
@@ -255,6 +271,11 @@ export function createMiniAppAdminService(
} }
} }
const household = await repository.getHouseholdChatByHouseholdId(input.householdId)
if (!household) {
throw new Error('Failed to resolve household chat for mini app settings')
}
const [settings, assistantConfig, categories, members, memberAbsencePolicies, topics] = const [settings, assistantConfig, categories, members, memberAbsencePolicies, topics] =
await Promise.all([ await Promise.all([
repository.getHouseholdBillingSettings(input.householdId), repository.getHouseholdBillingSettings(input.householdId),
@@ -269,6 +290,7 @@ export function createMiniAppAdminService(
return { return {
status: 'ok', status: 'ok',
householdName: household.householdName,
settings, settings,
assistantConfig, assistantConfig,
categories, categories,
@@ -303,8 +325,11 @@ export function createMiniAppAdminService(
const assistantContext = normalizeAssistantText(input.assistantContext, 1200) const assistantContext = normalizeAssistantText(input.assistantContext, 1200)
const assistantTone = normalizeAssistantText(input.assistantTone, 160) const assistantTone = normalizeAssistantText(input.assistantTone, 160)
const householdName = normalizeHouseholdName(input.householdName)
const nextHouseholdName = householdName ?? undefined
if ( if (
(input.householdName !== undefined && householdName === null) ||
(input.assistantContext !== undefined && (input.assistantContext !== undefined &&
assistantContext === null && assistantContext === null &&
input.assistantContext.trim().length > 0) || input.assistantContext.trim().length > 0) ||
@@ -349,7 +374,7 @@ export function createMiniAppAdminService(
const shouldUpdateAssistantConfig = const shouldUpdateAssistantConfig =
assistantContext !== undefined || assistantTone !== undefined assistantContext !== undefined || assistantTone !== undefined
const [settings, nextAssistantConfig] = await Promise.all([ const [settings, nextAssistantConfig, household] = await Promise.all([
repository.updateHouseholdBillingSettings({ repository.updateHouseholdBillingSettings({
householdId: input.householdId, householdId: input.householdId,
...(settlementCurrency ...(settlementCurrency
@@ -398,11 +423,19 @@ export function createMiniAppAdminService(
householdId: input.householdId, householdId: input.householdId,
assistantContext: assistantContext ?? null, assistantContext: assistantContext ?? null,
assistantTone: assistantTone ?? null assistantTone: assistantTone ?? null
}) }),
nextHouseholdName !== undefined && repository.updateHouseholdName
? repository.updateHouseholdName(input.householdId, nextHouseholdName)
: repository.getHouseholdChatByHouseholdId(input.householdId)
]) ])
if (!household) {
throw new Error('Failed to resolve household chat after settings update')
}
return { return {
status: 'ok', status: 'ok',
householdName: household.householdName,
settings, settings,
assistantConfig: nextAssistantConfig assistantConfig: nextAssistantConfig
} }

View File

@@ -213,6 +213,10 @@ export interface HouseholdConfigurationRepository {
householdId: string, householdId: string,
locale: SupportedLocale locale: SupportedLocale
): Promise<HouseholdTelegramChatRecord> ): Promise<HouseholdTelegramChatRecord>
updateHouseholdName?(
householdId: string,
householdName: string
): Promise<HouseholdTelegramChatRecord>
updateMemberPreferredLocale( updateMemberPreferredLocale(
householdId: string, householdId: string,
telegramUserId: string, telegramUserId: string,