✨ Payment and subscription notification
This commit is contained in:
parent
049a5c9b6f
commit
feb612afcd
@ -134,5 +134,29 @@ namespace DysonNetwork.Sphere.Resources.Localization {
|
|||||||
return ResourceManager.GetString("AuthCodeBody", resourceCulture);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,4 +63,17 @@
|
|||||||
<data name="AuthCodeBody" xml:space="preserve">
|
<data name="AuthCodeBody" xml:space="preserve">
|
||||||
<value>{0} is your disposable code, it will expires in 5 minutes</value>
|
<value>{0} is your disposable code, it will expires in 5 minutes</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SubscriptionAppliedTitle" xml:space="preserve">
|
||||||
|
Subscription {0} just activated for your account
|
||||||
|
</data>
|
||||||
|
<data name="SubscriptionAppliedBody" xml:space="preserve">
|
||||||
|
Thank for supporting the Solar Network! Your {0} days {1} subscription just begun,
|
||||||
|
feel free to explore the newly unlocked features!
|
||||||
|
</data>
|
||||||
|
<data name="OrderPaidTitle" xml:space="preserve">
|
||||||
|
Order {0} recipent
|
||||||
|
</data>
|
||||||
|
<data name="OrderPaidBody" xml:space="preserve">
|
||||||
|
{0} {1} was removed from your wallet to pay {2}
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
@ -56,4 +56,17 @@
|
|||||||
<data name="AuthCodeBody" xml:space="preserve">
|
<data name="AuthCodeBody" xml:space="preserve">
|
||||||
<value>{0} 是你的一次性验证码,它将会在五分钟内过期</value>
|
<value>{0} 是你的一次性验证码,它将会在五分钟内过期</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SubscriptionAppliedTitle" xml:space="preserve">
|
||||||
|
{0} 的订阅激活成功
|
||||||
|
</data>
|
||||||
|
<data name="SubscriptionAppliedBody" xml:space="preserve">
|
||||||
|
感谢你支持 Solar Network 的开发!你的 {0} 天 {1} 订阅刚刚开始,
|
||||||
|
接下来来探索新解锁的新功能吧!
|
||||||
|
</data>
|
||||||
|
<data name="OrderPaidTitle" xml:space="preserve">
|
||||||
|
订单回执 {0}
|
||||||
|
</data>
|
||||||
|
<data name="OrderPaidBody" xml:space="preserve">
|
||||||
|
{0} {1} 已从你的帐户中扣除来支付 {2}
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
@ -1,10 +1,19 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using DysonNetwork.Sphere.Account;
|
||||||
|
using DysonNetwork.Sphere.Localization;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Storage;
|
using Microsoft.EntityFrameworkCore.Storage;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Wallet;
|
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(
|
public async Task<Order> CreateOrderAsync(
|
||||||
Guid? payeeWalletId,
|
Guid? payeeWalletId,
|
||||||
@ -21,11 +30,11 @@ public class PaymentService(AppDatabase db, WalletService wat)
|
|||||||
{
|
{
|
||||||
var existingOrder = await db.PaymentOrders
|
var existingOrder = await db.PaymentOrders
|
||||||
.Where(o => o.Status == OrderStatus.Unpaid &&
|
.Where(o => o.Status == OrderStatus.Unpaid &&
|
||||||
o.PayeeWalletId == payeeWalletId &&
|
o.PayeeWalletId == payeeWalletId &&
|
||||||
o.Currency == currency &&
|
o.Currency == currency &&
|
||||||
o.Amount == amount &&
|
o.Amount == amount &&
|
||||||
o.AppIdentifier == appIdentifier &&
|
o.AppIdentifier == appIdentifier &&
|
||||||
o.ExpiredAt > SystemClock.Instance.GetCurrentInstant())
|
o.ExpiredAt > SystemClock.Instance.GetCurrentInstant())
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
// If an existing order is found, check if meta matches
|
// If an existing order is found, check if meta matches
|
||||||
@ -178,9 +187,36 @@ public class PaymentService(AppDatabase db, WalletService wat)
|
|||||||
order.Status = OrderStatus.Paid;
|
order.Status = OrderStatus.Paid;
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await NotifyOrderPaid(order);
|
||||||
|
|
||||||
return 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)
|
public async Task<Order> CancelOrderAsync(Guid orderId)
|
||||||
{
|
{
|
||||||
var order = await db.PaymentOrders.FindAsync(orderId);
|
var order = await db.PaymentOrders.FindAsync(orderId);
|
||||||
|
@ -18,6 +18,15 @@ public record class SubscriptionTypeData(
|
|||||||
[SubscriptionType.Nova] = new SubscriptionTypeData(SubscriptionType.Nova, 20),
|
[SubscriptionType.Nova] = new SubscriptionTypeData(SubscriptionType.Nova, 20),
|
||||||
[SubscriptionType.Supernova] = new SubscriptionTypeData(SubscriptionType.Supernova, 30)
|
[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
|
public abstract class SubscriptionType
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
|
using DysonNetwork.Sphere.Localization;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using DysonNetwork.Sphere.Wallet.PaymentHandlers;
|
using DysonNetwork.Sphere.Wallet.PaymentHandlers;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Wallet;
|
namespace DysonNetwork.Sphere.Wallet;
|
||||||
@ -11,6 +13,8 @@ public class SubscriptionService(
|
|||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
PaymentService payment,
|
PaymentService payment,
|
||||||
AccountService accounts,
|
AccountService accounts,
|
||||||
|
NotificationService nty,
|
||||||
|
IStringLocalizer<NotificationResource> localizer,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ICacheService cache
|
ICacheService cache
|
||||||
)
|
)
|
||||||
@ -100,8 +104,6 @@ public class SubscriptionService(
|
|||||||
if (afdianPlan?.Key is not null) subscriptionIdentifier = afdianPlan.Value.Key;
|
if (afdianPlan?.Key is not null) subscriptionIdentifier = afdianPlan.Value.Key;
|
||||||
currency = "cny";
|
currency = "cny";
|
||||||
break;
|
break;
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscriptionTemplate = SubscriptionTypeData
|
var subscriptionTemplate = SubscriptionTypeData
|
||||||
@ -109,7 +111,8 @@ public class SubscriptionService(
|
|||||||
? template
|
? template
|
||||||
: null;
|
: null;
|
||||||
if (subscriptionTemplate is 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;
|
Account.Account? account = null;
|
||||||
if (!string.IsNullOrEmpty(provider))
|
if (!string.IsNullOrEmpty(provider))
|
||||||
@ -124,7 +127,8 @@ public class SubscriptionService(
|
|||||||
|
|
||||||
var existingSubscription = await GetSubscriptionAsync(account.Id, subscriptionIdentifier);
|
var existingSubscription = await GetSubscriptionAsync(account.Id, subscriptionIdentifier);
|
||||||
if (existingSubscription is not null && existingSubscription.PaymentMethod != provider)
|
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)
|
if (existingSubscription?.PaymentDetails.OrderId == order.Id)
|
||||||
return existingSubscription;
|
return existingSubscription;
|
||||||
if (existingSubscription is not null)
|
if (existingSubscription is not null)
|
||||||
@ -146,7 +150,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.Unpaid,
|
Status = SubscriptionStatus.Paid,
|
||||||
PaymentMethod = provider,
|
PaymentMethod = provider,
|
||||||
PaymentDetails = new PaymentDetails
|
PaymentDetails = new PaymentDetails
|
||||||
{
|
{
|
||||||
@ -161,6 +165,8 @@ public class SubscriptionService(
|
|||||||
db.WalletSubscriptions.Add(subscription);
|
db.WalletSubscriptions.Add(subscription);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await NotifySubscriptionBegun(subscription);
|
||||||
|
|
||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,6 +272,8 @@ public class SubscriptionService(
|
|||||||
.ExecuteUpdateAsync(s => s.SetProperty(a => a.StellarMembership, subscription.ToReference()));
|
.ExecuteUpdateAsync(s => s.SetProperty(a => a.StellarMembership, subscription.ToReference()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await NotifySubscriptionBegun(subscription);
|
||||||
|
|
||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,9 +311,33 @@ public class SubscriptionService(
|
|||||||
return expiredSubscriptions.Count;
|
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)
|
public async Task<Subscription?> GetSubscriptionAsync(Guid accountId, string identifier)
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user