Payment grpc services and perks in proto

This commit is contained in:
2025-07-23 20:14:02 +08:00
parent 8e61a8b43d
commit 925ddd9e8b
10 changed files with 542 additions and 5 deletions

View File

@@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using DysonNetwork.Shared.Data;
using NodaTime;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Pass.Wallet;
@@ -30,11 +31,45 @@ public class Order : ModelBase
[Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { 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 Shared.Proto.Order ToProtoValue() => new()
{
Id = Id.ToString(),
Status = (Shared.Proto.OrderStatus)Status,
Currency = Currency,
Remarks = Remarks,
AppIdentifier = AppIdentifier,
Meta = Meta == null
? null
: Google.Protobuf.ByteString.CopyFrom(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(Meta)),
Amount = Amount.ToString(),
ExpiredAt = ExpiredAt.ToTimestamp(),
PayeeWalletId = PayeeWalletId?.ToString(),
TransactionId = TransactionId?.ToString(),
Transaction = Transaction?.ToProtoValue(),
};
public static Order FromProtoValue(Shared.Proto.Order proto) => new()
{
Id = Guid.Parse(proto.Id),
Status = (OrderStatus)proto.Status,
Currency = proto.Currency,
Remarks = proto.Remarks,
AppIdentifier = proto.AppIdentifier,
Meta = proto.HasMeta
? System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(proto.Meta.ToByteArray())
: null,
Amount = decimal.Parse(proto.Amount),
ExpiredAt = proto.ExpiredAt.ToInstant(),
PayeeWalletId = proto.HasPayeeWalletId ? Guid.Parse(proto.PayeeWalletId) : null,
TransactionId = proto.HasTransactionId ? Guid.Parse(proto.TransactionId) : null,
Transaction = proto.Transaction is not null ? Transaction.FromProtoValue(proto.Transaction) : null,
};
}
public enum TransactionType
@@ -51,11 +86,35 @@ public class Transaction : ModelBase
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; }
public Shared.Proto.Transaction ToProtoValue() => new()
{
Id = Id.ToString(),
Currency = Currency,
Amount = Amount.ToString(),
Remarks = Remarks,
Type = (Shared.Proto.TransactionType)Type,
PayerWalletId = PayerWalletId?.ToString(),
PayeeWalletId = PayeeWalletId?.ToString(),
};
public static Transaction FromProtoValue(Shared.Proto.Transaction proto) => new()
{
Id = Guid.Parse(proto.Id),
Currency = proto.Currency,
Amount = decimal.Parse(proto.Amount),
Remarks = proto.Remarks,
Type = (TransactionType)proto.Type,
PayerWalletId = proto.HasPayerWalletId ? Guid.Parse(proto.PayerWalletId) : null,
PayeeWalletId = proto.HasPayeeWalletId ? Guid.Parse(proto.PayeeWalletId) : null,
};
}

View File

@@ -0,0 +1,80 @@
using DysonNetwork.Shared.Proto;
using Grpc.Core;
using NodaTime;
namespace DysonNetwork.Pass.Wallet;
public class PaymentServiceGrpc(PaymentService paymentService) : Shared.Proto.PaymentService.PaymentServiceBase
{
public override async Task<Shared.Proto.Order> CreateOrder(CreateOrderRequest request, ServerCallContext context)
{
var order = await paymentService.CreateOrderAsync(
request.HasPayeeWalletId ? Guid.Parse(request.PayeeWalletId) : null,
request.Currency,
decimal.Parse(request.Amount),
request.Expiration is not null ? Duration.FromSeconds(request.Expiration.Seconds) : null,
request.HasAppIdentifier ? request.AppIdentifier : null,
// Assuming meta is a JSON string
request.HasMeta
? System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(request.Meta.ToStringUtf8())
: null,
request.Reuseable
);
return order.ToProtoValue();
}
public override async Task<Shared.Proto.Transaction> CreateTransactionWithAccount(
CreateTransactionWithAccountRequest request, ServerCallContext context)
{
var transaction = await paymentService.CreateTransactionWithAccountAsync(
request.HasPayerAccountId ? Guid.Parse(request.PayerAccountId) : null,
request.HasPayeeAccountId ? Guid.Parse(request.PayeeAccountId) : null,
request.Currency,
decimal.Parse(request.Amount),
request.HasRemarks ? request.Remarks : null,
(TransactionType)request.Type
);
return transaction.ToProtoValue();
}
public override async Task<Shared.Proto.Transaction> CreateTransaction(CreateTransactionRequest request,
ServerCallContext context)
{
var transaction = await paymentService.CreateTransactionAsync(
request.HasPayerWalletId ? Guid.Parse(request.PayerWalletId) : null,
request.HasPayeeWalletId ? Guid.Parse(request.PayeeWalletId) : null,
request.Currency,
decimal.Parse(request.Amount),
request.HasRemarks ? request.Remarks : null,
(TransactionType)request.Type
);
return transaction.ToProtoValue();
}
public override async Task<Shared.Proto.Order> CancelOrder(CancelOrderRequest request, ServerCallContext context)
{
var order = await paymentService.CancelOrderAsync(Guid.Parse(request.OrderId));
return order.ToProtoValue();
}
public override async Task<RefundOrderResponse> RefundOrder(RefundOrderRequest request, ServerCallContext context)
{
var (order, refundTransaction) = await paymentService.RefundOrderAsync(Guid.Parse(request.OrderId));
return new RefundOrderResponse
{
Order = order.ToProtoValue(),
RefundTransaction = refundTransaction.ToProtoValue()
};
}
public override async Task<Shared.Proto.Transaction> Transfer(TransferRequest request, ServerCallContext context)
{
var transaction = await paymentService.TransferAsync(
Guid.Parse(request.PayerAccountId),
Guid.Parse(request.PayeeAccountId),
request.Currency,
decimal.Parse(request.Amount)
);
return transaction.ToProtoValue();
}
}

View File

@@ -1,8 +1,10 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Globalization;
using DysonNetwork.Shared.Data;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Pass.Wallet;
@@ -199,6 +201,44 @@ public class Subscription : ModelBase
AccountId = AccountId
};
}
public Shared.Proto.Subscription ToProtoValue() => new()
{
Id = Id.ToString(),
BegunAt = BegunAt.ToTimestamp(),
EndedAt = EndedAt?.ToTimestamp(),
Identifier = Identifier,
IsActive = IsActive,
IsFreeTrial = IsFreeTrial,
Status = (Shared.Proto.SubscriptionStatus)Status,
PaymentMethod = PaymentMethod,
PaymentDetails = PaymentDetails.ToProtoValue(),
BasePrice = BasePrice.ToString(),
CouponId = CouponId?.ToString(),
Coupon = Coupon?.ToProtoValue(),
RenewalAt = RenewalAt?.ToTimestamp(),
AccountId = AccountId.ToString(),
IsAvailable = IsAvailable,
FinalPrice = FinalPrice.ToString(),
};
public static Subscription FromProtoValue(Shared.Proto.Subscription proto) => new()
{
Id = Guid.Parse(proto.Id),
BegunAt = proto.BegunAt.ToInstant(),
EndedAt = proto.EndedAt?.ToInstant(),
Identifier = proto.Identifier,
IsActive = proto.IsActive,
IsFreeTrial = proto.IsFreeTrial,
Status = (SubscriptionStatus)proto.Status,
PaymentMethod = proto.PaymentMethod,
PaymentDetails = PaymentDetails.FromProtoValue(proto.PaymentDetails),
BasePrice = decimal.Parse(proto.BasePrice),
CouponId = proto.HasCouponId ? Guid.Parse(proto.CouponId) : null,
Coupon = proto.Coupon is not null ? Coupon.FromProtoValue(proto.Coupon) : null,
RenewalAt = proto.RenewalAt?.ToInstant(),
AccountId = Guid.Parse(proto.AccountId),
};
}
/// <summary>
@@ -227,12 +267,57 @@ public class SubscriptionReferenceObject : ModelBase
public string? DisplayName => SubscriptionTypeData.SubscriptionHumanReadable.TryGetValue(Identifier, out var name)
? name
: null;
public Shared.Proto.SubscriptionReferenceObject ToProtoValue() => new()
{
Id = Id.ToString(),
Identifier = Identifier,
BegunAt = BegunAt.ToTimestamp(),
EndedAt = EndedAt?.ToTimestamp(),
IsActive = IsActive,
IsAvailable = IsAvailable,
IsFreeTrial = IsFreeTrial,
Status = (Shared.Proto.SubscriptionStatus)Status,
BasePrice = BasePrice.ToString(CultureInfo.CurrentCulture),
FinalPrice = FinalPrice.ToString(CultureInfo.CurrentCulture),
RenewalAt = RenewalAt?.ToTimestamp(),
AccountId = AccountId.ToString(),
DisplayName = DisplayName,
};
public static SubscriptionReferenceObject FromProtoValue(Shared.Proto.SubscriptionReferenceObject proto) => new()
{
Id = Guid.Parse(proto.Id),
Identifier = proto.Identifier,
BegunAt = proto.BegunAt.ToInstant(),
EndedAt = proto.EndedAt?.ToInstant(),
IsActive = proto.IsActive,
IsAvailable = proto.IsAvailable,
IsFreeTrial = proto.IsFreeTrial,
Status = (SubscriptionStatus)proto.Status,
BasePrice = decimal.Parse(proto.BasePrice),
FinalPrice = decimal.Parse(proto.FinalPrice),
RenewalAt = proto.RenewalAt?.ToInstant(),
AccountId = Guid.Parse(proto.AccountId),
};
}
public class PaymentDetails
{
public string Currency { get; set; } = null!;
public string? OrderId { get; set; }
public Shared.Proto.PaymentDetails ToProtoValue() => new()
{
Currency = Currency,
OrderId = OrderId,
};
public static PaymentDetails FromProtoValue(Shared.Proto.PaymentDetails proto) => new()
{
Currency = proto.Currency,
OrderId = proto.OrderId,
};
}
/// <summary>
@@ -281,4 +366,28 @@ public class Coupon : ModelBase
/// Leave it to null to use it unlimited.
/// </summary>
public int? MaxUsage { get; set; }
public Shared.Proto.Coupon ToProtoValue() => new()
{
Id = Id.ToString(),
Identifier = Identifier,
Code = Code,
AffectedAt = AffectedAt?.ToTimestamp(),
ExpiredAt = ExpiredAt?.ToTimestamp(),
DiscountAmount = DiscountAmount?.ToString(),
DiscountRate = DiscountRate,
MaxUsage = MaxUsage,
};
public static Coupon FromProtoValue(Shared.Proto.Coupon proto) => new()
{
Id = Guid.Parse(proto.Id),
Identifier = proto.Identifier,
Code = proto.Code,
AffectedAt = proto.AffectedAt?.ToInstant(),
ExpiredAt = proto.ExpiredAt?.ToInstant(),
DiscountAmount = proto.HasDiscountAmount ? decimal.Parse(proto.DiscountAmount) : null,
DiscountRate = proto.DiscountRate,
MaxUsage = proto.MaxUsage,
};
}

View File

@@ -1,6 +1,8 @@
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Data;
using NodaTime.Serialization.Protobuf;
namespace DysonNetwork.Pass.Wallet;
@@ -12,6 +14,29 @@ public class Wallet : ModelBase
public Guid AccountId { get; set; }
public Account.Account Account { get; set; } = null!;
public Shared.Proto.Wallet ToProtoValue()
{
var proto = new Shared.Proto.Wallet
{
Id = Id.ToString(),
AccountId = AccountId.ToString(),
};
foreach (var pocket in Pockets)
{
proto.Pockets.Add(pocket.ToProtoValue());
}
return proto;
}
public static Wallet FromProtoValue(Shared.Proto.Wallet proto) => new()
{
Id = Guid.Parse(proto.Id),
AccountId = Guid.Parse(proto.AccountId),
Pockets = proto.Pockets.Select(WalletPocket.FromProtoValue).ToList(),
};
}
public class WalletPocket : ModelBase
@@ -22,4 +47,20 @@ public class WalletPocket : ModelBase
public Guid WalletId { get; set; }
[JsonIgnore] public Wallet Wallet { get; set; } = null!;
public Shared.Proto.WalletPocket ToProtoValue() => new()
{
Id = Id.ToString(),
Currency = Currency,
Amount = Amount.ToString(CultureInfo.CurrentCulture),
WalletId = WalletId.ToString(),
};
public static WalletPocket FromProtoValue(Shared.Proto.WalletPocket proto) => new()
{
Id = Guid.Parse(proto.Id),
Currency = proto.Currency,
Amount = decimal.Parse(proto.Amount),
WalletId = Guid.Parse(proto.WalletId),
};
}

View File

@@ -0,0 +1,29 @@
using DysonNetwork.Shared.Proto;
using Grpc.Core;
namespace DysonNetwork.Pass.Wallet;
public class WalletServiceGrpc(WalletService walletService) : Shared.Proto.WalletService.WalletServiceBase
{
public override async Task<Shared.Proto.Wallet> GetWallet(GetWalletRequest request, ServerCallContext context)
{
var wallet = await walletService.GetWalletAsync(Guid.Parse(request.AccountId));
if (wallet == null)
{
throw new RpcException(new Status(StatusCode.NotFound, "Wallet not found."));
}
return wallet.ToProtoValue();
}
public override async Task<Shared.Proto.Wallet> CreateWallet(CreateWalletRequest request, ServerCallContext context)
{
var wallet = await walletService.CreateWalletAsync(Guid.Parse(request.AccountId));
return wallet.ToProtoValue();
}
public override async Task<Shared.Proto.WalletPocket> GetOrCreateWalletPocket(GetOrCreateWalletPocketRequest request, ServerCallContext context)
{
var (pocket, _) = await walletService.GetOrCreateWalletPocketAsync(Guid.Parse(request.WalletId), request.Currency, request.HasInitialAmount ? decimal.Parse(request.InitialAmount) : null);
return pocket.ToProtoValue();
}
}