mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 14:34:02 +00:00
feat(member): improve assistant roster awareness
This commit is contained in:
@@ -220,6 +220,7 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit
|
||||
rentShareWeight: 1,
|
||||
isAdmin: false
|
||||
}),
|
||||
updateHouseholdMemberDisplayName: async () => null,
|
||||
getHouseholdBillingSettings: async (householdId) => ({
|
||||
householdId,
|
||||
settlementCurrency: 'GEL',
|
||||
|
||||
@@ -131,6 +131,7 @@ function createRepository(isAdmin = false): HouseholdConfigurationRepository {
|
||||
throw new Error('not implemented')
|
||||
},
|
||||
updateMemberPreferredLocale: async () => null,
|
||||
updateHouseholdMemberDisplayName: async () => null,
|
||||
promoteHouseholdAdmin: async () => null,
|
||||
updateHouseholdMemberRentShareWeight: async () => null,
|
||||
updateHouseholdMemberStatus: async () => null,
|
||||
|
||||
@@ -157,7 +157,41 @@ function createHouseholdRepository(): HouseholdConfigurationRepository {
|
||||
rentShareWeight: 1,
|
||||
isAdmin: true
|
||||
}),
|
||||
listHouseholdMembers: async () => [],
|
||||
listHouseholdMembers: async () => [
|
||||
{
|
||||
id: 'member-1',
|
||||
householdId: 'household-1',
|
||||
telegramUserId: '123456',
|
||||
displayName: 'Stan',
|
||||
status: 'active',
|
||||
preferredLocale: null,
|
||||
householdDefaultLocale: 'en',
|
||||
rentShareWeight: 1,
|
||||
isAdmin: true
|
||||
},
|
||||
{
|
||||
id: 'member-2',
|
||||
householdId: 'household-1',
|
||||
telegramUserId: '222222',
|
||||
displayName: 'Dima',
|
||||
status: 'active',
|
||||
preferredLocale: null,
|
||||
householdDefaultLocale: 'en',
|
||||
rentShareWeight: 1,
|
||||
isAdmin: false
|
||||
},
|
||||
{
|
||||
id: 'member-3',
|
||||
householdId: 'household-1',
|
||||
telegramUserId: '333333',
|
||||
displayName: 'Chorbanaut',
|
||||
status: 'away',
|
||||
preferredLocale: null,
|
||||
householdDefaultLocale: 'en',
|
||||
rentShareWeight: 1,
|
||||
isAdmin: false
|
||||
}
|
||||
],
|
||||
getHouseholdBillingSettings: async () => ({
|
||||
householdId: 'household-1',
|
||||
settlementCurrency: 'GEL',
|
||||
@@ -193,6 +227,7 @@ function createHouseholdRepository(): HouseholdConfigurationRepository {
|
||||
approvePendingHouseholdMember: async () => null,
|
||||
updateHouseholdDefaultLocale: async () => household,
|
||||
updateMemberPreferredLocale: async () => null,
|
||||
updateHouseholdMemberDisplayName: async () => null,
|
||||
promoteHouseholdAdmin: async () => null,
|
||||
updateHouseholdMemberRentShareWeight: async () => null,
|
||||
updateHouseholdMemberStatus: async () => null,
|
||||
@@ -266,6 +301,28 @@ function createFinanceService(): FinanceCommandService {
|
||||
paid: Money.fromMajor('500.00', 'GEL'),
|
||||
remaining: Money.fromMajor('350.00', 'GEL'),
|
||||
explanations: []
|
||||
},
|
||||
{
|
||||
memberId: 'member-2',
|
||||
displayName: 'Dima',
|
||||
rentShare: Money.fromMajor('700.00', 'GEL'),
|
||||
utilityShare: Money.fromMajor('100.00', 'GEL'),
|
||||
purchaseOffset: Money.fromMajor('15.00', 'GEL'),
|
||||
netDue: Money.fromMajor('815.00', 'GEL'),
|
||||
paid: Money.fromMajor('200.00', 'GEL'),
|
||||
remaining: Money.fromMajor('615.00', 'GEL'),
|
||||
explanations: []
|
||||
},
|
||||
{
|
||||
memberId: 'member-3',
|
||||
displayName: 'Chorbanaut',
|
||||
rentShare: Money.fromMajor('700.00', 'GEL'),
|
||||
utilityShare: Money.fromMajor('0.00', 'GEL'),
|
||||
purchaseOffset: Money.fromMajor('-20.00', 'GEL'),
|
||||
netDue: Money.fromMajor('680.00', 'GEL'),
|
||||
paid: Money.fromMajor('100.00', 'GEL'),
|
||||
remaining: Money.fromMajor('580.00', 'GEL'),
|
||||
explanations: []
|
||||
}
|
||||
],
|
||||
ledger: [
|
||||
@@ -731,6 +788,87 @@ describe('registerDmAssistant', () => {
|
||||
expect(replyText).toContain('Suggested payment under utilities adjustment: 150.00 GEL')
|
||||
})
|
||||
|
||||
test('answers household roster questions from real member data', async () => {
|
||||
const bot = createTestBot()
|
||||
const calls: Array<{ method: string; payload: unknown }> = []
|
||||
|
||||
bot.api.config.use(async (_prev, method, payload) => {
|
||||
calls.push({ method, payload })
|
||||
return {
|
||||
ok: true,
|
||||
result: true
|
||||
} as never
|
||||
})
|
||||
|
||||
registerDmAssistant({
|
||||
bot,
|
||||
householdConfigurationRepository: createHouseholdRepository(),
|
||||
promptRepository: createPromptRepository(),
|
||||
financeServiceForHousehold: () => createFinanceService(),
|
||||
memoryStore: createInMemoryAssistantConversationMemoryStore(12),
|
||||
rateLimiter: createInMemoryAssistantRateLimiter({
|
||||
burstLimit: 5,
|
||||
burstWindowMs: 60_000,
|
||||
rollingLimit: 50,
|
||||
rollingWindowMs: 86_400_000
|
||||
}),
|
||||
usageTracker: createInMemoryAssistantUsageTracker()
|
||||
})
|
||||
|
||||
await bot.handleUpdate(privateMessageUpdate('Who do we have in the household?') as never)
|
||||
|
||||
const replyText = String(
|
||||
(
|
||||
calls.find((call) => call.method === 'sendMessage')?.payload as
|
||||
| { text?: unknown }
|
||||
| undefined
|
||||
)?.text ?? ''
|
||||
)
|
||||
expect(replyText).toContain('Current household members:')
|
||||
expect(replyText).toContain('Stan (active)')
|
||||
expect(replyText).toContain('Dima (active)')
|
||||
expect(replyText).toContain('Chorbanaut (away)')
|
||||
})
|
||||
|
||||
test('answers another member purchase balance from real data', async () => {
|
||||
const bot = createTestBot()
|
||||
const calls: Array<{ method: string; payload: unknown }> = []
|
||||
|
||||
bot.api.config.use(async (_prev, method, payload) => {
|
||||
calls.push({ method, payload })
|
||||
return {
|
||||
ok: true,
|
||||
result: true
|
||||
} as never
|
||||
})
|
||||
|
||||
registerDmAssistant({
|
||||
bot,
|
||||
householdConfigurationRepository: createHouseholdRepository(),
|
||||
promptRepository: createPromptRepository(),
|
||||
financeServiceForHousehold: () => createFinanceService(),
|
||||
memoryStore: createInMemoryAssistantConversationMemoryStore(12),
|
||||
rateLimiter: createInMemoryAssistantRateLimiter({
|
||||
burstLimit: 5,
|
||||
burstWindowMs: 60_000,
|
||||
rollingLimit: 50,
|
||||
rollingWindowMs: 86_400_000
|
||||
}),
|
||||
usageTracker: createInMemoryAssistantUsageTracker()
|
||||
})
|
||||
|
||||
await bot.handleUpdate(privateMessageUpdate('What is Dima shared purchase balance?') as never)
|
||||
|
||||
const replyText = String(
|
||||
(
|
||||
calls.find((call) => call.method === 'sendMessage')?.payload as
|
||||
| { text?: unknown }
|
||||
| undefined
|
||||
)?.text ?? ''
|
||||
)
|
||||
expect(replyText).toContain("Dima's shared purchase balance is 15.00 GEL.")
|
||||
})
|
||||
|
||||
test('routes obvious purchase-like DMs into purchase confirmation flow', async () => {
|
||||
const bot = createTestBot()
|
||||
const calls: Array<{ method: string; payload: unknown }> = []
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
maybeCreatePaymentProposal,
|
||||
parsePaymentProposalPayload
|
||||
} from './payment-proposals'
|
||||
import { maybeCreateMemberInsightReply } from './member-queries'
|
||||
import type {
|
||||
PurchaseMessageIngestionRepository,
|
||||
PurchaseProposalActionResult,
|
||||
@@ -439,10 +440,11 @@ async function buildHouseholdContext(input: {
|
||||
householdConfigurationRepository: HouseholdConfigurationRepository
|
||||
financeService: FinanceCommandService
|
||||
}): Promise<string> {
|
||||
const [household, settings, dashboard] = await Promise.all([
|
||||
const [household, settings, dashboard, members] = await Promise.all([
|
||||
input.householdConfigurationRepository.getHouseholdChatByHouseholdId(input.householdId),
|
||||
input.householdConfigurationRepository.getHouseholdBillingSettings(input.householdId),
|
||||
input.financeService.generateDashboard()
|
||||
input.financeService.generateDashboard(),
|
||||
input.householdConfigurationRepository.listHouseholdMembers(input.householdId)
|
||||
])
|
||||
|
||||
const lines = [
|
||||
@@ -491,6 +493,20 @@ async function buildHouseholdContext(input: {
|
||||
)
|
||||
}
|
||||
|
||||
if (members.length > 0) {
|
||||
const memberLines = members.map((member) => {
|
||||
const dashboardMember = dashboard.members.find((line) => line.memberId === member.id)
|
||||
|
||||
if (!dashboardMember) {
|
||||
return `- ${member.displayName}: status=${member.status}, dashboard_line=missing`
|
||||
}
|
||||
|
||||
return `- ${member.displayName}: status=${member.status}, rent=${dashboardMember.rentShare.toMajorString()} ${dashboard.currency}, utilities=${dashboardMember.utilityShare.toMajorString()} ${dashboard.currency}, purchases=${dashboardMember.purchaseOffset.toMajorString()} ${dashboard.currency}, remaining=${dashboardMember.remaining.toMajorString()} ${dashboard.currency}`
|
||||
})
|
||||
|
||||
lines.push(`Household roster and balances:\n${memberLines.join('\n')}`)
|
||||
}
|
||||
|
||||
lines.push(
|
||||
`Household total remaining: ${dashboard.totalRemaining.toMajorString()} ${dashboard.currency}`
|
||||
)
|
||||
@@ -1044,6 +1060,30 @@ export function registerDmAssistant(options: {
|
||||
return
|
||||
}
|
||||
|
||||
const memberInsightReply = await maybeCreateMemberInsightReply({
|
||||
rawText: ctx.msg.text,
|
||||
locale,
|
||||
householdId: member.householdId,
|
||||
currentMemberId: member.id,
|
||||
householdConfigurationRepository: options.householdConfigurationRepository,
|
||||
financeService,
|
||||
recentTurns: options.memoryStore.get(memoryKey).turns
|
||||
})
|
||||
|
||||
if (memberInsightReply) {
|
||||
options.memoryStore.appendTurn(memoryKey, {
|
||||
role: 'user',
|
||||
text: ctx.msg.text
|
||||
})
|
||||
options.memoryStore.appendTurn(memoryKey, {
|
||||
role: 'assistant',
|
||||
text: memberInsightReply
|
||||
})
|
||||
|
||||
await ctx.reply(memberInsightReply)
|
||||
return
|
||||
}
|
||||
|
||||
const paymentProposal = await maybeCreatePaymentProposal({
|
||||
rawText: ctx.msg.text,
|
||||
householdId: member.householdId,
|
||||
@@ -1203,6 +1243,11 @@ export function registerDmAssistant(options: {
|
||||
|
||||
try {
|
||||
const financeService = options.financeServiceForHousehold(household.householdId)
|
||||
const memoryKey = conversationMemoryKey({
|
||||
telegramUserId,
|
||||
telegramChatId,
|
||||
isPrivateChat: false
|
||||
})
|
||||
const paymentBalanceReply = await maybeCreatePaymentBalanceReply({
|
||||
rawText: mention.strippedText,
|
||||
householdId: household.householdId,
|
||||
@@ -1216,6 +1261,30 @@ export function registerDmAssistant(options: {
|
||||
return
|
||||
}
|
||||
|
||||
const memberInsightReply = await maybeCreateMemberInsightReply({
|
||||
rawText: mention.strippedText,
|
||||
locale,
|
||||
householdId: household.householdId,
|
||||
currentMemberId: member.id,
|
||||
householdConfigurationRepository: options.householdConfigurationRepository,
|
||||
financeService,
|
||||
recentTurns: options.memoryStore.get(memoryKey).turns
|
||||
})
|
||||
|
||||
if (memberInsightReply) {
|
||||
options.memoryStore.appendTurn(memoryKey, {
|
||||
role: 'user',
|
||||
text: mention.strippedText
|
||||
})
|
||||
options.memoryStore.appendTurn(memoryKey, {
|
||||
role: 'assistant',
|
||||
text: memberInsightReply
|
||||
})
|
||||
|
||||
await ctx.reply(memberInsightReply)
|
||||
return
|
||||
}
|
||||
|
||||
await replyWithAssistant({
|
||||
ctx,
|
||||
assistant: options.assistant,
|
||||
|
||||
@@ -109,6 +109,7 @@ function createRepository(): HouseholdConfigurationRepository {
|
||||
throw new Error('not implemented')
|
||||
},
|
||||
updateMemberPreferredLocale: async () => null,
|
||||
updateHouseholdMemberDisplayName: async () => null,
|
||||
promoteHouseholdAdmin: async () => null,
|
||||
updateHouseholdMemberRentShareWeight: async () => null,
|
||||
updateHouseholdMemberStatus: async () => null,
|
||||
|
||||
@@ -489,6 +489,9 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit
|
||||
}
|
||||
: null
|
||||
},
|
||||
async updateHouseholdMemberDisplayName() {
|
||||
return null
|
||||
},
|
||||
async promoteHouseholdAdmin() {
|
||||
return null
|
||||
},
|
||||
|
||||
@@ -49,7 +49,9 @@ import {
|
||||
createMiniAppPendingMembersHandler,
|
||||
createMiniAppPromoteMemberHandler,
|
||||
createMiniAppSettingsHandler,
|
||||
createMiniAppUpdateMemberDisplayNameHandler,
|
||||
createMiniAppUpdateMemberAbsencePolicyHandler,
|
||||
createMiniAppUpdateOwnDisplayNameHandler,
|
||||
createMiniAppUpdateMemberStatusHandler,
|
||||
createMiniAppUpdateMemberRentWeightHandler,
|
||||
createMiniAppUpdateSettingsHandler,
|
||||
@@ -529,6 +531,24 @@ const server = createBotWebhookServer({
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdateOwnDisplayName: householdOnboardingService
|
||||
? createMiniAppUpdateOwnDisplayNameHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdateMemberDisplayName: householdOnboardingService
|
||||
? createMiniAppUpdateMemberDisplayNameHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
botToken: runtime.telegramBotToken,
|
||||
onboardingService: householdOnboardingService,
|
||||
miniAppAdminService: miniAppAdminService!,
|
||||
logger: getLogger('miniapp-admin')
|
||||
})
|
||||
: undefined,
|
||||
miniAppUpdateMemberRentWeight: householdOnboardingService
|
||||
? createMiniAppUpdateMemberRentWeightHandler({
|
||||
allowedOrigins: runtime.miniAppAllowedOrigins,
|
||||
|
||||
304
apps/bot/src/member-queries.ts
Normal file
304
apps/bot/src/member-queries.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
import type { FinanceCommandService } from '@household/application'
|
||||
import type {
|
||||
HouseholdConfigurationRepository,
|
||||
HouseholdMemberLifecycleStatus,
|
||||
HouseholdMemberRecord
|
||||
} from '@household/ports'
|
||||
|
||||
import type { BotLocale } from './i18n'
|
||||
|
||||
type MemberBalanceMetric = 'purchase' | 'utilities' | 'rent'
|
||||
|
||||
const ROSTER_PATTERNS = [
|
||||
/\b(who do we have|who is in|members|member list|roster)\b/i,
|
||||
/кто у нас/i,
|
||||
/кто.*(в доме|в household|в домохозяйстве)/i,
|
||||
/участник/i,
|
||||
/состав/i
|
||||
] as const
|
||||
|
||||
const PURCHASE_PATTERNS = [
|
||||
/\b(purchase|purchases|shared purchase|shared purchases|common purchases?)\b/i,
|
||||
/покупк/i
|
||||
] as const
|
||||
|
||||
const UTILITIES_PATTERNS = [
|
||||
/\b(utilities|utility|gas|water|electricity|internet)\b/i,
|
||||
/коммун/i,
|
||||
/газ/i,
|
||||
/вод/i,
|
||||
/свет/i,
|
||||
/элект/i,
|
||||
/интернет/i
|
||||
] as const
|
||||
|
||||
const RENT_PATTERNS = [/\b(rent|landlord|apartment)\b/i, /аренд/i, /жиль[её]/i] as const
|
||||
const SELF_PATTERNS = [/\b(i|my|me)\b/i, /\bя\b/i, /\bмне\b/i, /\bмой\b/i, /\bмоя\b/i] as const
|
||||
const QUESTION_PATTERNS = [
|
||||
/\?/,
|
||||
/\b(how much|what|who|which|and)\b/i,
|
||||
/сколько/i,
|
||||
/кто/i,
|
||||
/како/i,
|
||||
/\bу\b/i
|
||||
] as const
|
||||
|
||||
function hasMatch(patterns: readonly RegExp[], value: string): boolean {
|
||||
return patterns.some((pattern) => pattern.test(value))
|
||||
}
|
||||
|
||||
function normalizeText(value: string): string {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/['’]s\b/g, '')
|
||||
.replace(/[^\p{L}\p{N}\s]/gu, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function aliasVariants(token: string): string[] {
|
||||
const aliases = new Set<string>([token])
|
||||
|
||||
if (token.endsWith('а') && token.length > 2) {
|
||||
aliases.add(`${token.slice(0, -1)}ы`)
|
||||
aliases.add(`${token.slice(0, -1)}е`)
|
||||
aliases.add(`${token.slice(0, -1)}у`)
|
||||
}
|
||||
|
||||
if (token.endsWith('я') && token.length > 2) {
|
||||
aliases.add(`${token.slice(0, -1)}и`)
|
||||
aliases.add(`${token.slice(0, -1)}ю`)
|
||||
}
|
||||
|
||||
return [...aliases]
|
||||
}
|
||||
|
||||
function memberAliases(member: HouseholdMemberRecord): string[] {
|
||||
const normalized = normalizeText(member.displayName)
|
||||
const tokens = normalized.split(' ').filter((token) => token.length >= 2)
|
||||
const aliases = new Set<string>([normalized, ...tokens])
|
||||
|
||||
for (const token of tokens) {
|
||||
for (const alias of aliasVariants(token)) {
|
||||
aliases.add(alias)
|
||||
}
|
||||
}
|
||||
|
||||
return [...aliases]
|
||||
}
|
||||
|
||||
function inferMetric(
|
||||
rawText: string,
|
||||
recentTurns: readonly { role: 'user' | 'assistant'; text: string }[]
|
||||
) {
|
||||
if (hasMatch(PURCHASE_PATTERNS, rawText)) {
|
||||
return 'purchase'
|
||||
}
|
||||
|
||||
if (hasMatch(UTILITIES_PATTERNS, rawText)) {
|
||||
return 'utilities'
|
||||
}
|
||||
|
||||
if (hasMatch(RENT_PATTERNS, rawText)) {
|
||||
return 'rent'
|
||||
}
|
||||
|
||||
const lastUserTurn = [...recentTurns].reverse().find((turn) => turn.role === 'user')
|
||||
if (!lastUserTurn) {
|
||||
return null
|
||||
}
|
||||
|
||||
return inferMetric(lastUserTurn.text, [])
|
||||
}
|
||||
|
||||
function resolveTargetMember(input: {
|
||||
rawText: string
|
||||
currentMemberId: string
|
||||
members: readonly HouseholdMemberRecord[]
|
||||
}): HouseholdMemberRecord | null {
|
||||
if (hasMatch(SELF_PATTERNS, input.rawText)) {
|
||||
return input.members.find((member) => member.id === input.currentMemberId) ?? null
|
||||
}
|
||||
|
||||
const normalizedText = ` ${normalizeText(input.rawText)} `
|
||||
const candidates = input.members
|
||||
.map((member) => ({
|
||||
member,
|
||||
score: memberAliases(member).reduce((best, alias) => {
|
||||
const normalizedAlias = alias.trim()
|
||||
if (normalizedAlias.length < 2) {
|
||||
return best
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedText.includes(` ${normalizedAlias} `) ||
|
||||
normalizedText.endsWith(` ${normalizedAlias}`) ||
|
||||
normalizedText.startsWith(`${normalizedAlias} `)
|
||||
) {
|
||||
return Math.max(best, normalizedAlias.length + 10)
|
||||
}
|
||||
|
||||
if (normalizedAlias.length >= 3 && normalizedText.includes(normalizedAlias)) {
|
||||
return Math.max(best, normalizedAlias.length)
|
||||
}
|
||||
|
||||
return best
|
||||
}, 0)
|
||||
}))
|
||||
.filter((entry) => entry.score > 0)
|
||||
.sort((left, right) => right.score - left.score)
|
||||
|
||||
if (candidates[0]) {
|
||||
return candidates[0].member
|
||||
}
|
||||
|
||||
return input.members.find((member) => member.id === input.currentMemberId) ?? null
|
||||
}
|
||||
|
||||
function formatStatus(locale: BotLocale, status: HouseholdMemberLifecycleStatus): string {
|
||||
if (locale === 'ru') {
|
||||
switch (status) {
|
||||
case 'away':
|
||||
return 'в отъезде'
|
||||
case 'left':
|
||||
return 'выехал'
|
||||
default:
|
||||
return 'активен'
|
||||
}
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case 'away':
|
||||
return 'away'
|
||||
case 'left':
|
||||
return 'left'
|
||||
default:
|
||||
return 'active'
|
||||
}
|
||||
}
|
||||
|
||||
function rosterReply(locale: BotLocale, members: readonly HouseholdMemberRecord[]): string {
|
||||
const lines = members.map(
|
||||
(member) => `- ${member.displayName} (${formatStatus(locale, member.status)})`
|
||||
)
|
||||
|
||||
if (locale === 'ru') {
|
||||
return `У нас в household сейчас:\n${lines.join('\n')}`
|
||||
}
|
||||
|
||||
return `Current household members:\n${lines.join('\n')}`
|
||||
}
|
||||
|
||||
function memberMetricReply(input: {
|
||||
locale: BotLocale
|
||||
metric: MemberBalanceMetric
|
||||
targetMember: HouseholdMemberRecord
|
||||
currentMemberId: string
|
||||
currency: 'GEL' | 'USD'
|
||||
values: {
|
||||
purchase: string
|
||||
utilities: string
|
||||
rent: string
|
||||
}
|
||||
}): string {
|
||||
const isCurrentMember = input.targetMember.id === input.currentMemberId
|
||||
|
||||
if (input.locale === 'ru') {
|
||||
switch (input.metric) {
|
||||
case 'purchase':
|
||||
return isCurrentMember
|
||||
? `Твой баланс по общим покупкам: ${input.values.purchase} ${input.currency}.`
|
||||
: `Баланс ${input.targetMember.displayName} по общим покупкам: ${input.values.purchase} ${input.currency}.`
|
||||
case 'utilities':
|
||||
return isCurrentMember
|
||||
? `Твоя коммуналка к оплате: ${input.values.utilities} ${input.currency}.`
|
||||
: `Коммуналка ${input.targetMember.displayName} к оплате: ${input.values.utilities} ${input.currency}.`
|
||||
case 'rent':
|
||||
return isCurrentMember
|
||||
? `Твоя аренда к оплате: ${input.values.rent} ${input.currency}.`
|
||||
: `Аренда ${input.targetMember.displayName} к оплате: ${input.values.rent} ${input.currency}.`
|
||||
}
|
||||
}
|
||||
|
||||
switch (input.metric) {
|
||||
case 'purchase':
|
||||
return isCurrentMember
|
||||
? `Your shared purchase balance is ${input.values.purchase} ${input.currency}.`
|
||||
: `${input.targetMember.displayName}'s shared purchase balance is ${input.values.purchase} ${input.currency}.`
|
||||
case 'utilities':
|
||||
return isCurrentMember
|
||||
? `Your utilities due is ${input.values.utilities} ${input.currency}.`
|
||||
: `${input.targetMember.displayName}'s utilities due is ${input.values.utilities} ${input.currency}.`
|
||||
case 'rent':
|
||||
return isCurrentMember
|
||||
? `Your rent due is ${input.values.rent} ${input.currency}.`
|
||||
: `${input.targetMember.displayName}'s rent due is ${input.values.rent} ${input.currency}.`
|
||||
}
|
||||
}
|
||||
|
||||
export async function maybeCreateMemberInsightReply(input: {
|
||||
rawText: string
|
||||
locale: BotLocale
|
||||
householdId: string
|
||||
currentMemberId: string
|
||||
householdConfigurationRepository: Pick<HouseholdConfigurationRepository, 'listHouseholdMembers'>
|
||||
financeService: FinanceCommandService
|
||||
recentTurns: readonly { role: 'user' | 'assistant'; text: string }[]
|
||||
}): Promise<string | null> {
|
||||
const normalizedText = input.rawText.trim()
|
||||
if (normalizedText.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const members = await input.householdConfigurationRepository.listHouseholdMembers(
|
||||
input.householdId
|
||||
)
|
||||
if (members.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (hasMatch(ROSTER_PATTERNS, normalizedText) && hasMatch(QUESTION_PATTERNS, normalizedText)) {
|
||||
return rosterReply(input.locale, members)
|
||||
}
|
||||
|
||||
if (!hasMatch(QUESTION_PATTERNS, normalizedText)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const metric = inferMetric(normalizedText, input.recentTurns)
|
||||
if (!metric) {
|
||||
return null
|
||||
}
|
||||
|
||||
const dashboard = await input.financeService.generateDashboard()
|
||||
if (!dashboard) {
|
||||
return null
|
||||
}
|
||||
|
||||
const targetMember = resolveTargetMember({
|
||||
rawText: normalizedText,
|
||||
currentMemberId: input.currentMemberId,
|
||||
members
|
||||
})
|
||||
if (!targetMember) {
|
||||
return null
|
||||
}
|
||||
|
||||
const memberLine = dashboard.members.find((member) => member.memberId === targetMember.id)
|
||||
if (!memberLine) {
|
||||
return null
|
||||
}
|
||||
|
||||
return memberMetricReply({
|
||||
locale: input.locale,
|
||||
metric,
|
||||
targetMember,
|
||||
currentMemberId: input.currentMemberId,
|
||||
currency: dashboard.currency,
|
||||
values: {
|
||||
purchase: memberLine.purchaseOffset.toMajorString(),
|
||||
utilities: memberLine.utilityShare.toMajorString(),
|
||||
rent: memberLine.rentShare.toMajorString()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
createMiniAppPendingMembersHandler,
|
||||
createMiniAppPromoteMemberHandler,
|
||||
createMiniAppSettingsHandler,
|
||||
createMiniAppUpdateMemberDisplayNameHandler,
|
||||
createMiniAppUpdateMemberAbsencePolicyHandler,
|
||||
createMiniAppUpdateOwnDisplayNameHandler,
|
||||
createMiniAppUpdateMemberStatusHandler,
|
||||
createMiniAppUpdateSettingsHandler
|
||||
} from './miniapp-admin'
|
||||
@@ -147,6 +149,20 @@ function onboardingRepository(): HouseholdConfigurationRepository {
|
||||
isAdmin: false
|
||||
}
|
||||
: null,
|
||||
updateHouseholdMemberDisplayName: async (_householdId, memberId, displayName) =>
|
||||
memberId === 'member-123456' || memberId === 'member-555777'
|
||||
? {
|
||||
id: memberId,
|
||||
householdId: 'household-1',
|
||||
telegramUserId: memberId === 'member-555777' ? '555777' : '123456',
|
||||
displayName,
|
||||
status: 'active',
|
||||
preferredLocale: null,
|
||||
householdDefaultLocale: 'ru',
|
||||
rentShareWeight: 1,
|
||||
isAdmin: memberId === 'member-123456'
|
||||
}
|
||||
: null,
|
||||
getHouseholdBillingSettings: async (householdId) => ({
|
||||
householdId,
|
||||
settlementCurrency: 'GEL',
|
||||
@@ -620,6 +636,137 @@ describe('createMiniAppPromoteMemberHandler', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('createMiniAppUpdateOwnDisplayNameHandler', () => {
|
||||
test('updates the acting member display name for an authenticated member', async () => {
|
||||
const authDate = Math.floor(Date.now() / 1000)
|
||||
const repository = onboardingRepository()
|
||||
repository.listHouseholdMembersByTelegramUserId = async () => [
|
||||
{
|
||||
id: 'member-555777',
|
||||
householdId: 'household-1',
|
||||
telegramUserId: '555777',
|
||||
displayName: 'Mia',
|
||||
status: 'active',
|
||||
preferredLocale: null,
|
||||
householdDefaultLocale: 'ru',
|
||||
rentShareWeight: 1,
|
||||
isAdmin: false
|
||||
}
|
||||
]
|
||||
|
||||
const handler = createMiniAppUpdateOwnDisplayNameHandler({
|
||||
allowedOrigins: ['http://localhost:5173'],
|
||||
botToken: 'test-bot-token',
|
||||
onboardingService: createHouseholdOnboardingService({
|
||||
repository
|
||||
}),
|
||||
miniAppAdminService: createMiniAppAdminService(repository)
|
||||
})
|
||||
|
||||
const response = await handler.handler(
|
||||
new Request('http://localhost/api/miniapp/member/display-name', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
origin: 'http://localhost:5173',
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
initData: buildMiniAppInitData('test-bot-token', authDate, {
|
||||
id: 555777,
|
||||
first_name: 'Mia',
|
||||
username: 'mia',
|
||||
language_code: 'ru'
|
||||
}),
|
||||
displayName: 'Mia Cozy'
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.json()).toEqual({
|
||||
ok: true,
|
||||
authorized: true,
|
||||
member: {
|
||||
id: 'member-555777',
|
||||
householdId: 'household-1',
|
||||
telegramUserId: '555777',
|
||||
displayName: 'Mia Cozy',
|
||||
status: 'active',
|
||||
preferredLocale: null,
|
||||
householdDefaultLocale: 'ru',
|
||||
rentShareWeight: 1,
|
||||
isAdmin: false
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('createMiniAppUpdateMemberDisplayNameHandler', () => {
|
||||
test('updates a household member display name for an authenticated admin', async () => {
|
||||
const authDate = Math.floor(Date.now() / 1000)
|
||||
const repository = onboardingRepository()
|
||||
repository.listHouseholdMembersByTelegramUserId = async () => [
|
||||
{
|
||||
id: 'member-123456',
|
||||
householdId: 'household-1',
|
||||
telegramUserId: '123456',
|
||||
displayName: 'Stan',
|
||||
status: 'active',
|
||||
preferredLocale: null,
|
||||
householdDefaultLocale: 'ru',
|
||||
rentShareWeight: 1,
|
||||
isAdmin: true
|
||||
}
|
||||
]
|
||||
|
||||
const handler = createMiniAppUpdateMemberDisplayNameHandler({
|
||||
allowedOrigins: ['http://localhost:5173'],
|
||||
botToken: 'test-bot-token',
|
||||
onboardingService: createHouseholdOnboardingService({
|
||||
repository
|
||||
}),
|
||||
miniAppAdminService: createMiniAppAdminService(repository)
|
||||
})
|
||||
|
||||
const response = await handler.handler(
|
||||
new Request('http://localhost/api/miniapp/admin/members/display-name', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
origin: 'http://localhost:5173',
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
initData: buildMiniAppInitData('test-bot-token', authDate, {
|
||||
id: 123456,
|
||||
first_name: 'Stan',
|
||||
username: 'stanislav',
|
||||
language_code: 'ru'
|
||||
}),
|
||||
memberId: 'member-555777',
|
||||
displayName: 'Mia Cozy'
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.json()).toEqual({
|
||||
ok: true,
|
||||
authorized: true,
|
||||
member: {
|
||||
id: 'member-555777',
|
||||
householdId: 'household-1',
|
||||
telegramUserId: '555777',
|
||||
displayName: 'Mia Cozy',
|
||||
status: 'active',
|
||||
preferredLocale: null,
|
||||
householdDefaultLocale: 'ru',
|
||||
rentShareWeight: 1,
|
||||
isAdmin: false
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('createMiniAppUpdateMemberStatusHandler', () => {
|
||||
test('updates a household member status for an authenticated admin', async () => {
|
||||
const authDate = Math.floor(Date.now() / 1000)
|
||||
|
||||
@@ -199,6 +199,41 @@ async function readPromoteMemberPayload(request: Request): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
async function readDisplayNamePayload(request: Request): Promise<{
|
||||
initData: string
|
||||
displayName: string
|
||||
memberId?: string
|
||||
}> {
|
||||
const clonedRequest = request.clone()
|
||||
const payload = await readMiniAppRequestPayload(request)
|
||||
if (!payload.initData) {
|
||||
throw new Error('Missing initData')
|
||||
}
|
||||
|
||||
const text = await clonedRequest.text()
|
||||
let parsed: { memberId?: string; displayName?: string }
|
||||
try {
|
||||
parsed = JSON.parse(text)
|
||||
} catch {
|
||||
throw new Error('Invalid JSON body')
|
||||
}
|
||||
|
||||
const displayName = parsed.displayName?.trim()
|
||||
if (!displayName) {
|
||||
throw new Error('Missing displayName')
|
||||
}
|
||||
|
||||
return {
|
||||
initData: payload.initData,
|
||||
displayName,
|
||||
...(typeof parsed.memberId === 'string' && parsed.memberId.trim().length > 0
|
||||
? {
|
||||
memberId: parsed.memberId.trim()
|
||||
}
|
||||
: {})
|
||||
}
|
||||
}
|
||||
|
||||
async function readRentWeightPayload(request: Request): Promise<{
|
||||
initData: string
|
||||
memberId: string
|
||||
@@ -798,6 +833,82 @@ export function createMiniAppPromoteMemberHandler(options: {
|
||||
}
|
||||
}
|
||||
|
||||
export function createMiniAppUpdateOwnDisplayNameHandler(options: {
|
||||
allowedOrigins: readonly string[]
|
||||
botToken: string
|
||||
onboardingService: HouseholdOnboardingService
|
||||
miniAppAdminService: MiniAppAdminService
|
||||
logger?: Logger
|
||||
}): {
|
||||
handler: (request: Request) => Promise<Response>
|
||||
} {
|
||||
const sessionService = createMiniAppSessionService({
|
||||
botToken: options.botToken,
|
||||
onboardingService: options.onboardingService
|
||||
})
|
||||
|
||||
return {
|
||||
handler: async (request) => {
|
||||
const origin = allowedMiniAppOrigin(request, options.allowedOrigins)
|
||||
|
||||
if (request.method === 'OPTIONS') {
|
||||
return miniAppJsonResponse({ ok: true }, 204, origin)
|
||||
}
|
||||
|
||||
if (request.method !== 'POST') {
|
||||
return miniAppJsonResponse({ ok: false, error: 'Method Not Allowed' }, 405, origin)
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await readDisplayNamePayload(request)
|
||||
const session = await sessionService.authenticate({
|
||||
initData: payload.initData
|
||||
})
|
||||
|
||||
if (!session || !session.authorized || !session.member) {
|
||||
return miniAppJsonResponse(
|
||||
{ ok: false, error: 'Active household membership required' },
|
||||
session ? 403 : 401,
|
||||
origin
|
||||
)
|
||||
}
|
||||
|
||||
const result = await options.miniAppAdminService.updateOwnDisplayName({
|
||||
householdId: session.member.householdId,
|
||||
actorMemberId: session.member.id,
|
||||
displayName: payload.displayName
|
||||
})
|
||||
|
||||
if (result.status === 'rejected') {
|
||||
return miniAppJsonResponse(
|
||||
{
|
||||
ok: false,
|
||||
error:
|
||||
result.reason === 'invalid_display_name'
|
||||
? 'Invalid display name'
|
||||
: 'Member not found'
|
||||
},
|
||||
result.reason === 'invalid_display_name' ? 400 : 404,
|
||||
origin
|
||||
)
|
||||
}
|
||||
|
||||
return miniAppJsonResponse(
|
||||
{
|
||||
ok: true,
|
||||
authorized: true,
|
||||
member: result.member
|
||||
},
|
||||
200,
|
||||
origin
|
||||
)
|
||||
} catch (error) {
|
||||
return miniAppErrorResponse(error, origin, options.logger)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createMiniAppUpdateMemberRentWeightHandler(options: {
|
||||
allowedOrigins: readonly string[]
|
||||
botToken: string
|
||||
@@ -894,6 +1005,99 @@ export function createMiniAppUpdateMemberRentWeightHandler(options: {
|
||||
}
|
||||
}
|
||||
|
||||
export function createMiniAppUpdateMemberDisplayNameHandler(options: {
|
||||
allowedOrigins: readonly string[]
|
||||
botToken: string
|
||||
onboardingService: HouseholdOnboardingService
|
||||
miniAppAdminService: MiniAppAdminService
|
||||
logger?: Logger
|
||||
}): {
|
||||
handler: (request: Request) => Promise<Response>
|
||||
} {
|
||||
const sessionService = createMiniAppSessionService({
|
||||
botToken: options.botToken,
|
||||
onboardingService: options.onboardingService
|
||||
})
|
||||
|
||||
return {
|
||||
handler: async (request) => {
|
||||
const origin = allowedMiniAppOrigin(request, options.allowedOrigins)
|
||||
|
||||
if (request.method === 'OPTIONS') {
|
||||
return miniAppJsonResponse({ ok: true }, 204, origin)
|
||||
}
|
||||
|
||||
if (request.method !== 'POST') {
|
||||
return miniAppJsonResponse({ ok: false, error: 'Method Not Allowed' }, 405, origin)
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await readDisplayNamePayload(request)
|
||||
const session = await sessionService.authenticate({
|
||||
initData: payload.initData
|
||||
})
|
||||
|
||||
if (
|
||||
!session ||
|
||||
!session.authorized ||
|
||||
!session.member ||
|
||||
session.member.status !== 'active' ||
|
||||
!session.member.isAdmin
|
||||
) {
|
||||
return miniAppJsonResponse(
|
||||
{ ok: false, error: 'Admin access required for active household members' },
|
||||
session ? 403 : 401,
|
||||
origin
|
||||
)
|
||||
}
|
||||
|
||||
if (!payload.memberId) {
|
||||
return miniAppJsonResponse({ ok: false, error: 'Missing memberId' }, 400, origin)
|
||||
}
|
||||
|
||||
const result = await options.miniAppAdminService.updateMemberDisplayName({
|
||||
householdId: session.member.householdId,
|
||||
actorIsAdmin: session.member.isAdmin,
|
||||
memberId: payload.memberId,
|
||||
displayName: payload.displayName
|
||||
})
|
||||
|
||||
if (result.status === 'rejected') {
|
||||
return miniAppJsonResponse(
|
||||
{
|
||||
ok: false,
|
||||
error:
|
||||
result.reason === 'invalid_display_name'
|
||||
? 'Invalid display name'
|
||||
: result.reason === 'member_not_found'
|
||||
? 'Member not found'
|
||||
: 'Admin access required'
|
||||
},
|
||||
result.reason === 'invalid_display_name'
|
||||
? 400
|
||||
: result.reason === 'member_not_found'
|
||||
? 404
|
||||
: 403,
|
||||
origin
|
||||
)
|
||||
}
|
||||
|
||||
return miniAppJsonResponse(
|
||||
{
|
||||
ok: true,
|
||||
authorized: true,
|
||||
member: result.member
|
||||
},
|
||||
200,
|
||||
origin
|
||||
)
|
||||
} catch (error) {
|
||||
return miniAppErrorResponse(error, origin, options.logger)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createMiniAppUpdateMemberStatusHandler(options: {
|
||||
allowedOrigins: readonly string[]
|
||||
botToken: string
|
||||
|
||||
@@ -136,6 +136,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
|
||||
}
|
||||
: null
|
||||
},
|
||||
updateHouseholdMemberDisplayName: async () => null,
|
||||
updateHouseholdMemberRentShareWeight: async (_householdId, memberId, rentShareWeight) => {
|
||||
const member = [...members.values()].find((entry) => entry.id === memberId)
|
||||
return member
|
||||
|
||||
@@ -131,6 +131,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
|
||||
defaultLocale: locale
|
||||
}),
|
||||
updateMemberPreferredLocale: async () => null,
|
||||
updateHouseholdMemberDisplayName: async () => null,
|
||||
promoteHouseholdAdmin: async () => null,
|
||||
updateHouseholdMemberRentShareWeight: async () => null,
|
||||
updateHouseholdMemberStatus: async () => null,
|
||||
|
||||
@@ -206,6 +206,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
|
||||
defaultLocale: locale
|
||||
}),
|
||||
updateMemberPreferredLocale: async () => null,
|
||||
updateHouseholdMemberDisplayName: async () => null,
|
||||
getHouseholdBillingSettings: async (householdId) => ({
|
||||
householdId,
|
||||
settlementCurrency: 'GEL',
|
||||
|
||||
@@ -132,6 +132,7 @@ function repository(): HouseholdConfigurationRepository {
|
||||
members.set(telegramUserId, next)
|
||||
return next
|
||||
},
|
||||
updateHouseholdMemberDisplayName: async () => null,
|
||||
getHouseholdBillingSettings: async (householdId) => ({
|
||||
householdId,
|
||||
settlementCurrency: 'GEL',
|
||||
|
||||
@@ -73,6 +73,24 @@ describe('createBotWebhookServer', () => {
|
||||
}
|
||||
})
|
||||
},
|
||||
miniAppUpdateOwnDisplayName: {
|
||||
handler: async () =>
|
||||
new Response(JSON.stringify({ ok: true, authorized: true, member: {} }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json; charset=utf-8'
|
||||
}
|
||||
})
|
||||
},
|
||||
miniAppUpdateMemberDisplayName: {
|
||||
handler: async () =>
|
||||
new Response(JSON.stringify({ ok: true, authorized: true, member: {} }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json; charset=utf-8'
|
||||
}
|
||||
})
|
||||
},
|
||||
miniAppUpdateMemberRentWeight: {
|
||||
handler: async () =>
|
||||
new Response(JSON.stringify({ ok: true, authorized: true, member: {} }), {
|
||||
@@ -340,6 +358,38 @@ describe('createBotWebhookServer', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('accepts mini app own display name update request', async () => {
|
||||
const response = await server.fetch(
|
||||
new Request('http://localhost/api/miniapp/member/display-name', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ initData: 'payload' })
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.json()).toEqual({
|
||||
ok: true,
|
||||
authorized: true,
|
||||
member: {}
|
||||
})
|
||||
})
|
||||
|
||||
test('accepts mini app member display name update request', async () => {
|
||||
const response = await server.fetch(
|
||||
new Request('http://localhost/api/miniapp/admin/members/display-name', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ initData: 'payload' })
|
||||
})
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.json()).toEqual({
|
||||
ok: true,
|
||||
authorized: true,
|
||||
member: {}
|
||||
})
|
||||
})
|
||||
|
||||
test('accepts mini app rent weight update request', async () => {
|
||||
const response = await server.fetch(
|
||||
new Request('http://localhost/api/miniapp/admin/members/rent-weight', {
|
||||
|
||||
@@ -56,6 +56,18 @@ export interface BotWebhookServerOptions {
|
||||
handler: (request: Request) => Promise<Response>
|
||||
}
|
||||
| undefined
|
||||
miniAppUpdateOwnDisplayName?:
|
||||
| {
|
||||
path?: string
|
||||
handler: (request: Request) => Promise<Response>
|
||||
}
|
||||
| undefined
|
||||
miniAppUpdateMemberDisplayName?:
|
||||
| {
|
||||
path?: string
|
||||
handler: (request: Request) => Promise<Response>
|
||||
}
|
||||
| undefined
|
||||
miniAppUpdateMemberRentWeight?:
|
||||
| {
|
||||
path?: string
|
||||
@@ -196,6 +208,10 @@ export function createBotWebhookServer(options: BotWebhookServerOptions): {
|
||||
options.miniAppUpsertUtilityCategory?.path ?? '/api/miniapp/admin/utility-categories/upsert'
|
||||
const miniAppPromoteMemberPath =
|
||||
options.miniAppPromoteMember?.path ?? '/api/miniapp/admin/members/promote'
|
||||
const miniAppUpdateOwnDisplayNamePath =
|
||||
options.miniAppUpdateOwnDisplayName?.path ?? '/api/miniapp/member/display-name'
|
||||
const miniAppUpdateMemberDisplayNamePath =
|
||||
options.miniAppUpdateMemberDisplayName?.path ?? '/api/miniapp/admin/members/display-name'
|
||||
const miniAppUpdateMemberRentWeightPath =
|
||||
options.miniAppUpdateMemberRentWeight?.path ?? '/api/miniapp/admin/members/rent-weight'
|
||||
const miniAppUpdateMemberStatusPath =
|
||||
@@ -277,6 +293,17 @@ export function createBotWebhookServer(options: BotWebhookServerOptions): {
|
||||
return await options.miniAppPromoteMember.handler(request)
|
||||
}
|
||||
|
||||
if (options.miniAppUpdateOwnDisplayName && url.pathname === miniAppUpdateOwnDisplayNamePath) {
|
||||
return await options.miniAppUpdateOwnDisplayName.handler(request)
|
||||
}
|
||||
|
||||
if (
|
||||
options.miniAppUpdateMemberDisplayName &&
|
||||
url.pathname === miniAppUpdateMemberDisplayNamePath
|
||||
) {
|
||||
return await options.miniAppUpdateMemberDisplayName.handler(request)
|
||||
}
|
||||
|
||||
if (
|
||||
options.miniAppUpdateMemberRentWeight &&
|
||||
url.pathname === miniAppUpdateMemberRentWeightPath
|
||||
|
||||
Reference in New Issue
Block a user