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