mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-02 03:53:24 +00:00
Expose workorders/{id}/uplifts list/create/cancel/revoke for the SH-196 dialog, aggregate upliftSummary on board rows, and add service/controller regression tests.
221 lines
7.4 KiB
C#
221 lines
7.4 KiB
C#
using Data.SeaHavenIndustries;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Options;
|
|
using SeaHaven.DataServices.Implementation;
|
|
using SeaHaven.Services.Configuration;
|
|
using SeaHaven.Services.DTOs;
|
|
using SeaHaven.Services.Implementation;
|
|
using SeaHaven.Services.Interfaces;
|
|
using System.Security.Claims;
|
|
using Xunit;
|
|
|
|
namespace SeaHavenIndustries.Tests;
|
|
|
|
public sealed class WorkOrderUpliftServiceTests
|
|
{
|
|
private static ApplicationDbContext CreateContext()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
return new ApplicationDbContext(options);
|
|
}
|
|
|
|
private static ApprovalsOptions NewOptions() => new()
|
|
{
|
|
UpliftTier1MaxUsd = 2500m,
|
|
Tier1Roles = new[] { "Approver" },
|
|
Tier2Roles = new[] { "Manager" },
|
|
};
|
|
|
|
private static WorkOrderUpliftService NewService(ApplicationDbContext context)
|
|
{
|
|
return new WorkOrderUpliftService(
|
|
new UpliftDataService(context),
|
|
new DispatchDataService(context),
|
|
new WorkOrderDetailDataService(context),
|
|
WorkOrderAccountTestHelpers.Resolver(context),
|
|
new UserDataService(context),
|
|
TimeProvider.System,
|
|
Options.Create(NewOptions()));
|
|
}
|
|
|
|
private static ClaimsPrincipal Dispatcher(string userId = "dispatcher-1")
|
|
=> WorkOrderAccountTestHelpers.OrgWideAdmin(userId);
|
|
|
|
private static async Task<(WorkOrder WorkOrder, Dispatch Dispatch)> SeedWorkOrderAsync(ApplicationDbContext context)
|
|
{
|
|
await WorkOrderAccountTestHelpers.EnsureAccountAsync(context);
|
|
context.Users.Add(new ApplicationUser
|
|
{
|
|
Id = "dispatcher-1",
|
|
UserName = "dispatcher-1",
|
|
FirstName = "Alex",
|
|
LastName = "Dispatcher",
|
|
});
|
|
context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Acme HVAC" });
|
|
context.Dispatches.Add(new Dispatch
|
|
{
|
|
Id = 10,
|
|
VendorId = 1,
|
|
WorkOrderId = 1,
|
|
NTEAmount = 1000m,
|
|
DispatchNumber = "DIS-10",
|
|
Status = "Completed",
|
|
});
|
|
var workOrder = new WorkOrder
|
|
{
|
|
Id = 1,
|
|
InternalWONumber = "10000000001",
|
|
PrimaryDispatchId = 10,
|
|
AccountId = 1,
|
|
};
|
|
context.workOrders.Add(workOrder);
|
|
await context.SaveChangesAsync();
|
|
return (workOrder, context.Dispatches.Single());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ListAsync_ReturnsWorkOrderScopedUplifts()
|
|
{
|
|
await using var context = CreateContext();
|
|
var (workOrder, dispatch) = await SeedWorkOrderAsync(context);
|
|
context.DispatchUpliftRequests.Add(new DispatchUpliftRequest
|
|
{
|
|
Id = 100,
|
|
DispatchId = dispatch.Id,
|
|
RequestedNTE = 1500m,
|
|
Status = "Pending",
|
|
RequiredTier = 1,
|
|
NotificationStatus = "Pending",
|
|
VendorReason = "Extra coil work",
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var service = NewService(context);
|
|
var items = await service.ListAsync(workOrder.Id, Dispatcher(), CancellationToken.None);
|
|
|
|
Assert.NotNull(items);
|
|
var uplift = Assert.Single(items!);
|
|
Assert.Equal("pending", uplift.Status);
|
|
Assert.Equal(1500m, uplift.Amount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateAsync_AboveCurrentNte_CreatesPendingRequest()
|
|
{
|
|
await using var context = CreateContext();
|
|
var (workOrder, _) = await SeedWorkOrderAsync(context);
|
|
var service = NewService(context);
|
|
|
|
var created = await service.CreateAsync(
|
|
workOrder.Id,
|
|
new CreateWorkOrderUpliftRequestDto { Amount = 1800m, Notes = "Parts" },
|
|
Dispatcher(),
|
|
CancellationToken.None);
|
|
|
|
Assert.NotNull(created);
|
|
Assert.Equal("pending", created!.Status);
|
|
Assert.Equal("dispatcher-1", created.RequestedById);
|
|
Assert.Equal("Pending", Assert.Single(context.DispatchUpliftRequests).Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateAsync_AtOrBelowCurrentNte_CreatesAutoApprovedRequest()
|
|
{
|
|
await using var context = CreateContext();
|
|
var (workOrder, _) = await SeedWorkOrderAsync(context);
|
|
var service = NewService(context);
|
|
|
|
var created = await service.CreateAsync(
|
|
workOrder.Id,
|
|
new CreateWorkOrderUpliftRequestDto { Amount = 900m, Notes = "Within limit" },
|
|
Dispatcher(),
|
|
CancellationToken.None);
|
|
|
|
Assert.NotNull(created);
|
|
Assert.Equal("auto_approved", created!.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CancelAsync_PendingRequest_Withdraws()
|
|
{
|
|
await using var context = CreateContext();
|
|
var (workOrder, dispatch) = await SeedWorkOrderAsync(context);
|
|
context.DispatchUpliftRequests.Add(new DispatchUpliftRequest
|
|
{
|
|
Id = 100,
|
|
DispatchId = dispatch.Id,
|
|
RequestedNTE = 1500m,
|
|
Status = "Pending",
|
|
RequiredTier = 1,
|
|
NotificationStatus = "Pending",
|
|
createdby = "dispatcher-1",
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var service = NewService(context);
|
|
var cancelled = await service.CancelAsync(workOrder.Id, 100, Dispatcher(), CancellationToken.None);
|
|
|
|
Assert.NotNull(cancelled);
|
|
Assert.Equal("cancelled", cancelled!.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RevokeAsync_ApprovedRequiresAdminReason()
|
|
{
|
|
await using var context = CreateContext();
|
|
var (workOrder, dispatch) = await SeedWorkOrderAsync(context);
|
|
context.DispatchUpliftRequests.Add(new DispatchUpliftRequest
|
|
{
|
|
Id = 100,
|
|
DispatchId = dispatch.Id,
|
|
RequestedNTE = 1500m,
|
|
CurrentNTE = 1000m,
|
|
Status = "Approved",
|
|
RequiredTier = 1,
|
|
NotificationStatus = "Sent",
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var service = NewService(context);
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
service.RevokeAsync(
|
|
workOrder.Id,
|
|
100,
|
|
new RevokeWorkOrderUpliftRequestDto(),
|
|
Dispatcher("dispatcher-1"),
|
|
CancellationToken.None));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RevokeAsync_AdminRevokesApproved_ReturnsRevokedStatus()
|
|
{
|
|
await using var context = CreateContext();
|
|
var (workOrder, dispatch) = await SeedWorkOrderAsync(context);
|
|
dispatch.NTEAmount = 1500m;
|
|
context.DispatchUpliftRequests.Add(new DispatchUpliftRequest
|
|
{
|
|
Id = 100,
|
|
DispatchId = dispatch.Id,
|
|
RequestedNTE = 1500m,
|
|
CurrentNTE = 1000m,
|
|
Status = "Approved",
|
|
RequiredTier = 1,
|
|
NotificationStatus = "Sent",
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var service = NewService(context);
|
|
var revoked = await service.RevokeAsync(
|
|
workOrder.Id,
|
|
100,
|
|
new RevokeWorkOrderUpliftRequestDto { Reason = "Policy change" },
|
|
WorkOrderAccountTestHelpers.OrgWideAdmin("admin-1"),
|
|
CancellationToken.None);
|
|
|
|
Assert.NotNull(revoked);
|
|
Assert.Equal("revoked", revoked!.Status);
|
|
Assert.Equal(1000m, dispatch.NTEAmount);
|
|
}
|
|
}
|