Skip to content

Commit 3744d14

Browse files
committed
Fix search results merge logic
1 parent 386ac58 commit 3744d14

5 files changed

Lines changed: 74 additions & 14 deletions

File tree

‎bot/handlers/search.py‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ async def _do_search(message: Message, state: FSMContext, session: AsyncSession,
6161
by_title.setdefault(key, {})[region.code] = RegionPrice(
6262
game.price, game.currency, game.base_price, game.discount_text, game.ps_id
6363
)
64-
if key not in rep_game:
64+
# Prefer ASCII title so localized prefixes ("Набір", "세트" etc.) don't win
65+
if key not in rep_game or game.title.isascii():
6566
rep_game[key] = game
6667
ps_ids_by_title.setdefault(key, {})[region.code] = game.ps_id
6768

@@ -70,17 +71,14 @@ async def _do_search(message: Message, state: FSMContext, session: AsyncSession,
7071
visible_keys = set(all_keys[:_MAX_SEARCH_RESULTS])
7172

7273
# Fallback: for regions that didn't find a game by name, try fetching by ps_id.
73-
# Each ps_id is tried only once per title to avoid redundant requests.
7474
fallback_tasks: list[tuple[str, object, str]] = []
7575
for title_key, found in by_title.items():
7676
if title_key not in visible_keys:
7777
continue
78-
tried: set[str] = set()
7978
for region in user_regions:
8079
if region.code not in found:
8180
best = _best_ps_id(region.code, ps_ids_by_title[title_key])
82-
if best and best not in tried:
83-
tried.add(best)
81+
if best:
8482
fallback_tasks.append((title_key, region, best))
8583

8684
if fallback_tasks:

‎deploy/docker-compose.prod.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ services:
66
POSTGRES_USER: pricestation
77
POSTGRES_PASSWORD: pricestation
88
POSTGRES_DB: pricestation
9+
ports:
10+
- "127.0.0.1:5432:5432"
911
volumes:
1012
- postgres_data:/var/lib/postgresql/data
1113
restart: unless-stopped

‎services/ps_store.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,12 @@
3838
_WARN_STATUSES = {403, 404, 410, 429}
3939

4040

41-
# Removes punctuation, trademark symbols, CJK/Korean language tags, and whitespace
42-
# so that titles like "Foo™: Bar (Standalone)" and "Foo Bar Stand Alone" collapse
43-
# to the same key, and Asian store suffixes like "(중국어, 한국어)" are ignored.
41+
# Removes punctuation, trademark symbols, non-ASCII characters (Cyrillic, CJK,
42+
# locale prefixes like "Набір", etc.), and whitespace so that titles collapse
43+
# to the same key regardless of regional language prefix.
4444
def normalize_title(title: str) -> str:
45-
t = re.sub(r"[™®©:().,'\"!?\-]", "", title.lower())
46-
t = re.sub(r"[぀-鿿가-퟿]", "", t)
45+
t = re.sub(r"[™®©:().,'\"!?\-/]", "", title.lower())
46+
t = re.sub(r"[^\x00-\x7f]", "", t)
4747
return re.sub(r"\s+", "", t)
4848

4949

‎tests/unit/test_ps_store.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,21 @@ def test_normalize_title_strips_japanese_chars():
3131
== normalize_title("FINAL FANTASY VII")
3232
)
3333

34+
def test_normalize_title_strips_cyrillic_prefix():
35+
assert (
36+
normalize_title("Набір FINAL FANTASY VII REMAKE & REBIRTH Twin Pack")
37+
== normalize_title("FINAL FANTASY VII REMAKE & REBIRTH Twin Pack")
38+
)
39+
3440
def test_normalize_title_collapses_spaces():
3541
assert normalize_title("God of War") == normalize_title("God of War")
3642

43+
def test_normalize_title_preserves_numbers():
44+
assert normalize_title("FIFA 23") == "fifa23"
45+
46+
def test_normalize_title_numbers_across_regions():
47+
assert normalize_title("FIFA 23 (중국어, 한국어)") == normalize_title("FIFA 23")
48+
3749

3850
# --- fixtures ---
3951

‎tests/unit/test_search.py‎

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,53 @@ def test_best_ps_id_multiple_ep_returns_first():
4545
assert result.startswith("EP")
4646

4747

48+
# --- rep_game title selection ---
49+
50+
@pytest.mark.asyncio
51+
async def test_ascii_title_preferred_over_cyrillic(mocker, common_mocks):
52+
"""ASCII title from a later region should replace a non-ASCII title found first."""
53+
regions = [_region("uk-ua"), _region("en-gb")]
54+
mocker.patch("bot.handlers.search.get_user_regions", new_callable=AsyncMock, return_value=regions)
55+
56+
cyrillic_game = _make_game(EP_ID, title="Набір FINAL FANTASY VII REMAKE & REBIRTH Twin Pack")
57+
ascii_game = _make_game(EP_ID, title="FINAL FANTASY VII REMAKE & REBIRTH Twin Pack")
58+
mocker.patch("bot.handlers.search.search_games", new_callable=AsyncMock,
59+
side_effect=[[cyrillic_game], [ascii_game]])
60+
mocker.patch("bot.handlers.search.get_game_info", new_callable=AsyncMock)
61+
62+
state = AsyncMock()
63+
captured = {}
64+
state.update_data = AsyncMock(side_effect=lambda **kw: captured.update(kw))
65+
66+
await _do_search(_make_message(), state, AsyncMock(), "final fantasy")
67+
68+
entries = captured.get("entries", [])
69+
assert len(entries) == 1
70+
assert entries[0]["game"]["title"] == "FINAL FANTASY VII REMAKE & REBIRTH Twin Pack"
71+
72+
73+
@pytest.mark.asyncio
74+
async def test_non_ascii_title_kept_when_no_ascii_alternative(mocker, common_mocks):
75+
"""If only non-ASCII title exists (regional exclusive), it should still be shown."""
76+
regions = [_region("uk-ua")]
77+
mocker.patch("bot.handlers.search.get_user_regions", new_callable=AsyncMock, return_value=regions)
78+
79+
cyrillic_game = _make_game(EP_ID, title="Набір FINAL FANTASY VII REMAKE & REBIRTH Twin Pack")
80+
mocker.patch("bot.handlers.search.search_games", new_callable=AsyncMock,
81+
side_effect=[[cyrillic_game]])
82+
mocker.patch("bot.handlers.search.get_game_info", new_callable=AsyncMock)
83+
84+
state = AsyncMock()
85+
captured = {}
86+
state.update_data = AsyncMock(side_effect=lambda **kw: captured.update(kw))
87+
88+
await _do_search(_make_message(), state, AsyncMock(), "final fantasy")
89+
90+
entries = captured.get("entries", [])
91+
assert len(entries) == 1
92+
assert entries[0]["game"]["title"] == "Набір FINAL FANTASY VII REMAKE & REBIRTH Twin Pack"
93+
94+
4895
# --- fallback helpers ---
4996

5097
def _make_game(ps_id, title="Test Game", price=49.99, currency="€"):
@@ -126,8 +173,8 @@ async def test_fallback_not_fired_when_all_regions_found(mocker, common_mocks):
126173

127174

128175
@pytest.mark.asyncio
129-
async def test_fallback_deduplication(mocker, common_mocks):
130-
"""en-gb finds game (EP id); de-de and fr-fr both miss → get_game_info called once, not twice."""
176+
async def test_fallback_fires_per_region(mocker, common_mocks):
177+
"""en-gb finds game (EP id); de-de and fr-fr both miss → get_game_info called for each missing region."""
131178
regions = [_region("en-gb"), _region("de-de"), _region("fr-fr")]
132179
mocker.patch("bot.handlers.search.get_user_regions", new_callable=AsyncMock, return_value=regions)
133180

@@ -139,8 +186,9 @@ async def test_fallback_deduplication(mocker, common_mocks):
139186

140187
await _do_search(_make_message(), AsyncMock(), AsyncMock(), "test game")
141188

142-
assert mock_get_info.call_count == 1
143-
mock_get_info.assert_called_once_with(EP_ID, "de-de")
189+
assert mock_get_info.call_count == 2
190+
calls = {call.args for call in mock_get_info.call_args_list}
191+
assert calls == {(EP_ID, "de-de"), (EP_ID, "fr-fr")}
144192

145193

146194
@pytest.mark.asyncio

0 commit comments

Comments
 (0)