using MediatR; using Microsoft.Extensions.Logging; using System.Text.Json; using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Domain.Tenants.Entities; using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Ids; namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// /// 自动续费处理器:生成续费账单(幂等)。 /// public sealed class ProcessAutoRenewalCommandHandler( ISubscriptionRepository subscriptionRepository, ITenantBillingRepository billingRepository, IIdGenerator idGenerator, ILogger logger) : IRequestHandler { /// public async Task Handle(ProcessAutoRenewalCommand request, CancellationToken cancellationToken) { // 1. 计算续费阈值时间 var now = DateTime.UtcNow; var renewalThreshold = now.AddDays(request.RenewalDaysBeforeExpiry); // 2. 查询候选订阅(含套餐) var candidates = await subscriptionRepository.FindAutoRenewalCandidatesAsync( now, renewalThreshold, cancellationToken); var createdBillCount = 0; // 3. 遍历候选订阅,生成账单 foreach (var candidate in candidates) { // 3.1 幂等校验:同一周期开始时间只允许存在一张未取消账单 var periodStart = candidate.Subscription.EffectiveTo; var exists = await billingRepository.ExistsNotCancelledByPeriodStartAsync( candidate.Subscription.TenantId, periodStart, cancellationToken); if (exists) { logger.LogInformation( "自动续费:租户 {TenantId} 订阅 {SubscriptionId} 已存在周期 {PeriodStart} 的续费账单,跳过", candidate.Subscription.TenantId, candidate.Subscription.Id, periodStart); continue; } // 3.2 计算续费周期(月) var durationMonths = CalculateDurationMonths(candidate.Subscription.EffectiveFrom, candidate.Subscription.EffectiveTo); if (durationMonths <= 0) { durationMonths = 1; } // 3.3 计算账单周期与金额 var periodEnd = periodStart.AddMonths(durationMonths); var amountDue = CalculateRenewalAmount(candidate.Package, durationMonths); // 3.4 生成账单(Pending) var statementNo = $"BILL-{now:yyyyMMddHHmmss}-{candidate.Subscription.TenantId}-{candidate.Subscription.Id}"; var lineItemsJson = JsonSerializer.Serialize(new { PackageName = candidate.Package.Name, RenewalMonths = durationMonths, SubscriptionId = candidate.Subscription.Id }); await billingRepository.AddAsync(new TenantBillingStatement { Id = idGenerator.NextId(), TenantId = candidate.Subscription.TenantId, StatementNo = statementNo, PeriodStart = periodStart, PeriodEnd = periodEnd, AmountDue = amountDue, AmountPaid = 0, DueDate = periodStart.AddDays(-1), LineItemsJson = lineItemsJson, CreatedAt = now }, cancellationToken); createdBillCount++; logger.LogInformation( "自动续费:为租户 {TenantId} 订阅 {SubscriptionId} 生成账单 {StatementNo},金额 {AmountDue},周期 {PeriodStart}~{PeriodEnd}", candidate.Subscription.TenantId, candidate.Subscription.Id, statementNo, amountDue, periodStart, periodEnd); } // 4. 保存账单变更 if (createdBillCount > 0) { await billingRepository.SaveChangesAsync(cancellationToken); } return new ProcessAutoRenewalResult { CandidateCount = candidates.Count, CreatedBillCount = createdBillCount }; } private static int CalculateDurationMonths(DateTime effectiveFrom, DateTime effectiveTo) { // 1. 以年月差作为周期(月),兼容“按月续费”模型 var months = (effectiveTo.Year - effectiveFrom.Year) * 12 + effectiveTo.Month - effectiveFrom.Month; // 2. 对不足 1 个月的情况兜底为 1 return months <= 0 ? 1 : months; } private static decimal CalculateRenewalAmount(TenantPackage package, int durationMonths) { // 1. 优先使用年付价(按整年计费),剩余月份按月付价补齐 var monthlyPrice = package.MonthlyPrice ?? 0m; var yearlyPrice = package.YearlyPrice; if (yearlyPrice is null || durationMonths < 12) { return monthlyPrice * durationMonths; } // 2. 按年 + 月组合计算金额 var years = durationMonths / 12; var remainingMonths = durationMonths % 12; return yearlyPrice.Value * years + monthlyPrice * remainingMonths; } }