mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-02 14:23:21 +00:00
fix(uplifts): resolve evidence without a primary dispatch
Upload and status use the same dispatch create already selects, so a work order with no primary can still attach evidence. Status no longer rejects a terminal work order or dispatch, and create, cancel, and revoke return the stored evidence file name and attachments. Co-authored-by: Arthur Bassi <bassi-arthurr@users.noreply.github.com>
This commit is contained in:
parent
2f4b783615
commit
b11b66538a
5 changed files with 352 additions and 35 deletions
|
|
@ -6,6 +6,10 @@ using SeaHaven.Services.Implementation;
|
|||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public sealed record StoredUpliftEvidence(
|
||||
string? FileName,
|
||||
IReadOnlyList<UpliftEvidenceFileData> Attachments);
|
||||
|
||||
public static class WorkOrderUpliftContractMapper
|
||||
{
|
||||
public static WorkOrderUpliftSummaryDto MapSummary(
|
||||
|
|
@ -53,13 +57,15 @@ namespace SeaHaven.Services.Helpers
|
|||
DecisionNote = row.DecisionNote ?? "",
|
||||
EvidenceDocumentId = row.EvidenceDocumentId,
|
||||
EvidenceFileName = row.EvidenceFileName ?? "",
|
||||
Attachments = row.Attachments
|
||||
.Select(file => new WorkOrderUpliftAttachmentDto { Id = file.Id, Name = file.Name })
|
||||
.ToList(),
|
||||
Attachments = MapAttachments(row.Attachments),
|
||||
};
|
||||
}
|
||||
|
||||
public static WorkOrderUpliftDto MapItem(DispatchUpliftRequest request, string? requestedByName, string? decidedByName)
|
||||
public static WorkOrderUpliftDto MapItem(
|
||||
DispatchUpliftRequest request,
|
||||
string? requestedByName,
|
||||
string? decidedByName,
|
||||
StoredUpliftEvidence evidence)
|
||||
=> new()
|
||||
{
|
||||
Id = request.Id,
|
||||
|
|
@ -73,18 +79,27 @@ namespace SeaHaven.Services.Helpers
|
|||
DecidedByName = decidedByName ?? "",
|
||||
DecisionNote = request.DecisionNote ?? "",
|
||||
EvidenceDocumentId = request.EvidenceDocumentId,
|
||||
EvidenceFileName = evidence.FileName ?? "",
|
||||
Attachments = MapAttachments(evidence.Attachments),
|
||||
};
|
||||
|
||||
public static WorkOrderUpliftDto MapRevokedItem(
|
||||
DispatchUpliftRequest request,
|
||||
string? requestedByName,
|
||||
string decidedByName)
|
||||
string decidedByName,
|
||||
StoredUpliftEvidence evidence)
|
||||
{
|
||||
var dto = MapItem(request, requestedByName, decidedByName);
|
||||
var dto = MapItem(request, requestedByName, decidedByName, evidence);
|
||||
dto.Status = "revoked";
|
||||
return dto;
|
||||
}
|
||||
|
||||
private static List<WorkOrderUpliftAttachmentDto> MapAttachments(
|
||||
IEnumerable<UpliftEvidenceFileData> files)
|
||||
=> files
|
||||
.Select(file => new WorkOrderUpliftAttachmentDto { Id = file.Id, Name = file.Name })
|
||||
.ToList();
|
||||
|
||||
public static string ToFrontendStatus(string? backendStatus)
|
||||
{
|
||||
return UpliftStatus.ToCanonical(backendStatus) switch
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ namespace SeaHaven.Services.Implementation
|
|||
};
|
||||
|
||||
private readonly IWorkOrderDetailDataService _detailData;
|
||||
private readonly IUpliftDataService _upliftData;
|
||||
private readonly IDispatchDataService _dispatchData;
|
||||
private readonly IVendorDocumentDataService _documentData;
|
||||
private readonly IVendorDocumentStoragePort _documentStorage;
|
||||
|
|
@ -26,6 +27,7 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
public WorkOrderUpliftEvidenceService(
|
||||
IWorkOrderDetailDataService detailData,
|
||||
IUpliftDataService upliftData,
|
||||
IDispatchDataService dispatchData,
|
||||
IVendorDocumentDataService documentData,
|
||||
IVendorDocumentStoragePort documentStorage,
|
||||
|
|
@ -33,6 +35,7 @@ namespace SeaHaven.Services.Implementation
|
|||
IOptions<VendorDocumentsOptions> documentOptions)
|
||||
{
|
||||
_detailData = detailData;
|
||||
_upliftData = upliftData;
|
||||
_dispatchData = dispatchData;
|
||||
_documentData = documentData;
|
||||
_documentStorage = documentStorage;
|
||||
|
|
@ -46,20 +49,29 @@ namespace SeaHaven.Services.Implementation
|
|||
ClaimsPrincipal user,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var target = await ResolveOpenDispatchAsync(workOrderId, user, cancellationToken);
|
||||
if (target == null)
|
||||
var workOrder = await FindAccessibleWorkOrderAsync(workOrderId, user, cancellationToken);
|
||||
if (workOrder == null)
|
||||
return null;
|
||||
|
||||
var dispatch = await ResolveUpliftDispatchAsync(
|
||||
workOrderId,
|
||||
workOrder.PrimaryDispatchId,
|
||||
cancellationToken);
|
||||
if (dispatch == null)
|
||||
throw new InvalidOperationException("Work order has no primary dispatch for uplift requests");
|
||||
|
||||
RejectTerminalTarget(workOrder, dispatch);
|
||||
|
||||
var (contentType, bytes) = await ReadAcceptedFileAsync(file, cancellationToken);
|
||||
var latest = await _documentData.GetLatestForDispatchAsync(target.Dispatch.Id, cancellationToken);
|
||||
var latest = await _documentData.GetLatestForDispatchAsync(dispatch.Id, cancellationToken);
|
||||
var version = (latest?.Version ?? 0) + 1;
|
||||
var storedFileName = $"{target.Dispatch.Id}_{version}_{Guid.NewGuid():N}{SafeExtension(file.FileName)}";
|
||||
var storedFileName = $"{dispatch.Id}_{version}_{Guid.NewGuid():N}{SafeExtension(file.FileName)}";
|
||||
var now = DateTime.UtcNow;
|
||||
var passWithoutScanner = _documentOptions.PassWhenScannerUnavailable;
|
||||
var document = new VendorCompletionDocument
|
||||
{
|
||||
VendorId = target.Dispatch.VendorId,
|
||||
DispatchId = target.Dispatch.Id,
|
||||
VendorId = dispatch.VendorId,
|
||||
DispatchId = dispatch.Id,
|
||||
WorkOrderId = workOrderId,
|
||||
OriginalFileName = Path.GetFileName(file.FileName),
|
||||
StoredFileName = storedFileName,
|
||||
|
|
@ -77,9 +89,9 @@ namespace SeaHaven.Services.Implementation
|
|||
await _dispatchData.StageAuditLogAsync(new WorkOrderAuditLog
|
||||
{
|
||||
WorkOrderId = workOrderId,
|
||||
DispatchId = target.Dispatch.Id,
|
||||
DispatchId = dispatch.Id,
|
||||
UserId = user.FindFirstValue(ClaimTypes.NameIdentifier),
|
||||
FieldName = $"Dispatch {target.Dispatch.DispatchNumber} Uplift Evidence",
|
||||
FieldName = $"Dispatch {dispatch.DispatchNumber} Uplift Evidence",
|
||||
NewValue = document.OriginalFileName,
|
||||
Action = "uplift_evidence_uploaded",
|
||||
ActorType = "internal",
|
||||
|
|
@ -89,8 +101,8 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
using var stored = new MemoryStream(bytes);
|
||||
await _documentStorage.SaveAsync(
|
||||
target.Dispatch.VendorId,
|
||||
target.Dispatch.Id,
|
||||
dispatch.VendorId,
|
||||
dispatch.Id,
|
||||
storedFileName,
|
||||
stored,
|
||||
cancellationToken);
|
||||
|
|
@ -111,14 +123,23 @@ namespace SeaHaven.Services.Implementation
|
|||
ClaimsPrincipal user,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var target = await ResolveOpenDispatchAsync(workOrderId, user, cancellationToken);
|
||||
if (target == null)
|
||||
// A poll reads the document on the dispatch create would pick. It does not
|
||||
// require that work order or dispatch to still accept a new upload.
|
||||
var workOrder = await FindAccessibleWorkOrderAsync(workOrderId, user, cancellationToken);
|
||||
if (workOrder == null)
|
||||
return null;
|
||||
|
||||
var dispatch = await ResolveUpliftDispatchAsync(
|
||||
workOrderId,
|
||||
workOrder.PrimaryDispatchId,
|
||||
cancellationToken);
|
||||
if (dispatch == null)
|
||||
return null;
|
||||
|
||||
var document = await _documentData.GetMetadataForVendorDispatchAsync(
|
||||
documentId,
|
||||
target.Dispatch.Id,
|
||||
target.Dispatch.VendorId,
|
||||
dispatch.Id,
|
||||
dispatch.VendorId,
|
||||
cancellationToken);
|
||||
if (document == null || document.Purpose != VendorDocumentPurpose.UpliftEvidence)
|
||||
return null;
|
||||
|
|
@ -133,7 +154,7 @@ namespace SeaHaven.Services.Implementation
|
|||
};
|
||||
}
|
||||
|
||||
private async Task<OpenDispatch?> ResolveOpenDispatchAsync(
|
||||
private async Task<WorkOrder?> FindAccessibleWorkOrderAsync(
|
||||
int workOrderId,
|
||||
ClaimsPrincipal user,
|
||||
CancellationToken cancellationToken)
|
||||
|
|
@ -142,27 +163,31 @@ namespace SeaHaven.Services.Implementation
|
|||
if (!await _detailData.ExistsAsync(workOrderId, cancellationToken, accountFilter))
|
||||
return null;
|
||||
|
||||
var workOrder = await _detailData.GetWorkOrderForMediaAsync(
|
||||
return await _detailData.GetWorkOrderForMediaAsync(
|
||||
workOrderId,
|
||||
cancellationToken,
|
||||
accountFilter);
|
||||
if (workOrder?.PrimaryDispatchId is not int dispatchId)
|
||||
throw new InvalidOperationException("Work order has no primary dispatch for uplift requests");
|
||||
}
|
||||
|
||||
private Task<Dispatch?> ResolveUpliftDispatchAsync(
|
||||
int workOrderId,
|
||||
int? primaryDispatchId,
|
||||
CancellationToken cancellationToken)
|
||||
=> _upliftData.GetUpliftDispatchForWorkOrderAsync(
|
||||
workOrderId,
|
||||
primaryDispatchId,
|
||||
cancellationToken);
|
||||
|
||||
private static void RejectTerminalTarget(WorkOrder workOrder, Dispatch dispatch)
|
||||
{
|
||||
if (workOrder.LifecycleStatus is LifecycleStatus.Completed or LifecycleStatus.Canceled)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot attach uplift evidence on a '{workOrder.LifecycleStatus}' work order");
|
||||
}
|
||||
|
||||
var dispatch = await _dispatchData.GetByIdAsync(dispatchId);
|
||||
if (dispatch == null)
|
||||
throw new KeyNotFoundException("Dispatch not found");
|
||||
|
||||
if (dispatch.Status is "Verified" or "Cancelled" or "Canceled" or "Refused")
|
||||
throw new InvalidOperationException($"Cannot attach uplift evidence on a '{dispatch.Status}' dispatch");
|
||||
|
||||
return new OpenDispatch(dispatch);
|
||||
}
|
||||
|
||||
private async Task<(string ContentType, byte[] Bytes)> ReadAcceptedFileAsync(
|
||||
|
|
@ -220,7 +245,5 @@ namespace SeaHaven.Services.Implementation
|
|||
var extension = Path.GetExtension(fileName);
|
||||
return string.IsNullOrWhiteSpace(extension) ? string.Empty : extension;
|
||||
}
|
||||
|
||||
private sealed record OpenDispatch(Dispatch Dispatch);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -284,7 +284,12 @@ namespace SeaHaven.Services.Implementation
|
|||
await _upliftData.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var requesterName = req.RequestedByVendorName ?? await ResolveUserDisplayNameAsync(req.createdby, cancellationToken);
|
||||
return WorkOrderUpliftContractMapper.MapItem(req, requesterName, await ResolveUserDisplayNameAsync(userId, cancellationToken));
|
||||
return await MapStoredRequestAsync(
|
||||
req,
|
||||
dispatch,
|
||||
requesterName,
|
||||
await ResolveUserDisplayNameAsync(userId, cancellationToken),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<WorkOrderUpliftDto?> RevokeAsync(
|
||||
|
|
@ -375,7 +380,7 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
var decidedByName = await ResolveUserDisplayNameAsync(userId, cancellationToken);
|
||||
var requesterName = req.RequestedByVendorName ?? await ResolveUserDisplayNameAsync(req.createdby, cancellationToken);
|
||||
return WorkOrderUpliftContractMapper.MapRevokedItem(req, requesterName, decidedByName);
|
||||
return await MapStoredRequestAsync(req, dispatch, requesterName, decidedByName, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task WithdrawPendingForWorkOrderAsync(
|
||||
|
|
@ -432,7 +437,54 @@ namespace SeaHaven.Services.Implementation
|
|||
_upliftData.StageEvidenceDocuments(created, evidenceDocumentIds);
|
||||
await StageAuditAsync(dispatch, workOrderId, userId, currentNte, amount, auditAction, now, cancellationToken);
|
||||
await _upliftData.SaveChangesAsync(cancellationToken);
|
||||
return WorkOrderUpliftContractMapper.MapItem(created, requesterName, null);
|
||||
return await MapStoredRequestAsync(created, dispatch, requesterName, null, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<WorkOrderUpliftDto> MapStoredRequestAsync(
|
||||
DispatchUpliftRequest request,
|
||||
Dispatch dispatch,
|
||||
string? requestedByName,
|
||||
string? decidedByName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var evidence = await LoadStoredEvidenceAsync(request, dispatch, cancellationToken);
|
||||
if (request.Status == UpliftStatus.Revoked)
|
||||
{
|
||||
return WorkOrderUpliftContractMapper.MapRevokedItem(
|
||||
request,
|
||||
requestedByName,
|
||||
decidedByName ?? "",
|
||||
evidence);
|
||||
}
|
||||
|
||||
return WorkOrderUpliftContractMapper.MapItem(
|
||||
request,
|
||||
requestedByName,
|
||||
decidedByName,
|
||||
evidence);
|
||||
}
|
||||
|
||||
private async Task<StoredUpliftEvidence> LoadStoredEvidenceAsync(
|
||||
DispatchUpliftRequest request,
|
||||
Dispatch dispatch,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var files = await _upliftData.GetEvidenceFilesAsync(new[] { request.Id }, cancellationToken);
|
||||
if (!files.TryGetValue(request.Id, out var attachments))
|
||||
attachments = Array.Empty<UpliftEvidenceFileData>();
|
||||
|
||||
string? fileName = null;
|
||||
if (request.EvidenceDocumentId is int documentId)
|
||||
{
|
||||
var document = await _documentData.GetMetadataForVendorDispatchAsync(
|
||||
documentId,
|
||||
dispatch.Id,
|
||||
dispatch.VendorId,
|
||||
cancellationToken);
|
||||
fileName = document?.OriginalFileName;
|
||||
}
|
||||
|
||||
return new StoredUpliftEvidence(fileName, attachments);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ using System.Security.Claims;
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
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 Xunit;
|
||||
|
|
@ -62,6 +64,7 @@ public sealed class WorkOrderUpliftEvidenceServiceTests
|
|||
var storage = new MemoryStorage();
|
||||
var service = new WorkOrderUpliftEvidenceService(
|
||||
new WorkOrderDetailDataService(context),
|
||||
new UpliftDataService(context),
|
||||
new DispatchDataService(context),
|
||||
new VendorDocumentDataService(context),
|
||||
storage,
|
||||
|
|
@ -186,11 +189,156 @@ public sealed class WorkOrderUpliftEvidenceServiceTests
|
|||
ScanStatus = "Passed",
|
||||
Version = 1,
|
||||
});
|
||||
context.Dispatches.Add(new Dispatch
|
||||
{
|
||||
Id = 11,
|
||||
VendorId = 1,
|
||||
WorkOrderId = workOrder.Id,
|
||||
DispatchNumber = "DIS-11",
|
||||
Status = "Scheduled",
|
||||
});
|
||||
context.VendorCompletionDocuments.Add(new VendorCompletionDocument
|
||||
{
|
||||
Id = 91,
|
||||
VendorId = 1,
|
||||
DispatchId = 11,
|
||||
WorkOrderId = workOrder.Id,
|
||||
OriginalFileName = "other.pdf",
|
||||
StoredFileName = "other.pdf",
|
||||
ContentType = "application/pdf",
|
||||
Purpose = VendorDocumentPurpose.UpliftEvidence,
|
||||
ScanStatus = "Passed",
|
||||
Version = 1,
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var passed = await service.GetStatusAsync(workOrder.Id, uploaded.Id, Dispatcher(), CancellationToken.None);
|
||||
Assert.Equal("Passed", passed!.ScanStatus);
|
||||
Assert.Null(await service.GetStatusAsync(workOrder.Id, 90, Dispatcher(), CancellationToken.None));
|
||||
Assert.Null(await service.GetStatusAsync(workOrder.Id, 91, Dispatcher(), CancellationToken.None));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(LifecycleStatus.Completed, "Scheduled")]
|
||||
[InlineData(LifecycleStatus.Canceled, "Scheduled")]
|
||||
[InlineData(LifecycleStatus.Scheduled, "Verified")]
|
||||
[InlineData(LifecycleStatus.Scheduled, "Cancelled")]
|
||||
[InlineData(LifecycleStatus.Scheduled, "Canceled")]
|
||||
[InlineData(LifecycleStatus.Scheduled, "Refused")]
|
||||
public async Task GetStatusAsync_ReturnsUpliftEvidenceWhenTargetIsTerminal(
|
||||
LifecycleStatus lifecycle,
|
||||
string dispatchStatus)
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var workOrder = await SeedWorkOrderAsync(context);
|
||||
var (service, _) = NewService(context);
|
||||
var uploaded = await service.UploadAsync(
|
||||
workOrder.Id,
|
||||
FormFile("%PDF-1.4"u8.ToArray(), "quote.pdf", "application/pdf"),
|
||||
Dispatcher(),
|
||||
CancellationToken.None);
|
||||
|
||||
workOrder.LifecycleStatus = lifecycle;
|
||||
context.Dispatches.Single().Status = dispatchStatus;
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var status = await service.GetStatusAsync(workOrder.Id, uploaded!.Id, Dispatcher(), CancellationToken.None);
|
||||
|
||||
Assert.Equal("Pending", status!.ScanStatus);
|
||||
Assert.Equal("quote.pdf", status.OriginalFileName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(LifecycleStatus.Completed, "Scheduled", "Completed")]
|
||||
[InlineData(LifecycleStatus.Scheduled, "Verified", "Verified")]
|
||||
public async Task UploadAsync_TerminalWorkOrderOrDispatch_Rejects(
|
||||
LifecycleStatus lifecycle,
|
||||
string dispatchStatus,
|
||||
string expected)
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var workOrder = await SeedWorkOrderAsync(context);
|
||||
workOrder.LifecycleStatus = lifecycle;
|
||||
context.Dispatches.Single().Status = dispatchStatus;
|
||||
await context.SaveChangesAsync();
|
||||
var (service, storage) = NewService(context);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
service.UploadAsync(
|
||||
workOrder.Id,
|
||||
FormFile("%PDF-1.4"u8.ToArray(), "quote.pdf", "application/pdf"),
|
||||
Dispatcher(),
|
||||
CancellationToken.None));
|
||||
|
||||
Assert.Contains(expected, ex.Message, StringComparison.Ordinal);
|
||||
Assert.Empty(context.VendorCompletionDocuments);
|
||||
Assert.Empty(storage.Saved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadThenCreate_WithoutPrimaryDispatch_PersistsUpliftOnTheSameDispatch()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var workOrder = await SeedWorkOrderAsync(context);
|
||||
workOrder.PrimaryDispatchId = null;
|
||||
var role = new IdentityRole("Admin");
|
||||
context.Roles.Add(role);
|
||||
context.UserRoles.Add(new IdentityUserRole<string>
|
||||
{
|
||||
UserId = "dispatcher-1",
|
||||
RoleId = role.Id,
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var (evidence, _) = NewService(context);
|
||||
var uploaded = await evidence.UploadAsync(
|
||||
workOrder.Id,
|
||||
FormFile("%PDF-1.4"u8.ToArray(), "quote.pdf", "application/pdf"),
|
||||
Dispatcher(),
|
||||
CancellationToken.None);
|
||||
|
||||
var pending = await evidence.GetStatusAsync(
|
||||
workOrder.Id,
|
||||
uploaded!.Id,
|
||||
Dispatcher(),
|
||||
CancellationToken.None);
|
||||
Assert.Equal("Pending", pending!.ScanStatus);
|
||||
|
||||
var document = Assert.Single(context.VendorCompletionDocuments);
|
||||
document.ScanStatus = "Passed";
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var created = await NewUpliftService(context).CreateAsync(
|
||||
workOrder.Id,
|
||||
new CreateWorkOrderUpliftRequestDto
|
||||
{
|
||||
Amount = 400m,
|
||||
Notes = "Quote",
|
||||
EvidenceDocumentId = uploaded.Id,
|
||||
},
|
||||
Dispatcher(),
|
||||
CancellationToken.None);
|
||||
|
||||
var request = Assert.Single(context.DispatchUpliftRequests);
|
||||
Assert.NotNull(created);
|
||||
Assert.Equal(uploaded.Id, request.EvidenceDocumentId);
|
||||
Assert.Equal(document.DispatchId, request.DispatchId);
|
||||
Assert.Equal(10, request.DispatchId);
|
||||
}
|
||||
|
||||
private static WorkOrderUpliftService NewUpliftService(ApplicationDbContext context)
|
||||
{
|
||||
return new WorkOrderUpliftService(
|
||||
new UpliftDataService(context),
|
||||
new DispatchDataService(context),
|
||||
new WorkOrderDetailDataService(context),
|
||||
WorkOrderAccountTestHelpers.Resolver(context),
|
||||
new UserDataService(context),
|
||||
new TeamPermissionOverrideDataService(context),
|
||||
new TeamPermissionPolicy(),
|
||||
TimeProvider.System,
|
||||
Options.Create(new ApprovalsOptions { UpliftTier1MaxUsd = 2500m }),
|
||||
new VendorDocumentDataService(context));
|
||||
}
|
||||
|
||||
private sealed class MemoryStorage : IVendorDocumentStoragePort
|
||||
|
|
|
|||
|
|
@ -515,6 +515,43 @@ public sealed class WorkOrderUpliftServiceTests
|
|||
Assert.Equal("cancelled", cancelled!.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CancelAsync_ReturnsStoredEvidenceFileNameAndAttachments()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var (workOrder, dispatch) = await SeedWorkOrderAsync(context);
|
||||
var document = SeedEvidence(context, dispatch.Id, dispatch.VendorId, workOrder.Id);
|
||||
context.DispatchUpliftRequests.Add(new DispatchUpliftRequest
|
||||
{
|
||||
Id = 100,
|
||||
DispatchId = dispatch.Id,
|
||||
RequestedNTE = 600m,
|
||||
Status = "Pending",
|
||||
RequiredTier = 1,
|
||||
NotificationStatus = "Pending",
|
||||
createdby = "dispatcher-1",
|
||||
EvidenceDocumentId = document.Id,
|
||||
});
|
||||
context.DispatchUpliftRequestDocuments.Add(new DispatchUpliftRequestDocument
|
||||
{
|
||||
UpliftRequestId = 100,
|
||||
DocumentId = document.Id,
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var cancelled = await NewService(context).CancelAsync(
|
||||
workOrder.Id,
|
||||
100,
|
||||
Dispatcher(),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal("cancelled", cancelled!.Status);
|
||||
Assert.Equal("quote.pdf", cancelled.EvidenceFileName);
|
||||
var attachment = Assert.Single(cancelled.Attachments);
|
||||
Assert.Equal(document.Id, attachment.Id);
|
||||
Assert.Equal("quote.pdf", attachment.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RevokeAsync_ApprovedRequiresAdminReason()
|
||||
{
|
||||
|
|
@ -761,6 +798,40 @@ public sealed class WorkOrderUpliftServiceTests
|
|||
.SumAutoApprovedAmountForWorkOrderAsync(workOrder.Id, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RevokeAsync_ReturnsStoredEvidenceFileNameAndAttachments()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
var (workOrder, dispatch) = await SeedWorkOrderAsync(context);
|
||||
var document = SeedEvidence(context, dispatch.Id, dispatch.VendorId, workOrder.Id);
|
||||
await context.SaveChangesAsync();
|
||||
var service = NewService(context);
|
||||
|
||||
var created = await service.CreateAsync(
|
||||
workOrder.Id,
|
||||
new CreateWorkOrderUpliftRequestDto
|
||||
{
|
||||
Amount = 400m,
|
||||
Notes = "Quote",
|
||||
EvidenceDocumentId = document.Id,
|
||||
},
|
||||
Dispatcher(),
|
||||
CancellationToken.None);
|
||||
|
||||
var revoked = await service.RevokeAsync(
|
||||
workOrder.Id,
|
||||
created!.Id,
|
||||
new RevokeWorkOrderUpliftRequestDto(),
|
||||
DispatcherRoleOnly(),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal("revoked", revoked!.Status);
|
||||
Assert.Equal("quote.pdf", revoked.EvidenceFileName);
|
||||
var attachment = Assert.Single(revoked.Attachments);
|
||||
Assert.Equal(document.Id, attachment.Id);
|
||||
Assert.Equal("quote.pdf", attachment.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RevokeAsync_AdminOwnerRevokingAutoApproved_IsForbiddenAndKeepsAllowanceConsumed()
|
||||
{
|
||||
|
|
@ -868,6 +939,10 @@ public sealed class WorkOrderUpliftServiceTests
|
|||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(document.Id, created!.EvidenceDocumentId);
|
||||
Assert.Equal("quote.pdf", created.EvidenceFileName);
|
||||
var attachment = Assert.Single(created.Attachments);
|
||||
Assert.Equal(document.Id, attachment.Id);
|
||||
Assert.Equal("quote.pdf", attachment.Name);
|
||||
Assert.Equal(document.Id, Assert.Single(context.DispatchUpliftRequests).EvidenceDocumentId);
|
||||
}
|
||||
|
||||
|
|
@ -942,6 +1017,10 @@ public sealed class WorkOrderUpliftServiceTests
|
|||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(quote.Id, created!.EvidenceDocumentId);
|
||||
Assert.Equal("quote.pdf", created.EvidenceFileName);
|
||||
Assert.Equal(
|
||||
new[] { "quote.pdf", "photo.jpg" },
|
||||
created.Attachments.Select(file => file.Name).ToArray());
|
||||
Assert.Equal(2, context.DispatchUpliftRequestDocuments.Count());
|
||||
var listed = await service.ListAsync(workOrder.Id, Dispatcher(), CancellationToken.None);
|
||||
var names = Assert.Single(listed!).Attachments.Select(file => file.Name).OrderBy(name => name).ToArray();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue