mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-05 03:42:12 +00:00
Merge pull request #177 from Sea-Haven-Industries/fix/ab/sh-397-uplift-decision-audit
fix(uplifts): let admins decide uplifts whose dispatch has no work order
This commit is contained in:
commit
50e5553e57
4 changed files with 371 additions and 10 deletions
|
|
@ -681,6 +681,8 @@ public sealed class UpliftWorkflowTests
|
||||||
var upliftData = new Mock<IUpliftDataService>();
|
var upliftData = new Mock<IUpliftDataService>();
|
||||||
upliftData.Setup(u => u.GetDueForExpiryAsync(It.IsAny<DateTime>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
upliftData.Setup(u => u.GetDueForExpiryAsync(It.IsAny<DateTime>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync(new List<DispatchUpliftRequest> { orphanReq, validReq });
|
.ReturnsAsync(new List<DispatchUpliftRequest> { orphanReq, validReq });
|
||||||
|
upliftData.Setup(u => u.GetWorkOrderIdForUpliftAsync(202, It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(99);
|
||||||
upliftData.Setup(u => u.SaveChangesAsync(It.IsAny<CancellationToken>()))
|
upliftData.Setup(u => u.SaveChangesAsync(It.IsAny<CancellationToken>()))
|
||||||
.Returns(Task.CompletedTask);
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,9 @@ namespace SeaHaven.Services.Implementation
|
||||||
req.DecidedAt = now;
|
req.DecidedAt = now;
|
||||||
req.LastModificationTime = now;
|
req.LastModificationTime = now;
|
||||||
|
|
||||||
await _dispatchData.StageAuditLogAsync(new WorkOrderAuditLog
|
await StageAuditAsync(req.Id, workOrderId => new WorkOrderAuditLog
|
||||||
{
|
{
|
||||||
WorkOrderId = dispatch?.WorkOrderId ?? 0,
|
WorkOrderId = workOrderId,
|
||||||
FieldName = $"Dispatch {dispatch?.DispatchNumber} Uplift",
|
FieldName = $"Dispatch {dispatch?.DispatchNumber} Uplift",
|
||||||
OldValue = previous,
|
OldValue = previous,
|
||||||
NewValue = UpliftStatus.Expired,
|
NewValue = UpliftStatus.Expired,
|
||||||
|
|
@ -136,9 +136,9 @@ namespace SeaHaven.Services.Implementation
|
||||||
req.EscalatedAt = now;
|
req.EscalatedAt = now;
|
||||||
req.LastModificationTime = now;
|
req.LastModificationTime = now;
|
||||||
|
|
||||||
await _dispatchData.StageAuditLogAsync(new WorkOrderAuditLog
|
await StageAuditAsync(req.Id, workOrderId => new WorkOrderAuditLog
|
||||||
{
|
{
|
||||||
WorkOrderId = dispatch.WorkOrderId ?? 0,
|
WorkOrderId = workOrderId,
|
||||||
FieldName = $"Dispatch {dispatch.DispatchNumber} Uplift",
|
FieldName = $"Dispatch {dispatch.DispatchNumber} Uplift",
|
||||||
OldValue = "Pending",
|
OldValue = "Pending",
|
||||||
NewValue = "Escalated",
|
NewValue = "Escalated",
|
||||||
|
|
@ -247,5 +247,19 @@ namespace SeaHaven.Services.Implementation
|
||||||
req.LastModificationTime = now;
|
req.LastModificationTime = now;
|
||||||
await _upliftData.SaveChangesAsync(cancellationToken);
|
await _upliftData.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The audit goes to the work order the uplift's dispatch belongs to (owner, else the
|
||||||
|
// one it is linked to). With neither, the status change is recorded on the request
|
||||||
|
// alone; a work-order id of 0 would fail the required audit foreign key and abort
|
||||||
|
// the sweep on every run.
|
||||||
|
private async Task StageAuditAsync(
|
||||||
|
int upliftId,
|
||||||
|
Func<int, WorkOrderAuditLog> buildEntry,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var workOrderId = await _upliftData.GetWorkOrderIdForUpliftAsync(upliftId, cancellationToken);
|
||||||
|
if (workOrderId is int resolved)
|
||||||
|
await _dispatchData.StageAuditLogAsync(buildEntry(resolved), cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -166,9 +166,9 @@ namespace SeaHaven.Services.Implementation
|
||||||
req.DecisionNote = string.IsNullOrWhiteSpace(note) ? null : note!.Trim();
|
req.DecisionNote = string.IsNullOrWhiteSpace(note) ? null : note!.Trim();
|
||||||
req.LastModificationTime = now;
|
req.LastModificationTime = now;
|
||||||
|
|
||||||
await _dispatchData.StageAuditLogAsync(new WorkOrderAuditLog
|
await StageDecisionAuditAsync(req.Id, workOrderId => new WorkOrderAuditLog
|
||||||
{
|
{
|
||||||
WorkOrderId = dispatch.WorkOrderId ?? 0,
|
WorkOrderId = workOrderId,
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
FieldName = $"Dispatch {dispatch.DispatchNumber} NTE",
|
FieldName = $"Dispatch {dispatch.DispatchNumber} NTE",
|
||||||
OldValue = $"${oldNTE:F2}",
|
OldValue = $"${oldNTE:F2}",
|
||||||
|
|
@ -252,9 +252,9 @@ namespace SeaHaven.Services.Implementation
|
||||||
req.DecisionNote = note!.Trim();
|
req.DecisionNote = note!.Trim();
|
||||||
req.LastModificationTime = now;
|
req.LastModificationTime = now;
|
||||||
|
|
||||||
await _dispatchData.StageAuditLogAsync(new WorkOrderAuditLog
|
await StageDecisionAuditAsync(req.Id, workOrderId => new WorkOrderAuditLog
|
||||||
{
|
{
|
||||||
WorkOrderId = dispatch?.WorkOrderId ?? 0,
|
WorkOrderId = workOrderId,
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
FieldName = $"Dispatch {dispatch?.DispatchNumber} Uplift",
|
FieldName = $"Dispatch {dispatch?.DispatchNumber} Uplift",
|
||||||
OldValue = previous,
|
OldValue = previous,
|
||||||
|
|
@ -290,9 +290,9 @@ namespace SeaHaven.Services.Implementation
|
||||||
req.DecisionNote = note.Trim();
|
req.DecisionNote = note.Trim();
|
||||||
req.LastModificationTime = now;
|
req.LastModificationTime = now;
|
||||||
|
|
||||||
await _dispatchData.StageAuditLogAsync(new WorkOrderAuditLog
|
await StageDecisionAuditAsync(req.Id, workOrderId => new WorkOrderAuditLog
|
||||||
{
|
{
|
||||||
WorkOrderId = dispatch?.WorkOrderId ?? 0,
|
WorkOrderId = workOrderId,
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
FieldName = $"Dispatch {dispatch?.DispatchNumber} Uplift",
|
FieldName = $"Dispatch {dispatch?.DispatchNumber} Uplift",
|
||||||
OldValue = previous,
|
OldValue = previous,
|
||||||
|
|
@ -307,6 +307,19 @@ namespace SeaHaven.Services.Implementation
|
||||||
|
|
||||||
public bool CanApprove(ClaimsPrincipal user, int tier) => UserCanApprove(user, tier);
|
public bool CanApprove(ClaimsPrincipal user, int tier) => UserCanApprove(user, tier);
|
||||||
|
|
||||||
|
// The decision audit goes to the work order the uplift's dispatch belongs to (owner,
|
||||||
|
// else the one it is linked to). A dispatch with neither has no work order to audit
|
||||||
|
// against, so the decision is recorded on the request alone.
|
||||||
|
private async Task StageDecisionAuditAsync(
|
||||||
|
int upliftId,
|
||||||
|
Func<int, WorkOrderAuditLog> buildEntry,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var workOrderId = await _upliftData.GetWorkOrderIdForUpliftAsync(upliftId, cancellationToken);
|
||||||
|
if (workOrderId is int resolved)
|
||||||
|
await _dispatchData.StageAuditLogAsync(buildEntry(resolved), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
// SH-101: authorized internal download of a Passed UpliftEvidence file. The data
|
// SH-101: authorized internal download of a Passed UpliftEvidence file. The data
|
||||||
// service resolves the document by the request's own linkage (no client-supplied
|
// service resolves the document by the request's own linkage (no client-supplied
|
||||||
// document id or vendor/public path). Only UpliftEvidence documents are exposed;
|
// document id or vendor/public path). Only UpliftEvidence documents are exposed;
|
||||||
|
|
|
||||||
332
SeaHavenIndustries.Tests/UpliftDecisionAuditRelationalTests.cs
Normal file
332
SeaHavenIndustries.Tests/UpliftDecisionAuditRelationalTests.cs
Normal file
|
|
@ -0,0 +1,332 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using SeaHaven.DataServices.Implementation;
|
||||||
|
using SeaHaven.Services.Configuration;
|
||||||
|
using SeaHaven.Services.Implementation;
|
||||||
|
using SeaHaven.Services.Interfaces;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace SeaHavenIndustries.Tests;
|
||||||
|
|
||||||
|
// Relational (SQLite, foreign keys on) coverage for the work-order audit entry written by
|
||||||
|
// uplift decisions. WorkOrderAuditLogs.WorkOrderId is a required FK, so the in-memory
|
||||||
|
// provider cannot prove a decision on a dispatch without an owning work order commits.
|
||||||
|
// The audit goes to the work order the dispatch belongs to (owner, else the one it is
|
||||||
|
// linked to through DispatchWorkOrders); when none resolves, the decision is still saved
|
||||||
|
// and no audit row is written.
|
||||||
|
public sealed class UpliftDecisionAuditRelationalTests
|
||||||
|
{
|
||||||
|
private const string AdminId = "admin-1";
|
||||||
|
|
||||||
|
public enum Ownership
|
||||||
|
{
|
||||||
|
Owned,
|
||||||
|
Linked,
|
||||||
|
Unresolvable
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Decision
|
||||||
|
{
|
||||||
|
Approve,
|
||||||
|
Reject,
|
||||||
|
RequestChanges
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(Decision.Approve, Ownership.Owned)]
|
||||||
|
[InlineData(Decision.Approve, Ownership.Linked)]
|
||||||
|
[InlineData(Decision.Approve, Ownership.Unresolvable)]
|
||||||
|
[InlineData(Decision.Reject, Ownership.Owned)]
|
||||||
|
[InlineData(Decision.Reject, Ownership.Linked)]
|
||||||
|
[InlineData(Decision.Reject, Ownership.Unresolvable)]
|
||||||
|
[InlineData(Decision.RequestChanges, Ownership.Owned)]
|
||||||
|
[InlineData(Decision.RequestChanges, Ownership.Linked)]
|
||||||
|
[InlineData(Decision.RequestChanges, Ownership.Unresolvable)]
|
||||||
|
public async Task Decision_CommitsAndAuditsResolvedWorkOrder(Decision decision, Ownership ownership)
|
||||||
|
{
|
||||||
|
await using var connection = new SqliteConnection("Data Source=:memory:;Foreign Keys=True");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>().UseSqlite(connection).Options;
|
||||||
|
|
||||||
|
int upliftId;
|
||||||
|
int? expectedWorkOrderId;
|
||||||
|
await using (var seed = new SqliteUpliftDecisionTestDbContext(options))
|
||||||
|
{
|
||||||
|
await seed.Database.EnsureCreatedAsync();
|
||||||
|
(upliftId, expectedWorkOrderId) = await SeedAsync(seed, ownership, expiresAt: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
await using (var context = new SqliteUpliftDecisionTestDbContext(options))
|
||||||
|
{
|
||||||
|
var service = new UpliftService(
|
||||||
|
new UpliftDataService(context),
|
||||||
|
new DispatchDataService(context),
|
||||||
|
new NoDocumentStorage(),
|
||||||
|
TimeProvider.System,
|
||||||
|
Options.Create(new ApprovalsOptions()));
|
||||||
|
var admin = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, AdminId),
|
||||||
|
new Claim(ClaimTypes.Role, "Admin")
|
||||||
|
}, "Test"));
|
||||||
|
|
||||||
|
switch (decision)
|
||||||
|
{
|
||||||
|
case Decision.Approve:
|
||||||
|
await service.ApproveAsync(admin, upliftId, null, CancellationToken.None);
|
||||||
|
break;
|
||||||
|
case Decision.Reject:
|
||||||
|
await service.RejectAsync(admin, upliftId, "too high", CancellationToken.None);
|
||||||
|
break;
|
||||||
|
case Decision.RequestChanges:
|
||||||
|
await service.RequestChangesAsync(admin, upliftId, "send a quote", CancellationToken.None);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var verify = new SqliteUpliftDecisionTestDbContext(options);
|
||||||
|
var saved = await verify.DispatchUpliftRequests.AsNoTracking().SingleAsync(u => u.Id == upliftId);
|
||||||
|
var expectedStatus = decision switch
|
||||||
|
{
|
||||||
|
Decision.Approve => UpliftStatus.Approved,
|
||||||
|
Decision.Reject => UpliftStatus.Rejected,
|
||||||
|
_ => UpliftStatus.ChangesRequested
|
||||||
|
};
|
||||||
|
Assert.Equal(expectedStatus, saved.Status);
|
||||||
|
Assert.Equal(AdminId, saved.DecidedByUserId);
|
||||||
|
if (decision == Decision.Approve)
|
||||||
|
{
|
||||||
|
var dispatch = await verify.Dispatches.AsNoTracking().SingleAsync(d => d.Id == saved.DispatchId);
|
||||||
|
Assert.Equal(5_000m, dispatch.NTEAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
var audits = await verify.WorkOrderAuditLogs.AsNoTracking().ToListAsync();
|
||||||
|
Assert.DoesNotContain(audits, a => a.WorkOrderId == 0);
|
||||||
|
if (expectedWorkOrderId is int workOrderId)
|
||||||
|
{
|
||||||
|
var audit = Assert.Single(audits);
|
||||||
|
Assert.Equal(workOrderId, audit.WorkOrderId);
|
||||||
|
Assert.StartsWith("uplift_", audit.Action);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Empty(audits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(Ownership.Linked)]
|
||||||
|
[InlineData(Ownership.Unresolvable)]
|
||||||
|
public async Task Expiry_CommitsAndAuditsResolvedWorkOrder(Ownership ownership)
|
||||||
|
{
|
||||||
|
await using var connection = new SqliteConnection("Data Source=:memory:;Foreign Keys=True");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>().UseSqlite(connection).Options;
|
||||||
|
|
||||||
|
int upliftId;
|
||||||
|
int? expectedWorkOrderId;
|
||||||
|
await using (var seed = new SqliteUpliftDecisionTestDbContext(options))
|
||||||
|
{
|
||||||
|
await seed.Database.EnsureCreatedAsync();
|
||||||
|
(upliftId, expectedWorkOrderId) = await SeedAsync(seed, ownership, expiresAt: DateTime.UtcNow.AddHours(-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
await using (var context = new SqliteUpliftDecisionTestDbContext(options))
|
||||||
|
{
|
||||||
|
var lifecycle = new UpliftLifecycleService(
|
||||||
|
new UpliftDataService(context),
|
||||||
|
new DispatchDataService(context),
|
||||||
|
null!,
|
||||||
|
new NoEmailSender(),
|
||||||
|
Options.Create(new FrontendOptions()),
|
||||||
|
Options.Create(new ApprovalsOptions()),
|
||||||
|
TimeProvider.System,
|
||||||
|
NullLogger<UpliftLifecycleService>.Instance);
|
||||||
|
|
||||||
|
Assert.Equal(1, await lifecycle.ExpireDueAsync(CancellationToken.None));
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var verify = new SqliteUpliftDecisionTestDbContext(options);
|
||||||
|
var saved = await verify.DispatchUpliftRequests.AsNoTracking().SingleAsync(u => u.Id == upliftId);
|
||||||
|
Assert.Equal(UpliftStatus.Expired, saved.Status);
|
||||||
|
|
||||||
|
var audits = await verify.WorkOrderAuditLogs.AsNoTracking().ToListAsync();
|
||||||
|
Assert.DoesNotContain(audits, a => a.WorkOrderId == 0);
|
||||||
|
if (expectedWorkOrderId is int workOrderId)
|
||||||
|
Assert.Equal(workOrderId, Assert.Single(audits).WorkOrderId);
|
||||||
|
else
|
||||||
|
Assert.Empty(audits);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(Ownership.Linked)]
|
||||||
|
[InlineData(Ownership.Unresolvable)]
|
||||||
|
public async Task Escalation_CommitsAndAuditsResolvedWorkOrder(Ownership ownership)
|
||||||
|
{
|
||||||
|
await using var connection = new SqliteConnection("Data Source=:memory:;Foreign Keys=True");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>().UseSqlite(connection).Options;
|
||||||
|
|
||||||
|
int upliftId;
|
||||||
|
int? expectedWorkOrderId;
|
||||||
|
await using (var seed = new SqliteUpliftDecisionTestDbContext(options))
|
||||||
|
{
|
||||||
|
await seed.Database.EnsureCreatedAsync();
|
||||||
|
(upliftId, expectedWorkOrderId) = await SeedAsync(
|
||||||
|
seed,
|
||||||
|
ownership,
|
||||||
|
expiresAt: null,
|
||||||
|
initialNotificationSentAt: DateTime.UtcNow.AddDays(-3));
|
||||||
|
}
|
||||||
|
|
||||||
|
var emailSender = new NoEmailSender();
|
||||||
|
await using (var context = new SqliteUpliftDecisionTestDbContext(options))
|
||||||
|
{
|
||||||
|
var lifecycle = new UpliftLifecycleService(
|
||||||
|
new UpliftDataService(context),
|
||||||
|
new DispatchDataService(context),
|
||||||
|
null!,
|
||||||
|
emailSender,
|
||||||
|
Options.Create(new FrontendOptions()),
|
||||||
|
Options.Create(new ApprovalsOptions { EscalationRecipients = new[] { "escalate@test.local" } }),
|
||||||
|
TimeProvider.System,
|
||||||
|
NullLogger<UpliftLifecycleService>.Instance);
|
||||||
|
|
||||||
|
Assert.Equal(1, await lifecycle.EscalateDueAsync(CancellationToken.None));
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var verify = new SqliteUpliftDecisionTestDbContext(options);
|
||||||
|
var saved = await verify.DispatchUpliftRequests.AsNoTracking().SingleAsync(u => u.Id == upliftId);
|
||||||
|
Assert.NotNull(saved.EscalatedAt);
|
||||||
|
Assert.Equal(1, emailSender.Calls);
|
||||||
|
|
||||||
|
var audits = await verify.WorkOrderAuditLogs.AsNoTracking().ToListAsync();
|
||||||
|
Assert.DoesNotContain(audits, a => a.WorkOrderId == 0);
|
||||||
|
if (expectedWorkOrderId is int workOrderId)
|
||||||
|
Assert.Equal(workOrderId, Assert.Single(audits).WorkOrderId);
|
||||||
|
else
|
||||||
|
Assert.Empty(audits);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<(int UpliftId, int? ExpectedWorkOrderId)> SeedAsync(
|
||||||
|
ApplicationDbContext context,
|
||||||
|
Ownership ownership,
|
||||||
|
DateTime? expiresAt,
|
||||||
|
DateTime? initialNotificationSentAt = null)
|
||||||
|
{
|
||||||
|
context.Users.Add(new ApplicationUser
|
||||||
|
{
|
||||||
|
Id = AdminId,
|
||||||
|
UserName = AdminId,
|
||||||
|
NormalizedUserName = AdminId.ToUpperInvariant(),
|
||||||
|
Email = "admin@test.local",
|
||||||
|
NormalizedEmail = "ADMIN@TEST.LOCAL"
|
||||||
|
});
|
||||||
|
var vendor = new Vendor { CompanyName = "Vinewood LLC", IsActive = true };
|
||||||
|
// A second work order keeps ids distinct from the first, so an audit landing on
|
||||||
|
// the wrong work order cannot pass by coincidence.
|
||||||
|
var unrelated = new WorkOrder { InternalWONumber = "WO-OTHER", WorkerOrderTitle = "Other" };
|
||||||
|
var workOrder = new WorkOrder
|
||||||
|
{
|
||||||
|
InternalWONumber = "WO-TARGET",
|
||||||
|
WorkerOrderTitle = "Repair",
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled
|
||||||
|
};
|
||||||
|
context.AddRange(vendor, unrelated, workOrder);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var dispatch = new Dispatch
|
||||||
|
{
|
||||||
|
VendorId = vendor.Id,
|
||||||
|
WorkOrderId = ownership == Ownership.Owned ? workOrder.Id : null,
|
||||||
|
DispatchNumber = "DIS-UPLIFT",
|
||||||
|
Status = "Scheduled",
|
||||||
|
NTEAmount = 1_000m
|
||||||
|
};
|
||||||
|
context.Dispatches.Add(dispatch);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
if (ownership == Ownership.Linked)
|
||||||
|
{
|
||||||
|
context.DispatchWorkOrders.Add(new DispatchWorkOrder
|
||||||
|
{
|
||||||
|
DispatchId = dispatch.Id,
|
||||||
|
WorkOrderId = workOrder.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = new DispatchUpliftRequest
|
||||||
|
{
|
||||||
|
DispatchId = dispatch.Id,
|
||||||
|
CurrentNTE = 1_000m,
|
||||||
|
RequestedNTE = 5_000m,
|
||||||
|
VendorReason = "Scope grew",
|
||||||
|
Status = UpliftStatus.Pending,
|
||||||
|
RequiredTier = 2,
|
||||||
|
CreatedDate = DateTime.UtcNow.AddDays(-4),
|
||||||
|
ExpiresAt = expiresAt,
|
||||||
|
InitialNotificationSentAt = initialNotificationSentAt,
|
||||||
|
NotificationStatus = initialNotificationSentAt == null ? "Pending" : "Sent"
|
||||||
|
};
|
||||||
|
context.DispatchUpliftRequests.Add(request);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return (request.Id, ownership == Ownership.Unresolvable ? null : workOrder.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NoDocumentStorage : IVendorDocumentStoragePort
|
||||||
|
{
|
||||||
|
public Task SaveAsync(int vendorId, int dispatchId, string storedFileName, Stream content, CancellationToken cancellationToken)
|
||||||
|
=> throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Stream OpenRead(int vendorId, int dispatchId, string storedFileName)
|
||||||
|
=> throw new NotSupportedException();
|
||||||
|
|
||||||
|
public void Delete(int vendorId, int dispatchId, string storedFileName)
|
||||||
|
=> throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Records sends without delivering anything.
|
||||||
|
private sealed class NoEmailSender : IEmailSender
|
||||||
|
{
|
||||||
|
public int Calls;
|
||||||
|
|
||||||
|
public Task<bool> SendEmailAsync(string emailTo, string subject, string body)
|
||||||
|
{
|
||||||
|
Calls++;
|
||||||
|
return Task.FromResult(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SqliteUpliftDecisionTestDbContext : ApplicationDbContext
|
||||||
|
{
|
||||||
|
public SqliteUpliftDecisionTestDbContext(DbContextOptions<ApplicationDbContext> options)
|
||||||
|
: base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(builder);
|
||||||
|
|
||||||
|
// SQL Server filtered index syntax is invalid on SQLite.
|
||||||
|
foreach (var index in builder.Model.GetEntityTypes().SelectMany(e => e.GetIndexes()))
|
||||||
|
{
|
||||||
|
if (index.GetFilter() != null)
|
||||||
|
index.SetFilter(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLite has no rowversion type; treat as plain nullable blobs.
|
||||||
|
foreach (var entityType in new[] { typeof(WorkOrder), typeof(Dispatch) })
|
||||||
|
{
|
||||||
|
var property = builder.Entity(entityType).Property("RowVersion").Metadata;
|
||||||
|
property.ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.Never;
|
||||||
|
property.IsConcurrencyToken = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue