97 lines
3.7 KiB
C#
97 lines
3.7 KiB
C#
using Dapper;
|
|
using Npgsql;
|
|
|
|
namespace Modules.OMS.Orders.Search;
|
|
|
|
public sealed class SearchOrdersHandler(NpgsqlDataSource dataSource)
|
|
{
|
|
public async Task<SearchOrdersResponse> HandleAsync(SearchOrdersRequest request, CancellationToken ct)
|
|
{
|
|
var page = Math.Max(request.Page, 1);
|
|
var pageSize = Math.Clamp(request.PageSize, 1, 500);
|
|
var offset = (page - 1) * pageSize;
|
|
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
|
|
// Read model query: UI-required projection in one server-side query boundary.
|
|
const string rowsSql = """
|
|
select
|
|
o.id,
|
|
o.order_no as OrderNo,
|
|
ch.name as ChannelName,
|
|
o.ordered_at as OrderedAt,
|
|
o.customer_name as CustomerName,
|
|
o.item_summary as ItemSummary,
|
|
o.total_qty as TotalQty,
|
|
o.amount as Amount,
|
|
o.allocation_status as AllocationStatus,
|
|
o.shipment_status as ShipmentStatus,
|
|
o.exception_count as ExceptionCount
|
|
from oms_order_search_projection o
|
|
join sales_channel ch on ch.id = o.channel_id
|
|
where o.ordered_at >= @From
|
|
and o.ordered_at < @ToExclusive
|
|
and (@ChannelId is null or o.channel_id = @ChannelId)
|
|
and (@Status is null or o.shipment_status = @Status)
|
|
and (@Keyword is null or o.search_text ilike '%' || @Keyword || '%')
|
|
and (@ExceptionOnly = false or o.exception_count > 0)
|
|
order by o.ordered_at desc, o.id desc
|
|
limit @PageSize offset @Offset;
|
|
""";
|
|
|
|
const string summarySql = """
|
|
select
|
|
count(*)::int as TotalCount,
|
|
coalesce(sum(o.total_qty), 0) as TotalQty,
|
|
coalesce(sum(o.amount), 0) as TotalAmount,
|
|
count(*) filter (where o.order_status = 'NEW')::int as NewCount,
|
|
count(*) filter (where o.shipment_status = 'READY')::int as ReadyToShipCount,
|
|
count(*) filter (where o.exception_count > 0)::int as ExceptionCount
|
|
from oms_order_search_projection o
|
|
where o.ordered_at >= @From
|
|
and o.ordered_at < @ToExclusive
|
|
and (@ChannelId is null or o.channel_id = @ChannelId)
|
|
and (@Status is null or o.shipment_status = @Status)
|
|
and (@Keyword is null or o.search_text ilike '%' || @Keyword || '%')
|
|
and (@ExceptionOnly = false or o.exception_count > 0);
|
|
""";
|
|
|
|
var args = new
|
|
{
|
|
From = request.From.ToDateTime(TimeOnly.MinValue),
|
|
ToExclusive = request.To.AddDays(1).ToDateTime(TimeOnly.MinValue),
|
|
request.ChannelId,
|
|
Status = string.IsNullOrWhiteSpace(request.Status) ? null : request.Status,
|
|
Keyword = string.IsNullOrWhiteSpace(request.Keyword) ? null : request.Keyword.Trim(),
|
|
request.ExceptionOnly,
|
|
PageSize = pageSize,
|
|
Offset = offset,
|
|
};
|
|
|
|
var rows = (await connection.QueryAsync<OrderSearchRow>(
|
|
new CommandDefinition(rowsSql, args, cancellationToken: ct))).AsList();
|
|
|
|
var summary = await connection.QuerySingleAsync<SummaryRow>(
|
|
new CommandDefinition(summarySql, args, cancellationToken: ct));
|
|
|
|
return new SearchOrdersResponse(
|
|
rows,
|
|
summary.TotalCount,
|
|
summary.TotalQty,
|
|
summary.TotalAmount,
|
|
new OrderSearchCounters(
|
|
summary.TotalCount,
|
|
summary.NewCount,
|
|
summary.ReadyToShipCount,
|
|
summary.ExceptionCount));
|
|
}
|
|
|
|
private sealed record SummaryRow(
|
|
int TotalCount,
|
|
decimal TotalQty,
|
|
decimal TotalAmount,
|
|
int NewCount,
|
|
int ReadyToShipCount,
|
|
int ExceptionCount);
|
|
}
|