diff --git a/apps/bot/src/finance-commands.test.ts b/apps/bot/src/finance-commands.test.ts index 948aa3c..1f1d81b 100644 --- a/apps/bot/src/finance-commands.test.ts +++ b/apps/bot/src/finance-commands.test.ts @@ -275,12 +275,13 @@ describe('createFinanceCommandsService', () => { await bot.handleUpdate(householdStatusUpdate('ru') as never) const payload = calls[0]?.payload as { text?: string } | undefined - expect(payload?.text).toContain('Статус дома за 2026-03') + expect(payload?.text).toContain('Статус на март 2026') expect(payload?.text).toContain('Аренда: 700.00 USD (~1890.00 GEL)') expect(payload?.text).toContain('Коммуналка: 82.00 GEL') expect(payload?.text).toContain('Общие покупки: 30.00 GEL') + expect(payload?.text).toContain('Срок оплаты аренды: до 20 марта') expect(payload?.text).toContain( - '- Стас: должен 210.00 GEL, оплачено 100.00 GEL, осталось 110.00 GEL' + '- Стас: баланс 210.00 GEL, оплачено 100.00 GEL, остаток 110.00 GEL' ) }) }) diff --git a/apps/bot/src/finance-commands.ts b/apps/bot/src/finance-commands.ts index 0f2491b..f2eb6ee 100644 --- a/apps/bot/src/finance-commands.ts +++ b/apps/bot/src/finance-commands.ts @@ -15,6 +15,51 @@ function commandArgs(ctx: Context): string[] { return raw.split(/\s+/).filter(Boolean) } +function formatBillingPeriodLabel( + locale: Parameters[0], + period: string +): string { + const [yearRaw, monthRaw] = period.split('-') + const year = Number(yearRaw) + const month = Number(monthRaw) + + if (!Number.isInteger(year) || !Number.isInteger(month) || month < 1 || month > 12) { + return period + } + + const formatter = new Intl.DateTimeFormat(locale === 'ru' ? 'ru-RU' : 'en-US', { + month: 'long', + year: 'numeric', + timeZone: 'UTC' + }) + + return formatter.format(new Date(Date.UTC(year, month - 1, 1))) +} + +function formatCycleDueDate( + locale: Parameters[0], + period: string, + dueDay: number +): string { + const [yearRaw, monthRaw] = period.split('-') + const year = Number(yearRaw) + const month = Number(monthRaw) + + if (!Number.isInteger(year) || !Number.isInteger(month) || month < 1 || month > 12) { + return period + } + + const maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate() + const day = Math.min(Math.max(dueDay, 1), maxDay) + const formatter = new Intl.DateTimeFormat(locale === 'ru' ? 'ru-RU' : 'en-US', { + day: 'numeric', + month: 'long', + timeZone: 'UTC' + }) + + return formatter.format(new Date(Date.UTC(year, month - 1, day))) +} + function isGroupChat(ctx: Context): boolean { return ctx.chat?.type === 'group' || ctx.chat?.type === 'supergroup' } @@ -42,7 +87,8 @@ export function createFinanceCommandsService(options: { function formatHouseholdStatus( locale: Parameters[0], - dashboard: NonNullable>> + dashboard: NonNullable>>, + dueDay: number ): string { const t = getBotTranslations(locale).finance const utilityTotal = dashboard.ledger @@ -66,7 +112,8 @@ export function createFinanceCommandsService(options: { ) return [ - t.householdStatusTitle(dashboard.period), + t.householdStatusTitle(formatBillingPeriodLabel(locale, dashboard.period)), + t.householdStatusDueDate(formatCycleDueDate(locale, dashboard.period, dueDay)), rentLine, t.householdStatusUtilities(utilityTotal.toMajorString(), dashboard.currency), t.householdStatusPurchases(purchaseTotal.toMajorString(), dashboard.currency), @@ -184,7 +231,11 @@ export function createFinanceCommandsService(options: { return } - await ctx.reply(formatHouseholdStatus(locale, dashboard)) + const settings = await options.householdConfigurationRepository.getHouseholdBillingSettings( + resolved.householdId + ) + + await ctx.reply(formatHouseholdStatus(locale, dashboard, settings.rentDueDay)) } catch (error) { await ctx.reply(t.statementFailed((error as Error).message)) } diff --git a/apps/bot/src/i18n/locales/en.ts b/apps/bot/src/i18n/locales/en.ts index 7bb38ae..1e787a9 100644 --- a/apps/bot/src/i18n/locales/en.ts +++ b/apps/bot/src/i18n/locales/en.ts @@ -142,15 +142,16 @@ export const enBotTranslations: BotTranslationCatalog = { paymentAddFailed: (message) => `Failed to record payment: ${message}`, noStatementCycle: 'No cycle found for statement.', householdStatusTitle: (period) => `Household status for ${period}`, + householdStatusDueDate: (dueDate) => `Rent due by ${dueDate}`, householdStatusRentDirect: (amount, currency) => `Rent: ${amount} ${currency}`, householdStatusRentConverted: (sourceAmount, sourceCurrency, displayAmount, displayCurrency) => `Rent: ${sourceAmount} ${sourceCurrency} (~${displayAmount} ${displayCurrency})`, householdStatusUtilities: (amount, currency) => `Utilities: ${amount} ${currency}`, householdStatusPurchases: (amount, currency) => `Shared purchases: ${amount} ${currency}`, - householdStatusMember: (displayName, due, paid, remaining, currency) => - `- ${displayName}: due ${due} ${currency}, paid ${paid} ${currency}, remaining ${remaining} ${currency}`, - householdStatusTotals: (due, paid, remaining, currency) => - `Totals: due ${due} ${currency}, paid ${paid} ${currency}, remaining ${remaining} ${currency}`, + householdStatusMember: (displayName, balance, paid, remaining, currency) => + `- ${displayName}: balance ${balance} ${currency}, paid ${paid} ${currency}, remaining ${remaining} ${currency}`, + householdStatusTotals: (balance, paid, remaining, currency) => + `Household total: balance ${balance} ${currency}, paid ${paid} ${currency}, remaining ${remaining} ${currency}`, statementTitle: (period) => `Statement for ${period}`, statementLine: (displayName, amount, currency) => `- ${displayName}: ${amount} ${currency}`, statementTotal: (amount, currency) => `Total: ${amount} ${currency}`, diff --git a/apps/bot/src/i18n/locales/ru.ts b/apps/bot/src/i18n/locales/ru.ts index ecd19fa..e60a04a 100644 --- a/apps/bot/src/i18n/locales/ru.ts +++ b/apps/bot/src/i18n/locales/ru.ts @@ -144,16 +144,17 @@ export const ruBotTranslations: BotTranslationCatalog = { `Оплата сохранена: ${kind === 'rent' ? 'аренда' : 'коммуналка'} ${amount} ${currency} за ${period}`, paymentAddFailed: (message) => `Не удалось сохранить оплату: ${message}`, noStatementCycle: 'Для выписки период не найден.', - householdStatusTitle: (period) => `Статус дома за ${period}`, + householdStatusTitle: (period) => `Статус на ${period}`, + householdStatusDueDate: (dueDate) => `Срок оплаты аренды: до ${dueDate}`, householdStatusRentDirect: (amount, currency) => `Аренда: ${amount} ${currency}`, householdStatusRentConverted: (sourceAmount, sourceCurrency, displayAmount, displayCurrency) => `Аренда: ${sourceAmount} ${sourceCurrency} (~${displayAmount} ${displayCurrency})`, householdStatusUtilities: (amount, currency) => `Коммуналка: ${amount} ${currency}`, householdStatusPurchases: (amount, currency) => `Общие покупки: ${amount} ${currency}`, - householdStatusMember: (displayName, due, paid, remaining, currency) => - `- ${displayName}: должен ${due} ${currency}, оплачено ${paid} ${currency}, осталось ${remaining} ${currency}`, - householdStatusTotals: (due, paid, remaining, currency) => - `Итого: должен ${due} ${currency}, оплачено ${paid} ${currency}, осталось ${remaining} ${currency}`, + householdStatusMember: (displayName, balance, paid, remaining, currency) => + `- ${displayName}: баланс ${balance} ${currency}, оплачено ${paid} ${currency}, остаток ${remaining} ${currency}`, + householdStatusTotals: (balance, paid, remaining, currency) => + `Итого по дому: баланс ${balance} ${currency}, оплачено ${paid} ${currency}, остаток ${remaining} ${currency}`, statementTitle: (period) => `Выписка за ${period}`, statementLine: (displayName, amount, currency) => `- ${displayName}: ${amount} ${currency}`, statementTotal: (amount, currency) => `Итого: ${amount} ${currency}`, diff --git a/apps/bot/src/i18n/types.ts b/apps/bot/src/i18n/types.ts index 58d800c..6e861b2 100644 --- a/apps/bot/src/i18n/types.ts +++ b/apps/bot/src/i18n/types.ts @@ -152,6 +152,7 @@ export interface BotTranslationCatalog { paymentAddFailed: (message: string) => string noStatementCycle: string householdStatusTitle: (period: string) => string + householdStatusDueDate: (dueDate: string) => string householdStatusRentDirect: (amount: string, currency: string) => string householdStatusRentConverted: ( sourceAmount: string, @@ -163,13 +164,13 @@ export interface BotTranslationCatalog { householdStatusPurchases: (amount: string, currency: string) => string householdStatusMember: ( displayName: string, - due: string, + balance: string, paid: string, remaining: string, currency: string ) => string householdStatusTotals: ( - due: string, + balance: string, paid: string, remaining: string, currency: string