Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace JoinRpg.Dal.Impl.Repositories;

internal static class KogdaIgraMissingGamesPredicate
{
/// <summary>
/// Возвращает Expression-предикат для фильтрации проектов, нуждающихся в привязке к КогдаИгра.
/// Используется с LinqKit (.AsExpandable() + .Invoke()) для трансляции запроса в SQL.
/// См. docs/linq-queries.md
///
/// Проект попадает в выборку если:
/// 1. Проект активен и не отключена привязка к КогдаИгра
/// 2. Нет активных КИ-привязок вообще, ИЛИ
/// все привязанные КИ-игры уже завершились (End &lt; now) И проект недавно обновлялся (&lt;60 дней)
/// </summary>
public static Expression<Func<Project, DateTime, bool>> GetPredicate(DateTime now)
{
var lastUpdateMax = now.AddDays(-60);
return (project, lastUpdated) =>
project.Active &&
!project.Details.DisableKogdaIgraMapping &&
!project.KogdaIgraGames.Any(g => g.Active && g.End >= now) &&
(!project.KogdaIgraGames.Any(g => g.Active) || lastUpdated > lastUpdateMax);
}
}
18 changes: 3 additions & 15 deletions src/JoinRpg.Dal.Impl/Repositories/ProjectRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,29 +232,17 @@ async Task<ProjectShortInfo[]> IProjectRepository.GetProjectsBySpecification(Pro

private async Task<ProjectShortInfo[]> GetKogdaIgraMissingGames()
{
var kogdaIgraStaleExpression60 = ProjectPredicates.KogdaIgraIsStaleFor(TimeSpan.FromDays(60));
Expression<Func<Project, bool>> hasNonStaleKogdaIgra = project => project.KogdaIgraGames.Count(e => kogdaIgraStaleExpression60.Invoke(e)) > 0;
var filterPredicate = PredicateBuilder.New<Project>()
.And(project => project.Active)
.And(project => !hasNonStaleKogdaIgra.Invoke(project))
.And(project => !project.Details.DisableKogdaIgraMapping);
var projection = GetProjectListProjection();

var lastUpdateMax = DateTime.Now.AddDays(-60);
var predicate = KogdaIgraMissingGamesPredicate.GetPredicate(DateTime.Now);

var query = from project in AllProjects
join update in GetProjectWithLastUpdateQuery() on project.ProjectId equals update.ProjectId
where filterPredicate.Invoke(project)
let item = projection.Invoke(project, update)
where item.KogdaIgraGames.Count() == 0 || item.LastUpdated > lastUpdateMax
select item;


where predicate.Invoke(project, update.LastUpdated)
select projection.Invoke(project, update);

var result = await query.ToListAsync();

return [.. result.Select(BuildProjectShortInfo)];

}

Task<ProjectPersonalizedInfo[]> IProjectRepository.GetProjectsByIds(UserIdentification? userId, ProjectIdentification[] ids)
Expand Down
162 changes: 162 additions & 0 deletions src/JoinRpg.DataModel.Mocks/KogdaIgraMissingGamesPredicateTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using JoinRpg.Dal.Impl.Repositories;
using JoinRpg.DataModel.Projects;
using Shouldly;
using Xunit;

namespace JoinRpg.DataModel.Mocks;

public class KogdaIgraMissingGamesPredicateTest
{
private static readonly DateTime Now = new DateTime(2025, 6, 1, 12, 0, 0);
private static readonly DateTime RecentlyUpdated = Now.AddDays(-10);
private static readonly DateTime StaleUpdated = Now.AddDays(-90);

private static KogdaIgraGame GameEndingIn(int days) => new()
{
KogdaIgraGameId = 1,
Active = true,
End = Now.AddDays(days),
Name = "Test Game",
};

private static KogdaIgraGame GameWithNullEnd() => new()
{
KogdaIgraGameId = 2,
Active = true,
End = null,
Name = "Test Game No End",
};

private static KogdaIgraGame InactiveGame(int endDaysOffset) => new()
{
KogdaIgraGameId = 3,
Active = false,
End = Now.AddDays(endDaysOffset),
Name = "Inactive Game",
};

private static Project CreateProject(
IEnumerable<KogdaIgraGame>? games = null,
bool active = true,
bool disableKogdaIgraMapping = false) => new()
{
Active = active,
Details = new ProjectDetails { DisableKogdaIgraMapping = disableKogdaIgraMapping },
KogdaIgraGames = [.. (games ?? [])],
};

private static bool TestPredicate(Project project, DateTime lastUpdated)
=> KogdaIgraMissingGamesPredicate.GetPredicate(Now).Compile()(project, lastUpdated);

// --- Нет привязок ---

[Fact]
public void NoGames_RecentlyUpdated_ShouldNeedBinding()
=> TestPredicate(CreateProject(), RecentlyUpdated).ShouldBeTrue();

[Fact]
public void NoGames_StaleProject_ShouldNeedBinding()
// Даже устаревший проект без привязки — кандидат (нужна привязка)
=> TestPredicate(CreateProject(), StaleUpdated).ShouldBeTrue();

// --- Будущие игры (ещё не прошли) ---

[Fact]
public void FutureGame_RecentlyUpdated_ShouldNotNeedBinding()
=> TestPredicate(CreateProject([GameEndingIn(30)]), RecentlyUpdated).ShouldBeFalse();

[Fact]
public void FutureGame_StaleProject_ShouldNotNeedBinding()
// Привязан к будущей игре — не нужна новая привязка
=> TestPredicate(CreateProject([GameEndingIn(30)]), StaleUpdated).ShouldBeFalse();

[Fact]
public void GameEndingToday_ShouldNotNeedBinding()
// End == Now: граница, игра «ещё не закончилась»
=> TestPredicate(CreateProject([GameEndingIn(0)]), RecentlyUpdated).ShouldBeFalse();

// --- Прошедшие игры ---

[Fact]
public void PastGame_RecentlyUpdated_ShouldNeedBinding()
// Сериал: игра прошла, проект активен → надо привязать следующую
=> TestPredicate(CreateProject([GameEndingIn(-1)]), RecentlyUpdated).ShouldBeTrue();

[Fact]
public void PastGame_StaleProject_ShouldNotNeedBinding()
// Игра прошла, проект неактивен — не показывать в выборке
=> TestPredicate(CreateProject([GameEndingIn(-1)]), StaleUpdated).ShouldBeFalse();

[Fact]
public void PastGame_UpdatedExactlyAtBoundary_ShouldNotNeedBinding()
{
// Граница: обновлено ровно 60 дней назад — не попадает в выборку
var updatedAt60DaysAgo = Now.AddDays(-60);
TestPredicate(CreateProject([GameEndingIn(-1)]), updatedAt60DaysAgo).ShouldBeFalse();
}

[Fact]
public void PastGame_UpdatedJustUnderBoundary_ShouldNeedBinding()
{
// Граница: обновлено 59 дней назад — попадает в выборку
var updatedAt59DaysAgo = Now.AddDays(-59);
TestPredicate(CreateProject([GameEndingIn(-1)]), updatedAt59DaysAgo).ShouldBeTrue();
}

// --- Неактивные привязки ---

[Fact]
public void OnlyInactiveGame_RecentlyUpdated_ShouldNeedBinding()
// Привязка помечена как неактивная — игнорируется, как нет привязки
=> TestPredicate(CreateProject([InactiveGame(30)]), RecentlyUpdated).ShouldBeTrue();

[Fact]
public void OnlyInactiveGame_StaleProject_ShouldNeedBinding()
=> TestPredicate(CreateProject([InactiveGame(30)]), StaleUpdated).ShouldBeTrue();

// --- Несколько игр (сериал) ---

[Fact]
public void MixedGames_OnePastOneFuture_ShouldNotNeedBinding()
// Есть и прошедшая и будущая игра — привязан, не нуждается
=> TestPredicate(CreateProject([GameEndingIn(-30), GameEndingIn(30)]), RecentlyUpdated).ShouldBeFalse();

[Fact]
public void MultiplePastGames_RecentlyUpdated_ShouldNeedBinding()
// Все игры прошли, проект активен → нужна новая привязка
=> TestPredicate(
CreateProject([GameEndingIn(-60), GameEndingIn(-30), GameEndingIn(-1)]),
RecentlyUpdated).ShouldBeTrue();

[Fact]
public void MultiplePastGames_StaleProject_ShouldNotNeedBinding()
=> TestPredicate(
CreateProject([GameEndingIn(-60), GameEndingIn(-30), GameEndingIn(-1)]),
StaleUpdated).ShouldBeFalse();

// --- Граничный случай: End = null ---

[Fact]
public void GameWithNullEnd_RecentlyUpdated_ShouldNeedBinding()
// Игра без даты окончания: End=null → null >= now = false → hasActiveOrFutureGame=false
// Но hasAnyGame=true, поэтому проверяется активность проекта
=> TestPredicate(CreateProject([GameWithNullEnd()]), RecentlyUpdated).ShouldBeTrue();

[Fact]
public void GameWithNullEnd_StaleProject_ShouldNotNeedBinding()
=> TestPredicate(CreateProject([GameWithNullEnd()]), StaleUpdated).ShouldBeFalse();

// --- Проект с DisableKogdaIgraMapping ---

[Fact]
public void DisableKogdaIgraMapping_ShouldNotNeedBinding()
// Проект с отключённой привязкой к КогдаИгра — не попадает в выборку
=> TestPredicate(CreateProject(disableKogdaIgraMapping: true), RecentlyUpdated).ShouldBeFalse();

// --- Неактивный проект ---

[Fact]
public void InactiveProject_ShouldNotNeedBinding()
// Неактивный проект — не попадает в выборку
=> TestPredicate(CreateProject(active: false), RecentlyUpdated).ShouldBeFalse();
}
Loading