✨ Wallet, payment, developer apps, feature flags of publishers
♻️ Simplified the permission check of chat room, realm, publishers
This commit is contained in:
51
DysonNetwork.Sphere/Wallet/Payment.cs
Normal file
51
DysonNetwork.Sphere/Wallet/Payment.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Wallet;
|
||||
|
||||
public enum OrderStatus
|
||||
{
|
||||
Unpaid,
|
||||
Paid,
|
||||
Cancelled,
|
||||
Finished,
|
||||
Expired
|
||||
}
|
||||
|
||||
public class Order : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public OrderStatus Status { get; set; } = OrderStatus.Unpaid;
|
||||
[MaxLength(128)] public string Currency { get; set; } = null!;
|
||||
[MaxLength(4096)] public string? Remarks { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public Instant ExpiredAt { get; set; }
|
||||
|
||||
public Guid PayeeWalletId { get; set; }
|
||||
public Wallet PayeeWallet { get; set; } = null!;
|
||||
public Guid? TransactionId { get; set; }
|
||||
public Transaction? Transaction { get; set; }
|
||||
}
|
||||
|
||||
public enum TransactionType
|
||||
{
|
||||
System,
|
||||
Transfer,
|
||||
Order
|
||||
}
|
||||
|
||||
public class Transaction : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(128)] public string Currency { get; set; } = null!;
|
||||
public decimal Amount { get; set; }
|
||||
[MaxLength(4096)] public string? Remarks { get; set; }
|
||||
public TransactionType Type { get; set; }
|
||||
|
||||
// When the payer is null, it's pay from the system
|
||||
public Guid? PayerWalletId { get; set; }
|
||||
public Wallet? PayerWallet { get; set; }
|
||||
// When the payee is null, it's pay for the system
|
||||
public Guid? PayeeWalletId { get; set; }
|
||||
public Wallet? PayeeWallet { get; set; }
|
||||
}
|
184
DysonNetwork.Sphere/Wallet/PaymentService.cs
Normal file
184
DysonNetwork.Sphere/Wallet/PaymentService.cs
Normal file
@ -0,0 +1,184 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Wallet;
|
||||
|
||||
public class PaymentService(AppDatabase db, WalletService wat)
|
||||
{
|
||||
public async Task<Order> CreateOrderAsync(Guid payeeWalletId, string currency, decimal amount, Duration expiration)
|
||||
{
|
||||
var order = new Order
|
||||
{
|
||||
PayeeWalletId = payeeWalletId,
|
||||
Currency = currency,
|
||||
Amount = amount,
|
||||
ExpiredAt = SystemClock.Instance.GetCurrentInstant().Plus(expiration)
|
||||
};
|
||||
|
||||
db.PaymentOrders.Add(order);
|
||||
await db.SaveChangesAsync();
|
||||
return order;
|
||||
}
|
||||
|
||||
public async Task<Transaction> CreateTransactionAsync(
|
||||
Guid? payerWalletId,
|
||||
Guid? payeeWalletId,
|
||||
string currency,
|
||||
decimal amount,
|
||||
string? remarks = null,
|
||||
TransactionType type = TransactionType.System
|
||||
)
|
||||
{
|
||||
var transaction = new Transaction
|
||||
{
|
||||
PayerWalletId = payerWalletId,
|
||||
PayeeWalletId = payeeWalletId,
|
||||
Currency = currency,
|
||||
Amount = amount,
|
||||
Remarks = remarks,
|
||||
Type = type
|
||||
};
|
||||
|
||||
if (payerWalletId.HasValue)
|
||||
{
|
||||
var payerPocket = await wat.GetOrCreateWalletPocketAsync(
|
||||
(await db.Wallets.FindAsync(payerWalletId.Value))!.AccountId,
|
||||
currency);
|
||||
|
||||
if (payerPocket.Amount < amount)
|
||||
{
|
||||
throw new InvalidOperationException("Insufficient funds");
|
||||
}
|
||||
|
||||
payerPocket.Amount -= amount;
|
||||
}
|
||||
|
||||
if (payeeWalletId.HasValue)
|
||||
{
|
||||
var payeeWallet = await db.Wallets.FindAsync(payeeWalletId.Value);
|
||||
var payeePocket = await wat.GetOrCreateWalletPocketAsync(
|
||||
payeeWallet!.AccountId,
|
||||
currency);
|
||||
|
||||
payeePocket.Amount += amount;
|
||||
}
|
||||
|
||||
db.PaymentTransactions.Add(transaction);
|
||||
await db.SaveChangesAsync();
|
||||
return transaction;
|
||||
}
|
||||
|
||||
public async Task<Order> PayOrderAsync(Guid orderId, Guid payerWalletId)
|
||||
{
|
||||
var order = await db.PaymentOrders
|
||||
.Include(o => o.Transaction)
|
||||
.FirstOrDefaultAsync(o => o.Id == orderId);
|
||||
|
||||
if (order == null)
|
||||
{
|
||||
throw new InvalidOperationException("Order not found");
|
||||
}
|
||||
|
||||
if (order.Status != OrderStatus.Unpaid)
|
||||
{
|
||||
throw new InvalidOperationException($"Order is in invalid status: {order.Status}");
|
||||
}
|
||||
|
||||
if (order.ExpiredAt < SystemClock.Instance.GetCurrentInstant())
|
||||
{
|
||||
order.Status = OrderStatus.Expired;
|
||||
await db.SaveChangesAsync();
|
||||
throw new InvalidOperationException("Order has expired");
|
||||
}
|
||||
|
||||
var transaction = await CreateTransactionAsync(
|
||||
payerWalletId,
|
||||
order.PayeeWalletId,
|
||||
order.Currency,
|
||||
order.Amount,
|
||||
order.Remarks ?? $"Payment for Order #{order.Id}",
|
||||
type: TransactionType.Order);
|
||||
|
||||
order.TransactionId = transaction.Id;
|
||||
order.Transaction = transaction;
|
||||
order.Status = OrderStatus.Paid;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return order;
|
||||
}
|
||||
|
||||
public async Task<Order> CancelOrderAsync(Guid orderId)
|
||||
{
|
||||
var order = await db.PaymentOrders.FindAsync(orderId);
|
||||
if (order == null)
|
||||
{
|
||||
throw new InvalidOperationException("Order not found");
|
||||
}
|
||||
|
||||
if (order.Status != OrderStatus.Unpaid)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot cancel order in status: {order.Status}");
|
||||
}
|
||||
|
||||
order.Status = OrderStatus.Cancelled;
|
||||
await db.SaveChangesAsync();
|
||||
return order;
|
||||
}
|
||||
|
||||
public async Task<(Order Order, Transaction RefundTransaction)> RefundOrderAsync(Guid orderId)
|
||||
{
|
||||
var order = await db.PaymentOrders
|
||||
.Include(o => o.Transaction)
|
||||
.FirstOrDefaultAsync(o => o.Id == orderId);
|
||||
|
||||
if (order == null)
|
||||
{
|
||||
throw new InvalidOperationException("Order not found");
|
||||
}
|
||||
|
||||
if (order.Status != OrderStatus.Paid)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot refund order in status: {order.Status}");
|
||||
}
|
||||
|
||||
if (order.Transaction == null)
|
||||
{
|
||||
throw new InvalidOperationException("Order has no associated transaction");
|
||||
}
|
||||
|
||||
var refundTransaction = await CreateTransactionAsync(
|
||||
order.PayeeWalletId,
|
||||
order.Transaction.PayerWalletId,
|
||||
order.Currency,
|
||||
order.Amount,
|
||||
$"Refund for order {order.Id}");
|
||||
|
||||
order.Status = OrderStatus.Finished;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return (order, refundTransaction);
|
||||
}
|
||||
|
||||
public async Task<Transaction> TransferAsync(Guid payerAccountId, Guid payeeAccountId, string currency, decimal amount)
|
||||
{
|
||||
var payerWallet = await wat.GetWalletAsync(payerAccountId);
|
||||
if (payerWallet == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Payer wallet not found for account {payerAccountId}");
|
||||
}
|
||||
|
||||
var payeeWallet = await wat.GetWalletAsync(payeeAccountId);
|
||||
if (payeeWallet == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Payee wallet not found for account {payeeAccountId}");
|
||||
}
|
||||
|
||||
return await CreateTransactionAsync(
|
||||
payerWallet.Id,
|
||||
payeeWallet.Id,
|
||||
currency,
|
||||
amount,
|
||||
$"Transfer from account {payerAccountId} to {payeeAccountId}",
|
||||
TransactionType.Transfer);
|
||||
}
|
||||
}
|
23
DysonNetwork.Sphere/Wallet/Wallet.cs
Normal file
23
DysonNetwork.Sphere/Wallet/Wallet.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace DysonNetwork.Sphere.Wallet;
|
||||
|
||||
public class Wallet : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
public ICollection<WalletPocket> Pockets { get; set; } = new List<WalletPocket>();
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
public Account.Account Account { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class WalletPocket : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(128)] public string Currency { get; set; } = null!;
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
public Guid WalletId { get; set; }
|
||||
public Wallet Wallet { get; set; } = null!;
|
||||
}
|
54
DysonNetwork.Sphere/Wallet/WalletService.cs
Normal file
54
DysonNetwork.Sphere/Wallet/WalletService.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Sphere.Wallet;
|
||||
|
||||
public class WalletService(AppDatabase db)
|
||||
{
|
||||
public async Task<Wallet?> GetWalletAsync(Guid accountId)
|
||||
{
|
||||
return await db.Wallets
|
||||
.Include(w => w.Pockets)
|
||||
.FirstOrDefaultAsync(w => w.AccountId == accountId);
|
||||
}
|
||||
|
||||
public async Task<Wallet> CreateWalletAsync(Guid accountId)
|
||||
{
|
||||
var wallet = new Wallet
|
||||
{
|
||||
AccountId = accountId
|
||||
};
|
||||
|
||||
db.Wallets.Add(wallet);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public async Task<WalletPocket> GetOrCreateWalletPocketAsync(Guid accountId, string currency)
|
||||
{
|
||||
var wallet = await db.Wallets
|
||||
.Include(w => w.Pockets)
|
||||
.FirstOrDefaultAsync(w => w.AccountId == accountId);
|
||||
|
||||
if (wallet == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Wallet not found for account {accountId}");
|
||||
}
|
||||
|
||||
var pocket = wallet.Pockets.FirstOrDefault(p => p.Currency == currency);
|
||||
|
||||
if (pocket != null) return pocket;
|
||||
|
||||
pocket = new WalletPocket
|
||||
{
|
||||
Currency = currency,
|
||||
Amount = 0,
|
||||
WalletId = wallet.Id
|
||||
};
|
||||
|
||||
wallet.Pockets.Add(pocket);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return pocket;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user