finance-telegram-bot/app/categories.py
2026-07-14 12:21:12 +07:00

129 lines
6.8 KiB
Python
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.

from __future__ import annotations
import re
from app.domain import Category
CATEGORY_META: dict[Category, tuple[str, str]] = {
Category.PRODUCTS: ("🛒", "Продукты"),
Category.ENTERTAINMENT: ("🎉", "Развлечения"),
Category.CAR: ("🚗", "Машина"),
Category.CREDITS: ("🏦", "Кредиты"),
Category.RENOVATION: ("🛠", "Ремонт"),
Category.OTHER: ("📦", "Другое"),
Category.INCOME: ("💚", "Доход"),
}
# Сначала проверяются более специфичные слова. Список легко расширять без миграции БД.
CATEGORY_KEYWORDS: dict[Category, tuple[str, ...]] = {
Category.PRODUCTS: (
"ярче", "мария ра", "пятерочка", "пятёрочка", "магнит", "лента",
"ашан", "metro", "продукт", "еда", "молоко", "хлеб", "мясо",
"овощ", "фрукт", "кофе", "кафе", "ресторан", "доставка еды",
"алкоголь", "пиво", "вино", "супермаркет",
),
Category.ENTERTAINMENT: (
"кино", "театр", "концерт", "бар", "клуб", "игра", "steam",
"подписка", "музей", "развлеч", "боулинг", "караоке", "хобби",
),
Category.CAR: (
"бензин", "топливо", "азс", "газпромнефть", "лукойл", "g-drive",
"шиномонтаж", "автосервис", "мойка", "запчаст", "масло", "осаго",
"парковка", "штраф гибдд", "машин", "авто",
),
Category.CREDITS: (
"кредит", "ипотек", "долг", "рассроч", "процент банку", "заём", "займ",
),
Category.RENOVATION: (
"ремонт квартир", "стройматериал", "обои", "краска", "ламинат",
"сантехник", "электрик", "мебель", "инструмент", "стройка", "леруа", "ремонт",
),
}
def normalize(text: str) -> str:
text = text.casefold().replace("ё", "е")
return re.sub(r"[^a-zа-я0-9]+", " ", text).strip()
def classify(description: str) -> Category:
normalized_text = normalize(description)
normalized = f" {normalized_text} "
words = normalized_text.split()
for category, keywords in CATEGORY_KEYWORDS.items():
for keyword in keywords:
normalized_keyword = normalize(keyword)
is_phrase = " " in normalized_keyword
exact_phrase = is_phrase and f" {normalized_keyword} " in normalized
word_match = not is_phrase and any(
word == normalized_keyword
or (len(normalized_keyword) >= 4 and word.startswith(normalized_keyword))
for word in words
)
if exact_phrase or word_match:
return category
return Category.OTHER
def category_label(category: Category) -> str:
icon, name = CATEGORY_META[category]
return f"{icon} {name}"
EXPENSE_CATEGORIES = tuple(category for category in Category if category is not Category.INCOME)
# system_key, icon, visible name, keywords. These are seeded for every family budget.
DEFAULT_SUBCATEGORIES: dict[Category, tuple[tuple[str, str, str, tuple[str, ...]], ...]] = {
Category.PRODUCTS: (
("products_food", "🥦", "Еда", ("продукт", "еда", "ярче", "магнит", "пятер", "лента", "молоко", "хлеб", "мясо", "овощ", "фрукт")),
("products_alcohol", "🍷", "Алкоголь", ("алкоголь", "пиво", "вино", "водка")),
("products_cafe", "", "Кафе и рестораны", ("кафе", "ресторан", "кофе", "доставка еды")),
("products_household", "🧻", "Бытовые товары", ("бытовая химия", "хозтовар", "порошок", "салфетк")),
),
Category.ENTERTAINMENT: (
("entertainment_cinema", "🎬", "Кино и театр", ("кино", "театр", "концерт", "музей")),
("entertainment_games", "🎮", "Игры", ("игра", "steam", "playstation", "xbox")),
("entertainment_subscriptions", "📺", "Подписки", ("подписка", "кинопоиск", "ivi", "netflix")),
("entertainment_hobby", "🎨", "Хобби", ("хобби", "боулинг", "караоке")),
),
Category.CAR: (
("car_fuel", "", "Бензин", ("бензин", "топливо", "азс", "газпромнефть", "лукойл", "g drive")),
("car_repair", "🔧", "Ремонт", ("автосервис", "ремонт машины", "ремонт авто", "шиномонтаж")),
("car_parts", "⚙️", "Запчасти", ("запчаст", "масло", "аккумулятор", "шина", "резина")),
("car_service", "🅿️", "Мойка и парковка", ("мойка", "парковка")),
("car_insurance", "🛡", "Страховка", ("осаго", "каско", "страховка авто")),
),
Category.CREDITS: (
("credits_mortgage", "🏠", "Ипотека", ("ипотек",)),
("credits_bank", "💳", "Кредиты", ("кредит", "процент банку")),
("credits_installment", "📆", "Рассрочки", ("рассроч",)),
),
Category.RENOVATION: (
("renovation_materials", "🧱", "Материалы", ("стройматериал", "обои", "краска", "ламинат", "леруа")),
("renovation_furniture", "🪑", "Мебель", ("мебель", "диван", "шкаф", "стол")),
("renovation_work", "👷", "Работы", ("сантехник", "электрик", "ремонт квартир", "стройка")),
("renovation_tools", "🧰", "Инструменты", ("инструмент", "дрель", "шуруповерт")),
),
Category.OTHER: (),
}
def suggest_subcategory(category: Category, description: str) -> str | None:
normalized_text = normalize(description)
words = normalized_text.split()
padded = f" {normalized_text} "
for system_key, _icon, _name, keywords in DEFAULT_SUBCATEGORIES.get(category, ()):
for keyword in keywords:
normalized_keyword = normalize(keyword)
if " " in normalized_keyword:
if f" {normalized_keyword} " in padded:
return system_key
elif any(
word == normalized_keyword
or (len(normalized_keyword) >= 4 and word.startswith(normalized_keyword))
for word in words
):
return system_key
return None