|
| 1 | +using System.Text; |
| 2 | +using Dapper; |
| 3 | +using OrderMonitor.Core.Interfaces; |
| 4 | +using OrderMonitor.Core.Models; |
| 5 | + |
| 6 | +namespace OrderMonitor.Infrastructure.Data; |
| 7 | + |
| 8 | +/// <summary> |
| 9 | +/// Repository implementation for order data access using Dapper. |
| 10 | +/// </summary> |
| 11 | +public class OrderRepository : IOrderRepository |
| 12 | +{ |
| 13 | + private readonly IDbConnectionFactory _connectionFactory; |
| 14 | + |
| 15 | + public OrderRepository(IDbConnectionFactory connectionFactory) |
| 16 | + { |
| 17 | + _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); |
| 18 | + } |
| 19 | + |
| 20 | + /// <inheritdoc /> |
| 21 | + public async Task<IEnumerable<StuckOrderDto>> GetStuckOrdersAsync( |
| 22 | + StuckOrderQueryParams queryParams, |
| 23 | + CancellationToken cancellationToken = default) |
| 24 | + { |
| 25 | + const string baseSql = @" |
| 26 | + SELECT |
| 27 | + o.CONumber AS OrderId, |
| 28 | + o.OrderNumber, |
| 29 | + o.StatusId, |
| 30 | + s.StatusName AS Status, |
| 31 | + o.ProductType, |
| 32 | + o.StatusUpdatedAt AS StuckSince, |
| 33 | + DATEDIFF(HOUR, o.StatusUpdatedAt, GETUTCDATE()) AS HoursStuck, |
| 34 | + CASE |
| 35 | + WHEN o.StatusId BETWEEN 3001 AND 3910 THEN 6 |
| 36 | + WHEN o.StatusId BETWEEN 4001 AND 5830 THEN 48 |
| 37 | + ELSE 24 |
| 38 | + END AS ThresholdHours, |
| 39 | + o.Region, |
| 40 | + o.CustomerEmail |
| 41 | + FROM Orders o |
| 42 | + INNER JOIN OrderStatuses s ON o.StatusId = s.StatusId |
| 43 | + WHERE |
| 44 | + ( |
| 45 | + (o.StatusId BETWEEN 3001 AND 3910 |
| 46 | + AND DATEDIFF(HOUR, o.StatusUpdatedAt, GETUTCDATE()) > 6) |
| 47 | + OR |
| 48 | + (o.StatusId BETWEEN 4001 AND 5830 |
| 49 | + AND DATEDIFF(HOUR, o.StatusUpdatedAt, GETUTCDATE()) > 48) |
| 50 | + )"; |
| 51 | + |
| 52 | + var sqlBuilder = new StringBuilder(baseSql); |
| 53 | + var parameters = new DynamicParameters(); |
| 54 | + |
| 55 | + // Apply optional filters |
| 56 | + if (queryParams.StatusId.HasValue) |
| 57 | + { |
| 58 | + sqlBuilder.Append(" AND o.StatusId = @StatusId"); |
| 59 | + parameters.Add("StatusId", queryParams.StatusId.Value); |
| 60 | + } |
| 61 | + |
| 62 | + if (!string.IsNullOrWhiteSpace(queryParams.Status)) |
| 63 | + { |
| 64 | + sqlBuilder.Append(" AND s.StatusName LIKE @Status"); |
| 65 | + parameters.Add("Status", $"%{queryParams.Status}%"); |
| 66 | + } |
| 67 | + |
| 68 | + if (queryParams.MinHours.HasValue) |
| 69 | + { |
| 70 | + sqlBuilder.Append(" AND DATEDIFF(HOUR, o.StatusUpdatedAt, GETUTCDATE()) >= @MinHours"); |
| 71 | + parameters.Add("MinHours", queryParams.MinHours.Value); |
| 72 | + } |
| 73 | + |
| 74 | + if (queryParams.MaxHours.HasValue) |
| 75 | + { |
| 76 | + sqlBuilder.Append(" AND DATEDIFF(HOUR, o.StatusUpdatedAt, GETUTCDATE()) <= @MaxHours"); |
| 77 | + parameters.Add("MaxHours", queryParams.MaxHours.Value); |
| 78 | + } |
| 79 | + |
| 80 | + // Order by hours stuck descending (oldest first) |
| 81 | + sqlBuilder.Append(" ORDER BY DATEDIFF(HOUR, o.StatusUpdatedAt, GETUTCDATE()) DESC"); |
| 82 | + |
| 83 | + // Apply pagination |
| 84 | + sqlBuilder.Append(" OFFSET @Offset ROWS FETCH NEXT @Limit ROWS ONLY"); |
| 85 | + parameters.Add("Offset", queryParams.Offset); |
| 86 | + parameters.Add("Limit", queryParams.Limit); |
| 87 | + |
| 88 | + using var connection = _connectionFactory.CreateConnection(); |
| 89 | + return await connection.QueryAsync<StuckOrderDto>( |
| 90 | + new CommandDefinition(sqlBuilder.ToString(), parameters, cancellationToken: cancellationToken)); |
| 91 | + } |
| 92 | + |
| 93 | + /// <inheritdoc /> |
| 94 | + public async Task<int> GetStuckOrdersCountAsync(CancellationToken cancellationToken = default) |
| 95 | + { |
| 96 | + const string sql = @" |
| 97 | + SELECT COUNT(*) |
| 98 | + FROM Orders o |
| 99 | + WHERE |
| 100 | + ( |
| 101 | + (o.StatusId BETWEEN 3001 AND 3910 |
| 102 | + AND DATEDIFF(HOUR, o.StatusUpdatedAt, GETUTCDATE()) > 6) |
| 103 | + OR |
| 104 | + (o.StatusId BETWEEN 4001 AND 5830 |
| 105 | + AND DATEDIFF(HOUR, o.StatusUpdatedAt, GETUTCDATE()) > 48) |
| 106 | + )"; |
| 107 | + |
| 108 | + using var connection = _connectionFactory.CreateConnection(); |
| 109 | + return await connection.ExecuteScalarAsync<int>( |
| 110 | + new CommandDefinition(sql, cancellationToken: cancellationToken)); |
| 111 | + } |
| 112 | + |
| 113 | + /// <inheritdoc /> |
| 114 | + public async Task<IEnumerable<OrderStatusHistoryDto>> GetOrderStatusHistoryAsync( |
| 115 | + string orderId, |
| 116 | + CancellationToken cancellationToken = default) |
| 117 | + { |
| 118 | + const string sql = @" |
| 119 | + WITH StatusDurations AS ( |
| 120 | + SELECT |
| 121 | + sh.StatusId, |
| 122 | + s.StatusName AS Status, |
| 123 | + sh.Timestamp, |
| 124 | + LEAD(sh.Timestamp) OVER (ORDER BY sh.Timestamp) AS NextTimestamp |
| 125 | + FROM OrderStatusHistory sh |
| 126 | + INNER JOIN OrderStatuses s ON sh.StatusId = s.StatusId |
| 127 | + WHERE sh.OrderId = @OrderId |
| 128 | + ) |
| 129 | + SELECT |
| 130 | + StatusId, |
| 131 | + Status, |
| 132 | + Timestamp, |
| 133 | + CASE |
| 134 | + WHEN NextTimestamp IS NULL THEN |
| 135 | + CASE |
| 136 | + WHEN (StatusId BETWEEN 3001 AND 3910 AND DATEDIFF(HOUR, Timestamp, GETUTCDATE()) > 6) |
| 137 | + OR (StatusId BETWEEN 4001 AND 5830 AND DATEDIFF(HOUR, Timestamp, GETUTCDATE()) > 48) |
| 138 | + THEN CONCAT(DATEDIFF(HOUR, Timestamp, GETUTCDATE()), 'h+ (STUCK)') |
| 139 | + ELSE CONCAT(DATEDIFF(HOUR, Timestamp, GETUTCDATE()), 'h (Current)') |
| 140 | + END |
| 141 | + ELSE |
| 142 | + CASE |
| 143 | + WHEN DATEDIFF(MINUTE, Timestamp, NextTimestamp) < 60 |
| 144 | + THEN CONCAT(DATEDIFF(MINUTE, Timestamp, NextTimestamp), 'm') |
| 145 | + ELSE CONCAT(DATEDIFF(HOUR, Timestamp, NextTimestamp), 'h ', |
| 146 | + DATEDIFF(MINUTE, Timestamp, NextTimestamp) % 60, 'm') |
| 147 | + END |
| 148 | + END AS Duration, |
| 149 | + CASE |
| 150 | + WHEN NextTimestamp IS NULL AND |
| 151 | + ((StatusId BETWEEN 3001 AND 3910 AND DATEDIFF(HOUR, Timestamp, GETUTCDATE()) > 6) |
| 152 | + OR (StatusId BETWEEN 4001 AND 5830 AND DATEDIFF(HOUR, Timestamp, GETUTCDATE()) > 48)) |
| 153 | + THEN 1 |
| 154 | + ELSE 0 |
| 155 | + END AS IsStuck |
| 156 | + FROM StatusDurations |
| 157 | + ORDER BY Timestamp ASC"; |
| 158 | + |
| 159 | + using var connection = _connectionFactory.CreateConnection(); |
| 160 | + return await connection.QueryAsync<OrderStatusHistoryDto>( |
| 161 | + new CommandDefinition(sql, new { OrderId = orderId }, cancellationToken: cancellationToken)); |
| 162 | + } |
| 163 | +} |
0 commit comments