43 lines
2.3 KiB
C#
43 lines
2.3 KiB
C#
using Dapper;
|
|
using FastEndpoints;
|
|
using Npgsql;
|
|
using Shared.Operations;
|
|
|
|
namespace Modules.Common.Operations.Resolve;
|
|
|
|
public sealed record ResolveWorkItemsRequest(IReadOnlyList<Guid> Ids, string Reason);
|
|
public sealed record ResolveWorkItemsResponse(int ResolvedCount, int RejectedCount);
|
|
|
|
public sealed class Endpoint(NpgsqlDataSource dataSource) : Endpoint<ResolveWorkItemsRequest, ResolveWorkItemsResponse>
|
|
{
|
|
public override void Configure() { Post("/api/operations/work-items/resolve"); Permissions("common.operations.resolve"); }
|
|
|
|
public override async Task HandleAsync(ResolveWorkItemsRequest req, CancellationToken ct)
|
|
{
|
|
var ids = req.Ids.Distinct().Take(500).ToArray();
|
|
var tenant = OperationsIdentity.TenantId(User);
|
|
var actorId = OperationsIdentity.UserId(User);
|
|
var actorName = OperationsIdentity.UserName(User);
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
await using var tx = await connection.BeginTransactionAsync(ct);
|
|
|
|
// Manual resolution is opt-in. Domain-originated exceptions should normally be resolved
|
|
// by source-module events through OperationsProjectionWriter.ResolveBySourceAsync().
|
|
var changed = (await connection.QueryAsync<Guid>(new CommandDefinition("""
|
|
update kbx.work_items
|
|
set status='resolved', resolution_reason=@Reason, resolved_at=now(), version=version+1, updated_at=now()
|
|
where tenant_id=@Tenant and id=any(@Ids) and allow_manual_resolution=true and status in ('open','claimed')
|
|
returning id;
|
|
""", new { Tenant = tenant, Ids = ids, Reason = req.Reason }, tx, cancellationToken: ct))).AsList();
|
|
|
|
foreach (var id in changed)
|
|
await connection.ExecuteAsync(new CommandDefinition("""
|
|
insert into kbx.work_item_audit(id, work_item_id, tenant_id, action, actor_id, actor_name, reason, after_status)
|
|
values(@Id, @WorkItemId, @Tenant, 'manual-resolve', @ActorId, @ActorName, @Reason, 'resolved');
|
|
""", new { Id = Guid.NewGuid(), WorkItemId = id, Tenant = tenant, ActorId = actorId, ActorName = actorName, Reason = req.Reason }, tx, cancellationToken: ct));
|
|
|
|
await tx.CommitAsync(ct);
|
|
await Send.OkAsync(new(changed.Count, ids.Length - changed.Count), ct);
|
|
}
|
|
}
|