Public Access
feat(rewards): add game points endpoint with reason codes
Frontend shows why a game earned no points after a status save. Settle runs first. Rules and amounts are not changed.
This commit is contained in:
1 parent
4ed8a341af
commit
7c4b475321
12 files changed
+708
-17
No files matched your search
@@ -34,6 +34,8 @@ class RewardControllerTest extends AbstractIntegrationTest {
|
||||
private static final long USER = 1L;
|
||||
private static final long ADMIN = 2L;
|
||||
private static final long GAME = 4_100_001L;
|
||||
private static final long EDITION = 4_100_002L;
|
||||
private static final long OTHER_USER = 99L;
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@@ -49,7 +51,11 @@ class RewardControllerTest extends AbstractIntegrationTest {
|
||||
jdbc.update("delete from reward_ledger where user_id in (?, ?)", USER, ADMIN);
|
||||
jdbc.update("delete from reward_equipped where user_id in (?, ?)", USER, ADMIN);
|
||||
jdbc.update("delete from user_game_playtime_day where user_id = ?", USER);
|
||||
jdbc.update("delete from user_game where user_id = ?", USER);
|
||||
jdbc.update("delete from user_game where user_id in (?, ?)", USER, OTHER_USER);
|
||||
jdbc.update("delete from user_psn_title where user_id = ?", USER);
|
||||
jdbc.update("delete from game_link where appid in (?, ?)", GAME, EDITION);
|
||||
jdbc.update("delete from game_achievement_schema_meta where appid = ?", GAME);
|
||||
jdbc.update("delete from reward_ledger where user_id = ?", OTHER_USER);
|
||||
}
|
||||
|
||||
private void game(long appid, String source, String status, int playtime) {
|
||||
@@ -117,6 +123,232 @@ class RewardControllerTest extends AbstractIntegrationTest {
|
||||
assertThat(rows("FINISHED_MANUAL")).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamePoints_ProvenSteamFinish_PaysOnTheCallAndSaysBookedNow() throws Exception {
|
||||
game(GAME, "STEAM", "BACKLOG", 120);
|
||||
jdbc.update("insert into user_game_playtime_day (user_id, day, appid, minutes) values (?, current_date, ?, 30)",
|
||||
USER, GAME);
|
||||
setStatus(GAME, "FINISHED");
|
||||
|
||||
mockMvc.perform(get("/api/rewards/games/" + GAME))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.balance").value(30))
|
||||
.andExpect(jsonPath("$.paid.length()").value(1))
|
||||
.andExpect(jsonPath("$.paid[0].kind").value("FINISHED"))
|
||||
.andExpect(jsonPath("$.paid[0].points").value(30))
|
||||
.andExpect(jsonPath("$.paid[0].bookedNow").value(true));
|
||||
}
|
||||
|
||||
private void playedToday(long appid) {
|
||||
jdbc.update("insert into user_game_playtime_day (user_id, day, appid, minutes) values (?, current_date, ?, 30)",
|
||||
USER, appid);
|
||||
}
|
||||
|
||||
private ResultActions gamePoints(long appid) throws Exception {
|
||||
return mockMvc.perform(get("/api/rewards/games/" + appid));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamePoints_SecondCall_SameRowNotBookedNow() throws Exception {
|
||||
game(GAME, "STEAM", "BACKLOG", 120);
|
||||
playedToday(GAME);
|
||||
setStatus(GAME, "FINISHED");
|
||||
gamePoints(GAME).andExpect(jsonPath("$.paid[0].bookedNow").value(true));
|
||||
|
||||
gamePoints(GAME)
|
||||
.andExpect(jsonPath("$.paid.length()").value(1))
|
||||
.andExpect(jsonPath("$.paid[0].bookedNow").value(false))
|
||||
.andExpect(jsonPath("$.balance").value(30));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamePoints_SteamFinishWithoutProof_MissingFinishIsNoProof() throws Exception {
|
||||
game(GAME, "STEAM", "BACKLOG", 120);
|
||||
setStatus(GAME, "FINISHED");
|
||||
|
||||
gamePoints(GAME)
|
||||
.andExpect(jsonPath("$.paid").isEmpty())
|
||||
.andExpect(jsonPath("$.missing.FINISHED").value("NO_PROOF"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamePoints_FourthManualFinishInAWeek_MissingFinishIsWeeklyLimit() throws Exception {
|
||||
for (long appid = -4_100_010L; appid > -4_100_014L; appid--) {
|
||||
game(appid, "MANUAL", "BACKLOG", 0);
|
||||
setStatus(appid, "FINISHED");
|
||||
}
|
||||
|
||||
gamePoints(-4_100_013L)
|
||||
.andExpect(jsonPath("$.paid").isEmpty())
|
||||
.andExpect(jsonPath("$.missing.FINISHED").value("WEEKLY_LIMIT"));
|
||||
gamePoints(-4_100_010L).andExpect(jsonPath("$.paid[0].kind").value("FINISHED_MANUAL"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamePoints_OtherEditionPaid_ListsItsRow() throws Exception {
|
||||
game(GAME, "STEAM", "BACKLOG", 120);
|
||||
game(EDITION, "STEAM", "BACKLOG", 0);
|
||||
jdbc.update("insert into game_link (appid, set_id) values (?, 4100), (?, 4100)", GAME, EDITION);
|
||||
playedToday(GAME);
|
||||
jdbc.update("update user_game set status = 'FINISHED' where user_id = ?", USER);
|
||||
|
||||
gamePoints(EDITION)
|
||||
.andExpect(jsonPath("$.paid.length()").value(1))
|
||||
.andExpect(jsonPath("$.paid[0].kind").value("FINISHED"))
|
||||
.andExpect(jsonPath("$.paid[0].appid").value(GAME))
|
||||
.andExpect(jsonPath("$.missing.FINISHED").doesNotExist());
|
||||
}
|
||||
|
||||
private void allAchievements() {
|
||||
jdbc.update("insert into game_achievement_schema_meta (appid, achievements_total, has_schema) values (?, 10, true)",
|
||||
GAME);
|
||||
jdbc.update("insert into user_game_achievement_meta (user_id, appid, achievements_unlocked) values (?, ?, 10)"
|
||||
+ " on conflict (user_id, appid) do update set achievements_unlocked = 10", USER, GAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamePoints_StartedFinishedCompleted_ListsAllThreeRowsWithDates() throws Exception {
|
||||
game(GAME, "STEAM", "BACKLOG", 120);
|
||||
playedToday(GAME);
|
||||
allAchievements();
|
||||
setStatus(GAME, "PLAYING");
|
||||
setStatus(GAME, "FINISHED");
|
||||
setStatus(GAME, "COMPLETED");
|
||||
|
||||
gamePoints(GAME)
|
||||
.andExpect(jsonPath("$.paid.length()").value(3))
|
||||
.andExpect(jsonPath("$.paid[?(@.kind == 'STARTED')].points").value(5))
|
||||
.andExpect(jsonPath("$.paid[?(@.kind == 'FINISHED')].points").value(30))
|
||||
.andExpect(jsonPath("$.paid[?(@.kind == 'COMPLETED')].points").value(40))
|
||||
.andExpect(jsonPath("$.paid[0].paidAt").isNotEmpty())
|
||||
.andExpect(jsonPath("$.missing").isEmpty())
|
||||
.andExpect(jsonPath("$.balance").value(75));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamePoints_CompletedWithoutAllAchievements_MissingCompletedIsNotAllAchievements() throws Exception {
|
||||
game(GAME, "STEAM", "BACKLOG", 120);
|
||||
playedToday(GAME);
|
||||
setStatus(GAME, "FINISHED");
|
||||
setStatus(GAME, "COMPLETED");
|
||||
|
||||
gamePoints(GAME)
|
||||
.andExpect(jsonPath("$.paid.length()").value(1))
|
||||
.andExpect(jsonPath("$.paid[0].kind").value("FINISHED"))
|
||||
.andExpect(jsonPath("$.missing.COMPLETED").value("NOT_ALL_ACHIEVEMENTS"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamePoints_HiddenGame_OneHiddenReasonOnly() throws Exception {
|
||||
game(GAME, "STEAM", "BACKLOG", 120);
|
||||
playedToday(GAME);
|
||||
setStatus(GAME, "FINISHED");
|
||||
jdbc.update("update user_game set hidden_at = now() where user_id = ?", USER);
|
||||
|
||||
gamePoints(GAME)
|
||||
.andExpect(jsonPath("$.paid").isEmpty())
|
||||
.andExpect(jsonPath("$.missing.length()").value(1))
|
||||
.andExpect(jsonPath("$.missing.FINISHED").value("HIDDEN"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamePoints_HiddenAfterPaidStart_MissingFinishIsHidden() throws Exception {
|
||||
game(GAME, "STEAM", "BACKLOG", 120);
|
||||
playedToday(GAME);
|
||||
setStatus(GAME, "PLAYING");
|
||||
gamePoints(GAME).andExpect(jsonPath("$.paid[0].kind").value("STARTED"));
|
||||
jdbc.update("update user_game set hidden_at = now() where user_id = ?", USER);
|
||||
setStatus(GAME, "FINISHED");
|
||||
|
||||
gamePoints(GAME).andExpect(jsonPath("$.missing.FINISHED").value("HIDDEN"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamePoints_HiddenAfterPaidFinish_MissingCompletedIsHidden() throws Exception {
|
||||
game(GAME, "STEAM", "BACKLOG", 120);
|
||||
playedToday(GAME);
|
||||
setStatus(GAME, "FINISHED");
|
||||
gamePoints(GAME).andExpect(jsonPath("$.paid[0].kind").value("FINISHED"));
|
||||
jdbc.update("update user_game set hidden_at = now() where user_id = ?", USER);
|
||||
allAchievements();
|
||||
setStatus(GAME, "COMPLETED");
|
||||
|
||||
gamePoints(GAME).andExpect(jsonPath("$.missing.COMPLETED").value("HIDDEN"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamePoints_HiddenStartedGame_MissingStartIsHidden() throws Exception {
|
||||
game(GAME, "STEAM", "BACKLOG", 120);
|
||||
playedToday(GAME);
|
||||
jdbc.update("update user_game set hidden_at = now() where user_id = ?", USER);
|
||||
setStatus(GAME, "PLAYING");
|
||||
|
||||
gamePoints(GAME).andExpect(jsonPath("$.missing.STARTED").value("HIDDEN"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamePoints_PsnRow_SaysPlaystation() throws Exception {
|
||||
game(GAME, "PSN", "BACKLOG", 120);
|
||||
setStatus(GAME, "FINISHED");
|
||||
|
||||
gamePoints(GAME).andExpect(jsonPath("$.missing.FINISHED").value("PLAYSTATION"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamePoints_LinkedPsnTitle_SaysPlaystationLinked() throws Exception {
|
||||
game(GAME, "STEAM", "BACKLOG", 120);
|
||||
playedToday(GAME);
|
||||
setStatus(GAME, "FINISHED");
|
||||
jdbc.update("insert into user_psn_title (user_id, psn_key, state, appid) values (?, 'c1', 'LINKED', ?)",
|
||||
USER, GAME);
|
||||
|
||||
gamePoints(GAME).andExpect(jsonPath("$.missing.FINISHED").value("PLAYSTATION_LINKED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamePoints_FinishBeforeTheStatusLog_SaysBeforeHistory() throws Exception {
|
||||
game(GAME, "STEAM", "FINISHED", 120);
|
||||
jdbc.update("delete from user_game_status_change where user_id = ?", USER);
|
||||
|
||||
gamePoints(GAME).andExpect(jsonPath("$.missing.FINISHED").value("BEFORE_HISTORY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamePoints_FinishedPlayingFinished_PaysOnce() throws Exception {
|
||||
game(GAME, "STEAM", "BACKLOG", 120);
|
||||
playedToday(GAME);
|
||||
setStatus(GAME, "FINISHED");
|
||||
gamePoints(GAME).andExpect(jsonPath("$.balance").value(30));
|
||||
|
||||
setStatus(GAME, "PLAYING");
|
||||
setStatus(GAME, "FINISHED");
|
||||
|
||||
gamePoints(GAME).andExpect(jsonPath("$.balance").value(30));
|
||||
assertThat(rows("FINISHED")).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamePoints_GameNotInLibrary_Is404() throws Exception {
|
||||
gamePoints(GAME)
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.code").value("GAME_NOT_FOUND"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gamePoints_AnotherUsersGame_Is404AndBooksNothingForThem() throws Exception {
|
||||
jdbc.update("insert into steam_game (appid, name) values (?, 'Game') on conflict do nothing", GAME);
|
||||
jdbc.update("insert into user_game (user_id, appid, status, playtime_minutes, source) values (?, ?, 'BACKLOG', 120, 'STEAM')",
|
||||
OTHER_USER, GAME);
|
||||
jdbc.update("insert into user_game_playtime_day (user_id, day, appid, minutes) values (?, current_date, ?, 30)",
|
||||
OTHER_USER, GAME);
|
||||
jdbc.update("update user_game set status = 'FINISHED' where user_id = ?", OTHER_USER);
|
||||
|
||||
gamePoints(GAME).andExpect(status().isNotFound());
|
||||
|
||||
assertThat(jdbc.queryForObject("select count(*) from reward_ledger where user_id = ?", Integer.class, OTHER_USER))
|
||||
.isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void wallet_ManualGameInTheHistoryHasItsName() throws Exception {
|
||||
game(-4_100_020L, "MANUAL", "BACKLOG", 0);
|
||||
|
||||
@@ -24,6 +24,86 @@ class RewardRulesTest extends AbstractUnitTest {
|
||||
return RewardRules.finish(facts).map(RewardEntry::points).orElse(0);
|
||||
}
|
||||
|
||||
private static GameRewardFacts game(boolean manual, boolean finished, boolean completed, boolean finishLogged) {
|
||||
return new GameRewardFacts(manual, finished, completed, false, false, false, true, finishLogged);
|
||||
}
|
||||
|
||||
private static final GameRewardFacts STEAM_FINISHED = game(false, true, false, true);
|
||||
|
||||
@Test
|
||||
void blocked_HiddenBeforePlaystation() {
|
||||
GameRewardFacts all = new GameRewardFacts(false, true, false, true, true, true, true, true);
|
||||
|
||||
assertThat(RewardRules.blocked(all)).isEqualTo(NoPointsReason.HIDDEN);
|
||||
assertThat(RewardRules.blocked(new GameRewardFacts(false, true, false, false, true, true, true, true)))
|
||||
.isEqualTo(NoPointsReason.PLAYSTATION);
|
||||
assertThat(RewardRules.blocked(new GameRewardFacts(false, true, false, false, false, true, true, true)))
|
||||
.isEqualTo(NoPointsReason.PLAYSTATION_LINKED);
|
||||
assertThat(RewardRules.blocked(STEAM_FINISHED)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void noFinishReason_ManualOverTheCap_IsWeeklyLimit() {
|
||||
FinishFacts manual = new FinishFacts(-1L, true, false, 0, 0, null, false, false, false, AT);
|
||||
GameRewardFacts g = game(true, true, false, true);
|
||||
|
||||
assertThat(RewardRules.noFinishReason(g, manual, true)).isEqualTo(NoPointsReason.WEEKLY_LIMIT);
|
||||
assertThat(RewardRules.noFinishReason(g, manual, false)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void noFinishReason_SteamWithoutProof_IsNoProof() {
|
||||
assertThat(RewardRules.noFinishReason(STEAM_FINISHED, steam(60, 300, false, false), false))
|
||||
.isEqualTo(NoPointsReason.NO_PROOF);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noFinishReason_SteamWithoutPlaytime_IsNoPlaytime() {
|
||||
assertThat(RewardRules.noFinishReason(STEAM_FINISHED, steam(0, 300, true, true), false))
|
||||
.isEqualTo(NoPointsReason.NO_PLAYTIME);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noFinishReason_NoStatusLog_IsBeforeHistory() {
|
||||
assertThat(RewardRules.noFinishReason(game(false, true, false, false), null, false))
|
||||
.isEqualTo(NoPointsReason.BEFORE_HISTORY);
|
||||
assertThat(RewardRules.noFinishReason(game(false, false, false, false), null, false))
|
||||
.isEqualTo(NoPointsReason.NOT_FINISHED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noStartReason_ManualGame_IsManualGame() {
|
||||
assertThat(RewardRules.noStartReason(game(true, false, false, false), null)).isEqualTo(NoPointsReason.MANUAL_GAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noStartReason_NoPlayAfterStart() {
|
||||
StartFacts idle = new StartFacts(220L, false, AT, false);
|
||||
|
||||
assertThat(RewardRules.noStartReason(game(false, false, false, false), idle))
|
||||
.isEqualTo(NoPointsReason.NO_PLAY_AFTER_START);
|
||||
assertThat(RewardRules.noStartReason(new GameRewardFacts(false, false, false, false, false, false, false, false),
|
||||
null)).isEqualTo(NoPointsReason.NOT_STARTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noCompletedReason_WithoutProvenFinish_IsNeedsProvenFinish() {
|
||||
GameRewardFacts g = game(false, true, true, true);
|
||||
|
||||
assertThat(RewardRules.noCompletedReason(g, null, false)).isEqualTo(NoPointsReason.NEEDS_PROVEN_FINISH);
|
||||
assertThat(RewardRules.noCompletedReason(STEAM_FINISHED, null, true)).isEqualTo(NoPointsReason.NOT_COMPLETED);
|
||||
assertThat(RewardRules.noCompletedReason(game(true, true, true, true), null, true))
|
||||
.isEqualTo(NoPointsReason.MANUAL_GAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noCompletedReason_MissingAchievements() {
|
||||
FinishFacts open = new FinishFacts(220L, false, true, 60, 0, null, false, false, false, AT);
|
||||
|
||||
assertThat(RewardRules.noCompletedReason(game(false, true, true, true), open, true))
|
||||
.isEqualTo(NoPointsReason.NOT_ALL_ACHIEVEMENTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void finish_PaysByTheLengthOfTheGame() {
|
||||
assertThat(points(steam(60, null, true, false))).isEqualTo(30);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package core_service.modules.rewards.repo;
|
||||
|
||||
import core_service.modules.rewards.domain.FinishFacts;
|
||||
import core_service.modules.rewards.domain.GameRewardFacts;
|
||||
import core_service.modules.rewards.domain.RewardEntry;
|
||||
import core_service.modules.rewards.domain.RewardKind;
|
||||
import core_service.modules.rewards.domain.StartFacts;
|
||||
@@ -64,6 +65,68 @@ class RewardFactsRepositoryTest extends AbstractPersistenceTest {
|
||||
USER, day, GAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
void game_FlagsHiddenPsnLinkedManualAndStatusLog() {
|
||||
game("BACKLOG", 120);
|
||||
setStatus("PLAYING");
|
||||
setStatus("FINISHED");
|
||||
jdbc.update("update user_game set hidden_at = now() where user_id = ?", USER);
|
||||
jdbc.update("insert into user_psn_title (user_id, psn_key, state, appid) values (?, 'c1', 'LINKED', ?)", USER, GAME);
|
||||
|
||||
assertThat(repository.game(USER, GAME)).hasValue(
|
||||
new GameRewardFacts(false, true, false, true, false, true, true, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void game_RemovedGame_IsEmpty() {
|
||||
game("BACKLOG", 120);
|
||||
jdbc.update("update user_game set removed_at = now() where user_id = ?", USER);
|
||||
|
||||
assertThat(repository.game(USER, GAME)).isEmpty();
|
||||
assertThat(repository.game(USER, EDITION)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void startFinishCompletedCandidates_WithAppid_OnlyThatGame() {
|
||||
game("BACKLOG", 120);
|
||||
jdbc.update("insert into steam_game (appid, name) values (?, 'Other') on conflict do nothing", EDITION);
|
||||
jdbc.update("insert into user_game (user_id, appid, status, playtime_minutes) values (?, ?, 'BACKLOG', 120)",
|
||||
USER, EDITION);
|
||||
jdbc.update("update user_game set status = 'PLAYING' where user_id = ?", USER);
|
||||
jdbc.update("update user_game set status = 'COMPLETED' where user_id = ?", USER);
|
||||
|
||||
assertThat(repository.startCandidates(USER, BERLIN)).hasSize(2);
|
||||
assertThat(repository.startCandidates(USER, BERLIN, EDITION)).extracting(StartFacts::appid)
|
||||
.containsExactly(EDITION);
|
||||
assertThat(repository.finishCandidates(USER, BERLIN)).hasSize(2);
|
||||
assertThat(repository.finishCandidates(USER, BERLIN, GAME)).extracting(FinishFacts::appid)
|
||||
.containsExactly(GAME);
|
||||
|
||||
ledger.add(USER, new RewardEntry(RewardKind.FINISHED, String.valueOf(GAME), 30, Instant.now()));
|
||||
ledger.add(USER, new RewardEntry(RewardKind.FINISHED, String.valueOf(EDITION), 30, Instant.now()));
|
||||
|
||||
assertThat(repository.completedCandidates(USER)).hasSize(2);
|
||||
assertThat(repository.completedCandidates(USER, GAME)).extracting(FinishFacts::appid).containsExactly(GAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
void paidRows_ListsTheRowOfAnotherEditionOfTheSet() {
|
||||
game("BACKLOG", 120);
|
||||
jdbc.update("insert into steam_game (appid, name) values (?, 'Edition') on conflict do nothing", EDITION);
|
||||
jdbc.update("insert into user_game (user_id, appid, status, playtime_minutes) values (?, ?, 'BACKLOG', 0)",
|
||||
USER, EDITION);
|
||||
jdbc.update("insert into game_link (appid, set_id) values (?, 1), (?, 1)", GAME, EDITION);
|
||||
ledger.add(USER, new RewardEntry(RewardKind.FINISHED, String.valueOf(GAME), 30, Instant.now()));
|
||||
ledger.add(USER, new RewardEntry(RewardKind.MONTH, "2026-09", 40, Instant.now()));
|
||||
|
||||
assertThat(repository.paidRows(USER, EDITION)).singleElement().satisfies(row -> {
|
||||
assertThat(row.kind()).isEqualTo(RewardKind.FINISHED);
|
||||
assertThat(row.appid()).isEqualTo(GAME);
|
||||
assertThat(row.points()).isEqualTo(30);
|
||||
});
|
||||
assertThat(repository.paidRows(7777L, GAME)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void finishCandidates_AppearsAfterTheChangeWithItsFacts() {
|
||||
game("BACKLOG", 120);
|
||||
|
||||
Reference in new issue
Block a user