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 { 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(new CommandDefinition(rowsSql, args, cancellationToken: ct))).AsList(); var summary = await connection.QuerySingleAsync(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(); }