diff --git a/src/CMSMicroservice.Infrastructure/BackgroundServices/MagicWalletCycleSeedService.cs b/src/CMSMicroservice.Infrastructure/BackgroundServices/MagicWalletCycleSeedService.cs
new file mode 100644
index 0000000..72cdd87
--- /dev/null
+++ b/src/CMSMicroservice.Infrastructure/BackgroundServices/MagicWalletCycleSeedService.cs
@@ -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;
+
+///
+/// سرویس یکباره برای Seed کردن ClubMembershipCycle برای عضویتهای فعال موجود.
+/// عضویتهایی که قبل از اضافه شدن سیستم Cycle ایجاد شدهاند رکورد ندارند.
+/// این Worker در استارتاپ اجرا شده، برای آنها CycleNumber=1 میسازد و سپس متوقف میشود.
+///
+/// فعال/غیرفعال از appsettings:
+/// "SeedWorkers": { "MagicWalletCycleSeed": { "Enabled": true } }
+///
+/// بعد از اجرای موفق، Enabled را false کنید.
+///
+public class MagicWalletCycleSeedService : BackgroundService
+{
+ private readonly IServiceScopeFactory _scopeFactory;
+ private readonly ILogger _logger;
+
+ public MagicWalletCycleSeedService(
+ IServiceScopeFactory scopeFactory,
+ ILogger 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();
+
+ 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 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;
+ }
+}
diff --git a/src/CMSMicroservice.Infrastructure/ConfigureServices.cs b/src/CMSMicroservice.Infrastructure/ConfigureServices.cs
index bb47e93..016fb9d 100644
--- a/src/CMSMicroservice.Infrastructure/ConfigureServices.cs
+++ b/src/CMSMicroservice.Infrastructure/ConfigureServices.cs
@@ -126,6 +126,10 @@ public static class ConfigureServices
// One-time: Initialize inventory records for existing products
services.AddHostedService();
+ // One-time: Seed ClubMembershipCycle for existing active memberships
+ if (configuration.GetValue("SeedWorkers:MagicWalletCycleSeed:Enabled"))
+ services.AddHostedService();
+
if (configuration.GetValue("UseInMemoryDatabase"))
{
services.AddDbContext(options =>
diff --git a/src/CMSMicroservice.WebApi/appsettings.Staging.json b/src/CMSMicroservice.WebApi/appsettings.Staging.json
index c0f39e7..608c072 100644
--- a/src/CMSMicroservice.WebApi/appsettings.Staging.json
+++ b/src/CMSMicroservice.WebApi/appsettings.Staging.json
@@ -68,6 +68,11 @@
"CronExpression": "5 0 * * 0"
}
},
+ "SeedWorkers": {
+ "MagicWalletCycleSeed": {
+ "Enabled": true
+ }
+ },
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
diff --git a/src/CMSMicroservice.WebApi/appsettings.json b/src/CMSMicroservice.WebApi/appsettings.json
index d8263d5..424513b 100644
--- a/src/CMSMicroservice.WebApi/appsettings.json
+++ b/src/CMSMicroservice.WebApi/appsettings.json
@@ -75,6 +75,11 @@
"CronExpression": "5 0 * * 0"
}
},
+ "SeedWorkers": {
+ "MagicWalletCycleSeed": {
+ "Enabled": true
+ }
+ },
"AllowedHosts": "*",
"Authentication": {
"Authority": "https://ids.domain.com/",