✨ Subscription and stellar program
This commit is contained in:
parent
9fd6016308
commit
698442ad13
@ -66,7 +66,7 @@ public class PublisherMember : ModelBase
|
|||||||
public Instant? JoinedAt { get; set; }
|
public Instant? JoinedAt { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SubscriptionStatus
|
public enum PublisherSubscriptionStatus
|
||||||
{
|
{
|
||||||
Active,
|
Active,
|
||||||
Expired,
|
Expired,
|
||||||
@ -82,7 +82,7 @@ public class PublisherSubscription : ModelBase
|
|||||||
public Guid AccountId { get; set; }
|
public Guid AccountId { get; set; }
|
||||||
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
[JsonIgnore] public Account.Account Account { get; set; } = null!;
|
||||||
|
|
||||||
public SubscriptionStatus Status { get; set; } = SubscriptionStatus.Active;
|
public PublisherSubscriptionStatus Status { get; set; } = PublisherSubscriptionStatus.Active;
|
||||||
public int Tier { get; set; } = 0;
|
public int Tier { get; set; } = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ public class PublisherService(AppDatabase db, FileReferenceService fileRefServic
|
|||||||
// If not in cache, fetch from a database
|
// If not in cache, fetch from a database
|
||||||
var publishersId = await db.PublisherSubscriptions
|
var publishersId = await db.PublisherSubscriptions
|
||||||
.Where(p => p.AccountId == userId)
|
.Where(p => p.AccountId == userId)
|
||||||
.Where(p => p.Status == SubscriptionStatus.Active)
|
.Where(p => p.Status == PublisherSubscriptionStatus.Active)
|
||||||
.Select(p => p.PublisherId)
|
.Select(p => p.PublisherId)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
publishers = await db.Publishers
|
publishers = await db.Publishers
|
||||||
|
@ -26,7 +26,7 @@ public class PublisherSubscriptionService(
|
|||||||
return await db.PublisherSubscriptions
|
return await db.PublisherSubscriptions
|
||||||
.AnyAsync(ps => ps.AccountId == accountId &&
|
.AnyAsync(ps => ps.AccountId == accountId &&
|
||||||
ps.PublisherId == publisherId &&
|
ps.PublisherId == publisherId &&
|
||||||
ps.Status == SubscriptionStatus.Active);
|
ps.Status == PublisherSubscriptionStatus.Active);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -52,7 +52,7 @@ public class PublisherSubscriptionService(
|
|||||||
var subscribers = await db.PublisherSubscriptions
|
var subscribers = await db.PublisherSubscriptions
|
||||||
.Include(p => p.Account)
|
.Include(p => p.Account)
|
||||||
.Where(p => p.PublisherId == post.PublisherId &&
|
.Where(p => p.PublisherId == post.PublisherId &&
|
||||||
p.Status == SubscriptionStatus.Active)
|
p.Status == PublisherSubscriptionStatus.Active)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
if (subscribers.Count == 0)
|
if (subscribers.Count == 0)
|
||||||
return 0;
|
return 0;
|
||||||
@ -105,7 +105,7 @@ public class PublisherSubscriptionService(
|
|||||||
{
|
{
|
||||||
return await db.PublisherSubscriptions
|
return await db.PublisherSubscriptions
|
||||||
.Include(ps => ps.Publisher)
|
.Include(ps => ps.Publisher)
|
||||||
.Where(ps => ps.AccountId == accountId && ps.Status == SubscriptionStatus.Active)
|
.Where(ps => ps.AccountId == accountId && ps.Status == PublisherSubscriptionStatus.Active)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ public class PublisherSubscriptionService(
|
|||||||
{
|
{
|
||||||
return await db.PublisherSubscriptions
|
return await db.PublisherSubscriptions
|
||||||
.Include(ps => ps.Account)
|
.Include(ps => ps.Account)
|
||||||
.Where(ps => ps.PublisherId == publisherId && ps.Status == SubscriptionStatus.Active)
|
.Where(ps => ps.PublisherId == publisherId && ps.Status == PublisherSubscriptionStatus.Active)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,8 +141,8 @@ public class PublisherSubscriptionService(
|
|||||||
if (existingSubscription != null)
|
if (existingSubscription != null)
|
||||||
{
|
{
|
||||||
// If it exists but is not active, reactivate it
|
// If it exists but is not active, reactivate it
|
||||||
if (existingSubscription.Status == SubscriptionStatus.Active) return existingSubscription;
|
if (existingSubscription.Status == PublisherSubscriptionStatus.Active) return existingSubscription;
|
||||||
existingSubscription.Status = SubscriptionStatus.Active;
|
existingSubscription.Status = PublisherSubscriptionStatus.Active;
|
||||||
existingSubscription.Tier = tier;
|
existingSubscription.Tier = tier;
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
@ -156,7 +156,7 @@ public class PublisherSubscriptionService(
|
|||||||
{
|
{
|
||||||
AccountId = accountId,
|
AccountId = accountId,
|
||||||
PublisherId = publisherId,
|
PublisherId = publisherId,
|
||||||
Status = SubscriptionStatus.Active,
|
Status = PublisherSubscriptionStatus.Active,
|
||||||
Tier = tier,
|
Tier = tier,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -177,10 +177,10 @@ public class PublisherSubscriptionService(
|
|||||||
public async Task<bool> CancelSubscriptionAsync(Guid accountId, Guid publisherId)
|
public async Task<bool> CancelSubscriptionAsync(Guid accountId, Guid publisherId)
|
||||||
{
|
{
|
||||||
var subscription = await GetSubscriptionAsync(accountId, publisherId);
|
var subscription = await GetSubscriptionAsync(accountId, publisherId);
|
||||||
if (subscription is not { Status: SubscriptionStatus.Active })
|
if (subscription is not { Status: PublisherSubscriptionStatus.Active })
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
subscription.Status = SubscriptionStatus.Cancelled;
|
subscription.Status = PublisherSubscriptionStatus.Cancelled;
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await cache.RemoveAsync(string.Format(PublisherService.SubscribedPublishersCacheKey, accountId));
|
await cache.RemoveAsync(string.Format(PublisherService.SubscribedPublishersCacheKey, accountId));
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using DysonNetwork.Sphere.Storage.Handlers;
|
using DysonNetwork.Sphere.Storage.Handlers;
|
||||||
|
using DysonNetwork.Sphere.Wallet;
|
||||||
using Quartz;
|
using Quartz;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Startup;
|
namespace DysonNetwork.Sphere.Startup;
|
||||||
@ -64,6 +65,16 @@ public static class ScheduledJobsConfiguration
|
|||||||
.WithIntervalInMinutes(1)
|
.WithIntervalInMinutes(1)
|
||||||
.RepeatForever())
|
.RepeatForever())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
var subscriptionRenewalJob = new JobKey("SubscriptionRenewal");
|
||||||
|
q.AddJob<SubscriptionRenewalJob>(opts => opts.WithIdentity(subscriptionRenewalJob));
|
||||||
|
q.AddTrigger(opts => opts
|
||||||
|
.ForJob(subscriptionRenewalJob)
|
||||||
|
.WithIdentity("SubscriptionRenewalTrigger")
|
||||||
|
.WithSimpleSchedule(o => o
|
||||||
|
.WithIntervalInMinutes(30)
|
||||||
|
.RepeatForever())
|
||||||
|
);
|
||||||
});
|
});
|
||||||
services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
||||||
|
|
||||||
|
57
DysonNetwork.Sphere/Wallet/OrderController.cs
Normal file
57
DysonNetwork.Sphere/Wallet/OrderController.cs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
using DysonNetwork.Sphere.Auth;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Wallet;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("/orders")]
|
||||||
|
public class OrderController(PaymentService payment, AuthService auth, AppDatabase db) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet("{id:guid}")]
|
||||||
|
public async Task<ActionResult<Order>> GetOrderById(Guid id)
|
||||||
|
{
|
||||||
|
var order = await db.PaymentOrders.FindAsync(id);
|
||||||
|
|
||||||
|
if (order == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/pay")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<Order>> PayOrder(Guid id, [FromBody] PayOrderRequest request)
|
||||||
|
{
|
||||||
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser ||
|
||||||
|
HttpContext.Items["CurrentSession"] is not Session currentSession) return Unauthorized();
|
||||||
|
|
||||||
|
// Validate PIN code
|
||||||
|
if (!await auth.ValidatePinCode(currentUser.Id, request.PinCode))
|
||||||
|
return StatusCode(403, "Invalid PIN Code");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get the wallet for the current user
|
||||||
|
var wallet = await db.Wallets.FirstOrDefaultAsync(w => w.AccountId == currentUser.Id);
|
||||||
|
if (wallet == null)
|
||||||
|
return BadRequest("Wallet was not found.");
|
||||||
|
|
||||||
|
// Pay the order
|
||||||
|
var paidOrder = await payment.PayOrderAsync(id, wallet.Id);
|
||||||
|
return Ok(paidOrder);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PayOrderRequest
|
||||||
|
{
|
||||||
|
public string PinCode { get; set; } = string.Empty;
|
||||||
|
}
|
@ -12,9 +12,37 @@ public class PaymentService(AppDatabase db, WalletService wat)
|
|||||||
decimal amount,
|
decimal amount,
|
||||||
Duration? expiration = null,
|
Duration? expiration = null,
|
||||||
string? appIdentifier = null,
|
string? appIdentifier = null,
|
||||||
Dictionary<string, object>? meta = null
|
Dictionary<string, object>? meta = null,
|
||||||
|
bool reuseable = true
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
// Check if there's an existing unpaid order that can be reused
|
||||||
|
if (reuseable && appIdentifier != null)
|
||||||
|
{
|
||||||
|
var existingOrder = await db.PaymentOrders
|
||||||
|
.Where(o => o.Status == OrderStatus.Unpaid &&
|
||||||
|
o.PayeeWalletId == payeeWalletId &&
|
||||||
|
o.Currency == currency &&
|
||||||
|
o.Amount == amount &&
|
||||||
|
o.AppIdentifier == appIdentifier &&
|
||||||
|
o.ExpiredAt > SystemClock.Instance.GetCurrentInstant())
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
// If an existing order is found, check if meta matches
|
||||||
|
if (existingOrder != null && meta != null && existingOrder.Meta != null)
|
||||||
|
{
|
||||||
|
// Compare meta dictionaries - if they are equivalent, reuse the order
|
||||||
|
var metaMatches = existingOrder.Meta.Count == meta.Count &&
|
||||||
|
!existingOrder.Meta.Except(meta).Any();
|
||||||
|
|
||||||
|
if (metaMatches)
|
||||||
|
{
|
||||||
|
return existingOrder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new order if no reusable order was found
|
||||||
var order = new Order
|
var order = new Order
|
||||||
{
|
{
|
||||||
PayeeWalletId = payeeWalletId,
|
PayeeWalletId = payeeWalletId,
|
||||||
|
@ -127,6 +127,7 @@ public class Subscription : ModelBase
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
if (IsFreeTrial) return 0;
|
||||||
if (Coupon == null) return BasePrice;
|
if (Coupon == null) return BasePrice;
|
||||||
|
|
||||||
var now = SystemClock.Instance.GetCurrentInstant();
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
@ -61,7 +61,10 @@ public class SubscriptionController(SubscriptionService subscriptions, AppDataba
|
|||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<ActionResult<Subscription>> CreateSubscription([FromBody] CreateSubscriptionRequest request)
|
public async Task<ActionResult<Subscription>> CreateSubscription(
|
||||||
|
[FromBody] CreateSubscriptionRequest request,
|
||||||
|
[FromHeader(Name = "X-Noop")] bool noop = false
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||||
|
|
||||||
@ -81,7 +84,8 @@ public class SubscriptionController(SubscriptionService subscriptions, AppDataba
|
|||||||
cycleDuration,
|
cycleDuration,
|
||||||
request.Coupon,
|
request.Coupon,
|
||||||
request.IsFreeTrial,
|
request.IsFreeTrial,
|
||||||
request.IsAutoRenewal
|
request.IsAutoRenewal,
|
||||||
|
noop
|
||||||
);
|
);
|
||||||
|
|
||||||
return subscription;
|
return subscription;
|
||||||
|
123
DysonNetwork.Sphere/Wallet/SubscriptionRenewalJob.cs
Normal file
123
DysonNetwork.Sphere/Wallet/SubscriptionRenewalJob.cs
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NodaTime;
|
||||||
|
using Quartz;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Wallet;
|
||||||
|
|
||||||
|
public class SubscriptionRenewalJob(
|
||||||
|
AppDatabase db,
|
||||||
|
SubscriptionService subscriptionService,
|
||||||
|
PaymentService paymentService,
|
||||||
|
WalletService walletService,
|
||||||
|
ILogger<SubscriptionRenewalJob> logger
|
||||||
|
) : IJob
|
||||||
|
{
|
||||||
|
public async Task Execute(IJobExecutionContext context)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Starting subscription auto-renewal job...");
|
||||||
|
|
||||||
|
// First update expired subscriptions
|
||||||
|
var expiredCount = await subscriptionService.UpdateExpiredSubscriptionsAsync();
|
||||||
|
logger.LogInformation("Updated {ExpiredCount} expired subscriptions", expiredCount);
|
||||||
|
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
const int batchSize = 100; // Process in smaller batches
|
||||||
|
var processedCount = 0;
|
||||||
|
var renewedCount = 0;
|
||||||
|
var failedCount = 0;
|
||||||
|
|
||||||
|
// Find subscriptions that need renewal (due for renewal and are still active)
|
||||||
|
var subscriptionsToRenew = await db.WalletSubscriptions
|
||||||
|
.Where(s => s.RenewalAt.HasValue && s.RenewalAt.Value <= now) // Due for renewal
|
||||||
|
.Where(s => s.Status == SubscriptionStatus.Paid) // Only paid subscriptions
|
||||||
|
.Where(s => s.IsActive) // Only active subscriptions
|
||||||
|
.Where(s => !s.IsFreeTrial) // Exclude free trials
|
||||||
|
.OrderBy(s => s.RenewalAt) // Process oldest first
|
||||||
|
.Take(batchSize)
|
||||||
|
.Include(s => s.Coupon) // Include coupon information
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var totalSubscriptions = subscriptionsToRenew.Count;
|
||||||
|
logger.LogInformation("Found {TotalSubscriptions} subscriptions due for renewal", totalSubscriptions);
|
||||||
|
|
||||||
|
foreach (var subscription in subscriptionsToRenew)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
processedCount++;
|
||||||
|
logger.LogDebug("Processing renewal for subscription {SubscriptionId} (Identifier: {Identifier}) for account {AccountId}",
|
||||||
|
subscription.Id, subscription.Identifier, subscription.AccountId);
|
||||||
|
|
||||||
|
// Calculate next cycle duration based on current cycle
|
||||||
|
var currentCycle = subscription.EndedAt!.Value - subscription.BegunAt;
|
||||||
|
|
||||||
|
// Create an order for the renewal payment
|
||||||
|
var order = await paymentService.CreateOrderAsync(
|
||||||
|
null,
|
||||||
|
WalletCurrency.GoldenPoint,
|
||||||
|
subscription.FinalPrice,
|
||||||
|
appIdentifier: SubscriptionService.SubscriptionOrderIdentifier,
|
||||||
|
meta: new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
["subscription_id"] = subscription.Id.ToString(),
|
||||||
|
["subscription_identifier"] = subscription.Identifier,
|
||||||
|
["is_renewal"] = true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try to process the payment automatically
|
||||||
|
if (subscription.PaymentMethod == SubscriptionPaymentMethod.InAppWallet)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var wallet = await walletService.GetWalletAsync(subscription.AccountId);
|
||||||
|
if (wallet is null) continue;
|
||||||
|
|
||||||
|
// Process automatic payment from wallet
|
||||||
|
await paymentService.PayOrderAsync(order.Id, wallet.Id);
|
||||||
|
|
||||||
|
// Update subscription details
|
||||||
|
subscription.BegunAt = subscription.EndedAt!.Value;
|
||||||
|
subscription.EndedAt = subscription.BegunAt.Plus(currentCycle);
|
||||||
|
subscription.RenewalAt = subscription.EndedAt;
|
||||||
|
|
||||||
|
db.WalletSubscriptions.Update(subscription);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
renewedCount++;
|
||||||
|
logger.LogInformation("Successfully renewed subscription {SubscriptionId}", subscription.Id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// If auto-payment fails, mark for manual payment
|
||||||
|
logger.LogWarning(ex, "Failed to auto-renew subscription {SubscriptionId} with wallet payment", subscription.Id);
|
||||||
|
failedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// For other payment methods, mark as pending payment
|
||||||
|
logger.LogInformation("Subscription {SubscriptionId} requires manual payment via {PaymentMethod}",
|
||||||
|
subscription.Id, subscription.PaymentMethod);
|
||||||
|
failedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error processing subscription {SubscriptionId}", subscription.Id);
|
||||||
|
failedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log progress periodically
|
||||||
|
if (processedCount % 20 == 0 || processedCount == totalSubscriptions)
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"Progress: processed {ProcessedCount}/{TotalSubscriptions} subscriptions, {RenewedCount} renewed, {FailedCount} failed",
|
||||||
|
processedCount, totalSubscriptions, renewedCount, failedCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Completed subscription renewal job. Processed: {ProcessedCount}, Renewed: {RenewedCount}, Failed: {FailedCount}",
|
||||||
|
processedCount, renewedCount, failedCount);
|
||||||
|
}
|
||||||
|
}
|
@ -13,8 +13,9 @@ public class SubscriptionService(AppDatabase db, PaymentService payment, ICacheS
|
|||||||
PaymentDetails paymentDetails,
|
PaymentDetails paymentDetails,
|
||||||
Duration? cycleDuration = null,
|
Duration? cycleDuration = null,
|
||||||
string? coupon = null,
|
string? coupon = null,
|
||||||
bool isFreeTrail = false,
|
bool isFreeTrial = false,
|
||||||
bool isAutoRenewal = true
|
bool isAutoRenewal = true,
|
||||||
|
bool noop = false
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var subscriptionTemplate = SubscriptionTypeData
|
var subscriptionTemplate = SubscriptionTypeData
|
||||||
@ -27,9 +28,18 @@ public class SubscriptionService(AppDatabase db, PaymentService payment, ICacheS
|
|||||||
cycleDuration ??= Duration.FromDays(30);
|
cycleDuration ??= Duration.FromDays(30);
|
||||||
|
|
||||||
var existingSubscription = await GetSubscriptionAsync(account.Id, identifier);
|
var existingSubscription = await GetSubscriptionAsync(account.Id, identifier);
|
||||||
if (existingSubscription is not null)
|
if (existingSubscription is not null && !noop)
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Active subscription with identifier {identifier} already exists.");
|
throw new InvalidOperationException($"Active subscription with identifier {identifier} already exists.");
|
||||||
|
if (existingSubscription is not null)
|
||||||
|
return existingSubscription;
|
||||||
|
|
||||||
|
if (isFreeTrial)
|
||||||
|
{
|
||||||
|
var prevFreeTrial = await db.WalletSubscriptions
|
||||||
|
.Where(s => s.AccountId == account.Id && s.Identifier == identifier && s.IsFreeTrial)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (prevFreeTrial is not null)
|
||||||
|
throw new InvalidOperationException("Free trial already exists.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Coupon? couponData = null;
|
Coupon? couponData = null;
|
||||||
@ -49,14 +59,14 @@ public class SubscriptionService(AppDatabase db, PaymentService payment, ICacheS
|
|||||||
EndedAt = now.Plus(cycleDuration.Value),
|
EndedAt = now.Plus(cycleDuration.Value),
|
||||||
Identifier = identifier,
|
Identifier = identifier,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
IsFreeTrial = isFreeTrail,
|
IsFreeTrial = isFreeTrial,
|
||||||
Status = SubscriptionStatus.Unpaid,
|
Status = SubscriptionStatus.Unpaid,
|
||||||
PaymentMethod = paymentMethod,
|
PaymentMethod = paymentMethod,
|
||||||
PaymentDetails = paymentDetails,
|
PaymentDetails = paymentDetails,
|
||||||
BasePrice = subscriptionTemplate.BasePrice,
|
BasePrice = subscriptionTemplate.BasePrice,
|
||||||
CouponId = couponData?.Id,
|
CouponId = couponData?.Id,
|
||||||
Coupon = couponData,
|
Coupon = couponData,
|
||||||
RenewalAt = (isFreeTrail || !isAutoRenewal) ? null : now.Plus(cycleDuration.Value),
|
RenewalAt = (isFreeTrial || !isAutoRenewal) ? null : now.Plus(cycleDuration.Value),
|
||||||
AccountId = account.Id,
|
AccountId = account.Id,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -157,9 +167,50 @@ public class SubscriptionService(AppDatabase db, PaymentService payment, ICacheS
|
|||||||
db.Update(subscription);
|
db.Update(subscription);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
if (subscription.Identifier.StartsWith(SubscriptionType.StellarProgram))
|
||||||
|
{
|
||||||
|
await db.AccountProfiles
|
||||||
|
.Where(a => a.AccountId == subscription.AccountId)
|
||||||
|
.ExecuteUpdateAsync(s => s.SetProperty(a => a.StellarMembership, subscription.ToReference()));
|
||||||
|
}
|
||||||
|
|
||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the status of expired subscriptions to reflect their current state.
|
||||||
|
/// This helps maintain accurate subscription records and is typically called periodically.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="batchSize">Maximum number of subscriptions to process</param>
|
||||||
|
/// <returns>Number of subscriptions that were marked as expired</returns>
|
||||||
|
public async Task<int> UpdateExpiredSubscriptionsAsync(int batchSize = 100)
|
||||||
|
{
|
||||||
|
var now = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
|
||||||
|
// Find active subscriptions that have passed their end date
|
||||||
|
var expiredSubscriptions = await db.WalletSubscriptions
|
||||||
|
.Where(s => s.IsActive)
|
||||||
|
.Where(s => s.Status == SubscriptionStatus.Paid)
|
||||||
|
.Where(s => s.EndedAt.HasValue && s.EndedAt.Value < now)
|
||||||
|
.Take(batchSize)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (expiredSubscriptions.Count == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
foreach (var subscription in expiredSubscriptions)
|
||||||
|
{
|
||||||
|
subscription.Status = SubscriptionStatus.Expired;
|
||||||
|
|
||||||
|
// Clear the cache for this subscription
|
||||||
|
var cacheKey = $"{SubscriptionCacheKeyPrefix}{subscription.AccountId}:{subscription.Identifier}";
|
||||||
|
await cache.RemoveAsync(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return expiredSubscriptions.Count;
|
||||||
|
}
|
||||||
|
|
||||||
private const string SubscriptionCacheKeyPrefix = "subscription:";
|
private const string SubscriptionCacheKeyPrefix = "subscription:";
|
||||||
|
|
||||||
public async Task<Subscription?> GetSubscriptionAsync(Guid accountId, string identifier)
|
public async Task<Subscription?> GetSubscriptionAsync(Guid accountId, string identifier)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user