finance-telegram-bot/app/messages.py

212 lines
9.8 KiB
Python
Raw Permalink 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.

from __future__ import annotations
from html import escape
from app.categories import category_label
from app.domain import FamilyInfo, Totals, Transaction, TransactionKind
from app.parser import format_money
MONTHS = (
"",
"январь",
"февраль",
"март",
"апрель",
"май",
"июнь",
"июль",
"август",
"сентябрь",
"октябрь",
"ноябрь",
"декабрь",
)
WELCOME = """<b>Привет! Я помогу держать деньги под контролем 👋</b>
Просто присылай сумму и описание — я сам определю категорию:
<code>467 ярче</code>
<code>3000 бензин</code>
<code>85000 зарплата</code>
<code>-5000 рабочий закуп</code>
Обычная запись — расход; слова <b>зп</b>/<b>зарплата</b> или <b>доход</b> — поступление. Знак <b></b> принудительно уменьшает общий долг.
Каждый вечер я пришлю короткий итог дня и месяца. А в разделе «Семья» можно подключить общий бюджет с супругом. Начнём?"""
HELP_TEXT = """<b>Как записывать операции</b>
Расход:
<code>1250 продукты</code>
<code>3 500 ремонт машины</code>
<code>-5000 рабочий закуп</code> — расход, который уменьшит общий долг
Доход:
<code>75000 зарплата</code>
<code>доход 5000 подработка</code>
Можно вводить копейки: <code>199,90 кофе</code>.
После записи категорию можно изменить, а ошибочную операцию — удалить. Кнопки внизу показывают итоги и аналитику.
В семейном режиме кнопка <b>«Общий долг»</b> включает автоматический расчёт: расходы увеличивают долг, доходы уменьшают.
Через <b>«Категории»</b> можно добавить свои категории доходов и расходов и подкатегории. Кнопка «Изменить категорию» переносит любую запись, в том числе из «Другое»."""
def transaction_confirmation(transaction: Transaction, debt: int | None = None) -> str:
is_income = transaction.kind is TransactionKind.INCOME
heading = "Доход записан" if is_income else "Расход записан"
icon = "🟢" if is_income else "🔴"
signed_amount = transaction.amount_kopecks if is_income else -transaction.amount_kopecks
debt_line = f"\n\n{debt_short_text(debt)}" if debt is not None else ""
return (
f"{icon} <b>{heading}</b>\n\n"
f"<b>{format_money(signed_amount, signed=True)}</b> · {escape(transaction.description)}\n"
f"{transaction_category_text(transaction)}{debt_line}"
)
def summary_text(title: str, totals: Totals) -> str:
balance_icon = "🟢" if totals.balance >= 0 else "🔴"
return (
f"<b>{escape(title)}</b>\n\n"
f"↗️ Доходы <b>{format_money(totals.income)}</b>\n"
f"↘️ Расходы <b>{format_money(totals.expense)}</b>\n"
f"━━━━━━━━━━━━\n"
f"{balance_icon} Баланс <b>{format_money(totals.balance, signed=True)}</b>"
)
def analytics_text(period_name: str, category_totals: dict, total: int) -> str:
title = f"📊 <b>Расходы {escape(period_name)}</b>"
if total == 0:
return f"{title}\n\nПока расходов нет. Самое время записать первый 👇"
lines = [title, f"Всего: <b>{format_money(total)}</b>", ""]
for label, amount in category_totals.items():
percent = round(amount / total * 100)
filled = max(1, round(percent / 10))
bar = "" * filled + "" * (10 - filled)
lines.append(escape(label))
lines.append(f"<code>{bar}</code> {percent}% · <b>{format_money(amount)}</b>")
lines.append("")
return "\n".join(lines).rstrip()
def recent_text(transactions: list[Transaction]) -> str:
if not transactions:
return "🧾 <b>Последние записи</b>\n\nЗдесь пока пусто."
lines = ["🧾 <b>Последние записи</b>", ""]
for transaction in transactions:
is_income = transaction.kind is TransactionKind.INCOME
amount = transaction.amount_kopecks if is_income else -transaction.amount_kopecks
author = f" · {escape(transaction.author_name)}" if transaction.author_name else ""
lines.append(
f"{transaction.occurred_at:%d.%m} <b>{format_money(amount, signed=True)}</b> "
f"{escape(transaction.description)}{author}"
)
lines.append("")
lines.append("Удалить или исправить запись можно сразу после её добавления.")
return "\n".join(lines)
def family_status_text(info: FamilyInfo | None) -> str:
if info is None:
return (
"👨‍👩‍👧 <b>Семейный бюджет</b>\n\n"
"Объедините два Telegram-аккаунта. После подключения вы оба будете видеть "
"общие итоги, аналитику и новые записи в реальном времени."
)
members = "\n".join(f"{escape(name)}" for name in info.members)
waiting = (
"\n\nОтправьте код супругу. Подключение произойдёт только после вашего подтверждения."
if len(info.members) < 2
else ""
)
chat = (
f"\n\n💬 Общий чат: <b>{escape(info.notification_chat_title or 'Подключён')}</b>"
if info.notification_chat_id is not None
else (
"\n\n💬 <b>Общий чат пока не подключён.</b> Создатель бюджета должен добавить "
"бота в семейную группу, стать администратором группы и отправить там /family_chat."
)
)
return (
"👨‍👩‍👧 <b>Семейный бюджет подключён</b>\n\n"
f"<b>Участники:</b>\n{members}\n\n"
f"Код приглашения: <code>{escape(info.invite_code)}</code>"
f"{waiting}{chat}"
)
def family_transaction_notice(transaction: Transaction, debt: int | None = None) -> str:
is_income = transaction.kind is TransactionKind.INCOME
action = "добавил(а) доход" if is_income else "добавил(а) расход"
icon = "🟢" if is_income else "🔴"
amount = transaction.amount_kopecks if is_income else -transaction.amount_kopecks
author = escape(transaction.author_name or "Супруг")
debt_line = f"\n\n{debt_short_text(debt)}" if debt is not None else ""
return (
f"👨‍👩‍👧 <b>{author} {action}</b>\n\n"
f"{icon} <b>{format_money(amount, signed=True)}</b> · {escape(transaction.description)}\n"
f"{transaction_category_text(transaction)}{debt_line}"
)
def daily_report_text(day: Totals, month: Totals, debt: int | None = None) -> str:
balance_icon = "" if day.balance >= 0 else "📌"
debt_section = f"\n\n{debt_short_text(debt)}" if debt is not None else ""
return (
"🌙 <b>Итог дня</b>\n\n"
f"Потрачено сегодня: <b>{format_money(day.expense)}</b>\n"
f"Получено сегодня: <b>{format_money(day.income)}</b>\n"
f"{balance_icon} Баланс дня: <b>{format_money(day.balance, signed=True)}</b>\n\n"
"<b>С начала месяца</b>\n"
f"Расходы: <b>{format_money(month.expense)}</b>\n"
f"Доходы: <b>{format_money(month.income)}</b>\n"
f"Баланс: <b>{format_money(month.balance, signed=True)}</b>"
f"{debt_section}"
)
def debt_short_text(debt: int) -> str:
if debt > 0:
return f"💳 Общий долг: <b>{format_money(debt)}</b>"
if debt < 0:
return f"🌿 Семейный резерв: <b>{format_money(abs(debt))}</b>"
return "🎉 Общий долг: <b>погашен</b>"
def debt_status_text(debt: int | None, can_edit: bool) -> str:
if debt is None:
status = "Общий долг пока не указан."
else:
status = debt_short_text(debt)
edit_hint = (
"\n\nУкажите текущую сумму долга. С этого момента каждый расход будет её увеличивать, "
"а каждый доход — уменьшать."
if can_edit
else "\n\nИзменить исходную сумму может только создатель семейного бюджета."
)
return f"<b>💳 Общий долг</b>\n\n{status}{edit_hint}"
def transaction_category_text(transaction: Transaction) -> str:
if transaction.budget_category_name and transaction.parent_category_name:
return (
f"{escape(transaction.parent_category_icon or '📁')} "
f"{escape(transaction.parent_category_name)}"
f"{escape(transaction.budget_category_icon or '📁')} "
f"{escape(transaction.budget_category_name)}"
)
if transaction.budget_category_name:
return (
f"{escape(transaction.budget_category_icon or '📁')} "
f"{escape(transaction.budget_category_name)}"
)
return category_label(transaction.category)