✨ Payment and subscription notification
This commit is contained in:
		| @@ -1,10 +1,19 @@ | ||||
| using System.Globalization; | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using DysonNetwork.Sphere.Localization; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Storage; | ||||
| using Microsoft.Extensions.Localization; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Wallet; | ||||
|  | ||||
| public class PaymentService(AppDatabase db, WalletService wat) | ||||
| public class PaymentService( | ||||
|     AppDatabase db, | ||||
|     WalletService wat, | ||||
|     NotificationService nty, | ||||
|     IStringLocalizer<NotificationResource> localizer | ||||
| ) | ||||
| { | ||||
|     public async Task<Order> CreateOrderAsync( | ||||
|         Guid? payeeWalletId, | ||||
| @@ -21,11 +30,11 @@ public class PaymentService(AppDatabase db, WalletService wat) | ||||
|         { | ||||
|             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()) | ||||
|                             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 | ||||
| @@ -34,7 +43,7 @@ public class PaymentService(AppDatabase db, WalletService wat) | ||||
|                 // 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; | ||||
| @@ -176,11 +185,38 @@ public class PaymentService(AppDatabase db, WalletService wat) | ||||
|         order.TransactionId = transaction.Id; | ||||
|         order.Transaction = transaction; | ||||
|         order.Status = OrderStatus.Paid; | ||||
|  | ||||
|          | ||||
|         await db.SaveChangesAsync(); | ||||
|          | ||||
|         await NotifyOrderPaid(order); | ||||
|          | ||||
|         return order; | ||||
|     } | ||||
|  | ||||
|     private async Task NotifyOrderPaid(Order order) | ||||
|     { | ||||
|         if (order.PayeeWallet is null) return; | ||||
|         var account = await db.Accounts.FirstOrDefaultAsync(a => a.Id == order.PayeeWallet.AccountId); | ||||
|         if (account is null) return; | ||||
|  | ||||
|         // Due to ID is uuid, it longer than 8 words for sure | ||||
|         var readableOrderId = order.Id.ToString().Replace("-", "")[..8]; | ||||
|         var readableOrderRemark = order.Remarks ?? $"#{readableOrderId}"; | ||||
|  | ||||
|         await nty.SendNotification( | ||||
|             account, | ||||
|             "wallets.orders.paid", | ||||
|             localizer["OrderPaidTitle", $"#{readableOrderId}"], | ||||
|             null, | ||||
|             localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture), order.Currency, | ||||
|                 readableOrderRemark], | ||||
|             new Dictionary<string, object>() | ||||
|             { | ||||
|                 ["order_id"] = order.Id.ToString() | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public async Task<Order> CancelOrderAsync(Guid orderId) | ||||
|     { | ||||
|         var order = await db.PaymentOrders.FindAsync(orderId); | ||||
|   | ||||
| @@ -18,6 +18,15 @@ public record class SubscriptionTypeData( | ||||
|             [SubscriptionType.Nova] = new SubscriptionTypeData(SubscriptionType.Nova, 20), | ||||
|             [SubscriptionType.Supernova] = new SubscriptionTypeData(SubscriptionType.Supernova, 30) | ||||
|         }; | ||||
|  | ||||
|     public static readonly Dictionary<string, string> SubscriptionHumanReadable = | ||||
|         new() | ||||
|         { | ||||
|             [SubscriptionType.Twinkle] = "Stellar Program Twinkle", | ||||
|             [SubscriptionType.Stellar] = "Stellar Program", | ||||
|             [SubscriptionType.Nova] = "Stellar Program Nova", | ||||
|             [SubscriptionType.Supernova] = "Stellar Program Supernova" | ||||
|         }; | ||||
| } | ||||
|  | ||||
| public abstract class SubscriptionType | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using DysonNetwork.Sphere.Localization; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using DysonNetwork.Sphere.Wallet.PaymentHandlers; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Localization; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Wallet; | ||||
| @@ -11,6 +13,8 @@ public class SubscriptionService( | ||||
|     AppDatabase db, | ||||
|     PaymentService payment, | ||||
|     AccountService accounts, | ||||
|     NotificationService nty, | ||||
|     IStringLocalizer<NotificationResource> localizer, | ||||
|     IConfiguration configuration, | ||||
|     ICacheService cache | ||||
| ) | ||||
| @@ -100,8 +104,6 @@ public class SubscriptionService( | ||||
|                 if (afdianPlan?.Key is not null) subscriptionIdentifier = afdianPlan.Value.Key; | ||||
|                 currency = "cny"; | ||||
|                 break; | ||||
|             default: | ||||
|                 break; | ||||
|         } | ||||
|  | ||||
|         var subscriptionTemplate = SubscriptionTypeData | ||||
| @@ -109,7 +111,8 @@ public class SubscriptionService( | ||||
|             ? template | ||||
|             : null; | ||||
|         if (subscriptionTemplate is null) | ||||
|             throw new ArgumentOutOfRangeException(nameof(subscriptionIdentifier), $@"Subscription {subscriptionIdentifier} was not found."); | ||||
|             throw new ArgumentOutOfRangeException(nameof(subscriptionIdentifier), | ||||
|                 $@"Subscription {subscriptionIdentifier} was not found."); | ||||
|  | ||||
|         Account.Account? account = null; | ||||
|         if (!string.IsNullOrEmpty(provider)) | ||||
| @@ -124,7 +127,8 @@ public class SubscriptionService( | ||||
|  | ||||
|         var existingSubscription = await GetSubscriptionAsync(account.Id, subscriptionIdentifier); | ||||
|         if (existingSubscription is not null && existingSubscription.PaymentMethod != provider) | ||||
|             throw new InvalidOperationException($"Active subscription with identifier {subscriptionIdentifier} already exists."); | ||||
|             throw new InvalidOperationException( | ||||
|                 $"Active subscription with identifier {subscriptionIdentifier} already exists."); | ||||
|         if (existingSubscription?.PaymentDetails.OrderId == order.Id) | ||||
|             return existingSubscription; | ||||
|         if (existingSubscription is not null) | ||||
| @@ -146,7 +150,7 @@ public class SubscriptionService( | ||||
|             BegunAt = order.BegunAt, | ||||
|             EndedAt = order.BegunAt.Plus(cycleDuration), | ||||
|             IsActive = true, | ||||
|             Status = SubscriptionStatus.Unpaid, | ||||
|             Status = SubscriptionStatus.Paid, | ||||
|             PaymentMethod = provider, | ||||
|             PaymentDetails = new PaymentDetails | ||||
|             { | ||||
| @@ -160,6 +164,8 @@ public class SubscriptionService( | ||||
|  | ||||
|         db.WalletSubscriptions.Add(subscription); | ||||
|         await db.SaveChangesAsync(); | ||||
|          | ||||
|         await NotifySubscriptionBegun(subscription); | ||||
|  | ||||
|         return subscription; | ||||
|     } | ||||
| @@ -265,6 +271,8 @@ public class SubscriptionService( | ||||
|                 .Where(a => a.AccountId == subscription.AccountId) | ||||
|                 .ExecuteUpdateAsync(s => s.SetProperty(a => a.StellarMembership, subscription.ToReference())); | ||||
|         } | ||||
|          | ||||
|         await NotifySubscriptionBegun(subscription); | ||||
|  | ||||
|         return subscription; | ||||
|     } | ||||
| @@ -303,9 +311,33 @@ public class SubscriptionService( | ||||
|         return expiredSubscriptions.Count; | ||||
|     } | ||||
|  | ||||
|     private const string SubscriptionCacheKeyPrefix = "subscription:"; | ||||
|     private async Task NotifySubscriptionBegun(Subscription subscription) | ||||
|     { | ||||
|         var account = await db.Accounts.FirstOrDefaultAsync(a => a.Id == subscription.AccountId); | ||||
|         if (account is null) return; | ||||
|  | ||||
|     public AccountService Accounts { get; } = accounts; | ||||
|         var humanReadableName = | ||||
|             SubscriptionTypeData.SubscriptionHumanReadable.TryGetValue(subscription.Identifier, out var humanReadable) | ||||
|                 ? humanReadable | ||||
|                 : subscription.Identifier; | ||||
|         var duration = subscription.EndedAt is not null | ||||
|             ? subscription.EndedAt.Value.Minus(subscription.BegunAt).ToString() | ||||
|             : "infinite"; | ||||
|  | ||||
|         await nty.SendNotification( | ||||
|             account, | ||||
|             "subscriptions.begun", | ||||
|             localizer["SubscriptionAppliedTitle", humanReadableName], | ||||
|             null, | ||||
|             localizer["SubscriptionAppliedBody", duration, humanReadableName], | ||||
|             new Dictionary<string, object>() | ||||
|             { | ||||
|                 ["subscription_id"] = subscription.Id.ToString(), | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     private const string SubscriptionCacheKeyPrefix = "subscription:"; | ||||
|  | ||||
|     public async Task<Subscription?> GetSubscriptionAsync(Guid accountId, string identifier) | ||||
|     { | ||||
| @@ -333,4 +365,4 @@ public class SubscriptionService( | ||||
|  | ||||
|         return subscription; | ||||
|     } | ||||
| } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user