diff --git a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.Designer.cs b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.Designer.cs index c207cff..c604395 100644 --- a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.Designer.cs +++ b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.Designer.cs @@ -134,5 +134,29 @@ namespace DysonNetwork.Sphere.Resources.Localization { return ResourceManager.GetString("AuthCodeBody", resourceCulture); } } + + internal static string SubscriptionAppliedTitle { + get { + return ResourceManager.GetString("SubscriptionAppliedTitle", resourceCulture); + } + } + + internal static string SubscriptionAppliedBody { + get { + return ResourceManager.GetString("SubscriptionAppliedBody", resourceCulture); + } + } + + internal static string OrderPaidTitle { + get { + return ResourceManager.GetString("OrderPaidTitle", resourceCulture); + } + } + + internal static string OrderPaidBody { + get { + return ResourceManager.GetString("OrderPaidBody", resourceCulture); + } + } } } diff --git a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.resx b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.resx index 57e3a6c..f8762b9 100644 --- a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.resx +++ b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.resx @@ -63,4 +63,17 @@ {0} is your disposable code, it will expires in 5 minutes + + Subscription {0} just activated for your account + + + Thank for supporting the Solar Network! Your {0} days {1} subscription just begun, + feel free to explore the newly unlocked features! + + + Order {0} recipent + + + {0} {1} was removed from your wallet to pay {2} + \ No newline at end of file diff --git a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.zh-hans.resx b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.zh-hans.resx index 0ab6d81..6167fc6 100644 --- a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.zh-hans.resx +++ b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.zh-hans.resx @@ -56,4 +56,17 @@ {0} 是你的一次性验证码,它将会在五分钟内过期 + + {0} 的订阅激活成功 + + + 感谢你支持 Solar Network 的开发!你的 {0} 天 {1} 订阅刚刚开始, + 接下来来探索新解锁的新功能吧! + + + 订单回执 {0} + + + {0} {1} 已从你的帐户中扣除来支付 {2} + \ No newline at end of file diff --git a/DysonNetwork.Sphere/Wallet/PaymentService.cs b/DysonNetwork.Sphere/Wallet/PaymentService.cs index 477c342..92e696d 100644 --- a/DysonNetwork.Sphere/Wallet/PaymentService.cs +++ b/DysonNetwork.Sphere/Wallet/PaymentService.cs @@ -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 localizer +) { public async Task 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() + { + ["order_id"] = order.Id.ToString() + } + ); + } + public async Task CancelOrderAsync(Guid orderId) { var order = await db.PaymentOrders.FindAsync(orderId); diff --git a/DysonNetwork.Sphere/Wallet/Subscription.cs b/DysonNetwork.Sphere/Wallet/Subscription.cs index 49e2226..654ecd9 100644 --- a/DysonNetwork.Sphere/Wallet/Subscription.cs +++ b/DysonNetwork.Sphere/Wallet/Subscription.cs @@ -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 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 diff --git a/DysonNetwork.Sphere/Wallet/SubscriptionService.cs b/DysonNetwork.Sphere/Wallet/SubscriptionService.cs index f21fe7b..dbd72d9 100644 --- a/DysonNetwork.Sphere/Wallet/SubscriptionService.cs +++ b/DysonNetwork.Sphere/Wallet/SubscriptionService.cs @@ -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 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() + { + ["subscription_id"] = subscription.Id.ToString(), + } + ); + } + + private const string SubscriptionCacheKeyPrefix = "subscription:"; public async Task GetSubscriptionAsync(Guid accountId, string identifier) { @@ -333,4 +365,4 @@ public class SubscriptionService( return subscription; } -} +} \ No newline at end of file