From 925ddd9e8b060e11905ab18cf1f258be18332416 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 23 Jul 2025 20:14:02 +0800 Subject: [PATCH] :sparkles: Payment grpc services and perks in proto --- DysonNetwork.Pass/Account/Account.cs | 6 +- .../Account/AccountCurrentController.cs | 2 +- .../Account/AccountEventService.cs | 2 +- DysonNetwork.Pass/Wallet/Payment.cs | 63 +++++- .../Wallet/PaymentServiceGrpc.cs | 80 +++++++ DysonNetwork.Pass/Wallet/Subscription.cs | 109 +++++++++ DysonNetwork.Pass/Wallet/Wallet.cs | 41 ++++ DysonNetwork.Pass/Wallet/WalletServiceGrpc.cs | 29 +++ DysonNetwork.Shared/Proto/account.proto | 2 + DysonNetwork.Shared/Proto/wallet.proto | 213 ++++++++++++++++++ 10 files changed, 542 insertions(+), 5 deletions(-) create mode 100644 DysonNetwork.Pass/Wallet/PaymentServiceGrpc.cs create mode 100644 DysonNetwork.Pass/Wallet/WalletServiceGrpc.cs create mode 100644 DysonNetwork.Shared/Proto/wallet.proto diff --git a/DysonNetwork.Pass/Account/Account.cs b/DysonNetwork.Pass/Account/Account.cs index 1f24fcd..527a664 100644 --- a/DysonNetwork.Pass/Account/Account.cs +++ b/DysonNetwork.Pass/Account/Account.cs @@ -32,7 +32,7 @@ public class Account : ModelBase [JsonIgnore] public ICollection OutgoingRelationships { get; set; } = new List(); [JsonIgnore] public ICollection IncomingRelationships { get; set; } = new List(); - + [NotMapped] public SubscriptionReferenceObject? PerkSubscription { get; set; } public Shared.Proto.Account ToProtoValue() @@ -46,6 +46,7 @@ public class Account : ModelBase ActivatedAt = ActivatedAt?.ToTimestamp(), IsSuperuser = IsSuperuser, Profile = Profile.ToProtoValue(), + PerkSubscription = PerkSubscription?.ToProtoValue(), CreatedAt = CreatedAt.ToTimestamp(), UpdatedAt = UpdatedAt.ToTimestamp() }; @@ -72,6 +73,9 @@ public class Account : ModelBase Language = proto.Language, ActivatedAt = proto.ActivatedAt?.ToInstant(), IsSuperuser = proto.IsSuperuser, + PerkSubscription = proto.PerkSubscription is not null + ? SubscriptionReferenceObject.FromProtoValue(proto.PerkSubscription) + : null, CreatedAt = proto.CreatedAt.ToInstant(), UpdatedAt = proto.UpdatedAt.ToInstant(), }; diff --git a/DysonNetwork.Pass/Account/AccountCurrentController.cs b/DysonNetwork.Pass/Account/AccountCurrentController.cs index 53a6810..ce1415e 100644 --- a/DysonNetwork.Pass/Account/AccountCurrentController.cs +++ b/DysonNetwork.Pass/Account/AccountCurrentController.cs @@ -39,7 +39,7 @@ public class AccountCurrentController( .Where(e => e.Id == userId) .FirstOrDefaultAsync(); - var perk = await subscriptions.GetPerkSubscriptionAsync(account.Id); + var perk = await subscriptions.GetPerkSubscriptionAsync(account!.Id); account.PerkSubscription = perk?.ToReference(); return Ok(account); diff --git a/DysonNetwork.Pass/Account/AccountEventService.cs b/DysonNetwork.Pass/Account/AccountEventService.cs index 99eea9c..dfc9aea 100644 --- a/DysonNetwork.Pass/Account/AccountEventService.cs +++ b/DysonNetwork.Pass/Account/AccountEventService.cs @@ -10,7 +10,7 @@ namespace DysonNetwork.Pass.Account; public class AccountEventService( AppDatabase db, - PaymentService payment, + Wallet.PaymentService payment, ICacheService cache, IStringLocalizer localizer, PusherService.PusherServiceClient pusher diff --git a/DysonNetwork.Pass/Wallet/Payment.cs b/DysonNetwork.Pass/Wallet/Payment.cs index 9bd27e1..78da3be 100644 --- a/DysonNetwork.Pass/Wallet/Payment.cs +++ b/DysonNetwork.Pass/Wallet/Payment.cs @@ -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? 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>(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, + }; } \ No newline at end of file diff --git a/DysonNetwork.Pass/Wallet/PaymentServiceGrpc.cs b/DysonNetwork.Pass/Wallet/PaymentServiceGrpc.cs new file mode 100644 index 0000000..7540fa3 --- /dev/null +++ b/DysonNetwork.Pass/Wallet/PaymentServiceGrpc.cs @@ -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 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>(request.Meta.ToStringUtf8()) + : null, + request.Reuseable + ); + return order.ToProtoValue(); + } + + public override async Task 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 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 CancelOrder(CancelOrderRequest request, ServerCallContext context) + { + var order = await paymentService.CancelOrderAsync(Guid.Parse(request.OrderId)); + return order.ToProtoValue(); + } + + public override async Task 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 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(); + } +} \ No newline at end of file diff --git a/DysonNetwork.Pass/Wallet/Subscription.cs b/DysonNetwork.Pass/Wallet/Subscription.cs index 19e1974..0aecd6e 100644 --- a/DysonNetwork.Pass/Wallet/Subscription.cs +++ b/DysonNetwork.Pass/Wallet/Subscription.cs @@ -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), + }; } /// @@ -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, + }; } /// @@ -281,4 +366,28 @@ public class Coupon : ModelBase /// Leave it to null to use it unlimited. /// 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, + }; } \ No newline at end of file diff --git a/DysonNetwork.Pass/Wallet/Wallet.cs b/DysonNetwork.Pass/Wallet/Wallet.cs index 2480b0c..d85134d 100644 --- a/DysonNetwork.Pass/Wallet/Wallet.cs +++ b/DysonNetwork.Pass/Wallet/Wallet.cs @@ -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), + }; } \ No newline at end of file diff --git a/DysonNetwork.Pass/Wallet/WalletServiceGrpc.cs b/DysonNetwork.Pass/Wallet/WalletServiceGrpc.cs new file mode 100644 index 0000000..2b5d2cd --- /dev/null +++ b/DysonNetwork.Pass/Wallet/WalletServiceGrpc.cs @@ -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 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 CreateWallet(CreateWalletRequest request, ServerCallContext context) + { + var wallet = await walletService.CreateWalletAsync(Guid.Parse(request.AccountId)); + return wallet.ToProtoValue(); + } + + public override async Task 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(); + } +} diff --git a/DysonNetwork.Shared/Proto/account.proto b/DysonNetwork.Shared/Proto/account.proto index 91a5a9a..6ec4add 100644 --- a/DysonNetwork.Shared/Proto/account.proto +++ b/DysonNetwork.Shared/Proto/account.proto @@ -10,6 +10,7 @@ import "google/protobuf/field_mask.proto"; import "google/protobuf/struct.proto"; import 'file.proto'; +import 'wallet.proto'; // Account represents a user account in the system message Account { @@ -21,6 +22,7 @@ message Account { bool is_superuser = 6; AccountProfile profile = 7; + optional SubscriptionReferenceObject perk_subscription = 16; repeated AccountContact contacts = 8; repeated AccountBadge badges = 9; repeated AccountAuthFactor auth_factors = 10; diff --git a/DysonNetwork.Shared/Proto/wallet.proto b/DysonNetwork.Shared/Proto/wallet.proto new file mode 100644 index 0000000..f892f6a --- /dev/null +++ b/DysonNetwork.Shared/Proto/wallet.proto @@ -0,0 +1,213 @@ +syntax = "proto3"; + +package proto; + +option csharp_namespace = "DysonNetwork.Shared.Proto"; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; +import "google/protobuf/duration.proto"; + +message Wallet { + string id = 1; + repeated WalletPocket pockets = 2; + string account_id = 3; +} + +message WalletPocket { + string id = 1; + string currency = 2; + // Using string for decimal to avoid precision loss. + string amount = 3; + string wallet_id = 4; +} + +enum SubscriptionStatus { + // Using proto3 enum naming convention + SUBSCRIPTION_STATUS_UNSPECIFIED = 0; + SUBSCRIPTION_STATUS_UNPAID = 1; + SUBSCRIPTION_STATUS_ACTIVE = 2; + SUBSCRIPTION_STATUS_EXPIRED = 3; + SUBSCRIPTION_STATUS_CANCELLED = 4; +} + +message Subscription { + string id = 1; + google.protobuf.Timestamp begun_at = 2; + optional google.protobuf.Timestamp ended_at = 3; + string identifier = 4; + bool is_active = 5; + bool is_free_trial = 6; + SubscriptionStatus status = 7; + string payment_method = 8; + PaymentDetails payment_details = 9; + // Using string for decimal to avoid precision loss. + string base_price = 10; + optional string coupon_id = 11; + optional Coupon coupon = 12; + optional google.protobuf.Timestamp renewal_at = 13; + string account_id = 14; + bool is_available = 15; + // Using string for decimal to avoid precision loss. + string final_price = 16; +} + +message SubscriptionReferenceObject { + string id = 1; + string identifier = 2; + google.protobuf.Timestamp begun_at = 3; + optional google.protobuf.Timestamp ended_at = 4; + bool is_active = 5; + bool is_available = 6; + bool is_free_trial = 7; + SubscriptionStatus status = 8; + // Using string for decimal to avoid precision loss. + string base_price = 9; + // Using string for decimal to avoid precision loss. + string final_price = 10; + optional google.protobuf.Timestamp renewal_at = 11; + string account_id = 12; + optional string display_name = 13; +} + +message PaymentDetails { + string currency = 1; + optional string order_id = 2; +} + +message Coupon { + string id = 1; + optional string identifier = 2; + optional string code = 3; + optional google.protobuf.Timestamp affected_at = 4; + optional google.protobuf.Timestamp expired_at = 5; + // Using string for decimal to avoid precision loss. + optional string discount_amount = 6; + optional google.protobuf.DoubleValue discount_rate = 7; + optional google.protobuf.Int32Value max_usage = 8; +} + +service WalletService { + rpc GetWallet(GetWalletRequest) returns (Wallet); + rpc CreateWallet(CreateWalletRequest) returns (Wallet); + rpc GetOrCreateWalletPocket(GetOrCreateWalletPocketRequest) returns (WalletPocket); +} + +message GetWalletRequest { + string account_id = 1; +} + +message CreateWalletRequest { + string account_id = 1; +} + +message GetOrCreateWalletPocketRequest { + string wallet_id = 1; + string currency = 2; + optional string initial_amount = 3; +} + +service PaymentService { + rpc CreateOrder(CreateOrderRequest) returns (Order); + rpc CreateTransactionWithAccount(CreateTransactionWithAccountRequest) returns (Transaction); + rpc CreateTransaction(CreateTransactionRequest) returns (Transaction); + rpc PayOrder(PayOrderRequest) returns (Order); + rpc CancelOrder(CancelOrderRequest) returns (Order); + rpc RefundOrder(RefundOrderRequest) returns (RefundOrderResponse); + rpc Transfer(TransferRequest) returns (Transaction); +} + +message CreateOrderRequest { + optional string payee_wallet_id = 1; + string currency = 2; + string amount = 3; + optional google.protobuf.Duration expiration = 4; + optional string app_identifier = 5; + // Using bytes for meta to represent JSON. + optional bytes meta = 6; + bool reuseable = 7; +} + +message Order { + string id = 1; + optional string payee_wallet_id = 2; + string currency = 3; + string amount = 4; + google.protobuf.Timestamp expired_at = 5; + optional string app_identifier = 6; + // Using bytes for meta to represent JSON. + optional bytes meta = 7; + OrderStatus status = 8; + optional string transaction_id = 9; + optional Transaction transaction = 10; + optional string remarks = 11; +} + +enum OrderStatus { + ORDER_STATUS_UNSPECIFIED = 0; + ORDER_STATUS_UNPAID = 1; + ORDER_STATUS_PAID = 2; + ORDER_STATUS_EXPIRED = 3; + ORDER_STATUS_CANCELLED = 4; + ORDER_STATUS_FINISHED = 5; +} + +message Transaction { + string id = 1; + optional string payer_wallet_id = 2; + optional string payee_wallet_id = 3; + string currency = 4; + string amount = 5; + optional string remarks = 6; + TransactionType type = 7; +} + +enum TransactionType { + TRANSACTION_TYPE_UNSPECIFIED = 0; + TRANSACTION_TYPE_SYSTEM = 1; + TRANSACTION_TYPE_ORDER = 2; + TRANSACTION_TYPE_TRANSFER = 3; +} + +message CreateTransactionWithAccountRequest { + optional string payer_account_id = 1; + optional string payee_account_id = 2; + string currency = 3; + string amount = 4; + optional string remarks = 5; + TransactionType type = 6; +} + +message CreateTransactionRequest { + optional string payer_wallet_id = 1; + optional string payee_wallet_id = 2; + string currency = 3; + string amount = 4; + optional string remarks = 5; + TransactionType type = 6; +} + +message PayOrderRequest { + string order_id = 1; + string payer_wallet_id = 2; +} + +message CancelOrderRequest { + string order_id = 1; +} + +message RefundOrderRequest { + string order_id = 1; +} + +message RefundOrderResponse { + Order order = 1; + Transaction refund_transaction = 2; +} + +message TransferRequest { + string payer_account_id = 1; + string payee_account_id = 2; + string currency = 3; + string amount = 4; +}