♻️ Refactored order handling
This commit is contained in:
		| @@ -14,7 +14,7 @@ public class BroadcastEventHandler( | |||||||
| { | { | ||||||
|     protected override async Task ExecuteAsync(CancellationToken stoppingToken) |     protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||||
|     { |     { | ||||||
|         await foreach (var msg in nats.SubscribeAsync<byte[]>("accounts.deleted", cancellationToken: stoppingToken)) |         await foreach (var msg in nats.SubscribeAsync<byte[]>(AccountDeletedEvent.Type, cancellationToken: stoppingToken)) | ||||||
|         { |         { | ||||||
|             try |             try | ||||||
|             { |             { | ||||||
|   | |||||||
							
								
								
									
										2021
									
								
								DysonNetwork.Pass/Migrations/20250904144723_AddOrderProductIdentifier.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2021
									
								
								DysonNetwork.Pass/Migrations/20250904144723_AddOrderProductIdentifier.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | using Microsoft.EntityFrameworkCore.Migrations; | ||||||
|  |  | ||||||
|  | #nullable disable | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Pass.Migrations | ||||||
|  | { | ||||||
|  |     /// <inheritdoc /> | ||||||
|  |     public partial class AddOrderProductIdentifier : Migration | ||||||
|  |     { | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Up(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.AddColumn<string>( | ||||||
|  |                 name: "product_identifier", | ||||||
|  |                 table: "payment_orders", | ||||||
|  |                 type: "character varying(4096)", | ||||||
|  |                 maxLength: 4096, | ||||||
|  |                 nullable: true); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /// <inheritdoc /> | ||||||
|  |         protected override void Down(MigrationBuilder migrationBuilder) | ||||||
|  |         { | ||||||
|  |             migrationBuilder.DropColumn( | ||||||
|  |                 name: "product_identifier", | ||||||
|  |                 table: "payment_orders"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1381,6 +1381,11 @@ namespace DysonNetwork.Pass.Migrations | |||||||
|                         .HasColumnType("uuid") |                         .HasColumnType("uuid") | ||||||
|                         .HasColumnName("payee_wallet_id"); |                         .HasColumnName("payee_wallet_id"); | ||||||
|  |  | ||||||
|  |                     b.Property<string>("ProductIdentifier") | ||||||
|  |                         .HasMaxLength(4096) | ||||||
|  |                         .HasColumnType("character varying(4096)") | ||||||
|  |                         .HasColumnName("product_identifier"); | ||||||
|  |  | ||||||
|                     b.Property<string>("Remarks") |                     b.Property<string>("Remarks") | ||||||
|                         .HasMaxLength(4096) |                         .HasMaxLength(4096) | ||||||
|                         .HasColumnType("character varying(4096)") |                         .HasColumnType("character varying(4096)") | ||||||
|   | |||||||
| @@ -170,5 +170,47 @@ namespace DysonNetwork.Sphere.Resources.Localization { | |||||||
|                 return ResourceManager.GetString("NewLoginBody", resourceCulture); |                 return ResourceManager.GetString("NewLoginBody", resourceCulture); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |          | ||||||
|  |         internal static string FriendRequestTitle { | ||||||
|  |             get { | ||||||
|  |                 return ResourceManager.GetString("FriendRequestTitle", resourceCulture); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         internal static string FriendRequestBody { | ||||||
|  |             get { | ||||||
|  |                 return ResourceManager.GetString("FriendRequestBody", resourceCulture); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         internal static string OrderReceivedTitle { | ||||||
|  |             get { | ||||||
|  |                 return ResourceManager.GetString("OrderReceivedTitle", resourceCulture); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         internal static string OrderReceivedBody { | ||||||
|  |             get { | ||||||
|  |                 return ResourceManager.GetString("OrderReceivedBody", resourceCulture); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         internal static string TransactionNewTitle { | ||||||
|  |             get { | ||||||
|  |                 return ResourceManager.GetString("TransactionNewTitle", resourceCulture); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         internal static string TransactionNewBodyPlus { | ||||||
|  |             get { | ||||||
|  |                 return ResourceManager.GetString("TransactionNewBodyPlus", resourceCulture); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         internal static string TransactionNewBodyMinus { | ||||||
|  |             get { | ||||||
|  |                 return ResourceManager.GetString("TransactionNewBodyMinus", resourceCulture); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -78,7 +78,7 @@ | |||||||
|         <value>Order {0} recipent</value> |         <value>Order {0} recipent</value> | ||||||
|     </data> |     </data> | ||||||
|     <data name="OrderPaidBody" xml:space="preserve"> |     <data name="OrderPaidBody" xml:space="preserve"> | ||||||
|        <value>{0} {1} was removed from your wallet to pay {2}</value> |         <value>Paid order {2} with {0} {1}</value> | ||||||
|     </data> |     </data> | ||||||
|     <data name="NewLoginTitle" xml:space="preserve"> |     <data name="NewLoginTitle" xml:space="preserve"> | ||||||
|         <value>New login detected</value> |         <value>New login detected</value> | ||||||
| @@ -92,4 +92,19 @@ | |||||||
|     <data name="FriendRequestBody" xml:space="preserve"> |     <data name="FriendRequestBody" xml:space="preserve"> | ||||||
|         <value>You can go to relationships page and decide accept their request or not.</value> |         <value>You can go to relationships page and decide accept their request or not.</value> | ||||||
|     </data> |     </data> | ||||||
|  |     <data name="OrderReceivedTitle" xml:space="preserve"> | ||||||
|  |         <value>Order {0} recipent</value> | ||||||
|  |     </data> | ||||||
|  |     <data name="OrderReceivedBody" xml:space="preserve"> | ||||||
|  |         <value>Received {2} payment of {0} {1}</value> | ||||||
|  |     </data> | ||||||
|  |     <data name="TransactionNewTitle" xml:space="preserve"> | ||||||
|  |         <value>Transaction {0}</value> | ||||||
|  |     </data> | ||||||
|  |     <data name="TransactionNewBodyPlus" xml:space="preserve"> | ||||||
|  |         <value>{0} {1} added to your wallet</value> | ||||||
|  |     </data> | ||||||
|  |     <data name="TransactionNewBodyMinus" xml:space="preserve"> | ||||||
|  |         <value>{0} {1} removed from your wallet</value> | ||||||
|  |     </data> | ||||||
| </root> | </root> | ||||||
| @@ -67,10 +67,10 @@ | |||||||
|         <value>感谢你支持 Solar Network 的开发!你的 {0} 天 {1} 订阅刚刚开始,接下来来探索新解锁的新功能吧!</value> |         <value>感谢你支持 Solar Network 的开发!你的 {0} 天 {1} 订阅刚刚开始,接下来来探索新解锁的新功能吧!</value> | ||||||
|     </data> |     </data> | ||||||
|     <data name="OrderPaidTitle" xml:space="preserve"> |     <data name="OrderPaidTitle" xml:space="preserve"> | ||||||
|         <value>订单回执 {0}</value> |         <value>订单收据 {0}</value> | ||||||
|     </data> |     </data> | ||||||
|     <data name="OrderPaidBody" xml:space="preserve"> |     <data name="OrderPaidBody" xml:space="preserve"> | ||||||
|         <value>{0} {1} 已从你的帐户中扣除来支付 {2}</value> |         <value>已支付订单 {2} 的 {0} {1}</value> | ||||||
|     </data> |     </data> | ||||||
|     <data name="NewLoginTitle" xml:space="preserve"> |     <data name="NewLoginTitle" xml:space="preserve"> | ||||||
|         <value>检测到新登陆</value> |         <value>检测到新登陆</value> | ||||||
| @@ -84,4 +84,19 @@ | |||||||
|     <data name="FriendRequestBody" xml:space="preserve"> |     <data name="FriendRequestBody" xml:space="preserve"> | ||||||
|         <value>您可以前往人际关系页面来决定时候要接受他们的邀请。</value> |         <value>您可以前往人际关系页面来决定时候要接受他们的邀请。</value> | ||||||
|     </data> |     </data> | ||||||
|  |     <data name="OrderReceivedTitle" xml:space="preserve"> | ||||||
|  |         <value>订单收据 {0}</value> | ||||||
|  |     </data> | ||||||
|  |     <data name="OrderReceivedBody" xml:space="preserve"> | ||||||
|  |         <value>收到订单 {2} 支付的 {0} {1}</value> | ||||||
|  |     </data> | ||||||
|  |     <data name="TransactionNewTitle" xml:space="preserve"> | ||||||
|  |         <value>交易 {0}</value> | ||||||
|  |     </data> | ||||||
|  |     <data name="TransactionNewBodyPlus" xml:space="preserve"> | ||||||
|  |         <value>{0} {1} 添加到了您的钱包</value> | ||||||
|  |     </data> | ||||||
|  |     <data name="TransactionNewBodyMinus" xml:space="preserve"> | ||||||
|  |         <value>{0} {1} 从您的钱包移除</value> | ||||||
|  |     </data> | ||||||
| </root> | </root> | ||||||
| @@ -4,6 +4,7 @@ using DysonNetwork.Pass.Auth; | |||||||
| using DysonNetwork.Pass.Credit; | using DysonNetwork.Pass.Credit; | ||||||
| using DysonNetwork.Pass.Leveling; | using DysonNetwork.Pass.Leveling; | ||||||
| using DysonNetwork.Pass.Permission; | using DysonNetwork.Pass.Permission; | ||||||
|  | using DysonNetwork.Pass.Wallet; | ||||||
| using Microsoft.AspNetCore.HttpOverrides; | using Microsoft.AspNetCore.HttpOverrides; | ||||||
| using Microsoft.Extensions.FileProviders; | using Microsoft.Extensions.FileProviders; | ||||||
| using Prometheus; | using Prometheus; | ||||||
| @@ -81,6 +82,8 @@ public static class ApplicationConfiguration | |||||||
|         app.MapGrpcService<SocialCreditServiceGrpc>(); |         app.MapGrpcService<SocialCreditServiceGrpc>(); | ||||||
|         app.MapGrpcService<ExperienceServiceGrpc>(); |         app.MapGrpcService<ExperienceServiceGrpc>(); | ||||||
|         app.MapGrpcService<BotAccountReceiverGrpc>(); |         app.MapGrpcService<BotAccountReceiverGrpc>(); | ||||||
|  |         app.MapGrpcService<WalletServiceGrpc>(); | ||||||
|  |         app.MapGrpcService<PaymentServiceGrpc>(); | ||||||
|  |  | ||||||
|         return app; |         return app; | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								DysonNetwork.Pass/Startup/BroadcastEventHandler.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								DysonNetwork.Pass/Startup/BroadcastEventHandler.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | using System.Text.Json; | ||||||
|  | using DysonNetwork.Pass.Wallet; | ||||||
|  | using DysonNetwork.Shared.Stream; | ||||||
|  | using NATS.Client.Core; | ||||||
|  |  | ||||||
|  | namespace DysonNetwork.Pass.Startup; | ||||||
|  |  | ||||||
|  | public class BroadcastEventHandler( | ||||||
|  |     INatsConnection nats, | ||||||
|  |     ILogger<BroadcastEventHandler> logger, | ||||||
|  |     IServiceProvider serviceProvider | ||||||
|  | ) : BackgroundService | ||||||
|  | { | ||||||
|  |     protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||||
|  |     { | ||||||
|  |         await foreach (var msg in nats.SubscribeAsync<byte[]>(PaymentOrderEvent.Type, cancellationToken: stoppingToken)) | ||||||
|  |         { | ||||||
|  |             try | ||||||
|  |             { | ||||||
|  |                 var evt = JsonSerializer.Deserialize<PaymentOrderEvent>(msg.Data); | ||||||
|  |  | ||||||
|  |                 if (evt?.ProductIdentifier is null || !evt.ProductIdentifier.StartsWith(SubscriptionType.StellarProgram)) | ||||||
|  |                     continue; | ||||||
|  |                  | ||||||
|  |                 logger.LogInformation("Stellar program order paid: {OrderId}", evt.OrderId); | ||||||
|  |                  | ||||||
|  |             } | ||||||
|  |             catch (Exception ex) | ||||||
|  |             { | ||||||
|  |                 logger.LogError(ex, "Error processing AccountDeleted"); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -54,10 +54,6 @@ public static class ServiceCollectionExtensions | |||||||
|          |          | ||||||
|         services.AddPusherService(); |         services.AddPusherService(); | ||||||
|          |          | ||||||
|         // Register gRPC services |  | ||||||
|         services.AddScoped<AccountServiceGrpc>(); |  | ||||||
|         services.AddScoped<AuthServiceGrpc>(); |  | ||||||
|  |  | ||||||
|         // Register OIDC services |         // Register OIDC services | ||||||
|         services.AddScoped<OidcService, GoogleOidcService>(); |         services.AddScoped<OidcService, GoogleOidcService>(); | ||||||
|         services.AddScoped<OidcService, AppleOidcService>(); |         services.AddScoped<OidcService, AppleOidcService>(); | ||||||
|   | |||||||
| @@ -15,9 +15,7 @@ public class OrderController(PaymentService payment, AuthService auth, AppDataba | |||||||
|         var order = await db.PaymentOrders.FindAsync(id); |         var order = await db.PaymentOrders.FindAsync(id); | ||||||
|          |          | ||||||
|         if (order == null) |         if (order == null) | ||||||
|         { |  | ||||||
|             return NotFound(); |             return NotFound(); | ||||||
|         } |  | ||||||
|          |          | ||||||
|         return Ok(order); |         return Ok(order); | ||||||
|     } |     } | ||||||
| @@ -41,7 +39,7 @@ public class OrderController(PaymentService payment, AuthService auth, AppDataba | |||||||
|                 return BadRequest("Wallet was not found."); |                 return BadRequest("Wallet was not found."); | ||||||
|          |          | ||||||
|             // Pay the order |             // Pay the order | ||||||
|             var paidOrder = await payment.PayOrderAsync(id, wallet.Id); |             var paidOrder = await payment.PayOrderAsync(id, wallet); | ||||||
|             return Ok(paidOrder); |             return Ok(paidOrder); | ||||||
|         } |         } | ||||||
|         catch (InvalidOperationException ex) |         catch (InvalidOperationException ex) | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| using System.ComponentModel.DataAnnotations; | using System.ComponentModel.DataAnnotations; | ||||||
| using System.ComponentModel.DataAnnotations.Schema; | using System.ComponentModel.DataAnnotations.Schema; | ||||||
|  | using System.Globalization; | ||||||
| using DysonNetwork.Shared.Data; | using DysonNetwork.Shared.Data; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using NodaTime.Serialization.Protobuf; | using NodaTime.Serialization.Protobuf; | ||||||
| @@ -23,11 +24,14 @@ public enum OrderStatus | |||||||
|  |  | ||||||
| public class Order : ModelBase | public class Order : ModelBase | ||||||
| { | { | ||||||
|  |     public const string InternalAppIdentifier = "internal"; | ||||||
|  |      | ||||||
|     public Guid Id { get; set; } = Guid.NewGuid(); |     public Guid Id { get; set; } = Guid.NewGuid(); | ||||||
|     public OrderStatus Status { get; set; } = OrderStatus.Unpaid; |     public OrderStatus Status { get; set; } = OrderStatus.Unpaid; | ||||||
|     [MaxLength(128)] public string Currency { get; set; } = null!; |     [MaxLength(128)] public string Currency { get; set; } = null!; | ||||||
|     [MaxLength(4096)] public string? Remarks { get; set; } |     [MaxLength(4096)] public string? Remarks { get; set; } | ||||||
|     [MaxLength(4096)] public string? AppIdentifier { get; set; } |     [MaxLength(4096)] public string? AppIdentifier { get; set; } | ||||||
|  |     [MaxLength(4096)] public string? ProductIdentifier { get; set; } | ||||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; } |     [Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; } | ||||||
|     public decimal Amount { get; set; } |     public decimal Amount { get; set; } | ||||||
|     public Instant ExpiredAt { get; set; } |     public Instant ExpiredAt { get; set; } | ||||||
| @@ -44,10 +48,11 @@ public class Order : ModelBase | |||||||
|         Currency = Currency, |         Currency = Currency, | ||||||
|         Remarks = Remarks, |         Remarks = Remarks, | ||||||
|         AppIdentifier = AppIdentifier, |         AppIdentifier = AppIdentifier, | ||||||
|  |         ProductIdentifier = ProductIdentifier, | ||||||
|         Meta = Meta == null |         Meta = Meta == null | ||||||
|             ? null |             ? null | ||||||
|             : Google.Protobuf.ByteString.CopyFrom(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(Meta)), |             : Google.Protobuf.ByteString.CopyFrom(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(Meta)), | ||||||
|         Amount = Amount.ToString(), |         Amount = Amount.ToString(CultureInfo.InvariantCulture), | ||||||
|         ExpiredAt = ExpiredAt.ToTimestamp(), |         ExpiredAt = ExpiredAt.ToTimestamp(), | ||||||
|         PayeeWalletId = PayeeWalletId?.ToString(), |         PayeeWalletId = PayeeWalletId?.ToString(), | ||||||
|         TransactionId = TransactionId?.ToString(), |         TransactionId = TransactionId?.ToString(), | ||||||
| @@ -61,6 +66,7 @@ public class Order : ModelBase | |||||||
|         Currency = proto.Currency, |         Currency = proto.Currency, | ||||||
|         Remarks = proto.Remarks, |         Remarks = proto.Remarks, | ||||||
|         AppIdentifier = proto.AppIdentifier, |         AppIdentifier = proto.AppIdentifier, | ||||||
|  |         ProductIdentifier = proto.ProductIdentifier, | ||||||
|         Meta = proto.HasMeta |         Meta = proto.HasMeta | ||||||
|             ? System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(proto.Meta.ToByteArray()) |             ? System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(proto.Meta.ToByteArray()) | ||||||
|             : null, |             : null, | ||||||
|   | |||||||
| @@ -1,9 +1,12 @@ | |||||||
| using System.Globalization; | using System.Globalization; | ||||||
|  | using System.Text.Json; | ||||||
| using DysonNetwork.Pass.Localization; | using DysonNetwork.Pass.Localization; | ||||||
| using DysonNetwork.Shared.Proto; | using DysonNetwork.Shared.Proto; | ||||||
|  | using DysonNetwork.Shared.Stream; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using Microsoft.EntityFrameworkCore.Storage; | using Microsoft.EntityFrameworkCore.Storage; | ||||||
| using Microsoft.Extensions.Localization; | using Microsoft.Extensions.Localization; | ||||||
|  | using NATS.Client.Core; | ||||||
| using NodaTime; | using NodaTime; | ||||||
| using AccountService = DysonNetwork.Pass.Account.AccountService; | using AccountService = DysonNetwork.Pass.Account.AccountService; | ||||||
|  |  | ||||||
| @@ -13,7 +16,8 @@ public class PaymentService( | |||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     WalletService wat, |     WalletService wat, | ||||||
|     PusherService.PusherServiceClient pusher, |     PusherService.PusherServiceClient pusher, | ||||||
|     IStringLocalizer<NotificationResource> localizer |     IStringLocalizer<NotificationResource> localizer, | ||||||
|  |     INatsConnection nats | ||||||
| ) | ) | ||||||
| { | { | ||||||
|     public async Task<Order> CreateOrderAsync( |     public async Task<Order> CreateOrderAsync( | ||||||
| @@ -22,6 +26,7 @@ public class PaymentService( | |||||||
|         decimal amount, |         decimal amount, | ||||||
|         Duration? expiration = null, |         Duration? expiration = null, | ||||||
|         string? appIdentifier = null, |         string? appIdentifier = null, | ||||||
|  |         string? productIdentifier = null, | ||||||
|         Dictionary<string, object>? meta = null, |         Dictionary<string, object>? meta = null, | ||||||
|         bool reuseable = true |         bool reuseable = true | ||||||
|     ) |     ) | ||||||
| @@ -29,26 +34,25 @@ public class PaymentService( | |||||||
|         // Check if there's an existing unpaid order that can be reused |         // Check if there's an existing unpaid order that can be reused | ||||||
|         if (reuseable && appIdentifier != null) |         if (reuseable && appIdentifier != null) | ||||||
|         { |         { | ||||||
|  |             var now = SystemClock.Instance.GetCurrentInstant(); | ||||||
|             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.ProductIdentifier == productIdentifier && | ||||||
|  |                             o.ExpiredAt > now) | ||||||
|                 .FirstOrDefaultAsync(); |                 .FirstOrDefaultAsync(); | ||||||
|  |  | ||||||
|             // If an existing order is found, check if meta matches |             // If an existing order is found, check if meta matches | ||||||
|             if (existingOrder != null && meta != null && existingOrder.Meta != null) |             if (existingOrder != null && meta != null && existingOrder.Meta != null) | ||||||
|             { |             { | ||||||
|                 // Compare meta dictionaries - if they are equivalent, reuse the order |                 // Compare the meta dictionary - if they are equivalent, reuse the order | ||||||
|                 var metaMatches = existingOrder.Meta.Count == meta.Count && |                 var metaMatches = existingOrder.Meta.Count == meta.Count && | ||||||
|                                   !existingOrder.Meta.Except(meta).Any(); |                                   !existingOrder.Meta.Except(meta).Any(); | ||||||
|  |  | ||||||
|                 if (metaMatches) |                 if (metaMatches) | ||||||
|                 { |  | ||||||
|                     return existingOrder; |                     return existingOrder; | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -60,6 +64,7 @@ public class PaymentService( | |||||||
|             Amount = amount, |             Amount = amount, | ||||||
|             ExpiredAt = SystemClock.Instance.GetCurrentInstant().Plus(expiration ?? Duration.FromHours(24)), |             ExpiredAt = SystemClock.Instance.GetCurrentInstant().Plus(expiration ?? Duration.FromHours(24)), | ||||||
|             AppIdentifier = appIdentifier, |             AppIdentifier = appIdentifier, | ||||||
|  |             ProductIdentifier = productIdentifier, | ||||||
|             Meta = meta |             Meta = meta | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
| @@ -104,7 +109,8 @@ public class PaymentService( | |||||||
|         string currency, |         string currency, | ||||||
|         decimal amount, |         decimal amount, | ||||||
|         string? remarks = null, |         string? remarks = null, | ||||||
|         TransactionType type = TransactionType.System |         TransactionType type = TransactionType.System, | ||||||
|  |         bool silent = false | ||||||
|     ) |     ) | ||||||
|     { |     { | ||||||
|         if (payerWalletId == null && payeeWalletId == null) |         if (payerWalletId == null && payeeWalletId == null) | ||||||
| @@ -121,8 +127,12 @@ public class PaymentService( | |||||||
|             Type = type |             Type = type | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|  |         Wallet? payerWallet = null, payeeWallet = null; | ||||||
|  |          | ||||||
|         if (payerWalletId.HasValue) |         if (payerWalletId.HasValue) | ||||||
|         { |         { | ||||||
|  |             payerWallet = await db.Wallets.FirstOrDefaultAsync(e => e.AccountId == payerWalletId.Value); | ||||||
|  |              | ||||||
|             var (payerPocket, isNewlyCreated) = |             var (payerPocket, isNewlyCreated) = | ||||||
|                 await wat.GetOrCreateWalletPocketAsync(payerWalletId.Value, currency); |                 await wat.GetOrCreateWalletPocketAsync(payerWalletId.Value, currency); | ||||||
|  |  | ||||||
| @@ -137,6 +147,8 @@ public class PaymentService( | |||||||
|  |  | ||||||
|         if (payeeWalletId.HasValue) |         if (payeeWalletId.HasValue) | ||||||
|         { |         { | ||||||
|  |             payeeWallet = await db.Wallets.FirstOrDefaultAsync(e => e.AccountId == payeeWalletId.Value); | ||||||
|  |              | ||||||
|             var (payeePocket, isNewlyCreated) = |             var (payeePocket, isNewlyCreated) = | ||||||
|                 await wat.GetOrCreateWalletPocketAsync(payeeWalletId.Value, currency, amount); |                 await wat.GetOrCreateWalletPocketAsync(payeeWalletId.Value, currency, amount); | ||||||
|  |  | ||||||
| @@ -149,13 +161,85 @@ public class PaymentService( | |||||||
|  |  | ||||||
|         db.PaymentTransactions.Add(transaction); |         db.PaymentTransactions.Add(transaction); | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|  |  | ||||||
|  |         if (!silent) | ||||||
|  |             await NotifyNewTransaction(transaction, payerWallet, payeeWallet); | ||||||
|  |          | ||||||
|         return transaction; |         return transaction; | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |     private async Task NotifyNewTransaction(Transaction transaction, Wallet? payerWallet, Wallet? payeeWallet) | ||||||
|  |     { | ||||||
|  |         if (payerWallet is not null) | ||||||
|  |         { | ||||||
|  |             var account = await db.Accounts | ||||||
|  |                 .Where(a => a.Id == payerWallet.AccountId) | ||||||
|  |                 .FirstOrDefaultAsync(); | ||||||
|  |             if (account is null) return; | ||||||
|  |  | ||||||
|     public async Task<Order> PayOrderAsync(Guid orderId, Guid payerWalletId) |             AccountService.SetCultureInfo(account); | ||||||
|  |  | ||||||
|  |             // Due to ID is uuid, it longer than 8 words for sure | ||||||
|  |             var readableTransactionId = transaction.Id.ToString().Replace("-", "")[..8]; | ||||||
|  |             var readableTransactionRemark = transaction.Remarks ?? $"#{readableTransactionId}"; | ||||||
|  |  | ||||||
|  |             await pusher.SendPushNotificationToUserAsync( | ||||||
|  |                 new SendPushNotificationToUserRequest | ||||||
|  |                 { | ||||||
|  |                     UserId = account.Id.ToString(), | ||||||
|  |                     Notification = new PushNotification | ||||||
|  |                     { | ||||||
|  |                         Topic = "wallets.transactions", | ||||||
|  |                         Title = transaction.Amount > 0 | ||||||
|  |                             ? localizer["TransactionNewBodyMinus", readableTransactionRemark] | ||||||
|  |                             : localizer["TransactionNewBodyPlus", readableTransactionRemark], | ||||||
|  |                         Body = localizer["TransactionNewTitle", | ||||||
|  |                             transaction.Amount.ToString(CultureInfo.InvariantCulture), | ||||||
|  |                             transaction.Currency], | ||||||
|  |                         IsSavable = true | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         if (payeeWallet is not null) | ||||||
|  |         { | ||||||
|  |             var account = await db.Accounts | ||||||
|  |                 .Where(a => a.Id == payeeWallet.AccountId) | ||||||
|  |                 .FirstOrDefaultAsync(); | ||||||
|  |             if (account is null) return; | ||||||
|  |  | ||||||
|  |             AccountService.SetCultureInfo(account); | ||||||
|  |  | ||||||
|  |             // Due to ID is uuid, it longer than 8 words for sure | ||||||
|  |             var readableTransactionId = transaction.Id.ToString().Replace("-", "")[..8]; | ||||||
|  |             var readableTransactionRemark = transaction.Remarks ?? $"#{readableTransactionId}"; | ||||||
|  |  | ||||||
|  |             await pusher.SendPushNotificationToUserAsync( | ||||||
|  |                 new SendPushNotificationToUserRequest | ||||||
|  |                 { | ||||||
|  |                     UserId = account.Id.ToString(), | ||||||
|  |                     Notification = new PushNotification | ||||||
|  |                     { | ||||||
|  |                         Topic = "wallets.transactions", | ||||||
|  |                         Title = transaction.Amount > 0 | ||||||
|  |                             ? localizer["TransactionNewBodyPlus", readableTransactionRemark] | ||||||
|  |                             : localizer["TransactionNewBodyMinus", readableTransactionRemark], | ||||||
|  |                         Body = localizer["TransactionNewTitle", | ||||||
|  |                             transaction.Amount.ToString(CultureInfo.InvariantCulture), | ||||||
|  |                             transaction.Currency], | ||||||
|  |                         IsSavable = true | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<Order> PayOrderAsync(Guid orderId, Wallet payerWallet) | ||||||
|     { |     { | ||||||
|         var order = await db.PaymentOrders |         var order = await db.PaymentOrders | ||||||
|             .Include(o => o.Transaction) |             .Include(o => o.Transaction) | ||||||
|  |             .Include(o => o.PayeeWallet) | ||||||
|             .FirstOrDefaultAsync(o => o.Id == orderId); |             .FirstOrDefaultAsync(o => o.Id == orderId); | ||||||
|  |  | ||||||
|         if (order == null) |         if (order == null) | ||||||
| @@ -176,7 +260,7 @@ public class PaymentService( | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         var transaction = await CreateTransactionAsync( |         var transaction = await CreateTransactionAsync( | ||||||
|             payerWalletId, |             payerWallet.Id, | ||||||
|             order.PayeeWalletId, |             order.PayeeWalletId, | ||||||
|             order.Currency, |             order.Currency, | ||||||
|             order.Amount, |             order.Amount, | ||||||
| @@ -186,41 +270,87 @@ public class PaymentService( | |||||||
|         order.TransactionId = transaction.Id; |         order.TransactionId = transaction.Id; | ||||||
|         order.Transaction = transaction; |         order.Transaction = transaction; | ||||||
|         order.Status = OrderStatus.Paid; |         order.Status = OrderStatus.Paid; | ||||||
|          |  | ||||||
|         await db.SaveChangesAsync(); |         await db.SaveChangesAsync(); | ||||||
|          |  | ||||||
|         await NotifyOrderPaid(order); |         await NotifyOrderPaid(order, payerWallet, order.PayeeWallet); | ||||||
|          |  | ||||||
|  |         await nats.PublishAsync(PaymentOrderEvent.Type, JsonSerializer.SerializeToUtf8Bytes(new PaymentOrderEvent | ||||||
|  |         { | ||||||
|  |             OrderId = order.Id, | ||||||
|  |             WalletId = payerWallet.Id, | ||||||
|  |             AccountId = payerWallet.AccountId, | ||||||
|  |             AppIdentifier = order.AppIdentifier, | ||||||
|  |             ProductIdentifier = order.ProductIdentifier, | ||||||
|  |             Meta = order.Meta ?? [], | ||||||
|  |             Status = (int)order.Status, | ||||||
|  |         })); | ||||||
|  |  | ||||||
|         return order; |         return order; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private async Task NotifyOrderPaid(Order order) |     private async Task NotifyOrderPaid(Order order, Wallet? payerWallet, Wallet? payeeWallet) | ||||||
|     { |     { | ||||||
|         if (order.PayeeWallet is null) return; |         if (payerWallet is not null) | ||||||
|         var account = await db.Accounts.FirstOrDefaultAsync(a => a.Id == order.PayeeWallet.AccountId); |         { | ||||||
|         if (account is null) return; |             var account = await db.Accounts | ||||||
|          |                 .Where(a => a.Id == payerWallet.AccountId) | ||||||
|         AccountService.SetCultureInfo(account); |                 .FirstOrDefaultAsync(); | ||||||
|  |             if (account is null) return; | ||||||
|  |  | ||||||
|         // Due to ID is uuid, it longer than 8 words for sure |             AccountService.SetCultureInfo(account); | ||||||
|         var readableOrderId = order.Id.ToString().Replace("-", "")[..8]; |  | ||||||
|         var readableOrderRemark = order.Remarks ?? $"#{readableOrderId}"; |  | ||||||
|  |  | ||||||
|          |             // Due to ID is uuid, it longer than 8 words for sure | ||||||
|         await pusher.SendPushNotificationToUserAsync( |             var readableOrderId = order.Id.ToString().Replace("-", "")[..8]; | ||||||
|             new SendPushNotificationToUserRequest |             var readableOrderRemark = order.Remarks ?? $"#{readableOrderId}"; | ||||||
|             { |  | ||||||
|                 UserId = account.Id.ToString(), |  | ||||||
|                 Notification = new PushNotification |             await pusher.SendPushNotificationToUserAsync( | ||||||
|  |                 new SendPushNotificationToUserRequest | ||||||
|                 { |                 { | ||||||
|                     Topic = "wallets.orders.paid", |                     UserId = account.Id.ToString(), | ||||||
|                     Title = localizer["OrderPaidTitle", $"#{readableOrderId}"], |                     Notification = new PushNotification | ||||||
|                     Body = localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture), order.Currency, |                     { | ||||||
|                         readableOrderRemark], |                         Topic = "wallets.orders.paid", | ||||||
|                     IsSavable = true |                         Title = localizer["OrderPaidTitle", $"#{readableOrderId}"], | ||||||
|  |                         Body = localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture), | ||||||
|  |                             order.Currency, | ||||||
|  |                             readableOrderRemark], | ||||||
|  |                         IsSavable = true | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|             } |             ); | ||||||
|         ); |         } | ||||||
|  |  | ||||||
|  |         if (payeeWallet is not null) | ||||||
|  |         { | ||||||
|  |             var account = await db.Accounts | ||||||
|  |                 .Where(a => a.Id == payeeWallet.AccountId) | ||||||
|  |                 .FirstOrDefaultAsync(); | ||||||
|  |             if (account is null) return; | ||||||
|  |  | ||||||
|  |             AccountService.SetCultureInfo(account); | ||||||
|  |  | ||||||
|  |             // 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 pusher.SendPushNotificationToUserAsync( | ||||||
|  |                 new SendPushNotificationToUserRequest | ||||||
|  |                 { | ||||||
|  |                     UserId = account.Id.ToString(), | ||||||
|  |                     Notification = new PushNotification | ||||||
|  |                     { | ||||||
|  |                         Topic = "wallets.orders.received", | ||||||
|  |                         Title = localizer["OrderReceivedTitle", $"#{readableOrderId}"], | ||||||
|  |                         Body = localizer["OrderReceivedBody", order.Amount.ToString(CultureInfo.InvariantCulture), | ||||||
|  |                             order.Currency, | ||||||
|  |                             readableOrderRemark], | ||||||
|  |                         IsSavable = true | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public async Task<Order> CancelOrderAsync(Guid orderId) |     public async Task<Order> CancelOrderAsync(Guid orderId) | ||||||
|   | |||||||
| @@ -13,10 +13,10 @@ public class PaymentServiceGrpc(PaymentService paymentService) : Shared.Proto.Pa | |||||||
|             request.Currency, |             request.Currency, | ||||||
|             decimal.Parse(request.Amount), |             decimal.Parse(request.Amount), | ||||||
|             request.Expiration is not null ? Duration.FromSeconds(request.Expiration.Seconds) : null, |             request.Expiration is not null ? Duration.FromSeconds(request.Expiration.Seconds) : null, | ||||||
|             request.HasAppIdentifier ? request.AppIdentifier : null, |             request.HasAppIdentifier ? request.AppIdentifier : Order.InternalAppIdentifier, | ||||||
|             // Assuming meta is a JSON string |             request.HasProductIdentifier ? request.ProductIdentifier : null, | ||||||
|             request.HasMeta |             request.HasMeta | ||||||
|                 ? System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(request.Meta.ToStringUtf8()) |                 ? GrpcTypeHelper.ConvertByteStringToObject<Dictionary<string, object>>(request.Meta) | ||||||
|                 : null, |                 : null, | ||||||
|             request.Reuseable |             request.Reuseable | ||||||
|         ); |         ); | ||||||
|   | |||||||
| @@ -86,7 +86,7 @@ public class SubscriptionRenewalJob( | |||||||
|                         if (wallet is null) continue; |                         if (wallet is null) continue; | ||||||
|  |  | ||||||
|                         // Process automatic payment from wallet |                         // Process automatic payment from wallet | ||||||
|                         await paymentService.PayOrderAsync(order.Id, wallet.Id); |                         await paymentService.PayOrderAsync(order.Id, wallet); | ||||||
|  |  | ||||||
|                         // Update subscription details |                         // Update subscription details | ||||||
|                         subscription.BegunAt = subscription.EndedAt!.Value; |                         subscription.BegunAt = subscription.EndedAt!.Value; | ||||||
|   | |||||||
| @@ -8,11 +8,7 @@ public class WalletServiceGrpc(WalletService walletService) : Shared.Proto.Walle | |||||||
|     public override async Task<Shared.Proto.Wallet> GetWallet(GetWalletRequest request, ServerCallContext context) |     public override async Task<Shared.Proto.Wallet> GetWallet(GetWalletRequest request, ServerCallContext context) | ||||||
|     { |     { | ||||||
|         var wallet = await walletService.GetWalletAsync(Guid.Parse(request.AccountId)); |         var wallet = await walletService.GetWalletAsync(Guid.Parse(request.AccountId)); | ||||||
|         if (wallet == null) |         return wallet == null ? throw new RpcException(new Status(StatusCode.NotFound, "Wallet not found.")) : wallet.ToProtoValue(); | ||||||
|         { |  | ||||||
|             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) |     public override async Task<Shared.Proto.Wallet> CreateWallet(CreateWalletRequest request, ServerCallContext context) | ||||||
|   | |||||||
| @@ -123,6 +123,7 @@ message CreateOrderRequest { | |||||||
|     string amount = 3; |     string amount = 3; | ||||||
|     optional google.protobuf.Duration expiration = 4; |     optional google.protobuf.Duration expiration = 4; | ||||||
|     optional string app_identifier = 5; |     optional string app_identifier = 5; | ||||||
|  |     optional string product_identifier = 8; | ||||||
|     // Using bytes for meta to represent JSON. |     // Using bytes for meta to represent JSON. | ||||||
|     optional bytes meta = 6; |     optional bytes meta = 6; | ||||||
|     bool reuseable = 7; |     bool reuseable = 7; | ||||||
| @@ -135,6 +136,7 @@ message Order { | |||||||
|     string amount = 4; |     string amount = 4; | ||||||
|     google.protobuf.Timestamp expired_at = 5; |     google.protobuf.Timestamp expired_at = 5; | ||||||
|     optional string app_identifier = 6; |     optional string app_identifier = 6; | ||||||
|  |     optional string product_identifier = 12; | ||||||
|     // Using bytes for meta to represent JSON. |     // Using bytes for meta to represent JSON. | ||||||
|     optional bytes meta = 7; |     optional bytes meta = 7; | ||||||
|     OrderStatus status = 8; |     OrderStatus status = 8; | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								DysonNetwork.Shared/Stream/PaymentEvent.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								DysonNetwork.Shared/Stream/PaymentEvent.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | namespace DysonNetwork.Shared.Stream; | ||||||
|  |  | ||||||
|  | public class PaymentOrderEvent | ||||||
|  | { | ||||||
|  |     public static string Type => "payments.orders"; | ||||||
|  |      | ||||||
|  |     public Guid OrderId { get; set; } | ||||||
|  |     public Guid WalletId { get; set; } | ||||||
|  |     public Guid AccountId { get; set; } | ||||||
|  |     public string? AppIdentifier { get; set; } | ||||||
|  |     public string? ProductIdentifier { get; set; } | ||||||
|  |     public Dictionary<string, object> Meta { get; set; } = null!; | ||||||
|  |     public int Status { get; set; } | ||||||
|  | } | ||||||
| @@ -31,6 +31,7 @@ public class AppDatabase( | |||||||
|  |  | ||||||
|     public DbSet<Post.Post> Posts { get; set; } = null!; |     public DbSet<Post.Post> Posts { get; set; } = null!; | ||||||
|     public DbSet<PostReaction> PostReactions { get; set; } = null!; |     public DbSet<PostReaction> PostReactions { get; set; } = null!; | ||||||
|  |     public DbSet<PostAward> PostAwards { get; set; } = null!; | ||||||
|     public DbSet<PostTag> PostTags { get; set; } = null!; |     public DbSet<PostTag> PostTags { get; set; } = null!; | ||||||
|     public DbSet<PostCategory> PostCategories { get; set; } = null!; |     public DbSet<PostCategory> PostCategories { get; set; } = null!; | ||||||
|     public DbSet<PostCollection> PostCollections { get; set; } = null!; |     public DbSet<PostCollection> PostCollections { get; set; } = null!; | ||||||
|   | |||||||
| @@ -51,6 +51,7 @@ public class Post : ModelBase, IIdentifiedResource, IActivity | |||||||
|     public int ViewsTotal { get; set; } |     public int ViewsTotal { get; set; } | ||||||
|     public int Upvotes { get; set; } |     public int Upvotes { get; set; } | ||||||
|     public int Downvotes { get; set; } |     public int Downvotes { get; set; } | ||||||
|  |     public decimal AwardedScore { get; set; } | ||||||
|     [NotMapped] public Dictionary<string, int> ReactionsCount { get; set; } = new(); |     [NotMapped] public Dictionary<string, int> ReactionsCount { get; set; } = new(); | ||||||
|     [NotMapped] public int RepliesCount { get; set; } |     [NotMapped] public int RepliesCount { get; set; } | ||||||
|     [NotMapped] public Dictionary<string, bool>? ReactionsMade { get; set; } |     [NotMapped] public Dictionary<string, bool>? ReactionsMade { get; set; } | ||||||
| @@ -73,6 +74,7 @@ public class Post : ModelBase, IIdentifiedResource, IActivity | |||||||
|     public Guid PublisherId { get; set; } |     public Guid PublisherId { get; set; } | ||||||
|     public Publisher.Publisher Publisher { get; set; } = null!; |     public Publisher.Publisher Publisher { get; set; } = null!; | ||||||
|  |  | ||||||
|  |     public ICollection<PostAward> Awards { get; set; } = null!; | ||||||
|     public ICollection<PostReaction> Reactions { get; set; } = new List<PostReaction>(); |     public ICollection<PostReaction> Reactions { get; set; } = new List<PostReaction>(); | ||||||
|     public ICollection<PostTag> Tags { get; set; } = new List<PostTag>(); |     public ICollection<PostTag> Tags { get; set; } = new List<PostTag>(); | ||||||
|     public ICollection<PostCategory> Categories { get; set; } = new List<PostCategory>(); |     public ICollection<PostCategory> Categories { get; set; } = new List<PostCategory>(); | ||||||
| @@ -168,3 +170,15 @@ public class PostReaction : ModelBase | |||||||
|     [JsonIgnore] public Post Post { get; set; } = null!; |     [JsonIgnore] public Post Post { get; set; } = null!; | ||||||
|     public Guid AccountId { get; set; } |     public Guid AccountId { get; set; } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | public class PostAward : ModelBase | ||||||
|  | { | ||||||
|  |     public Guid Id { get; set; } | ||||||
|  |     public decimal Amount { get; set; } | ||||||
|  |     public PostReactionAttitude Attitude { get; set; } | ||||||
|  |     [MaxLength(4096)] public string? Message { get; set; } | ||||||
|  |      | ||||||
|  |     public Guid PostId { get; set; } | ||||||
|  |     [JsonIgnore] public Post Post { get; set; } = null!; | ||||||
|  |     public Guid AccountId { get; set; } | ||||||
|  | } | ||||||
| @@ -579,6 +579,38 @@ public class PostController( | |||||||
|         return Ok(reaction); |         return Ok(reaction); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public class PostAwardRequest | ||||||
|  |     { | ||||||
|  |         public decimal Amount { get; set; } | ||||||
|  |         [MaxLength(4096)] public string? Message { get; set; } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     [HttpPost("{id:guid}/awards")] | ||||||
|  |     [Authorize] | ||||||
|  |     public async Task<ActionResult<PostAward>> AwardPost(Guid id, [FromBody] PostAwardRequest request) | ||||||
|  |     { | ||||||
|  |         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||||
|  |  | ||||||
|  |         var friendsResponse = | ||||||
|  |             await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest | ||||||
|  |                 { AccountId = currentUser.Id.ToString() }); | ||||||
|  |         var userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); | ||||||
|  |         var userPublishers = await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); | ||||||
|  |  | ||||||
|  |         var post = await db.Posts | ||||||
|  |             .Where(e => e.Id == id) | ||||||
|  |             .Include(e => e.Publisher) | ||||||
|  |             .FilterWithVisibility(currentUser, userFriends, userPublishers) | ||||||
|  |             .FirstOrDefaultAsync(); | ||||||
|  |         if (post is null) return NotFound(); | ||||||
|  |  | ||||||
|  |         var accountId = Guid.Parse(currentUser.Id); | ||||||
|  |          | ||||||
|  |         // TODO Make payment, add record | ||||||
|  |  | ||||||
|  |         return Ok(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public class PostPinRequest |     public class PostPinRequest | ||||||
|     { |     { | ||||||
|         [Required] public PostPinMode Mode { get; set; } |         [Required] public PostPinMode Mode { get; set; } | ||||||
| @@ -600,7 +632,7 @@ public class PostController( | |||||||
|         var accountId = Guid.Parse(currentUser.Id); |         var accountId = Guid.Parse(currentUser.Id); | ||||||
|         if (!await pub.IsMemberWithRole(post.PublisherId, accountId, PublisherMemberRole.Editor)) |         if (!await pub.IsMemberWithRole(post.PublisherId, accountId, PublisherMemberRole.Editor)) | ||||||
|             return StatusCode(403, "You are not an editor of this publisher"); |             return StatusCode(403, "You are not an editor of this publisher"); | ||||||
|          |  | ||||||
|         if (request.Mode == PostPinMode.RealmPage && post.RealmId != null) |         if (request.Mode == PostPinMode.RealmPage && post.RealmId != null) | ||||||
|         { |         { | ||||||
|             if (!await rs.IsMemberWithRole(post.RealmId.Value, accountId, RealmMemberRole.Moderator)) |             if (!await rs.IsMemberWithRole(post.RealmId.Value, accountId, RealmMemberRole.Moderator)) | ||||||
| @@ -644,11 +676,11 @@ public class PostController( | |||||||
|             .Include(e => e.RepliedPost) |             .Include(e => e.RepliedPost) | ||||||
|             .FirstOrDefaultAsync(); |             .FirstOrDefaultAsync(); | ||||||
|         if (post is null) return NotFound(); |         if (post is null) return NotFound(); | ||||||
|          |  | ||||||
|         var accountId = Guid.Parse(currentUser.Id); |         var accountId = Guid.Parse(currentUser.Id); | ||||||
|         if (!await pub.IsMemberWithRole(post.PublisherId, accountId, PublisherMemberRole.Editor)) |         if (!await pub.IsMemberWithRole(post.PublisherId, accountId, PublisherMemberRole.Editor)) | ||||||
|             return StatusCode(403, "You are not an editor of this publisher"); |             return StatusCode(403, "You are not an editor of this publisher"); | ||||||
|          |  | ||||||
|         if (post is { PinMode: PostPinMode.RealmPage, RealmId: not null }) |         if (post is { PinMode: PostPinMode.RealmPage, RealmId: not null }) | ||||||
|         { |         { | ||||||
|             if (!await rs.IsMemberWithRole(post.RealmId.Value, accountId, RealmMemberRole.Moderator)) |             if (!await rs.IsMemberWithRole(post.RealmId.Value, accountId, RealmMemberRole.Moderator)) | ||||||
| @@ -807,7 +839,7 @@ public class PostController( | |||||||
|         if (post is null) return NotFound(); |         if (post is null) return NotFound(); | ||||||
|  |  | ||||||
|         if (!await pub.IsMemberWithRole(post.Publisher.Id, Guid.Parse(currentUser.Id), |         if (!await pub.IsMemberWithRole(post.Publisher.Id, Guid.Parse(currentUser.Id), | ||||||
|                 Publisher.PublisherMemberRole.Editor)) |                 PublisherMemberRole.Editor)) | ||||||
|             return StatusCode(403, "You need at least be an editor to delete the publisher's post."); |             return StatusCode(403, "You need at least be an editor to delete the publisher's post."); | ||||||
|  |  | ||||||
|         await ps.DeletePostAsync(post); |         await ps.DeletePostAsync(post); | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ public class BroadcastEventHandler( | |||||||
| { | { | ||||||
|     protected override async Task ExecuteAsync(CancellationToken stoppingToken) |     protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||||
|     { |     { | ||||||
|         await foreach (var msg in nats.SubscribeAsync<byte[]>("accounts.deleted", cancellationToken: stoppingToken)) |         await foreach (var msg in nats.SubscribeAsync<byte[]>(AccountDeletedEvent.Type, cancellationToken: stoppingToken)) | ||||||
|         { |         { | ||||||
|             try |             try | ||||||
|             { |             { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user