2026-09-25 01:41:49 -03:00
|
|
|
|
using System.Globalization;
|
2026-09-24 20:51:44 -03:00
|
|
|
|
using Data.SeaHavenIndustries;
|
|
|
|
|
|
using Data.SeaHavenIndustries.Enums;
|
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
|
using SeaHaven.DataServices.Implementation;
|
|
|
|
|
|
using SeaHaven.Services.DTOs;
|
|
|
|
|
|
using SeaHaven.Services.Helpers;
|
|
|
|
|
|
using SeaHaven.Services.Implementation;
|
|
|
|
|
|
|
|
|
|
|
|
namespace SeaHavenIndustries.Tests;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// SH-391: an Advanced Filters date range narrows GET /board/search to rows
|
|
|
|
|
|
/// whose Schedule On (or week-only Target Week) is in the range. Rows with no
|
|
|
|
|
|
/// date come back only when the caller asks for them (no range selected).
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class WorkOrderAdvancedSearchDateRangeTests
|
|
|
|
|
|
{
|
|
|
|
|
|
private static readonly DateOnly RangeFrom = new(2026, 9, 21);
|
|
|
|
|
|
private static readonly DateOnly RangeTo = new(2026, 9, 25);
|
|
|
|
|
|
|
|
|
|
|
|
private static ApplicationDbContext CreateContext()
|
|
|
|
|
|
{
|
|
|
|
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
|
|
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
|
|
|
|
.Options;
|
|
|
|
|
|
return new ApplicationDbContext(options);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static WorkOrderAdvancedSearchService CreateService(ApplicationDbContext context)
|
|
|
|
|
|
=> new(new WorkOrderAdvancedSearchDataService(context), WorkOrderAccountTestHelpers.Resolver(context));
|
|
|
|
|
|
|
|
|
|
|
|
private static WorkOrder Wo(
|
|
|
|
|
|
int id,
|
|
|
|
|
|
DateTime? scheduled,
|
|
|
|
|
|
string? assignTo = null,
|
|
|
|
|
|
LifecycleStatus status = LifecycleStatus.Scheduled,
|
|
|
|
|
|
DateOnly? targetWeek = null,
|
|
|
|
|
|
int? accountId = null)
|
|
|
|
|
|
=> new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = id,
|
|
|
|
|
|
InternalWONumber = $"1000000000{id}",
|
|
|
|
|
|
AssignTo = assignTo,
|
|
|
|
|
|
ScheduledDate = scheduled,
|
|
|
|
|
|
ScheduleWeekOnly = targetWeek.HasValue,
|
|
|
|
|
|
TargetWeek = targetWeek,
|
|
|
|
|
|
LifecycleStatus = status,
|
|
|
|
|
|
AccountId = accountId,
|
|
|
|
|
|
istemplate = false
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-09-24 21:11:20 -03:00
|
|
|
|
private static WorkOrder NotWeekOnly(WorkOrder wo)
|
|
|
|
|
|
{
|
|
|
|
|
|
wo.ScheduleWeekOnly = false;
|
|
|
|
|
|
return wo;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-25 01:41:49 -03:00
|
|
|
|
private static WorkOrderAdvancedSearchQueryDto UnassignedInRange(bool? includeDateless = null) => new()
|
2026-09-24 20:51:44 -03:00
|
|
|
|
{
|
|
|
|
|
|
DatePreset = WorkOrderAdvancedSearchDatePreset.Custom,
|
|
|
|
|
|
DateFrom = RangeFrom,
|
|
|
|
|
|
DateTo = RangeTo,
|
|
|
|
|
|
Dispatchers = new List<string> { "__unassigned__" },
|
|
|
|
|
|
IncludeDateless = includeDateless,
|
|
|
|
|
|
PageSize = 50
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task SearchAsync_UnassignedWithRange_ReturnsOnlyUnassignedRowsScheduledInRange()
|
|
|
|
|
|
{
|
|
|
|
|
|
await using var context = CreateContext();
|
|
|
|
|
|
context.workOrders.AddRange(
|
|
|
|
|
|
Wo(1, new DateTime(2026, 9, 23)),
|
|
|
|
|
|
Wo(2, new DateTime(2026, 8, 4)),
|
|
|
|
|
|
Wo(3, null, status: LifecycleStatus.Incomplete),
|
|
|
|
|
|
Wo(4, new DateTime(2026, 9, 24), assignTo: "user-a"),
|
|
|
|
|
|
Wo(5, new DateTime(2026, 10, 2)));
|
|
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var result = await CreateService(context).SearchAsync(
|
|
|
|
|
|
UnassignedInRange(), WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Equal(1, result.TotalCount);
|
|
|
|
|
|
Assert.Equal(new[] { 1 }, result.Items.Select(i => i.Id).ToArray());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task SearchAsync_RangeWithoutDispatcherFilter_ExcludesUndatedRows()
|
|
|
|
|
|
{
|
|
|
|
|
|
await using var context = CreateContext();
|
|
|
|
|
|
context.workOrders.AddRange(
|
|
|
|
|
|
Wo(1, new DateTime(2026, 9, 22), assignTo: "user-a"),
|
|
|
|
|
|
Wo(2, null, assignTo: "user-a", status: LifecycleStatus.Incomplete),
|
|
|
|
|
|
Wo(3, new DateTime(2026, 9, 14), assignTo: "user-a", status: LifecycleStatus.Incomplete));
|
|
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var result = await CreateService(context).SearchAsync(new WorkOrderAdvancedSearchQueryDto
|
|
|
|
|
|
{
|
|
|
|
|
|
DatePreset = WorkOrderAdvancedSearchDatePreset.Custom,
|
|
|
|
|
|
DateFrom = RangeFrom,
|
|
|
|
|
|
DateTo = RangeTo
|
|
|
|
|
|
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Equal(new[] { 1 }, result.Items.Select(i => i.Id).ToArray());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task SearchAsync_WeekOnlyRow_MatchesRangeThatOverlapsItsTargetWeek()
|
|
|
|
|
|
{
|
|
|
|
|
|
await using var context = CreateContext();
|
|
|
|
|
|
context.workOrders.AddRange(
|
|
|
|
|
|
Wo(1, null, targetWeek: new DateOnly(2026, 9, 21)),
|
|
|
|
|
|
Wo(2, null, targetWeek: new DateOnly(2026, 9, 14)),
|
2026-09-24 21:11:20 -03:00
|
|
|
|
Wo(3, null, targetWeek: new DateOnly(2026, 9, 28)),
|
|
|
|
|
|
// Week-only with a leftover date outside the range: the week decides,
|
|
|
|
|
|
// as on the board page (SH-391).
|
|
|
|
|
|
Wo(4, new DateTime(2026, 10, 2), targetWeek: new DateOnly(2026, 9, 21)),
|
|
|
|
|
|
// Not week-only: its date decides, so a target week does not bring it in.
|
|
|
|
|
|
NotWeekOnly(Wo(5, new DateTime(2026, 10, 2), targetWeek: new DateOnly(2026, 9, 21))));
|
2026-09-24 20:51:44 -03:00
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
// Wed–Fri of the week of 9/21: the week-only row for that week overlaps it.
|
|
|
|
|
|
var result = await CreateService(context).SearchAsync(new WorkOrderAdvancedSearchQueryDto
|
|
|
|
|
|
{
|
|
|
|
|
|
DatePreset = WorkOrderAdvancedSearchDatePreset.Custom,
|
|
|
|
|
|
DateFrom = new DateOnly(2026, 9, 23),
|
|
|
|
|
|
DateTo = RangeTo,
|
|
|
|
|
|
Dispatchers = new List<string> { "__unassigned__" }
|
|
|
|
|
|
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
|
|
|
|
|
|
2026-09-24 21:11:20 -03:00
|
|
|
|
Assert.Equal(new[] { 1, 4 }, result.Items.Select(i => i.Id).OrderBy(id => id).ToArray());
|
2026-09-24 20:51:44 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task SearchAsync_IncludeDateless_AddsOpenUndatedRowsToTheRange()
|
|
|
|
|
|
{
|
|
|
|
|
|
await using var context = CreateContext();
|
|
|
|
|
|
context.workOrders.AddRange(
|
|
|
|
|
|
Wo(1, new DateTime(2026, 9, 23)),
|
|
|
|
|
|
Wo(2, new DateTime(2026, 8, 4)),
|
|
|
|
|
|
Wo(3, null, status: LifecycleStatus.Incomplete),
|
|
|
|
|
|
Wo(4, null, status: LifecycleStatus.Completed),
|
|
|
|
|
|
Wo(5, null, status: LifecycleStatus.Canceled));
|
|
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var result = await CreateService(context).SearchAsync(
|
|
|
|
|
|
UnassignedInRange(includeDateless: true), WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Equal(new[] { 1, 3 }, result.Items.Select(i => i.Id).OrderBy(id => id).ToArray());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-25 01:41:49 -03:00
|
|
|
|
// Payloads the frontend sent before includeDateless existed. They must keep
|
|
|
|
|
|
// their undated rows so this backend can deploy ahead of the client.
|
|
|
|
|
|
[Theory]
|
|
|
|
|
|
[InlineData(2000)] // no range selected: ADVANCED_SEARCH_ALL_WEEKS_FROM
|
|
|
|
|
|
[InlineData(1970)] // pinned Unassigned queue: UNASSIGNED_QUEUE_DATE_FROM
|
|
|
|
|
|
public async Task SearchAsync_AllWeeksWindowWithoutFlag_KeepsOpenUndatedRows(int fromYear)
|
|
|
|
|
|
{
|
|
|
|
|
|
await using var context = CreateContext();
|
|
|
|
|
|
context.workOrders.AddRange(
|
|
|
|
|
|
Wo(1, new DateTime(2026, 8, 4)),
|
|
|
|
|
|
Wo(2, null, status: LifecycleStatus.Incomplete),
|
|
|
|
|
|
Wo(3, null, status: LifecycleStatus.Completed));
|
|
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var result = await CreateService(context).SearchAsync(new WorkOrderAdvancedSearchQueryDto
|
|
|
|
|
|
{
|
|
|
|
|
|
DatePreset = WorkOrderAdvancedSearchDatePreset.Custom,
|
|
|
|
|
|
DateFrom = new DateOnly(fromYear, 1, 1),
|
|
|
|
|
|
DateTo = new DateOnly(2099, 12, 31),
|
|
|
|
|
|
Dispatchers = new List<string> { "__unassigned__" },
|
|
|
|
|
|
PageSize = 50
|
|
|
|
|
|
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Equal(new[] { 1, 2 }, result.Items.Select(i => i.Id).OrderBy(id => id).ToArray());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task SearchAsync_NoDateInputWithoutFlag_KeepsOpenUndatedRows()
|
|
|
|
|
|
{
|
|
|
|
|
|
// The WO# duplicate lookup sends only the search text and paging.
|
|
|
|
|
|
await using var context = CreateContext();
|
|
|
|
|
|
var thisWeek = WorkOrderSearchDateRangeResolver.GetIsoWeekStart(DateOnly.FromDateTime(DateTime.UtcNow));
|
|
|
|
|
|
context.workOrders.AddRange(
|
|
|
|
|
|
Wo(1, thisWeek.ToDateTime(TimeOnly.MinValue)),
|
|
|
|
|
|
Wo(2, null, status: LifecycleStatus.Incomplete),
|
|
|
|
|
|
Wo(3, thisWeek.AddDays(-7).ToDateTime(TimeOnly.MinValue)));
|
|
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var result = await CreateService(context).SearchAsync(
|
|
|
|
|
|
new WorkOrderAdvancedSearchQueryDto { PageSize = 100 },
|
|
|
|
|
|
WorkOrderAccountTestHelpers.OrgWideAdmin(),
|
|
|
|
|
|
null);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Equal(new[] { 1, 2 }, result.Items.Select(i => i.Id).OrderBy(id => id).ToArray());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task SearchAsync_AllWeeksWindowWithFlagFalse_ExcludesUndatedRows()
|
|
|
|
|
|
{
|
|
|
|
|
|
await using var context = CreateContext();
|
|
|
|
|
|
context.workOrders.AddRange(
|
|
|
|
|
|
Wo(1, new DateTime(2026, 8, 4)),
|
|
|
|
|
|
Wo(2, null, status: LifecycleStatus.Incomplete));
|
|
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var result = await CreateService(context).SearchAsync(new WorkOrderAdvancedSearchQueryDto
|
|
|
|
|
|
{
|
|
|
|
|
|
DatePreset = WorkOrderAdvancedSearchDatePreset.Custom,
|
|
|
|
|
|
DateFrom = new DateOnly(2000, 1, 1),
|
|
|
|
|
|
DateTo = new DateOnly(2099, 12, 31),
|
|
|
|
|
|
IncludeDateless = false,
|
|
|
|
|
|
PageSize = 50
|
|
|
|
|
|
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Equal(new[] { 1 }, result.Items.Select(i => i.Id).ToArray());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
|
[InlineData(null, null, null, null, true)]
|
|
|
|
|
|
[InlineData(null, WorkOrderAdvancedSearchDatePreset.Custom, "2000-01-01", "2099-12-31", true)]
|
|
|
|
|
|
[InlineData(null, WorkOrderAdvancedSearchDatePreset.Custom, "1970-01-01", "2099-12-31", true)]
|
|
|
|
|
|
[InlineData(null, WorkOrderAdvancedSearchDatePreset.Custom, "2000-01-02", "2099-12-31", false)]
|
|
|
|
|
|
[InlineData(null, WorkOrderAdvancedSearchDatePreset.Custom, "2026-09-21", "2026-09-25", false)]
|
|
|
|
|
|
[InlineData(null, WorkOrderAdvancedSearchDatePreset.ThisWeek, null, null, false)]
|
|
|
|
|
|
[InlineData(false, null, null, null, false)]
|
|
|
|
|
|
[InlineData(true, WorkOrderAdvancedSearchDatePreset.ThisWeek, null, null, true)]
|
|
|
|
|
|
public void ResolveIncludeDateless_InfersOnlyForNoRangeRequests(
|
|
|
|
|
|
bool? flag,
|
|
|
|
|
|
WorkOrderAdvancedSearchDatePreset? preset,
|
|
|
|
|
|
string? from,
|
|
|
|
|
|
string? to,
|
|
|
|
|
|
bool expected)
|
|
|
|
|
|
{
|
|
|
|
|
|
Assert.Equal(expected, WorkOrderSearchDateRangeResolver.ResolveIncludeDateless(
|
|
|
|
|
|
flag,
|
|
|
|
|
|
preset,
|
|
|
|
|
|
from is null ? null : DateOnly.Parse(from, CultureInfo.InvariantCulture),
|
|
|
|
|
|
to is null ? null : DateOnly.Parse(to, CultureInfo.InvariantCulture)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-24 20:51:44 -03:00
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task SearchAsync_RangeForAccountUser_NeverReturnsAnotherAccountsRows()
|
|
|
|
|
|
{
|
|
|
|
|
|
await using var context = CreateContext();
|
|
|
|
|
|
await WorkOrderAccountTestHelpers.EnsureAccountAsync(context, 1, "A");
|
|
|
|
|
|
await WorkOrderAccountTestHelpers.EnsureAccountAsync(context, 2, "B");
|
|
|
|
|
|
context.workOrders.AddRange(
|
|
|
|
|
|
Wo(1, new DateTime(2026, 9, 23), accountId: 1),
|
|
|
|
|
|
Wo(2, new DateTime(2026, 9, 23), accountId: 2),
|
|
|
|
|
|
Wo(3, null, status: LifecycleStatus.Incomplete, accountId: 2),
|
|
|
|
|
|
Wo(4, null, targetWeek: new DateOnly(2026, 9, 21), accountId: 2));
|
|
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var user = WorkOrderAccountTestHelpers.AccountUser("disp-1", 1, "Dispatcher");
|
|
|
|
|
|
var strict = await CreateService(context).SearchAsync(UnassignedInRange(), user, "disp-1");
|
|
|
|
|
|
var withDateless = await CreateService(context).SearchAsync(
|
|
|
|
|
|
UnassignedInRange(includeDateless: true), user, "disp-1");
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Equal(new[] { 1 }, strict.Items.Select(i => i.Id).ToArray());
|
|
|
|
|
|
Assert.Equal(new[] { 1 }, withDateless.Items.Select(i => i.Id).ToArray());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|