feat: add PaymentStatus to discount order proto + expire pending orders background service
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 7m53s

- 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
This commit is contained in:
masoodafar-web
2026-02-16 22:37:58 +03:30
parent 18a65de8c7
commit c435319025
5 changed files with 133 additions and 1 deletions
@@ -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;
/// <summary>
/// سرویس پس‌زمینه برای منقضی کردن سفارشات تخفیفی که بیش از ۳۰ دقیقه در وضعیت Pending مانده‌اند.
/// موجودی رزرو شده آزاد و وضعیت پرداخت به Reject تغییر می‌کند.
/// </summary>
public class ExpirePendingOrdersService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<ExpirePendingOrdersService> _logger;
/// <summary>
/// مدت زمان انقضا (۳۰ دقیقه)
/// </summary>
private static readonly TimeSpan ExpirationTime = TimeSpan.FromMinutes(30);
/// <summary>
/// هر ۵ دقیقه چک می‌شود
/// </summary>
private static readonly TimeSpan CheckInterval = TimeSpan.FromMinutes(5);
public ExpirePendingOrdersService(
IServiceScopeFactory scopeFactory,
ILogger<ExpirePendingOrdersService> 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<IApplicationDbContext>();
var inventoryService = scope.ServiceProvider.GetRequiredService<IInventoryService>();
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);
}
}
@@ -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<WeeklyCommissionJob>(); // Hangfire Job (Scoped for DI)
services.AddScoped<ChatikaAccountActivationJob>(); // Hangfire Job for Chatika activation
// Expire pending discount orders after 30 minutes
services.AddHostedService<ExpirePendingOrdersService>();
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
{
services.AddDbContext<ApplicationDbContext>(options =>