Merge kub-stage into production
Build and Deploy to Production / build-and-deploy (push) Successful in 11m49s

- Resolved appsettings.Production.json conflict (new MerchantId, SeedWorkers)
- Removed duplicate u21 migration (kept 155925 from production)
- All features: Magic Wallet, Discount Wallet, StockMovement fix, FullInformation expansion, Wallet user_name, gateway activation
This commit is contained in:
masoodafar-web
2026-02-22 22:21:36 +03:30
18 changed files with 5140 additions and 17 deletions
@@ -0,0 +1,126 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities.Club;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Infrastructure.BackgroundServices;
/// <summary>
/// سرویس یکباره برای Seed کردن ClubMembershipCycle برای عضویت‌های فعال موجود.
/// عضویت‌هایی که قبل از اضافه شدن سیستم Cycle ایجاد شده‌اند رکورد ندارند.
/// این Worker در استارتاپ اجرا شده، برای آنها CycleNumber=1 می‌سازد و سپس متوقف می‌شود.
///
/// فعال/غیرفعال از appsettings:
/// "SeedWorkers": { "MagicWalletCycleSeed": { "Enabled": true } }
///
/// بعد از اجرای موفق، Enabled را false کنید.
/// </summary>
public class MagicWalletCycleSeedService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<MagicWalletCycleSeedService> _logger;
public MagicWalletCycleSeedService(
IServiceScopeFactory scopeFactory,
ILogger<MagicWalletCycleSeedService> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// صبر کوتاه برای اطمینان از آماده شدن دیتابیس
await Task.Delay(TimeSpan.FromSeconds(15), stoppingToken);
_logger.LogInformation("MagicWalletCycleSeedService started — scanning active memberships without cycles...");
try
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<IApplicationDbContext>();
var seededCount = await SeedMissingCycles(context, stoppingToken);
_logger.LogInformation(
"MagicWalletCycleSeedService completed — seeded {Count} ClubMembershipCycle records",
seededCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "MagicWalletCycleSeedService encountered an error");
}
_logger.LogInformation(
"MagicWalletCycleSeedService finished — shutting down (one-time execution). " +
"Set SeedWorkers:MagicWalletCycleSeed:Enabled to false in appsettings.");
}
private async Task<int> SeedMissingCycles(IApplicationDbContext context, CancellationToken ct)
{
// پیدا کردن عضویت‌های فعالی که هنوز Cycle ندارند
var membershipIdsWithCycle = await context.ClubMembershipCycles
.Where(c => !c.IsDeleted)
.Select(c => c.ClubMembershipId)
.Distinct()
.ToListAsync(ct);
var membershipsWithoutCycle = await context.ClubMemberships
.Where(cm => !cm.IsDeleted && cm.IsActive && !membershipIdsWithCycle.Contains(cm.Id))
.Select(cm => new
{
cm.Id,
cm.UserId,
cm.ActivatedAt
})
.ToListAsync(ct);
if (membershipsWithoutCycle.Count == 0)
{
_logger.LogInformation("No active memberships without cycles found — nothing to seed");
return 0;
}
_logger.LogInformation("Found {Count} active memberships without ClubMembershipCycle — seeding...",
membershipsWithoutCycle.Count);
var seeded = 0;
foreach (var cm in membershipsWithoutCycle)
{
if (ct.IsCancellationRequested) break;
try
{
var cycle = new ClubMembershipCycle
{
UserId = cm.UserId,
ClubMembershipId = cm.Id,
CycleNumber = 1,
PackagePurchasedAt = cm.ActivatedAt ?? DateTime.UtcNow,
IsCurrentCycle = true,
PurchaseMethod = 0,
PackageAmount = 0
};
context.ClubMembershipCycles.Add(cycle);
seeded++;
_logger.LogDebug(
"Seeded ClubMembershipCycle for UserId={UserId}, MembershipId={MembershipId}, ActivatedAt={ActivatedAt}",
cm.UserId, cm.Id, cm.ActivatedAt);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Failed to seed cycle for MembershipId={MembershipId}, UserId={UserId}",
cm.Id, cm.UserId);
}
}
await context.SaveChangesAsync(ct);
return seeded;
}
}
@@ -126,6 +126,10 @@ public static class ConfigureServices
// One-time: Initialize inventory records for existing products
services.AddHostedService<InventoryInitializerService>();
// One-time: Seed ClubMembershipCycle for existing active memberships
if (configuration.GetValue<bool>("SeedWorkers:MagicWalletCycleSeed:Enabled"))
services.AddHostedService<MagicWalletCycleSeedService>();
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
{
services.AddDbContext<ApplicationDbContext>(options =>
@@ -26,8 +26,7 @@ public class DiscountProductConfiguration : IEntityTypeConfiguration<DiscountPro
.HasMaxLength(500);
builder.Property(entity => entity.FullInformation)
.IsRequired()
.HasMaxLength(2000);
.IsRequired();
builder.Property(entity => entity.Price)
.IsRequired();
@@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class ExpandDiscountProductFullInformation : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "FullInformation",
schema: "CMS",
table: "DiscountProducts",
type: "nvarchar(max)",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(2000)",
oldMaxLength: 2000);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "FullInformation",
schema: "CMS",
table: "DiscountProducts",
type: "nvarchar(2000)",
maxLength: 2000,
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(max)");
}
}
}
@@ -1404,8 +1404,7 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<string>("FullInformation")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
.HasColumnType("nvarchar(max)");
b.Property<string>("ImagePath")
.IsRequired()