Files
KArtSell.Aegis/docs/Design/kbx-foundation-v36/backend/Modules/Common/Reconcile/Search/Endpoint.cs
T

70 lines
3.3 KiB
C#

using Dapper;
using FastEndpoints;
using Npgsql;
using Shared.Operations;
namespace Modules.Common.Reconcile.Search;
public sealed class SearchReconcileRequest
{
public string? ReconcileType { get; init; }
public string? Status { get; init; }
public string? Keyword { get; init; }
public int Page { get; init; } = 1;
public int PageSize { get; init; } = 200;
}
public sealed class Endpoint(NpgsqlDataSource dataSource) : Endpoint<SearchReconcileRequest, ReconcileResponse>
{
public override void Configure() { Get("/api/reconcile/items"); Permissions("common.reconcile.read"); }
public override async Task HandleAsync(SearchReconcileRequest req, CancellationToken ct)
{
var tenant = OperationsIdentity.TenantId(User);
var page = Math.Max(1, req.Page);
var pageSize = Math.Clamp(req.PageSize, 1, 500);
var offset = (page - 1) * pageSize;
await using var connection = await dataSource.OpenConnectionAsync(ct);
const string rowsSql = """
select id, reconcile_type as ReconcileType, reference_no as ReferenceNo,
source_label as SourceLabel, target_label as TargetLabel,
expected_value as ExpectedValue, actual_value as ActualValue,
difference_value as DifferenceValue, reason_code as ReasonCode, reason_text as ReasonText,
status, occurred_at as OccurredAt, source_id as SourceId, target_id as TargetId, version
from kbx.reconcile_items
where tenant_id=@Tenant
and (@ReconcileType is null or reconcile_type=@ReconcileType)
and (@Status is null or status=@Status)
and (@Keyword is null or reference_no ilike '%' || @Keyword || '%' or reason_text ilike '%' || @Keyword || '%')
order by case status when 'mismatch' then 0 when 'pending' then 1 when 'resolved' then 2 else 3 end,
occurred_at desc
limit @PageSize offset @Offset;
""";
const string summarySql = """
select count(*)::int as TotalCount,
count(*) filter(where status='matched')::int as MatchedCount,
count(*) filter(where status='mismatch')::int as MismatchCount,
count(*) filter(where status='pending')::int as PendingCount,
count(*) filter(where status='resolved')::int as ResolvedCount
from kbx.reconcile_items
where tenant_id=@Tenant
and (@ReconcileType is null or reconcile_type=@ReconcileType)
and (@Keyword is null or reference_no ilike '%' || @Keyword || '%' or reason_text ilike '%' || @Keyword || '%');
""";
var args = new {
Tenant = tenant,
ReconcileType = Empty(req.ReconcileType),
Status = Empty(req.Status),
Keyword = Empty(req.Keyword),
PageSize = pageSize,
Offset = offset,
};
var rows = (await connection.QueryAsync<ReconcileItemDto>(new CommandDefinition(rowsSql, args, cancellationToken: ct))).AsList();
var summary = await connection.QuerySingleAsync<ReconcileSummary>(new CommandDefinition(summarySql, args, cancellationToken: ct));
await Send.OkAsync(new ReconcileResponse(rows, summary), ct);
}
private static string? Empty(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}