forked from retoor/devplacepy
feat: add telegram notification channel with outbox service and per-user preferences
Extend the notification system with a third channel (telegram) alongside existing in_app and push channels. Add `telegram_enabled` column to `notification_preferences` table, update `NOTIFICATION_CHANNELS` and `_NOTIFICATION_CHANNEL_COLUMNS` mappings, and set telegram default to off (`_NOTIFICATION_CHANNEL_DEFAULTS`). Create `telegram_outbox` table with columns for uid, user_uid, chat_id, text, status, attempts, created_at, and sent_at, plus an index on status/id for efficient polling. Register `TelegramOutboxService` in the service manager lifecycle. Update API documentation strings to describe the new channel and its pairing requirement. Extend admin notification defaults view to include telegram column. Pass `notif_telegram_paired` flag to profile template based on `telegram_store.is_paired()` check. Update `NotificationPrefForm` and `NotificationDefaultForm` model literals to accept "telegram" as a valid channel value.
This commit is contained in:
@@ -98,3 +98,62 @@ def test_daily_quests_deterministic_count_and_kinds():
|
||||
assert len(first) == economy.DAILY_QUEST_COUNT
|
||||
assert all(quest["kind"] in economy.QUEST_DEFS for quest in first)
|
||||
assert all(quest["goal"] > 0 for quest in first)
|
||||
|
||||
|
||||
def test_farm_score_empty_is_zero():
|
||||
assert economy.farm_score({}) == 0
|
||||
|
||||
|
||||
def test_farm_score_counts_every_factor():
|
||||
score = economy.farm_score(
|
||||
{
|
||||
"xp": 100,
|
||||
"prestige": 2,
|
||||
"total_harvests": 3,
|
||||
"coins": 200,
|
||||
"ci_tier": 3,
|
||||
"plot_count": economy.STARTING_PLOTS + 2,
|
||||
"perk_yield": 1,
|
||||
"perk_growth": 1,
|
||||
"perk_discount": 1,
|
||||
"perk_xp": 1,
|
||||
"streak": 4,
|
||||
}
|
||||
)
|
||||
expected = (
|
||||
100
|
||||
+ 2 * economy.SCORE_PRESTIGE
|
||||
+ 3 * economy.SCORE_HARVEST
|
||||
+ 200 // economy.SCORE_COIN_DIVISOR
|
||||
+ (3 - 1) * economy.SCORE_CI
|
||||
+ 2 * economy.SCORE_PLOT
|
||||
+ 4 * economy.SCORE_PERK
|
||||
+ 4 * economy.SCORE_STREAK
|
||||
)
|
||||
assert score == expected
|
||||
|
||||
|
||||
def test_farm_score_prestige_is_additive_and_substantial():
|
||||
base = {"xp": 800, "total_harvests": 5}
|
||||
with_prestige = {**base, "prestige": 1}
|
||||
gain = economy.farm_score(with_prestige) - economy.farm_score(base)
|
||||
assert gain == economy.SCORE_PRESTIGE
|
||||
assert gain >= economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL)
|
||||
|
||||
|
||||
def test_farm_score_prestige_lifts_above_lower_level_player():
|
||||
refactorer = {"xp": 200, "prestige": 1}
|
||||
higher_level_no_prestige = {"xp": economy.xp_threshold(5)}
|
||||
assert economy.farm_score(refactorer) > economy.farm_score(higher_level_no_prestige)
|
||||
|
||||
|
||||
def test_farm_score_streak_is_capped():
|
||||
capped = economy.farm_score({"streak": economy.SCORE_STREAK_CAP})
|
||||
over = economy.farm_score({"streak": economy.SCORE_STREAK_CAP + 50})
|
||||
assert capped == over
|
||||
|
||||
|
||||
def test_farm_score_handles_legacy_null_columns():
|
||||
assert economy.farm_score(
|
||||
{"xp": 50, "prestige": None, "perk_yield": None, "streak": None}
|
||||
) == 50
|
||||
|
||||
@@ -526,7 +526,7 @@ def test_serialize_handles_legacy_row_without_new_columns(local_db):
|
||||
# --- leaderboard -------------------------------------------------------------
|
||||
|
||||
|
||||
def test_leaderboard_orders_by_level_then_xp(local_db):
|
||||
def test_leaderboard_orders_by_score(local_db):
|
||||
high = _reset("unit_lb_high")
|
||||
low = _reset("unit_lb_low")
|
||||
_set(high, level=8, xp=economy.xp_threshold(8))
|
||||
@@ -535,3 +535,23 @@ def test_leaderboard_orders_by_level_then_xp(local_db):
|
||||
names = [entry["username"] for entry in board]
|
||||
assert "unit_lb_high" in names and "unit_lb_low" in names
|
||||
assert names.index("unit_lb_high") < names.index("unit_lb_low")
|
||||
|
||||
|
||||
def test_leaderboard_prestige_lifts_above_lower_level(local_db):
|
||||
refactorer = _reset("unit_lb_refactor")
|
||||
grinder = _reset("unit_lb_grind")
|
||||
_set(refactorer, level=1, xp=200, prestige=1)
|
||||
_set(grinder, level=5, xp=economy.xp_threshold(5))
|
||||
board = store.leaderboard(50)
|
||||
names = [entry["username"] for entry in board]
|
||||
assert names.index("unit_lb_refactor") < names.index("unit_lb_grind")
|
||||
|
||||
|
||||
def test_leaderboard_entry_exposes_score_and_prestige(local_db):
|
||||
user = _reset("unit_lb_fields")
|
||||
_set(user, prestige=3, total_harvests=12, ci_tier=2)
|
||||
entry = next(e for e in store.leaderboard(50) if e["username"] == "unit_lb_fields")
|
||||
farm = store.get_farm(user["uid"])
|
||||
assert entry["prestige"] == 3
|
||||
assert entry["score"] == economy.farm_score(farm)
|
||||
assert {"rank", "username", "level", "xp", "coins", "total_harvests", "prestige", "score"} <= set(entry)
|
||||
|
||||
Reference in New Issue
Block a user