mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 04:53:11 +00:00
Keep work-order uplift requests read-only in the Vendor Portal
A vendor could withdraw (or cancel) an uplift a dispatcher raised from the work order. Withdraw and its cancel alias now refuse requests with createdby set, using the portal's not-found response, and the portal read model reports RaisedByVendor so the portal can hide Revise and Withdraw on those requests.
This commit is contained in:
parent
3b69b9fbcc
commit
e02f9774dc
5 changed files with 98 additions and 1 deletions
|
|
@ -496,6 +496,92 @@ public sealed class UpliftWorkflowTests
|
|||
await act.Should().ThrowAsync<InvalidOperationException>();
|
||||
}
|
||||
|
||||
// Raised from the work order by an internal user: createdby is set and RequestedNTE
|
||||
// holds the increase. Vendor sessions have no identity user, so they never set it.
|
||||
private static DispatchUpliftRequest WorkOrderRequest(int dispatchId, string status) => new()
|
||||
{
|
||||
DispatchId = dispatchId,
|
||||
CurrentNTE = 600m,
|
||||
RequestedNTE = 90m,
|
||||
Status = status,
|
||||
RequiredTier = 1,
|
||||
VendorReason = "Extra parts",
|
||||
RequestedByVendorName = "Alex Dispatcher",
|
||||
NotificationStatus = "Sent",
|
||||
createdby = "dispatcher-1",
|
||||
CreatedDate = new DateTime(2026, 3, 1),
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[InlineData("Pending", "withdraw")]
|
||||
[InlineData("ChangesRequested", "withdraw")]
|
||||
[InlineData("Pending", "cancel")]
|
||||
[InlineData("ChangesRequested", "cancel")]
|
||||
public async Task Withdraw_WorkOrderRequest_IsRefusedAsNotFound_AndLeavesTheRowUnchanged(string status, string route)
|
||||
{
|
||||
using var context = NewContext();
|
||||
var (_, _, dispatch) = await SeedAsync(context, nte: 600m);
|
||||
var workOrderRequest = WorkOrderRequest(dispatch.Id, status);
|
||||
context.DispatchUpliftRequests.Add(workOrderRequest);
|
||||
await context.SaveChangesAsync();
|
||||
var service = NewPortalService(context, new FakeEmailSender(deliver: true));
|
||||
var session = await service.ResolveSessionAsync(Token, CancellationToken.None);
|
||||
|
||||
Func<Task> act = route == "cancel"
|
||||
? () => service.CancelUpliftRequestAsync(session!, dispatch.Id, workOrderRequest.Id, CancellationToken.None)
|
||||
: () => service.WithdrawUpliftAsync(session!, dispatch.Id, workOrderRequest.Id, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<KeyNotFoundException>();
|
||||
var after = context.DispatchUpliftRequests.AsNoTracking().Single(u => u.Id == workOrderRequest.Id);
|
||||
after.Status.Should().Be(status);
|
||||
after.DecidedAt.Should().BeNull();
|
||||
after.RequestedNTE.Should().Be(90m);
|
||||
after.createdby.Should().Be("dispatcher-1");
|
||||
context.WorkOrderAuditLogs.Should().NotContain(a => a.Action == "uplift_withdraw" || a.Action == "uplift_cancel");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cancel_VendorRaisedChangesRequested_StillWithdraws()
|
||||
{
|
||||
using var context = NewContext();
|
||||
var (vendor, workOrder, dispatch) = await SeedAsync(context, nte: 1000m);
|
||||
context.VendorCompletionDocuments.Add(EvidenceDocument(vendor.Id, dispatch.Id, workOrder.Id));
|
||||
await context.SaveChangesAsync();
|
||||
var service = NewPortalService(context, new FakeEmailSender(deliver: true));
|
||||
var session = await service.ResolveSessionAsync(Token, CancellationToken.None);
|
||||
var created = await service.RequestUpliftAsync(session!, dispatch.Id, 1500m, "reason", null, 1, CancellationToken.None);
|
||||
context.DispatchUpliftRequests.Single().Status = UpliftStatus.ChangesRequested;
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var result = await service.CancelUpliftRequestAsync(session!, dispatch.Id, created.Id, CancellationToken.None);
|
||||
|
||||
result.Status.Should().Be(UpliftStatus.Withdrawn);
|
||||
context.DispatchUpliftRequests.AsNoTracking().Single().Status.Should().Be(UpliftStatus.Withdrawn);
|
||||
context.WorkOrderAuditLogs.Should().Contain(a => a.Action == "uplift_cancel" && a.OldValue == UpliftStatus.ChangesRequested);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DispatchDetail_ReportsRaisedByVendor_PerCreationPath()
|
||||
{
|
||||
using var context = NewContext();
|
||||
var (vendor, workOrder, dispatch) = await SeedAsync(context, nte: 1000m);
|
||||
context.VendorCompletionDocuments.Add(EvidenceDocument(vendor.Id, dispatch.Id, workOrder.Id));
|
||||
await context.SaveChangesAsync();
|
||||
var service = NewPortalService(context, new FakeEmailSender(deliver: true));
|
||||
var session = await service.ResolveSessionAsync(Token, CancellationToken.None);
|
||||
var vendorRaised = await service.RequestUpliftAsync(session!, dispatch.Id, 1500m, "reason", null, 1, CancellationToken.None);
|
||||
await service.WithdrawUpliftAsync(session!, dispatch.Id, vendorRaised.Id, CancellationToken.None);
|
||||
var workOrderRequest = WorkOrderRequest(dispatch.Id, UpliftStatus.Pending);
|
||||
context.DispatchUpliftRequests.Add(workOrderRequest);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var detail = await service.GetDispatchDetailAsync(session!, dispatch.Id, CancellationToken.None);
|
||||
|
||||
detail!.UpliftRequests.Should().HaveCount(2);
|
||||
detail.UpliftRequests.Single(u => u.Id == vendorRaised.Id).RaisedByVendor.Should().BeTrue();
|
||||
detail.UpliftRequests.Single(u => u.Id == workOrderRequest.Id).RaisedByVendor.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RequestChanges_Pending_TransitionsToChangesRequested_AuditsOldValue()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ namespace Data.SeaHavenIndustries
|
|||
public string? Status { get; set; }
|
||||
public int RequiredTier { get; set; }
|
||||
public string? RequestedByVendorName { get; set; }
|
||||
// True when the vendor raised the request in the portal; false when it was raised
|
||||
// from the work order. Only vendor-raised requests are revisable or withdrawable.
|
||||
public bool RaisedByVendor { get; set; }
|
||||
public DateTime? CreatedDate { get; set; }
|
||||
public DateTime? DecidedAt { get; set; }
|
||||
public string? DecisionNote { get; set; }
|
||||
|
|
|
|||
|
|
@ -243,6 +243,9 @@ namespace SeaHaven.DataServices.Implementation
|
|||
Status = u.Status,
|
||||
RequiredTier = u.RequiredTier,
|
||||
RequestedByVendorName = u.RequestedByVendorName,
|
||||
// Vendor sessions have no identity user, so createdby is null
|
||||
// only on requests the vendor raised in the portal.
|
||||
RaisedByVendor = u.createdby == null,
|
||||
CreatedDate = u.CreatedDate,
|
||||
DecidedAt = u.DecidedAt,
|
||||
DecisionNote = u.DecisionNote,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ namespace SeaHaven.Services.DTOs
|
|||
public string? Status { get; set; }
|
||||
public int RequiredTier { get; set; }
|
||||
public string? RequestedByVendorName { get; set; }
|
||||
// False for requests raised from the work order: the portal shows them read-only.
|
||||
public bool RaisedByVendor { get; set; }
|
||||
public DateTime? RequestedAt { get; set; }
|
||||
public DateTime? DecidedAt { get; set; }
|
||||
public string? DecisionNote { get; set; }
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ namespace SeaHaven.Services.Implementation
|
|||
Status = UpliftStatus.ToCanonical(u.Status),
|
||||
RequiredTier = u.RequiredTier,
|
||||
RequestedByVendorName = u.RequestedByVendorName,
|
||||
RaisedByVendor = u.RaisedByVendor,
|
||||
RequestedAt = u.CreatedDate,
|
||||
DecidedAt = u.DecidedAt,
|
||||
DecisionNote = u.DecisionNote,
|
||||
|
|
@ -694,7 +695,9 @@ namespace SeaHaven.Services.Implementation
|
|||
if (dispatch == null) throw new KeyNotFoundException("Dispatch not found");
|
||||
|
||||
var req = await _upliftData.GetByIdAndDispatchAsync(requestId, id, cancellationToken);
|
||||
if (req == null) throw new KeyNotFoundException("Uplift request not found");
|
||||
// A vendor withdraws only requests it raised; requests raised from the work order
|
||||
// (createdby set) are read-only in the portal.
|
||||
if (req == null || req.createdby != null) throw new KeyNotFoundException("Uplift request not found");
|
||||
|
||||
if (!UpliftStatus.CanTransition(req.Status, UpliftStatus.Withdrawn))
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue