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;
}
}