Files
household-bot/apps/bot/src/payment-topic-ingestion.test.ts
whekin f38ee499ae feat(bot): unified topic processor replacing router+interpreter stack
Replace 3-layer architecture (gpt-5-nano router + gpt-4o-mini interpreter) with
single unified topic processor (gpt-4o-mini) for simplified message handling.

New components:
- HouseholdContextCache: TTL-based caching (5 min) for household config data
- TopicProcessor: Unified classification + parsing with structured JSON output

Key changes:
- Renamed ASSISTANT_ROUTER_MODEL → TOPIC_PROCESSOR_MODEL
- Added TOPIC_PROCESSOR_TIMEOUT_MS (default 10s)
- Refactored save() → saveWithInterpretation() for pre-parsed interpretations
- Removed deprecated createOpenAiTopicMessageRouter and ~300 lines legacy code
- Fixed typing indicator to only start when needed (purchase routes)
- Fixed amount formatting: convert minor units to major for rawText

Routes: silent, chat_reply, purchase, purchase_clarification, payment,
payment_clarification, topic_helper, dismiss_workflow

All 212 bot tests pass. Typecheck, lint, format, build clean.
2026-03-14 13:33:57 +04:00

909 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test } from 'bun:test'
import type { FinanceCommandService, PaymentConfirmationService } from '@household/application'
import { instantFromIso, Money } from '@household/domain'
import type { TelegramPendingActionRecord, TelegramPendingActionRepository } from '@household/ports'
import { createTelegramBot } from './bot'
import {
buildPaymentAcknowledgement,
registerConfiguredPaymentTopicIngestion,
resolveConfiguredPaymentTopicRecord,
type PaymentTopicCandidate
} from './payment-topic-ingestion'
import type { TopicProcessor } from './topic-processor'
function candidate(overrides: Partial<PaymentTopicCandidate> = {}): PaymentTopicCandidate {
return {
updateId: 1,
chatId: '-10012345',
messageId: '10',
threadId: '888',
senderTelegramUserId: '10002',
rawText: 'за жилье закинул',
attachmentCount: 0,
messageSentAt: instantFromIso('2026-03-20T00:00:00.000Z'),
...overrides
}
}
function paymentUpdate(text: string, threadId = 888) {
return {
update_id: 1001,
message: {
message_id: 55,
date: Math.floor(Date.now() / 1000),
message_thread_id: threadId,
is_topic_message: true,
chat: {
id: -10012345,
type: 'supergroup'
},
from: {
id: 10002,
is_bot: false,
first_name: 'Mia'
},
text
}
}
}
function createHouseholdRepository() {
return {
getHouseholdChatByHouseholdId: async () => ({
householdId: 'household-1',
householdName: 'Test bot',
telegramChatId: '-10012345',
telegramChatType: 'supergroup',
title: 'Test bot',
defaultLocale: 'ru' as const
}),
findHouseholdTopicByTelegramContext: async () => ({
householdId: 'household-1',
role: 'payments' as const,
telegramThreadId: '888',
topicName: 'Быт'
}),
getHouseholdBillingSettings: async () => ({
householdId: 'household-1',
settlementCurrency: 'GEL' as const,
rentAmountMinor: 70000n,
rentCurrency: 'USD' as const,
rentDueDay: 20,
rentWarningDay: 17,
utilitiesDueDay: 4,
utilitiesReminderDay: 3,
timezone: 'Asia/Tbilisi'
})
}
}
function paymentCallbackUpdate(data: string, fromId = 10002) {
return {
update_id: 1002,
callback_query: {
id: 'callback-1',
from: {
id: fromId,
is_bot: false,
first_name: 'Mia'
},
chat_instance: 'instance-1',
data,
message: {
message_id: 77,
date: Math.floor(Date.now() / 1000),
chat: {
id: -10012345,
type: 'supergroup'
},
text: 'placeholder'
}
}
}
}
function createPromptRepository(): TelegramPendingActionRepository {
let pending: TelegramPendingActionRecord | null = null
return {
async upsertPendingAction(input) {
pending = input
return input
},
async getPendingAction() {
return pending
},
async clearPendingAction() {
pending = null
},
async clearPendingActionsForChat(telegramChatId, action) {
if (!pending || pending.telegramChatId !== telegramChatId) {
return
}
if (action && pending.action !== action) {
return
}
pending = null
}
}
}
function createFinanceService(): FinanceCommandService {
return {
getMemberByTelegramUserId: async () => ({
id: 'member-1',
telegramUserId: '10002',
displayName: 'Mia',
rentShareWeight: 1,
isAdmin: false
}),
getOpenCycle: async () => null,
ensureExpectedCycle: async () => ({
id: 'cycle-1',
period: '2026-03',
currency: 'GEL'
}),
getAdminCycleState: async () => ({
cycle: null,
rentRule: null,
utilityBills: []
}),
openCycle: async () => ({
id: 'cycle-1',
period: '2026-03',
currency: 'GEL'
}),
closeCycle: async () => null,
setRent: async () => null,
addUtilityBill: async () => null,
updateUtilityBill: async () => null,
deleteUtilityBill: async () => false,
updatePurchase: async () => null,
deletePurchase: async () => false,
addPayment: async () => null,
addPurchase: async () => ({
purchaseId: 'test-purchase',
amount: Money.fromMinor(0n, 'GEL'),
currency: 'GEL'
}),
updatePayment: async () => null,
deletePayment: async () => false,
generateDashboard: async () => ({
period: '2026-03',
currency: 'GEL',
timezone: 'Asia/Tbilisi',
rentWarningDay: 17,
rentDueDay: 20,
utilitiesReminderDay: 3,
utilitiesDueDay: 4,
paymentBalanceAdjustmentPolicy: 'utilities',
rentPaymentDestinations: null,
totalDue: Money.fromMajor('1000', 'GEL'),
totalPaid: Money.zero('GEL'),
totalRemaining: Money.fromMajor('1000', 'GEL'),
rentSourceAmount: Money.fromMajor('700', 'USD'),
rentDisplayAmount: Money.fromMajor('1890', 'GEL'),
rentFxRateMicros: null,
rentFxEffectiveDate: null,
members: [
{
memberId: 'member-1',
displayName: 'Mia',
rentShare: Money.fromMajor('472.50', 'GEL'),
utilityShare: Money.fromMajor('40', 'GEL'),
purchaseOffset: Money.fromMajor('-12', 'GEL'),
netDue: Money.fromMajor('500.50', 'GEL'),
paid: Money.zero('GEL'),
remaining: Money.fromMajor('500.50', 'GEL'),
explanations: []
}
],
ledger: []
}),
generateStatement: async () => null
}
}
function createPaymentConfirmationService(): PaymentConfirmationService & {
submitted: Array<{
rawText: string
telegramMessageId: string
telegramThreadId: string
}>
} {
return {
submitted: [],
async submit(input) {
this.submitted.push({
rawText: input.rawText,
telegramMessageId: input.telegramMessageId,
telegramThreadId: input.telegramThreadId
})
return {
status: 'recorded',
kind: 'rent',
amount: Money.fromMajor('472.50', 'GEL')
}
}
}
}
// Mock topic processor that mimics LLM responses for testing
function createMockPaymentTopicProcessor(
route: 'payment' | 'silent' | 'topic_helper' | 'payment_clarification' | 'chat_reply' = 'payment'
): TopicProcessor {
return async () => {
if (route === 'silent') {
return { route: 'silent', reason: 'test' }
}
if (route === 'topic_helper') {
return { route: 'topic_helper', reason: 'test' }
}
if (route === 'chat_reply') {
return { route: 'chat_reply', replyText: 'Hello!', reason: 'test' }
}
if (route === 'payment_clarification') {
return {
route: 'payment_clarification',
clarificationQuestion: 'What kind of payment?',
reason: 'test'
}
}
// Default to payment route
return {
route: 'payment',
kind: 'rent',
amountMinor: '47250',
currency: 'GEL',
confidence: 95,
reason: 'test'
}
}
}
describe('resolveConfiguredPaymentTopicRecord', () => {
test('returns record when the topic role is payments', () => {
const record = resolveConfiguredPaymentTopicRecord(candidate(), {
householdId: 'household-1',
role: 'payments',
telegramThreadId: '888',
topicName: 'Быт'
})
expect(record).not.toBeNull()
expect(record?.householdId).toBe('household-1')
})
test('skips non-payments topic bindings', () => {
const record = resolveConfiguredPaymentTopicRecord(candidate(), {
householdId: 'household-1',
role: 'feedback',
telegramThreadId: '888',
topicName: 'Анонимно'
})
expect(record).toBeNull()
})
test('skips slash commands in payment topics', () => {
const record = resolveConfiguredPaymentTopicRecord(candidate({ rawText: '/unsetup' }), {
householdId: 'household-1',
role: 'payments',
telegramThreadId: '888',
topicName: 'Быт'
})
expect(record).toBeNull()
})
})
describe('buildPaymentAcknowledgement', () => {
test('returns localized recorded acknowledgement', () => {
expect(
buildPaymentAcknowledgement('ru', {
status: 'recorded',
kind: 'rent',
amountMajor: '472.50',
currency: 'GEL'
})
).toBe('Оплата аренды сохранена: 472.50 GEL')
})
test('returns review acknowledgement', () => {
expect(
buildPaymentAcknowledgement('en', {
status: 'needs_review'
})
).toBeNull()
})
})
describe('registerConfiguredPaymentTopicIngestion', () => {
test('replies in-topic with a payment proposal and buttons for a likely payment', async () => {
const bot = createTelegramBot('000000:test-token')
const calls: Array<{ method: string; payload: unknown }> = []
const promptRepository = createPromptRepository()
bot.botInfo = {
id: 999000,
is_bot: true,
first_name: 'Household Test Bot',
username: 'household_test_bot',
can_join_groups: true,
can_read_all_group_messages: false,
supports_inline_queries: false,
can_connect_to_business: false,
has_main_web_app: false,
has_topics_enabled: true,
allows_users_to_create_topics: false
}
bot.api.config.use(async (_prev, method, payload) => {
calls.push({ method, payload })
return {
ok: true,
result: {
message_id: calls.length,
date: Math.floor(Date.now() / 1000),
chat: {
id: -10012345,
type: 'supergroup'
},
text: 'ok'
}
} as never
})
const paymentConfirmationService = createPaymentConfirmationService()
registerConfiguredPaymentTopicIngestion(
bot,
createHouseholdRepository() as never,
promptRepository,
() => createFinanceService(),
() => paymentConfirmationService,
{ topicProcessor: createMockPaymentTopicProcessor() }
)
await bot.handleUpdate(paymentUpdate('за жилье закинул') as never)
expect(calls).toHaveLength(1)
expect(calls[0]?.method).toBe('sendMessage')
expect(calls[0]?.payload).toMatchObject({
chat_id: -10012345,
reply_parameters: {
message_id: 55
},
text: expect.stringContaining('Я могу записать эту оплату аренды: 472.50 GEL.'),
reply_markup: {
inline_keyboard: [
[
{
text: 'Подтвердить оплату',
callback_data: expect.stringMatching(/^payment_topic:confirm:[^:]+$/)
},
{
text: 'Отменить',
callback_data: expect.stringMatching(/^payment_topic:cancel:[^:]+$/)
}
]
]
}
})
expect(await promptRepository.getPendingAction('-10012345', '10002')).toMatchObject({
action: 'payment_topic_confirmation'
})
const proposalId = (
(await promptRepository.getPendingAction('-10012345', '10002'))?.payload as {
proposalId?: string
} | null
)?.proposalId
expect(`payment_topic:confirm:${proposalId ?? ''}`.length).toBeLessThanOrEqual(64)
expect(`payment_topic:cancel:${proposalId ?? ''}`.length).toBeLessThanOrEqual(64)
})
test('asks for clarification and resolves follow-up answers in the same payments topic', async () => {
const bot = createTelegramBot('000000:test-token')
const calls: Array<{ method: string; payload: unknown }> = []
const promptRepository = createPromptRepository()
bot.botInfo = {
id: 999000,
is_bot: true,
first_name: 'Household Test Bot',
username: 'household_test_bot',
can_join_groups: true,
can_read_all_group_messages: false,
supports_inline_queries: false,
can_connect_to_business: false,
has_main_web_app: false,
has_topics_enabled: true,
allows_users_to_create_topics: false
}
bot.api.config.use(async (_prev, method, payload) => {
calls.push({ method, payload })
return {
ok: true,
result: true
} as never
})
const paymentConfirmationService = createPaymentConfirmationService()
// Smart mock that returns clarification for vague messages, payment for clear ones
const smartTopicProcessor: TopicProcessor = async (input) => {
const text = input.messageText.toLowerCase()
// Vague messages like "готово" (done) need clarification
if (text === 'готово' || text === 'done') {
return {
route: 'payment_clarification',
clarificationQuestion:
'Пока не могу подтвердить эту оплату. Уточните, это аренда или коммуналка, и при необходимости напишите сумму и валюту.',
reason: 'test'
}
}
// Messages with rent keywords can proceed as payment
return {
route: 'payment',
kind: 'rent',
amountMinor: '47250',
currency: 'GEL',
confidence: 95,
reason: 'test'
}
}
registerConfiguredPaymentTopicIngestion(
bot,
createHouseholdRepository() as never,
promptRepository,
() => createFinanceService(),
() => paymentConfirmationService,
{ topicProcessor: smartTopicProcessor }
)
await bot.handleUpdate(paymentUpdate('готово') as never)
await bot.handleUpdate(paymentUpdate('за жилье') as never)
expect(calls).toHaveLength(2)
expect(calls[0]?.payload).toMatchObject({
text: 'Пока не могу подтвердить эту оплату. Уточните, это аренда или коммуналка, и при необходимости напишите сумму и валюту.'
})
expect(calls[1]?.payload).toMatchObject({
text: expect.stringContaining('Я могу записать эту оплату аренды: 472.50 GEL.')
})
})
test('clears a pending payment confirmation when a followup has no payment intent', async () => {
const bot = createTelegramBot('000000:test-token')
const calls: Array<{ method: string; payload: unknown }> = []
const promptRepository = createPromptRepository()
bot.botInfo = {
id: 999000,
is_bot: true,
first_name: 'Household Test Bot',
username: 'household_test_bot',
can_join_groups: true,
can_read_all_group_messages: false,
supports_inline_queries: false,
can_connect_to_business: false,
has_main_web_app: false,
has_topics_enabled: true,
allows_users_to_create_topics: false
}
bot.api.config.use(async (_prev, method, payload) => {
calls.push({ method, payload })
return {
ok: true,
result: true
} as never
})
await promptRepository.upsertPendingAction({
telegramUserId: '10002',
telegramChatId: '-10012345',
action: 'payment_topic_confirmation',
payload: {
proposalId: 'proposal-1',
householdId: 'household-1',
memberId: 'member-1',
kind: 'rent',
amountMinor: '47250',
currency: 'GEL',
rawText: 'За жилье отправил',
senderTelegramUserId: '10002',
telegramChatId: '-10012345',
telegramMessageId: '55',
telegramThreadId: '888',
telegramUpdateId: '1001',
attachmentCount: 0,
messageSentAt: null
},
expiresAt: null
})
registerConfiguredPaymentTopicIngestion(
bot,
createHouseholdRepository() as never,
promptRepository,
() => createFinanceService(),
() => createPaymentConfirmationService(),
{
topicProcessor: async () => ({
route: 'dismiss_workflow',
replyText: null,
reason: 'test'
})
}
)
await bot.handleUpdate(paymentUpdate('Я уже сказал выше') as never)
expect(calls).toHaveLength(0)
expect(await promptRepository.getPendingAction('-10012345', '10002')).toBeNull()
})
test('confirms a pending payment proposal from a topic callback', async () => {
const bot = createTelegramBot('000000:test-token')
const calls: Array<{ method: string; payload: unknown }> = []
const promptRepository = createPromptRepository()
bot.botInfo = {
id: 999000,
is_bot: true,
first_name: 'Household Test Bot',
username: 'household_test_bot',
can_join_groups: true,
can_read_all_group_messages: false,
supports_inline_queries: false,
can_connect_to_business: false,
has_main_web_app: false,
has_topics_enabled: true,
allows_users_to_create_topics: false
}
bot.api.config.use(async (_prev, method, payload) => {
calls.push({ method, payload })
return {
ok: true,
result: true
} as never
})
const paymentConfirmationService = createPaymentConfirmationService()
registerConfiguredPaymentTopicIngestion(
bot,
createHouseholdRepository() as never,
promptRepository,
() => createFinanceService(),
() => paymentConfirmationService,
{ topicProcessor: createMockPaymentTopicProcessor() }
)
await bot.handleUpdate(paymentUpdate('за жилье закинул') as never)
const pending = await promptRepository.getPendingAction('-10012345', '10002')
const proposalId = (pending?.payload as { proposalId?: string } | null)?.proposalId
calls.length = 0
await bot.handleUpdate(
paymentCallbackUpdate(`payment_topic:confirm:${proposalId ?? 'missing'}`) as never
)
expect(paymentConfirmationService.submitted).toEqual([
{
rawText: 'paid rent 472.50 GEL',
telegramMessageId: '55',
telegramThreadId: '888'
}
])
expect(calls[0]).toMatchObject({
method: 'answerCallbackQuery',
payload: {
callback_query_id: 'callback-1',
text: 'Recorded rent payment: 472.50 GEL'
}
})
expect(calls[1]).toMatchObject({
method: 'editMessageText',
payload: {
chat_id: -10012345,
message_id: 77,
text: 'Recorded rent payment: 472.50 GEL'
}
})
})
test('does not reply for non-payment chatter in the payments topic', async () => {
const bot = createTelegramBot('000000:test-token')
const calls: Array<{ method: string; payload: unknown }> = []
const promptRepository = createPromptRepository()
bot.botInfo = {
id: 999000,
is_bot: true,
first_name: 'Household Test Bot',
username: 'household_test_bot',
can_join_groups: true,
can_read_all_group_messages: false,
supports_inline_queries: false,
can_connect_to_business: false,
has_main_web_app: false,
has_topics_enabled: true,
allows_users_to_create_topics: false
}
bot.api.config.use(async (_prev, method, payload) => {
calls.push({ method, payload })
return {
ok: true,
result: true
} as never
})
const paymentConfirmationService = createPaymentConfirmationService()
registerConfiguredPaymentTopicIngestion(
bot,
createHouseholdRepository() as never,
promptRepository,
() => createFinanceService(),
() => paymentConfirmationService,
{ topicProcessor: createMockPaymentTopicProcessor('silent') }
)
await bot.handleUpdate(paymentUpdate('Так так)') as never)
expect(calls).toHaveLength(0)
})
test('does not ingest slash commands sent in the payments topic', async () => {
const bot = createTelegramBot('000000:test-token')
const promptRepository = createPromptRepository()
const paymentConfirmationService = createPaymentConfirmationService()
bot.botInfo = {
id: 999000,
is_bot: true,
first_name: 'Household Test Bot',
username: 'household_test_bot',
can_join_groups: true,
can_read_all_group_messages: false,
supports_inline_queries: false,
can_connect_to_business: false,
has_main_web_app: false,
has_topics_enabled: true,
allows_users_to_create_topics: false
}
registerConfiguredPaymentTopicIngestion(
bot,
createHouseholdRepository() as never,
promptRepository,
() => createFinanceService(),
() => paymentConfirmationService,
{ topicProcessor: createMockPaymentTopicProcessor() }
)
await bot.handleUpdate(paymentUpdate('/unsetup') as never)
expect(paymentConfirmationService.submitted).toHaveLength(0)
})
test('skips explicitly tagged bot messages in the payments topic', async () => {
const bot = createTelegramBot('000000:test-token')
const calls: Array<{ method: string; payload: unknown }> = []
const promptRepository = createPromptRepository()
const paymentConfirmationService = createPaymentConfirmationService()
bot.botInfo = {
id: 999000,
is_bot: true,
first_name: 'Household Test Bot',
username: 'household_test_bot',
can_join_groups: true,
can_read_all_group_messages: false,
supports_inline_queries: false,
can_connect_to_business: false,
has_main_web_app: false,
has_topics_enabled: true,
allows_users_to_create_topics: false
}
bot.api.config.use(async (_prev, method, payload) => {
calls.push({ method, payload })
return {
ok: true,
result: true
} as never
})
registerConfiguredPaymentTopicIngestion(
bot,
createHouseholdRepository() as never,
promptRepository,
() => createFinanceService(),
() => paymentConfirmationService,
{ topicProcessor: createMockPaymentTopicProcessor('topic_helper') }
)
await bot.handleUpdate(paymentUpdate('@household_test_bot как жизнь?') as never)
expect(calls).toHaveLength(0)
expect(await promptRepository.getPendingAction('-10012345', '10002')).toBeNull()
})
test('still handles tagged payment-like messages in the payments topic', async () => {
const bot = createTelegramBot('000000:test-token')
const calls: Array<{ method: string; payload: unknown }> = []
const promptRepository = createPromptRepository()
const paymentConfirmationService = createPaymentConfirmationService()
bot.botInfo = {
id: 999000,
is_bot: true,
first_name: 'Household Test Bot',
username: 'household_test_bot',
can_join_groups: true,
can_read_all_group_messages: false,
supports_inline_queries: false,
can_connect_to_business: false,
has_main_web_app: false,
has_topics_enabled: true,
allows_users_to_create_topics: false
}
bot.api.config.use(async (_prev, method, payload) => {
calls.push({ method, payload })
return {
ok: true,
result: true
} as never
})
registerConfiguredPaymentTopicIngestion(
bot,
createHouseholdRepository() as never,
promptRepository,
() => createFinanceService(),
() => paymentConfirmationService,
{ topicProcessor: createMockPaymentTopicProcessor() }
)
await bot.handleUpdate(paymentUpdate('@household_test_bot за жилье закинул') as never)
expect(calls).toHaveLength(1)
expect(calls[0]?.payload).toMatchObject({
text: expect.stringContaining('Я могу записать эту оплату аренды: 472.50 GEL.')
})
})
test('uses router for playful addressed replies in the payments topic', async () => {
const bot = createTelegramBot('000000:test-token')
const calls: Array<{ method: string; payload: unknown }> = []
const promptRepository = createPromptRepository()
bot.botInfo = {
id: 999000,
is_bot: true,
first_name: 'Household Test Bot',
username: 'household_test_bot',
can_join_groups: true,
can_read_all_group_messages: false,
supports_inline_queries: false,
can_connect_to_business: false,
has_main_web_app: false,
has_topics_enabled: true,
allows_users_to_create_topics: false
}
bot.api.config.use(async (_prev, method, payload) => {
calls.push({ method, payload })
return {
ok: true,
result: true
} as never
})
registerConfiguredPaymentTopicIngestion(
bot,
createHouseholdRepository() as never,
promptRepository,
() => createFinanceService(),
() => createPaymentConfirmationService(),
{
topicProcessor: async () => ({
route: 'chat_reply',
replyText: 'Тут. Если это про оплату, разберёмся.',
reason: 'smalltalk'
})
}
)
await bot.handleUpdate(paymentUpdate('@household_test_bot а ты тут?') as never)
expect(calls).toHaveLength(1)
expect(calls[0]).toMatchObject({
method: 'sendMessage',
payload: {
text: 'Тут. Если это про оплату, разберёмся.'
}
})
})
test('keeps a pending payment workflow in another thread when dismissing here', async () => {
const bot = createTelegramBot('000000:test-token')
const promptRepository = createPromptRepository()
bot.botInfo = {
id: 999000,
is_bot: true,
first_name: 'Household Test Bot',
username: 'household_test_bot',
can_join_groups: true,
can_read_all_group_messages: false,
supports_inline_queries: false,
can_connect_to_business: false,
has_main_web_app: false,
has_topics_enabled: true,
allows_users_to_create_topics: false
}
bot.api.config.use(async () => {
return {
ok: true,
result: true
} as never
})
await promptRepository.upsertPendingAction({
telegramUserId: '10002',
telegramChatId: '-10012345',
action: 'payment_topic_clarification',
payload: {
threadId: '999',
rawText: 'За жилье отправил'
},
expiresAt: null
})
registerConfiguredPaymentTopicIngestion(
bot,
createHouseholdRepository() as never,
promptRepository,
() => createFinanceService(),
() => createPaymentConfirmationService(),
{
topicProcessor: async () => ({
route: 'dismiss_workflow',
replyText: 'Окей, молчу.',
reason: 'backoff'
})
}
)
await bot.handleUpdate(paymentUpdate('@household_test_bot stop', 888) as never)
expect(await promptRepository.getPendingAction('-10012345', '10002')).toMatchObject({
action: 'payment_topic_clarification',
payload: {
threadId: '999',
rawText: 'За жилье отправил'
}
})
})
})