From c435319025ef8f05054f0a1168e8bece72425aa7 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Mon, 16 Feb 2026 22:37:58 +0330 Subject: [PATCH] feat: add PaymentStatus to discount order proto + expire pending orders background service - Added payment_status field to GetOrderByIdResponse and OrderSummaryDto in proto - Proto version bumped to 0.0.179 - Added PaymentStatus mapping in DiscountOrderService gRPC responses - Created ExpirePendingOrdersService: expires pending orders after 30 min, releases inventory - Registered background service in ConfigureServices --- .../ExpirePendingOrdersService.cs | 116 ++++++++++++++++++ .../ConfigureServices.cs | 4 + .../CMSMicroservice.Protobuf.csproj | 2 +- .../Protos/discountorder.proto | 2 + .../Services/DiscountOrderService.cs | 10 ++ 5 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/CMSMicroservice.Infrastructure/BackgroundServices/ExpirePendingOrdersService.cs diff --git a/src/CMSMicroservice.Infrastructure/BackgroundServices/ExpirePendingOrdersService.cs b/src/CMSMicroservice.Infrastructure/BackgroundServices/ExpirePendingOrdersService.cs new file mode 100644 index 0000000..a059bc4 --- /dev/null +++ b/src/CMSMicroservice.Infrastructure/BackgroundServices/ExpirePendingOrdersService.cs @@ -0,0 +1,116 @@ +using CMSMicroservice.Application.Common.Interfaces; +using CMSMicroservice.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace CMSMicroservice.Infrastructure.BackgroundServices; + +/// +/// سرویس پس‌زمینه برای منقضی کردن سفارشات تخفیفی که بیش از ۳۰ دقیقه در وضعیت Pending مانده‌اند. +/// موجودی رزرو شده آزاد و وضعیت پرداخت به Reject تغییر می‌کند. +/// +public class ExpirePendingOrdersService : BackgroundService +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + + /// + /// مدت زمان انقضا (۳۰ دقیقه) + /// + private static readonly TimeSpan ExpirationTime = TimeSpan.FromMinutes(30); + + /// + /// هر ۵ دقیقه چک می‌شود + /// + private static readonly TimeSpan CheckInterval = TimeSpan.FromMinutes(5); + + public ExpirePendingOrdersService( + IServiceScopeFactory scopeFactory, + ILogger logger) + { + _scopeFactory = scopeFactory; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("ExpirePendingOrdersService started — checking every {Interval} min, expiring after {Expiry} min", + CheckInterval.TotalMinutes, ExpirationTime.TotalMinutes); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + await ExpireOldPendingOrders(stoppingToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in ExpirePendingOrdersService"); + } + + await Task.Delay(CheckInterval, stoppingToken); + } + } + + private async Task ExpireOldPendingOrders(CancellationToken cancellationToken) + { + using var scope = _scopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var inventoryService = scope.ServiceProvider.GetRequiredService(); + + var cutoff = DateTime.UtcNow - ExpirationTime; + + // پیدا کردن سفارشات Pending قدیمی + var expiredOrders = await context.DiscountOrders + .Include(o => o.OrderDetails) + .Where(o => o.PaymentStatus == PaymentStatus.Pending && o.Created < cutoff) + .ToListAsync(cancellationToken); + + if (!expiredOrders.Any()) return; + + _logger.LogInformation("Found {Count} expired pending orders to clean up", expiredOrders.Count); + + foreach (var order in expiredOrders) + { + try + { + // آزادسازی رزرو موجودی + foreach (var detail in order.OrderDetails) + { + await inventoryService.ReleaseReservationAsync( + detail.ProductId, + ProductType.DiscountProduct, + detail.Count, + order.Id, + cancellationToken); + } + + // تغییر وضعیت به Reject + order.PaymentStatus = PaymentStatus.Reject; + + // آپدیت تراکنش مربوطه + if (order.TransactionId.HasValue) + { + var transaction = await context.Transactions + .FirstOrDefaultAsync(t => t.Id == order.TransactionId.Value, cancellationToken); + if (transaction != null) + { + transaction.PaymentStatus = PaymentStatus.Reject; + } + } + + _logger.LogInformation( + "Expired order #{OrderId} (created {Created:u}) — inventory released, status set to Reject", + order.Id, order.Created); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to expire order #{OrderId}", order.Id); + } + } + + await context.SaveChangesAsync(cancellationToken); + } +} diff --git a/src/CMSMicroservice.Infrastructure/ConfigureServices.cs b/src/CMSMicroservice.Infrastructure/ConfigureServices.cs index 314e26c..0503b4c 100644 --- a/src/CMSMicroservice.Infrastructure/ConfigureServices.cs +++ b/src/CMSMicroservice.Infrastructure/ConfigureServices.cs @@ -4,6 +4,7 @@ using CMSMicroservice.Application.DayaLoanCQ.Services; using CMSMicroservice.Infrastructure.Persistence; using CMSMicroservice.Infrastructure.Persistence.Interceptors; using CMSMicroservice.Infrastructure.BackgroundJobs; +using CMSMicroservice.Infrastructure.BackgroundServices; using CMSMicroservice.Infrastructure.Services.Monitoring; using CMSMicroservice.Infrastructure.Services.Authorization; using CMSMicroservice.Infrastructure.Configuration; @@ -119,6 +120,9 @@ public static class ConfigureServices services.AddScoped(); // Hangfire Job (Scoped for DI) services.AddScoped(); // Hangfire Job for Chatika activation + // Expire pending discount orders after 30 minutes + services.AddHostedService(); + if (configuration.GetValue("UseInMemoryDatabase")) { services.AddDbContext(options => diff --git a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj index 6dbf384..d888312 100644 --- a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj +++ b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj @@ -3,7 +3,7 @@ net9.0 enable enable - 0.0.178 + 0.0.179 None False False diff --git a/src/CMSMicroservice.Protobuf/Protos/discountorder.proto b/src/CMSMicroservice.Protobuf/Protos/discountorder.proto index 5e8e362..8de7f08 100644 --- a/src/CMSMicroservice.Protobuf/Protos/discountorder.proto +++ b/src/CMSMicroservice.Protobuf/Protos/discountorder.proto @@ -137,6 +137,7 @@ message GetOrderByIdResponse repeated OrderItemDto items = 14; google.protobuf.Timestamp created = 15; google.protobuf.Timestamp last_modified = 16; + PaymentStatus payment_status = 17; } message AddressInfo @@ -191,6 +192,7 @@ message OrderSummaryDto google.protobuf.StringValue tracking_code = 8; int32 items_count = 9; google.protobuf.Timestamp created = 10; + PaymentStatus payment_status = 11; } // ===== Admin: Get All Discount Orders ===== diff --git a/src/CMSMicroservice.WebApi/Services/DiscountOrderService.cs b/src/CMSMicroservice.WebApi/Services/DiscountOrderService.cs index 7d8dbf2..e6e44ce 100644 --- a/src/CMSMicroservice.WebApi/Services/DiscountOrderService.cs +++ b/src/CMSMicroservice.WebApi/Services/DiscountOrderService.cs @@ -92,6 +92,7 @@ public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractB DiscountBalanceUsed = result.DiscountBalanceUsed, GatewayAmount = result.GatewayAmountPaid, PaymentCompleted = result.PaymentStatus == DomainEnums.PaymentStatus.Success, + PaymentStatus = MapPaymentStatus(result.PaymentStatus), DeliveryStatus = (DeliveryStatus)(int)result.DeliveryStatus, Created = Timestamp.FromDateTime(DateTime.SpecifyKind(result.Created, DateTimeKind.Utc)), }; @@ -151,6 +152,7 @@ public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractB DiscountBalanceUsed = o.DiscountBalanceUsed, GatewayAmount = o.GatewayAmountPaid, PaymentCompleted = o.PaymentStatus == DomainEnums.PaymentStatus.Success, + PaymentStatus = MapPaymentStatus(o.PaymentStatus), DeliveryStatus = (DeliveryStatus)(int)o.DeliveryStatus, ItemsCount = o.ItemsCount, Created = Timestamp.FromDateTime(DateTime.SpecifyKind(o.Created, DateTimeKind.Utc)), @@ -171,4 +173,12 @@ public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractB { return await _dispatchRequestToCQRS.Handle(request, context); } + + private static CMSMicroservice.Protobuf.Protos.DiscountOrder.PaymentStatus MapPaymentStatus(DomainEnums.PaymentStatus status) => status switch + { + DomainEnums.PaymentStatus.Success => CMSMicroservice.Protobuf.Protos.DiscountOrder.PaymentStatus.PaymentCompleted, + DomainEnums.PaymentStatus.Reject => CMSMicroservice.Protobuf.Protos.DiscountOrder.PaymentStatus.PaymentFailed, + DomainEnums.PaymentStatus.Pending => CMSMicroservice.Protobuf.Protos.DiscountOrder.PaymentStatus.PaymentPending, + _ => CMSMicroservice.Protobuf.Protos.DiscountOrder.PaymentStatus.PaymentPending + }; }