Subscription required level, optimized cancellation logic

This commit is contained in:
LittleSheep 2025-07-02 21:58:44 +08:00
parent fb885e138d
commit 6449926334
3 changed files with 38 additions and 10 deletions

View File

@ -92,7 +92,7 @@ public abstract class SubscriptionPaymentMethod
public enum SubscriptionStatus public enum SubscriptionStatus
{ {
Unpaid, Unpaid,
Paid, Active,
Expired, Expired,
Cancelled Cancelled
} }
@ -152,7 +152,7 @@ public class Subscription : ModelBase
if (BegunAt > now) return false; if (BegunAt > now) return false;
if (EndedAt.HasValue && now > EndedAt.Value) return false; if (EndedAt.HasValue && now > EndedAt.Value) return false;
if (RenewalAt.HasValue && now > RenewalAt.Value) return false; if (RenewalAt.HasValue && now > RenewalAt.Value) return false;
if (Status != SubscriptionStatus.Paid) return false; if (Status != SubscriptionStatus.Active) return false;
return true; return true;
} }

View File

@ -29,7 +29,7 @@ public class SubscriptionRenewalJob(
// Find subscriptions that need renewal (due for renewal and are still active) // Find subscriptions that need renewal (due for renewal and are still active)
var subscriptionsToRenew = await db.WalletSubscriptions var subscriptionsToRenew = await db.WalletSubscriptions
.Where(s => s.RenewalAt.HasValue && s.RenewalAt.Value <= now) // Due for renewal .Where(s => s.RenewalAt.HasValue && s.RenewalAt.Value <= now) // Due for renewal
.Where(s => s.Status == SubscriptionStatus.Paid) // Only paid subscriptions .Where(s => s.Status == SubscriptionStatus.Active) // Only paid subscriptions
.Where(s => s.IsActive) // Only active subscriptions .Where(s => s.IsActive) // Only active subscriptions
.Where(s => !s.IsFreeTrial) // Exclude free trials .Where(s => !s.IsFreeTrial) // Exclude free trials
.OrderBy(s => s.RenewalAt) // Process oldest first .OrderBy(s => s.RenewalAt) // Process oldest first
@ -49,6 +49,17 @@ public class SubscriptionRenewalJob(
"Processing renewal for subscription {SubscriptionId} (Identifier: {Identifier}) for account {AccountId}", "Processing renewal for subscription {SubscriptionId} (Identifier: {Identifier}) for account {AccountId}",
subscription.Id, subscription.Identifier, subscription.AccountId); subscription.Id, subscription.Identifier, subscription.AccountId);
if (subscription.RenewalAt is null)
{
logger.LogWarning(
"Subscription {SubscriptionId} (Identifier: {Identifier}) has no renewal date or has been cancelled.",
subscription.Id, subscription.Identifier);
subscription.Status = SubscriptionStatus.Cancelled;
db.WalletSubscriptions.Update(subscription);
await db.SaveChangesAsync();
continue;
}
// Calculate next cycle duration based on current cycle // Calculate next cycle duration based on current cycle
var currentCycle = subscription.EndedAt!.Value - subscription.BegunAt; var currentCycle = subscription.EndedAt!.Value - subscription.BegunAt;

View File

@ -53,6 +53,18 @@ public class SubscriptionService(
if (existingSubscription is not null) if (existingSubscription is not null)
return existingSubscription; return existingSubscription;
if (subscriptionInfo.RequiredLevel > 0)
{
var profile = await db.AccountProfiles
.Where(p => p.AccountId == account.Id)
.FirstOrDefaultAsync();
if (profile is null) throw new InvalidOperationException("Account profile was not found.");
if (profile.Level < subscriptionInfo.RequiredLevel)
throw new InvalidOperationException(
$"Account level must be at least {subscriptionInfo.RequiredLevel} to subscribe to {identifier}."
);
}
if (isFreeTrial) if (isFreeTrial)
{ {
var prevFreeTrial = await db.WalletSubscriptions var prevFreeTrial = await db.WalletSubscriptions
@ -146,7 +158,7 @@ public class SubscriptionService(
existingSubscription.PaymentDetails.OrderId = order.Id; existingSubscription.PaymentDetails.OrderId = order.Id;
existingSubscription.EndedAt = order.BegunAt.Plus(cycleDuration); existingSubscription.EndedAt = order.BegunAt.Plus(cycleDuration);
existingSubscription.RenewalAt = order.BegunAt.Plus(cycleDuration); existingSubscription.RenewalAt = order.BegunAt.Plus(cycleDuration);
existingSubscription.Status = SubscriptionStatus.Paid; existingSubscription.Status = SubscriptionStatus.Active;
db.Update(existingSubscription); db.Update(existingSubscription);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
@ -159,7 +171,7 @@ public class SubscriptionService(
BegunAt = order.BegunAt, BegunAt = order.BegunAt,
EndedAt = order.BegunAt.Plus(cycleDuration), EndedAt = order.BegunAt.Plus(cycleDuration),
IsActive = true, IsActive = true,
Status = SubscriptionStatus.Paid, Status = SubscriptionStatus.Active,
Identifier = subscriptionIdentifier, Identifier = subscriptionIdentifier,
PaymentMethod = provider, PaymentMethod = provider,
PaymentDetails = new PaymentDetails PaymentDetails = new PaymentDetails
@ -192,10 +204,15 @@ public class SubscriptionService(
var subscription = await GetSubscriptionAsync(accountId, identifier); var subscription = await GetSubscriptionAsync(accountId, identifier);
if (subscription is null) if (subscription is null)
throw new InvalidOperationException($"Subscription with identifier {identifier} was not found."); throw new InvalidOperationException($"Subscription with identifier {identifier} was not found.");
if (subscription.Status == SubscriptionStatus.Cancelled) if (subscription.Status != SubscriptionStatus.Active)
throw new InvalidOperationException("Subscription is already cancelled."); throw new InvalidOperationException("Subscription is already cancelled.");
if (subscription.RenewalAt is null)
throw new InvalidOperationException("Subscription is no need to be cancelled.");
if (subscription.PaymentMethod != SubscriptionPaymentMethod.InAppWallet)
throw new InvalidOperationException(
"Only in-app wallet subscription can be cancelled. For other payment methods, please head to the payment provider."
);
subscription.Status = SubscriptionStatus.Cancelled;
subscription.RenewalAt = null; subscription.RenewalAt = null;
await db.SaveChangesAsync(); await db.SaveChangesAsync();
@ -276,7 +293,7 @@ public class SubscriptionService(
subscription.EndedAt = nextEndedAt; subscription.EndedAt = nextEndedAt;
} }
subscription.Status = SubscriptionStatus.Paid; subscription.Status = SubscriptionStatus.Active;
db.Update(subscription); db.Update(subscription);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
@ -306,7 +323,7 @@ public class SubscriptionService(
// Find active subscriptions that have passed their end date // Find active subscriptions that have passed their end date
var expiredSubscriptions = await db.WalletSubscriptions var expiredSubscriptions = await db.WalletSubscriptions
.Where(s => s.IsActive) .Where(s => s.IsActive)
.Where(s => s.Status == SubscriptionStatus.Paid) .Where(s => s.Status == SubscriptionStatus.Active)
.Where(s => s.EndedAt.HasValue && s.EndedAt.Value < now) .Where(s => s.EndedAt.HasValue && s.EndedAt.Value < now)
.Take(batchSize) .Take(batchSize)
.ToListAsync(); .ToListAsync();