feat: add PaymentStatus to discount order proto + expire pending orders background service
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 7m53s
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:
@@ -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 =>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>0.0.178</Version>
|
||||
<Version>0.0.179</Version>
|
||||
<DebugType>None</DebugType>
|
||||
<DebugSymbols>False</DebugSymbols>
|
||||
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
||||
|
||||
@@ -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 =====
|
||||
|
||||
@@ -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<GetDiscountSalesReportRequest, GetDiscountSalesReportQuery, GetDiscountSalesReportResponse>(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
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user