From 6bfb56f3491e27ca9d72342dcc3e440d21f55d84 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Fri, 25 Sep 2026 12:04:40 -0300 Subject: [PATCH] fix(data): stamp audit times in UTC and keep a caller-set work order creation time WorkOrderDataService.AddAsync overwrote the UTC CreatedDate set by WorkOrderService with local server time, offsetting the SLA response clock on any host not running in UTC. Data services now stamp CreatedDate, LastModificationTime and DeletionTime with DateTime.UtcNow, and a work order keeps the creation time its caller set. --- .../WorkOrderDataServiceTimestampTests.cs | 68 +++++++++++++++++++ .../Implementation/AccountDataService.cs | 4 +- .../Implementation/AssetDataService.cs | 4 +- .../Implementation/CategoryDataService.cs | 4 +- .../Implementation/ContactDataService.cs | 4 +- .../Implementation/DispatchDataService.cs | 4 +- .../Implementation/EmployeeDataService.cs | 4 +- .../Implementation/FollowUpDataService.cs | 10 +-- .../Implementation/LocationDataService.cs | 10 +-- .../Implementation/PMScheduleDataService.cs | 4 +- .../Implementation/WorkOrderDataService.cs | 4 +- 11 files changed, 94 insertions(+), 26 deletions(-) create mode 100644 Api.SeaHavenIndustries.Tests/WorkOrderDataServiceTimestampTests.cs diff --git a/Api.SeaHavenIndustries.Tests/WorkOrderDataServiceTimestampTests.cs b/Api.SeaHavenIndustries.Tests/WorkOrderDataServiceTimestampTests.cs new file mode 100644 index 0000000..512ad0e --- /dev/null +++ b/Api.SeaHavenIndustries.Tests/WorkOrderDataServiceTimestampTests.cs @@ -0,0 +1,68 @@ +using Data.SeaHavenIndustries; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using SeaHaven.DataServices.Implementation; +using Xunit; + +namespace Api.SeaHavenIndustries.Tests; + +/// +/// The SLA clock and every "created"/"modified" display read these stamps as UTC, so the data layer +/// must never write local server time into them. +/// +public class WorkOrderDataServiceTimestampTests +{ + private static ApplicationDbContext NewContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new ApplicationDbContext(options); + } + + private static WorkOrder NewWorkOrder() => + new() { InternalWONumber = "WO-1", WorkerOrderTitle = "Leak", LocationId = 100 }; + + [Fact] + public async Task AddAsync_KeepsTheCreationTimeTheCallerSet() + { + await using var context = NewContext(); + var createdAt = new DateTime(2026, 9, 25, 14, 0, 0, DateTimeKind.Utc); + var workOrder = NewWorkOrder(); + workOrder.CreatedDate = createdAt; + + var saved = await new WorkOrderDataService(context).AddAsync(workOrder); + + saved.CreatedDate.Should().Be(createdAt); + saved.CreatedDate!.Value.Kind.Should().Be(DateTimeKind.Utc); + (await context.workOrders.SingleAsync()).CreatedDate.Should().Be(createdAt); + } + + [Fact] + public async Task AddAsync_StampsUtcWhenTheCallerSetNoCreationTime() + { + await using var context = NewContext(); + var before = DateTime.UtcNow; + + var saved = await new WorkOrderDataService(context).AddAsync(NewWorkOrder()); + + saved.CreatedDate.Should().NotBeNull(); + saved.CreatedDate!.Value.Kind.Should().Be(DateTimeKind.Utc); + saved.CreatedDate.Value.Should().BeOnOrAfter(before).And.BeOnOrBefore(DateTime.UtcNow); + } + + [Fact] + public async Task UpdateAsync_StampsTheModificationTimeInUtc() + { + await using var context = NewContext(); + var service = new WorkOrderDataService(context); + var saved = await service.AddAsync(NewWorkOrder()); + var before = DateTime.UtcNow; + + await service.UpdateAsync(saved); + + saved.LastModificationTime.Should().NotBeNull(); + saved.LastModificationTime!.Value.Kind.Should().Be(DateTimeKind.Utc); + saved.LastModificationTime.Value.Should().BeOnOrAfter(before).And.BeOnOrBefore(DateTime.UtcNow); + } +} diff --git a/SeaHaven.DataServices/Implementation/AccountDataService.cs b/SeaHaven.DataServices/Implementation/AccountDataService.cs index 5b00af2..ec2f874 100644 --- a/SeaHaven.DataServices/Implementation/AccountDataService.cs +++ b/SeaHaven.DataServices/Implementation/AccountDataService.cs @@ -64,7 +64,7 @@ namespace SeaHaven.DataServices.Implementation public async Task AddAsync(Accounts account) { - account.CreatedDate = DateTime.Now; + account.CreatedDate = DateTime.UtcNow; await _context.Accounts.AddAsync(account); await _context.SaveChangesAsync(); return account; @@ -72,7 +72,7 @@ namespace SeaHaven.DataServices.Implementation public async Task UpdateAsync(Accounts account) { - account.LastModificationTime = DateTime.Now; + account.LastModificationTime = DateTime.UtcNow; _context.Accounts.Update(account); await _context.SaveChangesAsync(); } diff --git a/SeaHaven.DataServices/Implementation/AssetDataService.cs b/SeaHaven.DataServices/Implementation/AssetDataService.cs index 93a3f1e..1d60a7d 100644 --- a/SeaHaven.DataServices/Implementation/AssetDataService.cs +++ b/SeaHaven.DataServices/Implementation/AssetDataService.cs @@ -72,7 +72,7 @@ namespace SeaHaven.DataServices.Implementation public async Task AddAsync(Assets asset) { - asset.CreatedDate = DateTime.Now; + asset.CreatedDate = DateTime.UtcNow; await _context.Assets.AddAsync(asset); await _context.SaveChangesAsync(); return asset; @@ -80,7 +80,7 @@ namespace SeaHaven.DataServices.Implementation public async Task UpdateAsync(Assets asset) { - asset.LastModificationTime = DateTime.Now; + asset.LastModificationTime = DateTime.UtcNow; _context.Assets.Update(asset); await _context.SaveChangesAsync(); } diff --git a/SeaHaven.DataServices/Implementation/CategoryDataService.cs b/SeaHaven.DataServices/Implementation/CategoryDataService.cs index c5af096..994acc3 100644 --- a/SeaHaven.DataServices/Implementation/CategoryDataService.cs +++ b/SeaHaven.DataServices/Implementation/CategoryDataService.cs @@ -25,7 +25,7 @@ namespace SeaHaven.DataServices.Implementation public async Task AddAsync(Category category) { - category.CreatedDate = DateTime.Now; + category.CreatedDate = DateTime.UtcNow; await _context.Categories.AddAsync(category); await _context.SaveChangesAsync(); return category; @@ -33,7 +33,7 @@ namespace SeaHaven.DataServices.Implementation public async Task UpdateAsync(Category category) { - category.LastModificationTime = DateTime.Now; + category.LastModificationTime = DateTime.UtcNow; _context.Categories.Update(category); await _context.SaveChangesAsync(); } diff --git a/SeaHaven.DataServices/Implementation/ContactDataService.cs b/SeaHaven.DataServices/Implementation/ContactDataService.cs index 8196d2b..e4abf4f 100644 --- a/SeaHaven.DataServices/Implementation/ContactDataService.cs +++ b/SeaHaven.DataServices/Implementation/ContactDataService.cs @@ -100,7 +100,7 @@ namespace SeaHaven.DataServices.Implementation public async Task AddAsync(Contacts contact) { - contact.CreatedDate = DateTime.Now; + contact.CreatedDate = DateTime.UtcNow; await _context.Contacts.AddAsync(contact); await _context.SaveChangesAsync(); return contact; @@ -108,7 +108,7 @@ namespace SeaHaven.DataServices.Implementation public async Task UpdateAsync(Contacts contact) { - contact.LastModificationTime = DateTime.Now; + contact.LastModificationTime = DateTime.UtcNow; _context.Contacts.Update(contact); await _context.SaveChangesAsync(); } diff --git a/SeaHaven.DataServices/Implementation/DispatchDataService.cs b/SeaHaven.DataServices/Implementation/DispatchDataService.cs index 3274713..398051d 100644 --- a/SeaHaven.DataServices/Implementation/DispatchDataService.cs +++ b/SeaHaven.DataServices/Implementation/DispatchDataService.cs @@ -85,7 +85,7 @@ namespace SeaHaven.DataServices.Implementation public async Task AddAsync(Dispatch dispatch) { - dispatch.CreatedDate = DateTime.Now; + dispatch.CreatedDate = DateTime.UtcNow; await _context.Dispatches.AddAsync(dispatch); await _context.SaveChangesAsync(); return dispatch; @@ -93,7 +93,7 @@ namespace SeaHaven.DataServices.Implementation public async Task UpdateAsync(Dispatch dispatch) { - dispatch.LastModificationTime = DateTime.Now; + dispatch.LastModificationTime = DateTime.UtcNow; _context.Dispatches.Update(dispatch); await _context.SaveChangesAsync(); } diff --git a/SeaHaven.DataServices/Implementation/EmployeeDataService.cs b/SeaHaven.DataServices/Implementation/EmployeeDataService.cs index 4606d11..7b0b2c7 100644 --- a/SeaHaven.DataServices/Implementation/EmployeeDataService.cs +++ b/SeaHaven.DataServices/Implementation/EmployeeDataService.cs @@ -72,7 +72,7 @@ namespace SeaHaven.DataServices.Implementation public async Task AddAsync(Employee employee) { - employee.CreatedDate = DateTime.Now; + employee.CreatedDate = DateTime.UtcNow; await _context.Employees.AddAsync(employee); await _context.SaveChangesAsync(); return employee; @@ -80,7 +80,7 @@ namespace SeaHaven.DataServices.Implementation public async Task UpdateAsync(Employee employee) { - employee.LastModificationTime = DateTime.Now; + employee.LastModificationTime = DateTime.UtcNow; _context.Employees.Update(employee); await _context.SaveChangesAsync(); } diff --git a/SeaHaven.DataServices/Implementation/FollowUpDataService.cs b/SeaHaven.DataServices/Implementation/FollowUpDataService.cs index 4eebe82..72ef131 100644 --- a/SeaHaven.DataServices/Implementation/FollowUpDataService.cs +++ b/SeaHaven.DataServices/Implementation/FollowUpDataService.cs @@ -88,7 +88,7 @@ namespace SeaHaven.DataServices.Implementation public async Task AddAsync(FollowUps followUp) { - followUp.CreatedDate = DateTime.Now; + followUp.CreatedDate = DateTime.UtcNow; await _context.FollowUps.AddAsync(followUp); await _context.SaveChangesAsync(); return followUp; @@ -96,7 +96,7 @@ namespace SeaHaven.DataServices.Implementation public async Task UpdateAsync(FollowUps followUp) { - followUp.LastModificationTime = DateTime.Now; + followUp.LastModificationTime = DateTime.UtcNow; _context.FollowUps.Update(followUp); await _context.SaveChangesAsync(); } @@ -218,7 +218,7 @@ namespace SeaHaven.DataServices.Implementation public async Task AddAsync(FollowUps followUp, CancellationToken cancellationToken) { - followUp.CreatedDate = DateTime.Now; + followUp.CreatedDate = DateTime.UtcNow; await _context.FollowUps.AddAsync(followUp, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return followUp; @@ -226,7 +226,7 @@ namespace SeaHaven.DataServices.Implementation public async Task UpdateAsync(FollowUps followUp, CancellationToken cancellationToken) { - followUp.LastModificationTime = DateTime.Now; + followUp.LastModificationTime = DateTime.UtcNow; _context.FollowUps.Update(followUp); await _context.SaveChangesAsync(cancellationToken); } @@ -238,7 +238,7 @@ namespace SeaHaven.DataServices.Implementation return false; entity.Status = status; - entity.LastModificationTime = DateTime.Now; + entity.LastModificationTime = DateTime.UtcNow; await _context.SaveChangesAsync(cancellationToken); return true; } diff --git a/SeaHaven.DataServices/Implementation/LocationDataService.cs b/SeaHaven.DataServices/Implementation/LocationDataService.cs index 11cb89e..b331a01 100644 --- a/SeaHaven.DataServices/Implementation/LocationDataService.cs +++ b/SeaHaven.DataServices/Implementation/LocationDataService.cs @@ -98,7 +98,7 @@ namespace SeaHaven.DataServices.Implementation public async Task AddAsync(Locations location) { - location.CreatedDate = DateTime.Now; + location.CreatedDate = DateTime.UtcNow; await _context.Locations.AddAsync(location); await _context.SaveChangesAsync(); return location; @@ -106,7 +106,7 @@ namespace SeaHaven.DataServices.Implementation public async Task UpdateAsync(Locations location) { - location.LastModificationTime = DateTime.Now; + location.LastModificationTime = DateTime.UtcNow; _context.Locations.Update(location); await _context.SaveChangesAsync(); } @@ -228,7 +228,7 @@ namespace SeaHaven.DataServices.Implementation public async Task AddAsync(Locations location, CancellationToken cancellationToken) { - location.CreatedDate = DateTime.Now; + location.CreatedDate = DateTime.UtcNow; await _context.Locations.AddAsync(location, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return location; @@ -236,7 +236,7 @@ namespace SeaHaven.DataServices.Implementation public async Task UpdateAsync(Locations location, CancellationToken cancellationToken) { - location.LastModificationTime = DateTime.Now; + location.LastModificationTime = DateTime.UtcNow; _context.Locations.Update(location); await _context.SaveChangesAsync(cancellationToken); } @@ -249,7 +249,7 @@ namespace SeaHaven.DataServices.Implementation if (entity == null) return false; - var now = DateTime.Now; + var now = DateTime.UtcNow; foreach (var contact in entity.Contacts ?? Enumerable.Empty()) { if (contact.IsDeleted != true) diff --git a/SeaHaven.DataServices/Implementation/PMScheduleDataService.cs b/SeaHaven.DataServices/Implementation/PMScheduleDataService.cs index a2418ea..6bc925a 100644 --- a/SeaHaven.DataServices/Implementation/PMScheduleDataService.cs +++ b/SeaHaven.DataServices/Implementation/PMScheduleDataService.cs @@ -64,7 +64,7 @@ namespace SeaHaven.DataServices.Implementation public async Task AddAsync(PMSchedules schedule) { - schedule.CreatedDate = DateTime.Now; + schedule.CreatedDate = DateTime.UtcNow; await _context.PMSchedules.AddAsync(schedule); await _context.SaveChangesAsync(); return schedule; @@ -72,7 +72,7 @@ namespace SeaHaven.DataServices.Implementation public async Task UpdateAsync(PMSchedules schedule) { - schedule.LastModificationTime = DateTime.Now; + schedule.LastModificationTime = DateTime.UtcNow; _context.PMSchedules.Update(schedule); await _context.SaveChangesAsync(); } diff --git a/SeaHaven.DataServices/Implementation/WorkOrderDataService.cs b/SeaHaven.DataServices/Implementation/WorkOrderDataService.cs index d32b77b..b34db11 100644 --- a/SeaHaven.DataServices/Implementation/WorkOrderDataService.cs +++ b/SeaHaven.DataServices/Implementation/WorkOrderDataService.cs @@ -193,7 +193,7 @@ namespace SeaHaven.DataServices.Implementation public async Task AddAsync(WorkOrder workOrder) { - workOrder.CreatedDate = DateTime.Now; + workOrder.CreatedDate ??= DateTime.UtcNow; await _context.workOrders.AddAsync(workOrder); await _context.SaveChangesAsync(); return workOrder; @@ -201,7 +201,7 @@ namespace SeaHaven.DataServices.Implementation public async Task UpdateAsync(WorkOrder workOrder) { - workOrder.LastModificationTime = DateTime.Now; + workOrder.LastModificationTime = DateTime.UtcNow; _context.workOrders.Update(workOrder); await _context.SaveChangesAsync(); }