Яички снова стали пасхальным событием, а не круглогодичной валютой
Окно теперь считается само: с 1 апреля по православную Пасху (в 2026 году — 12 апреля) вместо зашитых дат 2025 года, из-за которых команда не работала вовсе. Вне окна /яички отвечает «рано» или «событие завершилось», а строка про яички не показывается ни в /инфо, ни в карточке админки. Если понадобится другое окно, даты фиксируются через EGG_EVENT_START/EGG_EVENT_END.
This commit is contained in:
parent
107a79d05e
commit
6b15d8ae04
|
|
@ -53,6 +53,11 @@ ANALYTICS_BUFFER_MAX=5000
|
||||||
# Правка в админке применяется без перезапуска в течение этого времени.
|
# Правка в админке применяется без перезапуска в течение этого времени.
|
||||||
TEMPLATE_CACHE_TTL=45
|
TEMPLATE_CACHE_TTL=45
|
||||||
|
|
||||||
|
# Пасхальное событие «Яички». Пусто — автоматически 1 апреля → Православная Пасха.
|
||||||
|
# Можно задать вручную (ISO): EGG_EVENT_START=2027-04-01, EGG_EVENT_END=2027-05-02.
|
||||||
|
EGG_EVENT_START=
|
||||||
|
EGG_EVENT_END=
|
||||||
|
|
||||||
# Админка (FastAPI, Блок 4). Порт на loopback хоста для docker compose.
|
# Админка (FastAPI, Блок 4). Порт на loopback хоста для docker compose.
|
||||||
ADMIN_HTTP_PORT=8000
|
ADMIN_HTTP_PORT=8000
|
||||||
ADMIN_BIND_IP=127.0.0.1
|
ADMIN_BIND_IP=127.0.0.1
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -145,7 +145,7 @@ make backup E=test # на домашнем сервере: ./scripts/clickho
|
||||||
|
|
||||||
## Версионирование
|
## Версионирование
|
||||||
|
|
||||||
- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.4.8`.
|
- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.4.9`.
|
||||||
- Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`).
|
- Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`).
|
||||||
- Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3).
|
- Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3).
|
||||||
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.
|
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.
|
||||||
|
|
@ -174,6 +174,14 @@ git tag -a vX.Y -m "Release vX.Y"
|
||||||
git push origin master --tags
|
git push origin master --tags
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Пасхальное событие «Яички»
|
||||||
|
|
||||||
|
`/яички` работает только в сезон: по умолчанию с **1 апреля по православную Пасху**
|
||||||
|
(в 2026 году — 12 апреля). Вне окна бот отвечает, что ещё рано, а строка про яички
|
||||||
|
не показывается ни в `/инфо`, ни в карточке пользователя в админке.
|
||||||
|
Даты можно зафиксировать переменными `EGG_EVENT_START`/`EGG_EVENT_END` (ISO,
|
||||||
|
например `2027-04-01` и `2027-05-02`).
|
||||||
|
|
||||||
## Команды бота
|
## Команды бота
|
||||||
|
|
||||||
`/ник`, `/список`, `/кто`, `/инфо`, `/помощь`, `/очищение`, `/яички`,
|
`/ник`, `/список`, `/кто`, `/инфо`, `/помощь`, `/очищение`, `/яички`,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ SAMPLE_CONTEXT: dict[str, Any] = {
|
||||||
"dt": "01.01.2026",
|
"dt": "01.01.2026",
|
||||||
"msg_count": 42,
|
"msg_count": 42,
|
||||||
"eggs": 2,
|
"eggs": 2,
|
||||||
|
"eggs_line": "<b>🥚 Количество яичек:</b> 2",
|
||||||
"h": "12",
|
"h": "12",
|
||||||
"m": "05",
|
"m": "05",
|
||||||
"s": "09",
|
"s": "09",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
|
||||||
from admin.security import check_csrf
|
from admin.security import check_csrf
|
||||||
from common import __version__
|
from common import __version__
|
||||||
|
from common.eggs import is_egg_event_active
|
||||||
from common.repositories import users_admin_repo
|
from common.repositories import users_admin_repo
|
||||||
|
|
||||||
router = APIRouter(prefix="/users", tags=["users"])
|
router = APIRouter(prefix="/users", tags=["users"])
|
||||||
|
|
@ -44,6 +45,7 @@ async def _card_response(
|
||||||
"history": await users_admin_repo.user_history(user_id),
|
"history": await users_admin_repo.user_history(user_id),
|
||||||
"errors": errors or [],
|
"errors": errors or [],
|
||||||
"saved": saved,
|
"saved": saved,
|
||||||
|
"eggs_visible": is_egg_event_active(request.app.state.settings),
|
||||||
"max_nick_length": users_admin_repo.MAX_NICK_LENGTH,
|
"max_nick_length": users_admin_repo.MAX_NICK_LENGTH,
|
||||||
"max_comment_length": users_admin_repo.MAX_COMMENT_LENGTH,
|
"max_comment_length": users_admin_repo.MAX_COMMENT_LENGTH,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -58,10 +58,12 @@
|
||||||
<div class="text-xs uppercase tracking-wide text-slate-500">Баланс DE</div>
|
<div class="text-xs uppercase tracking-wide text-slate-500">Баланс DE</div>
|
||||||
<div class="mt-1 text-xl font-semibold">{{ user.balance }}</div>
|
<div class="mt-1 text-xl font-semibold">{{ user.balance }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if eggs_visible %}
|
||||||
<div class="rounded-lg border border-slate-200 bg-white p-4">
|
<div class="rounded-lg border border-slate-200 bg-white p-4">
|
||||||
<div class="text-xs uppercase tracking-wide text-slate-500">Яички</div>
|
<div class="text-xs uppercase tracking-wide text-slate-500">Яички</div>
|
||||||
<div class="mt-1 text-xl font-semibold">{{ user.eggs }}</div>
|
<div class="mt-1 text-xl font-semibold">{{ user.eggs }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="rounded-lg border border-slate-200 bg-white p-4">
|
<div class="rounded-lg border border-slate-200 bg-white p-4">
|
||||||
<div class="text-xs uppercase tracking-wide text-slate-500">Сообщения</div>
|
<div class="text-xs uppercase tracking-wide text-slate-500">Сообщения</div>
|
||||||
<div class="mt-1 text-xl font-semibold">{{ user.msg_count }}</div>
|
<div class="mt-1 text-xl font-semibold">{{ user.msg_count }}</div>
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,9 @@ from aiogram.types import Message
|
||||||
from bot.services import access, stats, subscriptions
|
from bot.services import access, stats, subscriptions
|
||||||
from bot.services import users as users_service
|
from bot.services import users as users_service
|
||||||
from common.config import get_settings
|
from common.config import get_settings
|
||||||
|
from common.eggs import is_egg_event_active
|
||||||
from common.texts import system
|
from common.texts import system
|
||||||
from common.texts.render import format_tops, format_user_list, render
|
from common.texts.render import escape, format_tops, format_user_list, render
|
||||||
from common.texts.store import get_text
|
from common.texts.store import get_text
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -108,6 +109,9 @@ async def info_handler(message: Message) -> None:
|
||||||
"dt": user_info.dt,
|
"dt": user_info.dt,
|
||||||
"msg_count": user_info.msg_count,
|
"msg_count": user_info.msg_count,
|
||||||
"eggs": user_info.eggs,
|
"eggs": user_info.eggs,
|
||||||
|
"eggs_line": (
|
||||||
|
f"<b>🥚 Количество яичек:</b> {escape(user_info.eggs)}" if is_egg_event_active(get_settings()) else ""
|
||||||
|
),
|
||||||
}
|
}
|
||||||
await message.answer(render(await get_text("info.profile"), context))
|
await message.answer(render(await get_text("info.profile"), context))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import random
|
import random
|
||||||
from datetime import date, datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from aiogram import Router
|
from aiogram import Router
|
||||||
from aiogram.filters import Command
|
from aiogram.filters import Command
|
||||||
|
|
@ -12,16 +12,13 @@ from aiogram.types import Message
|
||||||
from bot.services import access, alerts
|
from bot.services import access, alerts
|
||||||
from bot.services import economy as economy_service
|
from bot.services import economy as economy_service
|
||||||
from common.config import get_settings
|
from common.config import get_settings
|
||||||
|
from common.eggs import egg_event_period, is_egg_event_active
|
||||||
from common.texts import system
|
from common.texts import system
|
||||||
from common.texts.render import render
|
from common.texts.render import render
|
||||||
from common.texts.store import get_text
|
from common.texts.store import get_text
|
||||||
|
|
||||||
router = Router(name="economy")
|
router = Router(name="economy")
|
||||||
|
|
||||||
#: Окно сезонного события «яички».
|
|
||||||
EGG_EVENT_START = date(2025, 4, 1)
|
|
||||||
EGG_EVENT_END = date(2025, 4, 20)
|
|
||||||
|
|
||||||
#: Шанс выпадения монетки на сообщение.
|
#: Шанс выпадения монетки на сообщение.
|
||||||
COIN_CHANCE = 27 # из 100
|
COIN_CHANCE = 27 # из 100
|
||||||
|
|
||||||
|
|
@ -48,9 +45,11 @@ async def purification_handler(message: Message) -> None:
|
||||||
|
|
||||||
@router.message(Command("яички", ignore_case=True))
|
@router.message(Command("яички", ignore_case=True))
|
||||||
async def eggs_handler(message: Message) -> None:
|
async def eggs_handler(message: Message) -> None:
|
||||||
today = datetime.now(get_settings().tzinfo).date()
|
settings = get_settings()
|
||||||
if not (EGG_EVENT_START <= today <= EGG_EVENT_END):
|
if not is_egg_event_active(settings):
|
||||||
await message.answer(system.MSG_EGGS_TOO_EARLY)
|
today = datetime.now(settings.tzinfo).date()
|
||||||
|
start, _ = egg_event_period(settings, today)
|
||||||
|
await message.answer(system.MSG_EGGS_TOO_EARLY if today < start else system.MSG_EGGS_EVENT_OVER)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not await access.check_user(message):
|
if not await access.check_user(message):
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
@ -70,6 +71,10 @@ class Settings(BaseSettings):
|
||||||
#: применяются без перезапуска). 0 — читать БД при каждом обращении.
|
#: применяются без перезапуска). 0 — читать БД при каждом обращении.
|
||||||
template_cache_ttl: float = 45.0
|
template_cache_ttl: float = 45.0
|
||||||
|
|
||||||
|
#: Окно пасхального события «Яички». Пусто — автоматически 1 апреля → Православная Пасха.
|
||||||
|
egg_event_start: date | None = None
|
||||||
|
egg_event_end: date | None = None
|
||||||
|
|
||||||
@field_validator("chat_id", "alert_chat_id", mode="before")
|
@field_validator("chat_id", "alert_chat_id", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def _empty_int_to_zero(cls, value: object) -> object:
|
def _empty_int_to_zero(cls, value: object) -> object:
|
||||||
|
|
@ -78,6 +83,14 @@ class Settings(BaseSettings):
|
||||||
return 0
|
return 0
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@field_validator("egg_event_start", "egg_event_end", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def _empty_date_to_none(cls, value: object) -> object:
|
||||||
|
"""Пустое значение в .env — «даты не заданы» (работает авто-расчёт)."""
|
||||||
|
if value is None or value == "":
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
@field_validator("admin_ids", mode="before")
|
@field_validator("admin_ids", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def _parse_admin_ids(cls, value: object) -> object:
|
def _parse_admin_ids(cls, value: object) -> object:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
"""Пасхальное событие «Яички» (Блок 4: сезонность).
|
||||||
|
|
||||||
|
По умолчанию окно — с 1 апреля по православную Пасху включительно (как было
|
||||||
|
в исходном боте: 01.04–20.04.2025). Даты можно переопределить переменными
|
||||||
|
``EGG_EVENT_START``/``EGG_EVENT_END`` (например, для другого года или правила).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
|
||||||
|
from common.config import Settings
|
||||||
|
|
||||||
|
#: Разница между юлианским и григорианским календарями для 1900–2099.
|
||||||
|
_JULIAN_TO_GREGORIAN_DAYS = 13
|
||||||
|
|
||||||
|
|
||||||
|
def orthodox_easter(year: int) -> date:
|
||||||
|
"""Дата православной Пасхи (по юлианской пасхалии, переведённая в григорианский стиль)."""
|
||||||
|
a = year % 4
|
||||||
|
b = year % 7
|
||||||
|
c = year % 19
|
||||||
|
d = (19 * c + 15) % 30
|
||||||
|
e = (2 * a + 4 * b - d + 34) % 7
|
||||||
|
month = (d + e + 114) // 31
|
||||||
|
day = ((d + e + 114) % 31) + 1
|
||||||
|
return date(year, month, day) + timedelta(days=_JULIAN_TO_GREGORIAN_DAYS)
|
||||||
|
|
||||||
|
|
||||||
|
def egg_event_period(settings: Settings, today: date) -> tuple[date, date]:
|
||||||
|
"""Начало и конец окна «яичек» (включительно).
|
||||||
|
|
||||||
|
Если в настройках заданы обе даты — берём их, иначе 1 апреля → православная
|
||||||
|
Пасха года ``today``.
|
||||||
|
"""
|
||||||
|
if settings.egg_event_start and settings.egg_event_end:
|
||||||
|
return settings.egg_event_start, settings.egg_event_end
|
||||||
|
return date(today.year, 4, 1), orthodox_easter(today.year)
|
||||||
|
|
||||||
|
|
||||||
|
def is_egg_event_active(settings: Settings, today: date | None = None) -> bool:
|
||||||
|
"""Идёт ли сейчас пасхальное событие (в таймзоне настроек)."""
|
||||||
|
if today is None:
|
||||||
|
today = datetime.now(settings.tzinfo).date()
|
||||||
|
start, end = egg_event_period(settings, today)
|
||||||
|
return start <= today <= end
|
||||||
|
|
@ -9,7 +9,7 @@ MSG_INFO = """<b>👤Информация о пользователе {username}
|
||||||
<b>💰 Деньги:</b> {money}
|
<b>💰 Деньги:</b> {money}
|
||||||
<b>📈 Начало активности в боте:</b> {dt}
|
<b>📈 Начало активности в боте:</b> {dt}
|
||||||
<b>📊 Количество сообщений в чате:</b> {msg_count}
|
<b>📊 Количество сообщений в чате:</b> {msg_count}
|
||||||
<b>🥚 Количество яичек:</b> {eggs}
|
{eggs_line}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MSG_HELP = """<b><u>Список команд бота</u></b>
|
MSG_HELP = """<b><u>Список команд бота</u></b>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ from typing import Any
|
||||||
_TAG_RE = re.compile(r"\{([a-z_][a-z0-9_]*)\}")
|
_TAG_RE = re.compile(r"\{([a-z_][a-z0-9_]*)\}")
|
||||||
|
|
||||||
#: Теги, чьи значения подставляются «как есть» (формируются helpers-функциями ниже).
|
#: Теги, чьи значения подставляются «как есть» (формируются helpers-функциями ниже).
|
||||||
RAW_TAGS: frozenset[str] = frozenset({"usernames", "users_list", "users_list_count", "tops"})
|
RAW_TAGS: frozenset[str] = frozenset({"usernames", "users_list", "users_list_count", "tops", "eggs_line"})
|
||||||
|
|
||||||
#: Полный реестр поддерживаемых тегов: имя -> короткое описание.
|
#: Полный реестр поддерживаемых тегов: имя -> короткое описание.
|
||||||
TAG_REGISTRY: dict[str, str] = {
|
TAG_REGISTRY: dict[str, str] = {
|
||||||
|
|
@ -32,6 +32,7 @@ TAG_REGISTRY: dict[str, str] = {
|
||||||
"dt": "Дата начала активности",
|
"dt": "Дата начала активности",
|
||||||
"msg_count": "Количество сообщений",
|
"msg_count": "Количество сообщений",
|
||||||
"eggs": "Количество яичек",
|
"eggs": "Количество яичек",
|
||||||
|
"eggs_line": "Готовая строка про яички (пусто вне пасхального события)",
|
||||||
"h": "Часы до следующего действия",
|
"h": "Часы до следующего действия",
|
||||||
"m": "Минуты до следующего действия",
|
"m": "Минуты до следующего действия",
|
||||||
"s": "Секунды до следующего действия",
|
"s": "Секунды до следующего действия",
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ MSG_PURCHASE_FAIL = "Не получилось купить :с"
|
||||||
MSG_DONAT_PROMO = "Назови этот код админам!\n`{promo}`"
|
MSG_DONAT_PROMO = "Назови этот код админам!\n`{promo}`"
|
||||||
|
|
||||||
MSG_EGGS_TOO_EARLY = "Слишком рано..."
|
MSG_EGGS_TOO_EARLY = "Слишком рано..."
|
||||||
|
MSG_EGGS_EVENT_OVER = "Событие с яичками уже завершилось — увидимся на следующую Пасху."
|
||||||
|
|
||||||
MSG_BOT_ADDED = "Я пришёл! Требую админские права!"
|
MSG_BOT_ADDED = "Я пришёл! Требую админские права!"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,4 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
__version__ = "0.4.8"
|
__version__ = "0.4.9"
|
||||||
|
|
|
||||||
|
|
@ -54,3 +54,7 @@ ADMIN_BIND_IP=127.0.0.1
|
||||||
|
|
||||||
# Шаблоны сообщений: TTL кеша бота (правки из админки без перезапуска).
|
# Шаблоны сообщений: TTL кеша бота (правки из админки без перезапуска).
|
||||||
TEMPLATE_CACHE_TTL=45
|
TEMPLATE_CACHE_TTL=45
|
||||||
|
|
||||||
|
# Пасхальное событие «Яички»: пусто — 1 апреля → Православная Пасха.
|
||||||
|
EGG_EVENT_START=
|
||||||
|
EGG_EVENT_END=
|
||||||
|
|
|
||||||
|
|
@ -52,3 +52,7 @@ ADMIN_BIND_IP=127.0.0.1
|
||||||
|
|
||||||
# Шаблоны сообщений: TTL кеша бота (правки из админки без перезапуска).
|
# Шаблоны сообщений: TTL кеша бота (правки из админки без перезапуска).
|
||||||
TEMPLATE_CACHE_TTL=45
|
TEMPLATE_CACHE_TTL=45
|
||||||
|
|
||||||
|
# Пасхальное событие «Яички»: пусто — 1 апреля → Православная Пасха.
|
||||||
|
EGG_EVENT_START=
|
||||||
|
EGG_EVENT_END=
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ VALUES
|
||||||
<b>💰 Деньги:</b> {money}
|
<b>💰 Деньги:</b> {money}
|
||||||
<b>📈 Начало активности в боте:</b> {dt}
|
<b>📈 Начало активности в боте:</b> {dt}
|
||||||
<b>📊 Количество сообщений в чате:</b> {msg_count}
|
<b>📊 Количество сообщений в чате:</b> {msg_count}
|
||||||
<b>🥚 Количество яичек:</b> {eggs}
|
{eggs_line}
|
||||||
$tpl$, 'Профиль (/инфо)'),
|
$tpl$, 'Профиль (/инфо)'),
|
||||||
('info.help', $tpl$<b><u>Список команд бота</u></b>
|
('info.help', $tpl$<b><u>Список команд бота</u></b>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, date, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -157,6 +157,20 @@ def test_user_card_404(fake_repo: dict[str, Any], client_factory) -> None:
|
||||||
assert client_factory().get("/users/99").status_code == 404
|
assert client_factory().get("/users/99").status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_eggs_hidden_outside_event(fake_repo: dict[str, Any], client_factory) -> None:
|
||||||
|
response = client_factory().get("/users/42")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Яички" not in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_eggs_visible_during_event(fake_repo: dict[str, Any], client_factory, settings_factory) -> None:
|
||||||
|
today = date.today()
|
||||||
|
client = client_factory(settings=settings_factory(egg_event_start=today, egg_event_end=today))
|
||||||
|
|
||||||
|
assert "Яички" in client.get("/users/42").text
|
||||||
|
|
||||||
|
|
||||||
def test_update_nick(fake_repo: dict[str, Any], client_factory, csrf_token) -> None:
|
def test_update_nick(fake_repo: dict[str, Any], client_factory, csrf_token) -> None:
|
||||||
client: TestClient = client_factory()
|
client: TestClient = client_factory()
|
||||||
token = _csrf(client, csrf_token)
|
token = _csrf(client, csrf_token)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
"""Тесты сезонности «яичек»: Пасха, окно события и переопределение дат."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from common.config import Settings
|
||||||
|
from common.eggs import egg_event_period, is_egg_event_active, orthodox_easter
|
||||||
|
|
||||||
|
|
||||||
|
def _settings(**env: str) -> Settings:
|
||||||
|
return Settings(_env_file=None, **{"BOT_TOKEN": "1:a", **env})
|
||||||
|
|
||||||
|
|
||||||
|
def test_orthodox_easter_known_dates() -> None:
|
||||||
|
assert orthodox_easter(2024) == date(2024, 5, 5)
|
||||||
|
assert orthodox_easter(2025) == date(2025, 4, 20)
|
||||||
|
assert orthodox_easter(2026) == date(2026, 4, 12)
|
||||||
|
assert orthodox_easter(2027) == date(2027, 5, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_period_is_april_first_to_easter() -> None:
|
||||||
|
settings = _settings()
|
||||||
|
|
||||||
|
assert egg_event_period(settings, date(2026, 2, 1)) == (date(2026, 4, 1), date(2026, 4, 12))
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_boundaries() -> None:
|
||||||
|
settings = _settings()
|
||||||
|
|
||||||
|
assert not is_egg_event_active(settings, date(2026, 3, 31))
|
||||||
|
assert is_egg_event_active(settings, date(2026, 4, 1))
|
||||||
|
assert is_egg_event_active(settings, date(2026, 4, 12))
|
||||||
|
assert not is_egg_event_active(settings, date(2026, 4, 13))
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_dates_can_be_overridden() -> None:
|
||||||
|
settings = _settings(EGG_EVENT_START="2026-04-05", EGG_EVENT_END="2026-04-10")
|
||||||
|
|
||||||
|
assert egg_event_period(settings, date(2026, 4, 1)) == (date(2026, 4, 5), date(2026, 4, 10))
|
||||||
|
assert not is_egg_event_active(settings, date(2026, 4, 4))
|
||||||
|
assert is_egg_event_active(settings, date(2026, 4, 6))
|
||||||
|
assert not is_egg_event_active(settings, date(2026, 4, 11))
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_dates_mean_auto() -> None:
|
||||||
|
settings = _settings(EGG_EVENT_START="", EGG_EVENT_END="")
|
||||||
|
|
||||||
|
assert settings.egg_event_start is None
|
||||||
|
assert settings.egg_event_end is None
|
||||||
|
assert is_egg_event_active(settings, date(2026, 4, 2))
|
||||||
|
|
@ -7,4 +7,4 @@ from common import __version__
|
||||||
|
|
||||||
def test_version_is_set():
|
def test_version_is_set():
|
||||||
assert __version__
|
assert __version__
|
||||||
assert __version__ == "0.4.8"
|
assert __version__ == "0.4.9"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue