Skip to content
Merged
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
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Threading.Tasks;

using Microsoft.EntityFrameworkCore;
Expand Down Expand Up @@ -108,9 +109,11 @@ public async Task CreateAsync_CreatesHandoverRequest_WhenSourceHasContent()
// Assert
Assert.That(result.Success, Is.True);
Assert.That(result.Model, Is.Not.Null);
Assert.That(result.Model.Status, Is.EqualTo("Pending"));
Assert.That(result.Model.FromSdkSitId, Is.EqualTo(1));
Assert.That(result.Model.ToSdkSitId, Is.EqualTo(2));
Assert.That(result.Model.Count, Is.EqualTo(1));
Assert.That(result.Model[0].Status, Is.EqualTo("Pending"));
Assert.That(result.Model[0].FromSdkSitId, Is.EqualTo(1));
Assert.That(result.Model[0].ToSdkSitId, Is.EqualTo(2));
Assert.That(result.Model[0].ShiftIndex, Is.Null);
}

[Test]
Expand Down Expand Up @@ -486,4 +489,231 @@ public async Task GetMineAsync_ReturnsRequestsFromSender()
Assert.That(result.Model.Count, Is.EqualTo(1));
Assert.That(result.Model[0].FromSdkSitId, Is.EqualTo(1));
}

// ---------------------------------------------------------------
// Partial-shift handover tests.
// ---------------------------------------------------------------

private async Task<(PlanRegistration source, PlanRegistration target)> SeedPartialShiftPairAsync(DateTime date)
{
// Accept path recomputes derived fields via PlanRegistrationHelper, which
// requires an AssignedSite row per SdkSitId. Seed both before the plan rows.
if (!await TimePlanningPnDbContext.AssignedSites.AnyAsync(a => a.SiteId == 1))
{
await new AssignedSite { SiteId = 1, CreatedByUserId = 1, UpdatedByUserId = 1 }
.Create(TimePlanningPnDbContext);
}
if (!await TimePlanningPnDbContext.AssignedSites.AnyAsync(a => a.SiteId == 2))
{
await new AssignedSite { SiteId = 2, CreatedByUserId = 1, UpdatedByUserId = 1 }
.Create(TimePlanningPnDbContext);
}

var source = new PlanRegistration
{
Date = date,
SdkSitId = 1,
PlannedStartOfShift1 = 8 * 60,
PlannedEndOfShift1 = 12 * 60,
PlannedStartOfShift2 = 13 * 60,
PlannedEndOfShift2 = 17 * 60,
PlannedStartOfShift3 = 18 * 60,
PlannedEndOfShift3 = 21 * 60,
PlanHoursInSeconds = 11 * 3600,
CreatedByUserId = 1,
UpdatedByUserId = 1
};
await source.Create(TimePlanningPnDbContext);

var target = new PlanRegistration
{
Date = date,
SdkSitId = 2,
CreatedByUserId = 1,
UpdatedByUserId = 1
};
await target.Create(TimePlanningPnDbContext);
return (source, target);
}

[Test]
public async Task CreateAsync_SingleShift_CreatesOneRowWithShiftIndex()
{
var date = new DateTime(2024, 2, 1);
var (source, _) = await SeedPartialShiftPairAsync(date);

var result = await _contentHandoverService.CreateAsync(source.Id,
new ContentHandoverRequestCreateModel { ToSdkSitId = 2, ShiftIndices = new() { 2 } });

Assert.That(result.Success, Is.True, result.Message);
Assert.That(result.Model.Count, Is.EqualTo(1));
Assert.That(result.Model[0].ShiftIndex, Is.EqualTo(2));

// Source/target planning untouched (pending only).
var s = await TimePlanningPnDbContext.PlanRegistrations.FindAsync(source.Id);
Assert.That(s.PlannedEndOfShift2, Is.EqualTo(17 * 60));
}

[Test]
public async Task CreateAsync_MultiShift_CreatesRowPerShift()
{
var date = new DateTime(2024, 2, 2);
var (source, _) = await SeedPartialShiftPairAsync(date);

var result = await _contentHandoverService.CreateAsync(source.Id,
new ContentHandoverRequestCreateModel { ToSdkSitId = 2, ShiftIndices = new() { 1, 3 } });

Assert.That(result.Success, Is.True, result.Message);
Assert.That(result.Model.Count, Is.EqualTo(2));
Assert.That(result.Model.Select(m => m.ShiftIndex).OrderBy(x => x),
Is.EqualTo(new int?[] { 1, 3 }));
}

[Test]
public async Task CreateAsync_MultiShift_AllOrNothing_WhenOneInvalid()
{
var date = new DateTime(2024, 2, 3);
var (source, _) = await SeedPartialShiftPairAsync(date);

// Shift 4 on source is empty — invalid.
var result = await _contentHandoverService.CreateAsync(source.Id,
new ContentHandoverRequestCreateModel { ToSdkSitId = 2, ShiftIndices = new() { 1, 4 } });

Assert.That(result.Success, Is.False);
// Per-shift error message should name the failing shift so the UI can
// show which one blocked the batch.
Assert.That(result.Message, Does.Contain("Shift 4"));

var rows = await TimePlanningPnDbContext.PlanRegistrationContentHandoverRequests
.Where(r => r.FromPlanRegistrationId == source.Id)
.ToListAsync();
Assert.That(rows, Is.Empty, "No rows should have been persisted (validation pre-flight blocks the whole batch)");
}

[Test]
public async Task CreateAsync_DifferentShifts_AreAllowed_SameShift_IsBlocked()
{
var date = new DateTime(2024, 2, 4);
var (source, _) = await SeedPartialShiftPairAsync(date);

var first = await _contentHandoverService.CreateAsync(source.Id,
new ContentHandoverRequestCreateModel { ToSdkSitId = 2, ShiftIndices = new() { 1 } });
Assert.That(first.Success, Is.True);

// Different shift: allowed.
var second = await _contentHandoverService.CreateAsync(source.Id,
new ContentHandoverRequestCreateModel { ToSdkSitId = 2, ShiftIndices = new() { 2 } });
Assert.That(second.Success, Is.True, second.Message);

// Same shift again: blocked.
var third = await _contentHandoverService.CreateAsync(source.Id,
new ContentHandoverRequestCreateModel { ToSdkSitId = 2, ShiftIndices = new() { 1 } });
Assert.That(third.Success, Is.False);
}

[Test]
public async Task CreateAsync_PendingFullDay_BlocksPartial_AndViceVersa()
{
var dateA = new DateTime(2024, 2, 5);
var (sourceA, _) = await SeedPartialShiftPairAsync(dateA);
var fullDay = await _contentHandoverService.CreateAsync(sourceA.Id,
new ContentHandoverRequestCreateModel { ToSdkSitId = 2 });
Assert.That(fullDay.Success, Is.True);

var partialBlocked = await _contentHandoverService.CreateAsync(sourceA.Id,
new ContentHandoverRequestCreateModel { ToSdkSitId = 2, ShiftIndices = new() { 1 } });
Assert.That(partialBlocked.Success, Is.False);

// Separate date to test the other direction.
var dateB = new DateTime(2024, 2, 6);
var (sourceB, _) = await SeedPartialShiftPairAsync(dateB);
var partial = await _contentHandoverService.CreateAsync(sourceB.Id,
new ContentHandoverRequestCreateModel { ToSdkSitId = 2, ShiftIndices = new() { 2 } });
Assert.That(partial.Success, Is.True);

var fullDayBlocked = await _contentHandoverService.CreateAsync(sourceB.Id,
new ContentHandoverRequestCreateModel { ToSdkSitId = 2 });
Assert.That(fullDayBlocked.Success, Is.False);
}

[Test]
public async Task AcceptAsync_SingleShift_MovesOnlyThatShift()
{
var date = new DateTime(2024, 2, 7);
var (source, target) = await SeedPartialShiftPairAsync(date);

var create = await _contentHandoverService.CreateAsync(source.Id,
new ContentHandoverRequestCreateModel { ToSdkSitId = 2, ShiftIndices = new() { 2 } });
Assert.That(create.Success, Is.True);

var requestId = create.Model[0].Id;
var accept = await _contentHandoverService.AcceptAsync(requestId, 2,
new ContentHandoverDecisionModel { DecisionComment = "ok" });
Assert.That(accept.Success, Is.True, accept.Message);

var s = await TimePlanningPnDbContext.PlanRegistrations.FindAsync(source.Id);
var t = await TimePlanningPnDbContext.PlanRegistrations.FindAsync(target.Id);

// Shift 2 moved
Assert.That(s.PlannedEndOfShift2, Is.EqualTo(0));
Assert.That(t.PlannedEndOfShift2, Is.EqualTo(17 * 60));
// Shift 1 and 3 untouched on source
Assert.That(s.PlannedEndOfShift1, Is.EqualTo(12 * 60));
Assert.That(s.PlannedEndOfShift3, Is.EqualTo(21 * 60));
}

[Test]
public async Task AcceptAllShifts_EquivalentToFullDay()
{
var date = new DateTime(2024, 2, 8);
var (source, target) = await SeedPartialShiftPairAsync(date);

var create = await _contentHandoverService.CreateAsync(source.Id,
new ContentHandoverRequestCreateModel { ToSdkSitId = 2, ShiftIndices = new() { 1, 2, 3 } });
Assert.That(create.Success, Is.True, create.Message);

foreach (var m in create.Model)
{
var res = await _contentHandoverService.AcceptAsync(m.Id, 2,
new ContentHandoverDecisionModel { DecisionComment = "ok" });
Assert.That(res.Success, Is.True, res.Message);
}

var s = await TimePlanningPnDbContext.PlanRegistrations.FindAsync(source.Id);
var t = await TimePlanningPnDbContext.PlanRegistrations.FindAsync(target.Id);

Assert.That(s.PlannedEndOfShift1, Is.EqualTo(0));
Assert.That(s.PlannedEndOfShift2, Is.EqualTo(0));
Assert.That(s.PlannedEndOfShift3, Is.EqualTo(0));
Assert.That(t.PlannedEndOfShift1, Is.EqualTo(12 * 60));
Assert.That(t.PlannedEndOfShift2, Is.EqualTo(17 * 60));
Assert.That(t.PlannedEndOfShift3, Is.EqualTo(21 * 60));
}

[Test]
public async Task RejectOneOfN_LeavesRemainingPending()
{
var date = new DateTime(2024, 2, 9);
var (source, _) = await SeedPartialShiftPairAsync(date);

var create = await _contentHandoverService.CreateAsync(source.Id,
new ContentHandoverRequestCreateModel { ToSdkSitId = 2, ShiftIndices = new() { 1, 2 } });
Assert.That(create.Success, Is.True);

var shift1Id = create.Model.Single(m => m.ShiftIndex == 1).Id;
var shift2Id = create.Model.Single(m => m.ShiftIndex == 2).Id;

var rej = await _contentHandoverService.RejectAsync(shift1Id, 2,
new ContentHandoverDecisionModel { DecisionComment = "no" });
Assert.That(rej.Success, Is.True);

var reloaded1 = await TimePlanningPnDbContext.PlanRegistrationContentHandoverRequests.FindAsync(shift1Id);
var reloaded2 = await TimePlanningPnDbContext.PlanRegistrationContentHandoverRequests.FindAsync(shift2Id);
Assert.That(reloaded1.Status, Is.EqualTo(HandoverRequestStatus.Rejected));
Assert.That(reloaded2.Status, Is.EqualTo(HandoverRequestStatus.Pending));

// Shift 1 still on source (not moved).
var s = await TimePlanningPnDbContext.PlanRegistrations.FindAsync(source.Id);
Assert.That(s.PlannedEndOfShift1, Is.EqualTo(12 * 60));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ public async Task CreateContentHandover_Success_ReturnsModel()
_service.CreateAsync(
Arg.Any<int>(),
Arg.Any<ContentHandoverRequestCreateModel>())
.Returns(new OperationDataResult<CsContentHandoverRequestModel>(true, csModel));
.Returns(new OperationDataResult<List<CsContentHandoverRequestModel>>(
true, new List<CsContentHandoverRequestModel> { csModel }));

var request = new CreateContentHandoverRequest
{
Expand All @@ -64,6 +65,9 @@ public async Task CreateContentHandover_Success_ReturnsModel()
Assert.That(response.Success, Is.True);
Assert.That(response.Model, Is.Not.Null);
Assert.That(response.Model.Id, Is.EqualTo(42));
// New contract: `models` is populated alongside the defensive `model`.
Assert.That(response.Models, Has.Count.EqualTo(1));
Assert.That(response.Models[0].Id, Is.EqualTo(42));
Assert.That(response.Model.FromSdkSiteId, Is.EqualTo(10));
Assert.That(response.Model.ToSdkSiteId, Is.EqualTo(20));
Assert.That(response.Model.Date, Is.EqualTo("2026-04-03T00:00:00"));
Expand All @@ -82,7 +86,7 @@ public async Task CreateContentHandover_Failure_ReturnsError()
_service.CreateAsync(
Arg.Any<int>(),
Arg.Any<ContentHandoverRequestCreateModel>())
.Returns(new OperationDataResult<CsContentHandoverRequestModel>(false, "Something went wrong"));
.Returns(new OperationDataResult<List<CsContentHandoverRequestModel>>(false, "Something went wrong"));

var request = new CreateContentHandoverRequest
{
Expand Down Expand Up @@ -235,4 +239,104 @@ public async Task GetMyContentHandovers_ReturnsList()
Assert.That(response.Models[0].Id, Is.EqualTo(5));
Assert.That(response.Models[0].Status, Is.EqualTo("Accepted"));
}

[Test]
public async Task CreateContentHandover_FansOutRepeatedModels_WhenShiftIndicesProvided()
{
var csModels = new List<CsContentHandoverRequestModel>
{
new()
{
Id = 101,
FromSdkSitId = 10,
ToSdkSitId = 20,
Date = new DateTime(2026, 4, 3),
FromPlanRegistrationId = 100,
ToPlanRegistrationId = 200,
Status = "Pending",
RequestedAtUtc = new DateTime(2026, 4, 3, 12, 0, 0),
RequestComment = "Partial 1",
ShiftIndex = 1
},
new()
{
Id = 102,
FromSdkSitId = 10,
ToSdkSitId = 20,
Date = new DateTime(2026, 4, 3),
FromPlanRegistrationId = 100,
ToPlanRegistrationId = 200,
Status = "Pending",
RequestedAtUtc = new DateTime(2026, 4, 3, 12, 0, 0),
RequestComment = "Partial 2",
ShiftIndex = 2
}
};

ContentHandoverRequestCreateModel? captured = null;
_service.CreateAsync(
Arg.Any<int>(),
Arg.Do<ContentHandoverRequestCreateModel>(m => captured = m))
.Returns(new OperationDataResult<List<CsContentHandoverRequestModel>>(true, csModels));

var request = new CreateContentHandoverRequest
{
FromPlanRegistrationId = 100,
ToSdkSiteId = 20,
RequestComment = "Partial handover",
ShiftIndices = { 1, 2 }
};

var response = await _grpcService.CreateContentHandover(
request, TestServerCallContextFactory.Create());

Assert.That(response.Success, Is.True);
Assert.That(captured, Is.Not.Null);
Assert.That(captured!.ShiftIndices, Is.EquivalentTo(new[] { 1, 2 }));

Assert.That(response.Models, Has.Count.EqualTo(2));
Assert.That(response.Models[0].Id, Is.EqualTo(101));
Assert.That(response.Models[0].ShiftIndex, Is.EqualTo(1));
Assert.That(response.Models[1].Id, Is.EqualTo(102));
Assert.That(response.Models[1].ShiftIndex, Is.EqualTo(2));

// `model` stays populated defensively (first item) for legacy clients.
Assert.That(response.Model, Is.Not.Null);
Assert.That(response.Model.Id, Is.EqualTo(101));
}

[Test]
public async Task GetHandoverEligibleCoworkers_PassesShiftIndicesToService()
{
List<int>? capturedShiftIndices = null;
DateTime capturedDate = default;

_service.GetHandoverEligibleCoworkersAsync(
Arg.Do<DateTime>(d => capturedDate = d),
Arg.Do<List<int>>(s => capturedShiftIndices = s))
.Returns(new OperationDataResult<List<HandoverCoworkerModel>>(
true, new List<HandoverCoworkerModel>
{
new() { SdkSiteId = 20, SiteName = "Alice", PlanRegistrationId = 200 }
}));

var request = new GetHandoverEligibleCoworkersRequest
{
Date = "2026-04-03T00:00:00Z",
ShiftIndices = { 0, 2 }
};

var response = await _grpcService.GetHandoverEligibleCoworkers(
request, TestServerCallContextFactory.Create());

Assert.That(response.Success, Is.True);
Assert.That(capturedShiftIndices, Is.Not.Null);
Assert.That(capturedShiftIndices!, Is.EquivalentTo(new[] { 0, 2 }));
Assert.That(capturedDate.Date, Is.EqualTo(new DateTime(2026, 4, 3)));
Assert.That(response.Coworkers, Has.Count.EqualTo(1));
Assert.That(response.Coworkers[0].SdkSiteId, Is.EqualTo(20));

await _service.Received(1).GetHandoverEligibleCoworkersAsync(
Arg.Any<DateTime>(), Arg.Any<List<int>>());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ public class ContentHandoverController(IContentHandoverService contentHandoverSe

[HttpPost]
[Route("plan-registrations/{id}/handover-requests")]
public async Task<OperationDataResult<ContentHandoverRequestModel>> Create(
int id,
public async Task<OperationDataResult<System.Collections.Generic.List<ContentHandoverRequestModel>>> Create(
int id,
[FromBody] ContentHandoverRequestCreateModel model)
{
return await _contentHandoverService.CreateAsync(id, model);
Expand Down
Loading
Loading