diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 5e4b726a..883c3546 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -27,8 +27,8 @@ jobs: run: | files="${{ steps.changed-files.outputs.files }}" matrix="{\"include\":[]}" - services=("Sphere" "Pass" "Ring" "Drive" "Develop" "Gateway" "Insight" "Zone" "Messager") - images=("sphere" "pass" "ring" "drive" "develop" "gateway" "insight" "zone" "messager") + services=("Sphere" "Pass" "Ring" "Drive" "Develop" "Gateway" "Insight" "Zone" "Messager" "Wallet") + images=("sphere" "pass" "ring" "drive" "develop" "gateway" "insight" "zone" "messager" "wallet") changed_services=() for file in $files; do diff --git a/DysonNetwork.Control/AppHost.cs b/DysonNetwork.Control/AppHost.cs index d1d1bb39..fee13802 100644 --- a/DysonNetwork.Control/AppHost.cs +++ b/DysonNetwork.Control/AppHost.cs @@ -39,7 +39,11 @@ var messagerService = builder.AddProject("messag .WithReference(developService) .WithReference(driveService); -passService.WithReference(developService).WithReference(driveService); +var walletService = builder.AddProject("wallet") + .WithReference(passService) + .WithReference(ringService); + +passService.WithReference(developService).WithReference(driveService).WithReference(walletService); List> services = [ @@ -50,7 +54,8 @@ List> services = developService, insightService, zoneService, - messagerService + messagerService, + walletService ]; for (var idx = 0; idx < services.Count; idx++) diff --git a/DysonNetwork.Control/DysonNetwork.Control.csproj b/DysonNetwork.Control/DysonNetwork.Control.csproj index e0384664..728d23c5 100644 --- a/DysonNetwork.Control/DysonNetwork.Control.csproj +++ b/DysonNetwork.Control/DysonNetwork.Control.csproj @@ -26,5 +26,6 @@ + diff --git a/DysonNetwork.Gateway/Configuration/GatewayEndpointsOptions.cs b/DysonNetwork.Gateway/Configuration/GatewayEndpointsOptions.cs index dd0562dc..2a29876b 100644 --- a/DysonNetwork.Gateway/Configuration/GatewayEndpointsOptions.cs +++ b/DysonNetwork.Gateway/Configuration/GatewayEndpointsOptions.cs @@ -28,7 +28,8 @@ public class GatewayEndpointsOptions "develop", "insight", "zone", - "messager" + "messager", + "wallet" ]; /// @@ -39,7 +40,8 @@ public class GatewayEndpointsOptions "ring", "pass", "drive", - "sphere" + "sphere", + "wallet" ]; /// diff --git a/DysonNetwork.Pass/Account/AccountController.cs b/DysonNetwork.Pass/Account/AccountController.cs index f0ef3ad4..ccc25fec 100644 --- a/DysonNetwork.Pass/Account/AccountController.cs +++ b/DysonNetwork.Pass/Account/AccountController.cs @@ -2,7 +2,6 @@ using System.ComponentModel.DataAnnotations; using DysonNetwork.Pass.Affiliation; using DysonNetwork.Pass.Auth; using DysonNetwork.Pass.Credit; -using DysonNetwork.Pass.Wallet; using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Geometry; using DysonNetwork.Shared.Models; @@ -20,7 +19,6 @@ public class AccountController( AppDatabase db, AuthService auth, AccountService accounts, - SubscriptionService subscriptions, AccountEventService events, SocialCreditService socialCreditService, AffiliationSpellService ars, diff --git a/DysonNetwork.Pass/Account/AccountCurrentController.cs b/DysonNetwork.Pass/Account/AccountCurrentController.cs index 4fd59276..1bab0e35 100644 --- a/DysonNetwork.Pass/Account/AccountCurrentController.cs +++ b/DysonNetwork.Pass/Account/AccountCurrentController.cs @@ -1,6 +1,5 @@ using System.ComponentModel.DataAnnotations; using DysonNetwork.Pass.Permission; -using DysonNetwork.Pass.Wallet; using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Networking; @@ -20,7 +19,6 @@ namespace DysonNetwork.Pass.Account; public class AccountCurrentController( AppDatabase db, AccountService accounts, - SubscriptionService subscriptions, AccountEventService events, AuthService auth, FileService.FileServiceClient files, @@ -41,9 +39,6 @@ public class AccountCurrentController( .Where(e => e.Id == userId) .FirstOrDefaultAsync(); - 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 05a2c95f..4f35d782 100644 --- a/DysonNetwork.Pass/Account/AccountEventService.cs +++ b/DysonNetwork.Pass/Account/AccountEventService.cs @@ -1,5 +1,4 @@ using System.Globalization; -using DysonNetwork.Pass.Wallet; using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Proto; @@ -14,11 +13,9 @@ namespace DysonNetwork.Pass.Account; public class AccountEventService( AppDatabase db, - Wallet.PaymentService payment, ICacheService cache, IStringLocalizer localizer, RingService.RingServiceClient pusher, - SubscriptionService subscriptions, Pass.Leveling.ExperienceService experienceService, INatsConnection nats ) @@ -234,9 +231,6 @@ public class AccountEventService( public async Task CheckInDailyDoAskCaptcha(SnAccount user) { - var perkSubscription = await subscriptions.GetPerkSubscriptionAsync(user.Id); - if (perkSubscription is not null) return false; - var cacheKey = $"{CaptchaCacheKey}{user.Id}"; var needsCaptcha = await cache.GetAsync(cacheKey); if (needsCaptcha is not null) @@ -426,22 +420,6 @@ public class AccountEventService( CreatedAt = backdated ?? SystemClock.Instance.GetCurrentInstant(), }; - try - { - if (result.RewardPoints.HasValue) - await payment.CreateTransactionWithAccountAsync( - null, - user.Id, - WalletCurrency.SourcePoint, - result.RewardPoints.Value, - $"Check-in reward on {now:yyyy/MM/dd}" - ); - } - catch - { - result.RewardPoints = null; - } - db.AccountCheckInResults.Add(result); await db.SaveChangesAsync(); // Remember to save changes to the database if (result.RewardExperience is not null) diff --git a/DysonNetwork.Pass/Account/AccountPublicController.cs b/DysonNetwork.Pass/Account/AccountPublicController.cs index b63f6acd..283a58eb 100644 --- a/DysonNetwork.Pass/Account/AccountPublicController.cs +++ b/DysonNetwork.Pass/Account/AccountPublicController.cs @@ -1,5 +1,4 @@ using DysonNetwork.Pass.Credit; -using DysonNetwork.Pass.Wallet; using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Networking; using Microsoft.AspNetCore.Mvc; @@ -11,7 +10,6 @@ namespace DysonNetwork.Pass.Account; [Route("/api/accounts")] public class AccountPublicController( AppDatabase db, - SubscriptionService subscriptions, SocialCreditService socialCreditService ) : ControllerBase { @@ -28,9 +26,6 @@ public class AccountPublicController( .FirstOrDefaultAsync(); if (account is null) return NotFound(ApiError.NotFound(name, traceId: HttpContext.TraceIdentifier)); - var perk = await subscriptions.GetPerkSubscriptionAsync(account.Id); - account.PerkSubscription = perk?.ToReference(); - return account; } diff --git a/DysonNetwork.Pass/Account/AccountServiceGrpc.cs b/DysonNetwork.Pass/Account/AccountServiceGrpc.cs index 6a0a7734..4a23c923 100644 --- a/DysonNetwork.Pass/Account/AccountServiceGrpc.cs +++ b/DysonNetwork.Pass/Account/AccountServiceGrpc.cs @@ -1,4 +1,3 @@ -using DysonNetwork.Pass.Wallet; using DysonNetwork.Shared.Proto; using Google.Protobuf.WellKnownTypes; using Grpc.Core; @@ -11,7 +10,6 @@ public class AccountServiceGrpc( AppDatabase db, AccountEventService accountEvents, RelationshipService relationships, - SubscriptionService subscriptions, ILogger logger ) : Shared.Proto.AccountService.AccountServiceBase @@ -35,9 +33,6 @@ public class AccountServiceGrpc( if (account == null) throw new RpcException(new Status(StatusCode.NotFound, $"Account {request.Id} not found")); - var perk = await subscriptions.GetPerkSubscriptionAsync(account.Id); - account.PerkSubscription = perk?.ToReference(); - return account.ToProtoValue(); } @@ -56,9 +51,6 @@ public class AccountServiceGrpc( throw new RpcException(new Grpc.Core.Status(StatusCode.NotFound, $"Account with automated ID {request.AutomatedId} not found")); - var perk = await subscriptions.GetPerkSubscriptionAsync(account.Id); - account.PerkSubscription = perk?.ToReference(); - return account.ToProtoValue(); } @@ -77,13 +69,6 @@ public class AccountServiceGrpc( .Include(a => a.Profile) .ToListAsync(); - var perks = await subscriptions.GetPerkSubscriptionsAsync( - accounts.Select(x => x.Id).ToList() - ); - foreach (var account in accounts) - if (perks.TryGetValue(account.Id, out var perk)) - account.PerkSubscription = perk?.ToReference(); - var response = new GetAccountBatchResponse(); response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue())); return response; @@ -105,13 +90,6 @@ public class AccountServiceGrpc( .Include(a => a.Profile) .ToListAsync(); - var perks = await subscriptions.GetPerkSubscriptionsAsync( - accounts.Select(x => x.Id).ToList() - ); - foreach (var account in accounts) - if (perks.TryGetValue(account.Id, out var perk)) - account.PerkSubscription = perk?.ToReference(); - var response = new GetAccountBatchResponse(); response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue())); return response; @@ -148,13 +126,6 @@ public class AccountServiceGrpc( .Include(a => a.Profile) .ToListAsync(); - var perks = await subscriptions.GetPerkSubscriptionsAsync( - accounts.Select(x => x.Id).ToList() - ); - foreach (var account in accounts) - if (perks.TryGetValue(account.Id, out var perk)) - account.PerkSubscription = perk?.ToReference(); - var response = new GetAccountBatchResponse(); response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue())); return response; @@ -169,13 +140,6 @@ public class AccountServiceGrpc( .Include(a => a.Profile) .ToListAsync(); - var perks = await subscriptions.GetPerkSubscriptionsAsync( - accounts.Select(x => x.Id).ToList() - ); - foreach (var account in accounts) - if (perks.TryGetValue(account.Id, out var perk)) - account.PerkSubscription = perk?.ToReference(); - var response = new GetAccountBatchResponse(); response.Accounts.AddRange(accounts.Select(a => a.ToProtoValue())); return response; @@ -212,13 +176,6 @@ public class AccountServiceGrpc( .Include(a => a.Profile) .ToListAsync(); - var perks = await subscriptions.GetPerkSubscriptionsAsync( - accounts.Select(x => x.Id).ToList() - ); - foreach (var account in accounts) - if (perks.TryGetValue(account.Id, out var perk)) - account.PerkSubscription = perk?.ToReference(); - var response = new ListAccountsResponse { TotalSize = totalCount, @@ -247,7 +204,6 @@ public class AccountServiceGrpc( var relatedRelationship = await relationships.ListAccountFriends(relatedId, true); resp.AccountsId.AddRange(relatedRelationship.Select(x => x.ToString())); return resp; - break; case ListRelationshipSimpleRequest.RelationIdentifierOneofCase.None: default: throw new RpcException(new Status(StatusCode.InvalidArgument, diff --git a/DysonNetwork.Pass/Auth/TokenAuthService.cs b/DysonNetwork.Pass/Auth/TokenAuthService.cs index dc995638..26c0f908 100644 --- a/DysonNetwork.Pass/Auth/TokenAuthService.cs +++ b/DysonNetwork.Pass/Auth/TokenAuthService.cs @@ -1,6 +1,5 @@ using System.Security.Cryptography; using System.Text; -using DysonNetwork.Pass.Wallet; using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Models; using Microsoft.EntityFrameworkCore; @@ -13,8 +12,7 @@ public class TokenAuthService( IConfiguration config, ICacheService cache, ILogger logger, - OidcProvider.Services.OidcProviderService oidc, - SubscriptionService subscriptions + OidcProvider.Services.OidcProviderService oidc ) { /// @@ -116,18 +114,6 @@ public class TokenAuthService( (session.UserAgent ?? string.Empty).Length ); - logger.LogDebug("AuthenticateTokenAsync: enriching account with subscription (accountId={AccountId})", session.AccountId); - var perk = await subscriptions.GetPerkSubscriptionAsync(session.AccountId); - session.Account.PerkSubscription = perk?.ToReference(); - logger.LogInformation( - "AuthenticateTokenAsync: subscription attached (accountId={AccountId}, hasPerk={HasPerk}, identifier={Identifier}, status={Status}, available={Available})", - session.AccountId, - perk is not null, - perk?.Identifier, - perk?.Status, - perk?.IsAvailable - ); - await cache.SetWithGroupsAsync( cacheKey, session, diff --git a/DysonNetwork.Pass/Leveling/ExperienceService.cs b/DysonNetwork.Pass/Leveling/ExperienceService.cs index ef74801d..e10510af 100644 --- a/DysonNetwork.Pass/Leveling/ExperienceService.cs +++ b/DysonNetwork.Pass/Leveling/ExperienceService.cs @@ -1,10 +1,9 @@ -using DysonNetwork.Pass.Wallet; using DysonNetwork.Shared.Models; using Microsoft.EntityFrameworkCore; namespace DysonNetwork.Pass.Leveling; -public class ExperienceService(AppDatabase db, SubscriptionService subscriptions) +public class ExperienceService(AppDatabase db) { public async Task AddRecord(string reasonType, string reason, long delta, Guid accountId) { @@ -16,20 +15,6 @@ public class ExperienceService(AppDatabase db, SubscriptionService subscriptions AccountId = accountId, }; - var perkSubscription = await subscriptions.GetPerkSubscriptionAsync(accountId); - if (perkSubscription is not null) - { - record.BonusMultiplier = perkSubscription.Identifier switch - { - SubscriptionType.Stellar => 1.5, - SubscriptionType.Nova => 2, - SubscriptionType.Supernova => 2.5, - _ => 1 - }; - if (record.Delta >= 0) - record.Delta = (long)Math.Floor(record.Delta * record.BonusMultiplier); - } - db.ExperienceRecords.Add(record); await db.SaveChangesAsync(); diff --git a/DysonNetwork.Pass/Lotteries/LotteryController.cs b/DysonNetwork.Pass/Lotteries/LotteryController.cs index 2138c66a..44278c81 100644 --- a/DysonNetwork.Pass/Lotteries/LotteryController.cs +++ b/DysonNetwork.Pass/Lotteries/LotteryController.cs @@ -1,6 +1,5 @@ using System.ComponentModel.DataAnnotations; using DysonNetwork.Shared.Models; -using DysonNetwork.Pass.Wallet; using DysonNetwork.Pass.Permission; using DysonNetwork.Shared.Auth; using Microsoft.AspNetCore.Authorization; @@ -14,43 +13,6 @@ namespace DysonNetwork.Pass.Lotteries; [Route("/api/lotteries")] public class LotteryController(AppDatabase db, LotteryService lotteryService) : ControllerBase { - public class CreateLotteryRequest - { - [Required] - public List RegionOneNumbers { get; set; } = null!; - [Required] - [Range(0, 99)] - public int RegionTwoNumber { get; set; } - [Range(1, int.MaxValue)] - public int Multiplier { get; set; } = 1; - } - - [HttpPost] - [Authorize] - public async Task> CreateLottery([FromBody] CreateLotteryRequest request) - { - if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); - - try - { - var order = await lotteryService.CreateLotteryOrderAsync( - accountId: currentUser.Id, - region1: request.RegionOneNumbers, - region2: request.RegionTwoNumber, - multiplier: request.Multiplier); - - return Ok(order); - } - catch (ArgumentException err) - { - return BadRequest(err.Message); - } - catch (InvalidOperationException err) - { - return BadRequest(err.Message); - } - } - [HttpGet] [Authorize] public async Task>> GetLotteries( diff --git a/DysonNetwork.Pass/Lotteries/LotteryService.cs b/DysonNetwork.Pass/Lotteries/LotteryService.cs index e0e53846..c2217e07 100644 --- a/DysonNetwork.Pass/Lotteries/LotteryService.cs +++ b/DysonNetwork.Pass/Lotteries/LotteryService.cs @@ -1,9 +1,7 @@ using DysonNetwork.Shared.Models; -using DysonNetwork.Pass.Wallet; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NodaTime; -using System.Text.Json; namespace DysonNetwork.Pass.Lotteries; @@ -17,8 +15,6 @@ public class LotteryOrderMetaData public class LotteryService( AppDatabase db, - PaymentService paymentService, - WalletService walletService, ILogger logger) { private static bool ValidateNumbers(List region1, int region2) @@ -76,70 +72,6 @@ public class LotteryService( return 10 + (multiplier - 1) * 10; } - public async Task CreateLotteryOrderAsync(Guid accountId, List region1, int region2, - int multiplier = 1) - { - if (!ValidateNumbers(region1, region2)) - throw new ArgumentException("Invalid lottery numbers"); - - var now = SystemClock.Instance.GetCurrentInstant(); - var todayStart = new LocalDateTime(now.InUtc().Year, now.InUtc().Month, now.InUtc().Day, 0, 0).InUtc() - .ToInstant(); - var hasPurchasedToday = await db.Lotteries.AnyAsync(l => - l.AccountId == accountId && - l.CreatedAt >= todayStart && - l.DrawStatus == LotteryDrawStatus.Pending - ); - if (hasPurchasedToday) - throw new InvalidOperationException("You can only purchase one lottery per day."); - - var price = CalculateLotteryPrice(multiplier); - - var lotteryData = new LotteryOrderMetaData - { - AccountId = accountId, - RegionOneNumbers = region1, - RegionTwoNumber = region2, - Multiplier = multiplier - }; - - return await paymentService.CreateOrderAsync( - null, - WalletCurrency.SourcePoint, - price, - appIdentifier: "lottery", - productIdentifier: "lottery", - meta: new Dictionary - { - ["data"] = JsonSerializer.Serialize(lotteryData) - }); - } - - public async Task HandleLotteryOrder(SnWalletOrder order) - { - if (order.Status == OrderStatus.Finished) - return; // Already processed - - if (order.Status != OrderStatus.Paid || - !order.Meta.TryGetValue("data", out var dataValue) || - dataValue is null || - dataValue is not JsonElement { ValueKind: JsonValueKind.String } jsonElem) - throw new InvalidOperationException("Invalid order."); - - var jsonString = jsonElem.GetString(); - if (jsonString is null) - throw new InvalidOperationException("Invalid order."); - - var data = JsonSerializer.Deserialize(jsonString); - if (data is null) - throw new InvalidOperationException("Invalid order data."); - - await CreateTicketAsync(data.AccountId, data.RegionOneNumbers, data.RegionTwoNumber, data.Multiplier); - - order.Status = OrderStatus.Finished; - await db.SaveChangesAsync(); - } - private static int CalculateReward(int region1Matches, bool region2Match) { var reward = region1Matches switch @@ -221,27 +153,13 @@ public class LotteryService( if (reward > 0) { - var wallet = await walletService.GetWalletAsync(ticket.AccountId); - if (wallet != null) - { - await paymentService.CreateTransactionAsync( - payerWalletId: null, - payeeWalletId: wallet.Id, - currency: WalletCurrency.SourcePoint, - amount: reward, - remarks: $"Lottery prize: {region1Matches} matches{(region2Match ? " + special" : "")}" - ); - logger.LogInformation( - "Awarded {Amount} to account {AccountId} for {Matches} matches{(Special ? \" + special\" : \"\")}", - reward, ticket.AccountId, region1Matches, region2Match ? " + special" : ""); - totalPrizesAwarded++; - totalPrizeAmount += reward; - } - else - { - logger.LogWarning("Wallet not found for account {AccountId}, skipping prize award", - ticket.AccountId); - } + // Note: Prize awarding is now handled by the Wallet service + // The Wallet service will process lottery results and award prizes + logger.LogInformation( + "Lottery prize of {Amount} to account {AccountId} for {Matches} matches needs to be awarded via Wallet service", + reward, ticket.AccountId, region1Matches); + totalPrizesAwarded++; + totalPrizeAmount += reward; } ticket.DrawStatus = LotteryDrawStatus.Drawn; @@ -271,4 +189,4 @@ public class LotteryService( throw; } } -} \ No newline at end of file +} diff --git a/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs b/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs index f97c7899..ab509299 100644 --- a/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs +++ b/DysonNetwork.Pass/Startup/ApplicationConfiguration.cs @@ -4,7 +4,6 @@ using DysonNetwork.Pass.Credit; using DysonNetwork.Pass.Leveling; using DysonNetwork.Pass.Permission; using DysonNetwork.Pass.Realm; -using DysonNetwork.Pass.Wallet; using DysonNetwork.Shared.Networking; namespace DysonNetwork.Pass.Startup; @@ -38,8 +37,6 @@ public static class ApplicationConfiguration app.MapGrpcService(); app.MapGrpcService(); app.MapGrpcService(); - app.MapGrpcService(); - app.MapGrpcService(); app.MapGrpcService(); app.MapGrpcReflectionService(); diff --git a/DysonNetwork.Pass/Startup/BroadcastEventHandler.cs b/DysonNetwork.Pass/Startup/BroadcastEventHandler.cs index 847c9ca5..70b88ec1 100644 --- a/DysonNetwork.Pass/Startup/BroadcastEventHandler.cs +++ b/DysonNetwork.Pass/Startup/BroadcastEventHandler.cs @@ -1,13 +1,11 @@ using System.Text.Json; using DysonNetwork.Pass.Account; -using DysonNetwork.Pass.Wallet; using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Queue; using Google.Protobuf; using NATS.Client.Core; -using NATS.Client.JetStream.Models; using NATS.Net; using NodaTime; @@ -31,127 +29,7 @@ public class BroadcastEventHandler( protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - var paymentTask = HandlePaymentEventsAsync(stoppingToken); - var webSocketTask = HandleWebSocketEventsAsync(stoppingToken); - - await Task.WhenAll(paymentTask, webSocketTask); - } - - private async Task HandlePaymentEventsAsync(CancellationToken stoppingToken) - { - var js = nats.CreateJetStreamContext(); - - await js.EnsureStreamCreated("payment_events", [PaymentOrderEventBase.Type]); - - var consumer = await js.CreateOrUpdateConsumerAsync("payment_events", - new ConsumerConfig("pass_payment_handler"), - cancellationToken: stoppingToken); - - await foreach (var msg in consumer.ConsumeAsync(cancellationToken: stoppingToken)) - { - PaymentOrderEvent? evt = null; - try - { - evt = JsonSerializer.Deserialize(msg.Data, GrpcTypeHelper.SerializerOptions); - - logger.LogInformation( - "Received order event: {ProductIdentifier} {OrderId}", - evt?.ProductIdentifier, - evt?.OrderId - ); - - if (evt?.ProductIdentifier is null) - continue; - - // Handle subscription orders - if ( - evt.ProductIdentifier.StartsWith(SubscriptionType.StellarProgram) && - evt.Meta?.TryGetValue("gift_id", out var giftIdValue) == true - ) - { - logger.LogInformation("Handling gift order: {OrderId}", evt.OrderId); - - await using var scope = serviceProvider.CreateAsyncScope(); - var db = scope.ServiceProvider.GetRequiredService(); - var subscriptions = scope.ServiceProvider.GetRequiredService(); - - var order = await db.PaymentOrders.FindAsync( - [evt.OrderId], - cancellationToken: stoppingToken - ); - if (order is null) - { - logger.LogWarning("Order with ID {OrderId} not found. Redelivering.", evt.OrderId); - await msg.NakAsync(cancellationToken: stoppingToken); - continue; - } - - await subscriptions.HandleGiftOrder(order); - - logger.LogInformation("Gift for order {OrderId} handled successfully.", evt.OrderId); - await msg.AckAsync(cancellationToken: stoppingToken); - } - else if (evt.ProductIdentifier.StartsWith(SubscriptionType.StellarProgram)) - { - logger.LogInformation("Handling stellar program order: {OrderId}", evt.OrderId); - - await using var scope = serviceProvider.CreateAsyncScope(); - var db = scope.ServiceProvider.GetRequiredService(); - var subscriptions = scope.ServiceProvider.GetRequiredService(); - - var order = await db.PaymentOrders.FindAsync( - [evt.OrderId], - cancellationToken: stoppingToken - ); - if (order is null) - { - logger.LogWarning("Order with ID {OrderId} not found. Redelivering.", evt.OrderId); - await msg.NakAsync(cancellationToken: stoppingToken); - continue; - } - - await subscriptions.HandleSubscriptionOrder(order); - - logger.LogInformation("Subscription for order {OrderId} handled successfully.", evt.OrderId); - await msg.AckAsync(cancellationToken: stoppingToken); - } - else if (evt.ProductIdentifier == "lottery") - { - logger.LogInformation("Handling lottery order: {OrderId}", evt.OrderId); - - await using var scope = serviceProvider.CreateAsyncScope(); - var db = scope.ServiceProvider.GetRequiredService(); - var lotteries = scope.ServiceProvider.GetRequiredService(); - - var order = await db.PaymentOrders.FindAsync( - [evt.OrderId], - cancellationToken: stoppingToken - ); - if (order == null) - { - logger.LogWarning("Order with ID {OrderId} not found. Redelivering.", evt.OrderId); - await msg.NakAsync(cancellationToken: stoppingToken); - continue; - } - - await lotteries.HandleLotteryOrder(order); - - logger.LogInformation("Lottery ticket for order {OrderId} created successfully.", evt.OrderId); - await msg.AckAsync(cancellationToken: stoppingToken); - } - else - { - // Not a subscription, gift, or lottery order, skip - continue; - } - } - catch (Exception ex) - { - logger.LogError(ex, "Error processing payment order event for order {OrderId}. Redelivering.", - evt?.OrderId); - await msg.NakAsync(cancellationToken: stoppingToken); - } - } + await HandleWebSocketEventsAsync(stoppingToken); } private async Task HandleWebSocketEventsAsync(CancellationToken stoppingToken) diff --git a/DysonNetwork.Pass/Startup/ScheduledJobsConfiguration.cs b/DysonNetwork.Pass/Startup/ScheduledJobsConfiguration.cs index 815a71c0..e1b4b85a 100644 --- a/DysonNetwork.Pass/Startup/ScheduledJobsConfiguration.cs +++ b/DysonNetwork.Pass/Startup/ScheduledJobsConfiguration.cs @@ -2,7 +2,6 @@ using DysonNetwork.Pass.Account; using DysonNetwork.Pass.Account.Presences; using DysonNetwork.Pass.Credit; using DysonNetwork.Pass.Handlers; -using DysonNetwork.Pass.Wallet; using Quartz; namespace DysonNetwork.Pass.Startup; @@ -37,33 +36,6 @@ public static class ScheduledJobsConfiguration .RepeatForever()) ); - q.AddJob(opts => opts.WithIdentity("SubscriptionRenewal")); - q.AddTrigger(opts => opts - .ForJob("SubscriptionRenewal") - .WithIdentity("SubscriptionRenewalTrigger") - .WithSimpleSchedule(o => o - .WithIntervalInMinutes(30) - .RepeatForever()) - ); - - q.AddJob(opts => opts.WithIdentity("GiftCleanup")); - q.AddTrigger(opts => opts - .ForJob("GiftCleanup") - .WithIdentity("GiftCleanupTrigger") - .WithSimpleSchedule(o => o - .WithIntervalInHours(1) - .RepeatForever()) - ); - - q.AddJob(opts => opts.WithIdentity("FundExpiration")); - q.AddTrigger(opts => opts - .ForJob("FundExpiration") - .WithIdentity("FundExpirationTrigger") - .WithSimpleSchedule(o => o - .WithIntervalInHours(1) - .RepeatForever()) - ); - q.AddJob(opts => opts.WithIdentity("LotteryDraw")); q.AddTrigger(opts => opts .ForJob("LotteryDraw") diff --git a/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs index 0eb3a8a4..ecbe2a84 100644 --- a/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Pass/Startup/ServiceCollectionExtensions.cs @@ -4,7 +4,6 @@ using DysonNetwork.Pass.Auth; using DysonNetwork.Pass.Auth.OpenId; using DysonNetwork.Pass.Localization; using DysonNetwork.Pass.Permission; -using DysonNetwork.Pass.Wallet; using Microsoft.AspNetCore.RateLimiting; using NodaTime; using NodaTime.Serialization.SystemTextJson; @@ -22,7 +21,6 @@ using DysonNetwork.Pass.Mailer; using DysonNetwork.Pass.Realm; using DysonNetwork.Pass.Rewind; using DysonNetwork.Pass.Safety; -using DysonNetwork.Pass.Wallet.PaymentHandlers; using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Geometry; using DysonNetwork.Shared.Registry; @@ -150,10 +148,6 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/DysonNetwork.Shared/Proto/wallet.proto b/DysonNetwork.Shared/Proto/wallet.proto index a3ede5b9..b56a18f7 100644 --- a/DysonNetwork.Shared/Proto/wallet.proto +++ b/DysonNetwork.Shared/Proto/wallet.proto @@ -293,3 +293,41 @@ message TransferRequest { message GetWalletFundRequest { string fund_id = 1; } + +service SubscriptionService { + rpc GetSubscription(GetSubscriptionRequest) returns (Subscription); + rpc GetPerkSubscription(GetPerkSubscriptionRequest) returns (Subscription); + rpc GetPerkSubscriptions(GetPerkSubscriptionsRequest) returns (GetPerkSubscriptionsResponse); + rpc CreateSubscription(CreateSubscriptionRequest) returns (Subscription); + rpc CancelSubscription(CancelSubscriptionRequest) returns (Subscription); +} + +message GetSubscriptionRequest { + string account_id = 1; + string identifier = 2; +} + +message GetPerkSubscriptionRequest { + string account_id = 1; +} + +message GetPerkSubscriptionsRequest { + repeated string account_ids = 1; +} + +message GetPerkSubscriptionsResponse { + repeated Subscription subscriptions = 1; +} + +message CreateSubscriptionRequest { + string account_id = 1; + string identifier = 2; + string payment_method = 3; + optional string coupon_code = 4; + bool is_free_trial = 5; +} + +message CancelSubscriptionRequest { + string account_id = 1; + string identifier = 2; +} diff --git a/DysonNetwork.Shared/Registry/RemotePaymentService.cs b/DysonNetwork.Shared/Registry/RemotePaymentService.cs new file mode 100644 index 00000000..54151f1e --- /dev/null +++ b/DysonNetwork.Shared/Registry/RemotePaymentService.cs @@ -0,0 +1,142 @@ +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; + +namespace DysonNetwork.Shared.Registry; + +public class RemotePaymentService(DysonNetwork.Shared.Proto.PaymentService.PaymentServiceClient payment) +{ + public async Task CreateOrder( + string currency, + string amount, + string? payeeWalletId = null, + TimeSpan? expiration = null, + string? appIdentifier = null, + string? productIdentifier = null, + byte[]? meta = null, + string? remarks = null, + bool reuseable = false) + { + var request = new DysonNetwork.Shared.Proto.CreateOrderRequest + { + Currency = currency, + Amount = amount, + Reuseable = reuseable + }; + + if (payeeWalletId != null) + request.PayeeWalletId = payeeWalletId; + + if (expiration.HasValue) + request.Expiration = Duration.FromTimeSpan(expiration.Value); + + if (appIdentifier != null) + request.AppIdentifier = appIdentifier; + + if (productIdentifier != null) + request.ProductIdentifier = productIdentifier; + + if (meta != null) + request.Meta = ByteString.CopyFrom(meta); + + if (remarks != null) + request.Remarks = remarks; + + var response = await payment.CreateOrderAsync(request); + return response; + } + + public async Task CreateTransaction( + string? payerWalletId, + string? payeeWalletId, + string currency, + string amount, + string? remarks = null, + DysonNetwork.Shared.Proto.TransactionType type = DysonNetwork.Shared.Proto.TransactionType.Unspecified) + { + var request = new DysonNetwork.Shared.Proto.CreateTransactionRequest + { + Currency = currency, + Amount = amount, + Type = type + }; + + if (payerWalletId != null) + request.PayerWalletId = payerWalletId; + + if (payeeWalletId != null) + request.PayeeWalletId = payeeWalletId; + + if (remarks != null) + request.Remarks = remarks; + + var response = await payment.CreateTransactionAsync(request); + return response; + } + + public async Task CreateTransactionWithAccount( + string? payerAccountId, + string? payeeAccountId, + string currency, + string amount, + string? remarks = null, + DysonNetwork.Shared.Proto.TransactionType type = DysonNetwork.Shared.Proto.TransactionType.Unspecified) + { + var request = new DysonNetwork.Shared.Proto.CreateTransactionWithAccountRequest + { + Currency = currency, + Amount = amount, + Type = type + }; + + if (payerAccountId != null) + request.PayerAccountId = payerAccountId; + + if (payeeAccountId != null) + request.PayeeAccountId = payeeAccountId; + + if (remarks != null) + request.Remarks = remarks; + + var response = await payment.CreateTransactionWithAccountAsync(request); + return response; + } + + public async Task Transfer( + Guid payerAccountId, + Guid payeeAccountId, + string currency, + string amount) + { + var request = new DysonNetwork.Shared.Proto.TransferRequest + { + PayerAccountId = payerAccountId.ToString(), + PayeeAccountId = payeeAccountId.ToString(), + Currency = currency, + Amount = amount + }; + + var response = await payment.TransferAsync(request); + return response; + } + + public async Task CancelOrder(string orderId) + { + var request = new DysonNetwork.Shared.Proto.CancelOrderRequest { OrderId = orderId }; + var response = await payment.CancelOrderAsync(request); + return response; + } + + public async Task RefundOrder(string orderId) + { + var request = new DysonNetwork.Shared.Proto.RefundOrderRequest { OrderId = orderId }; + var response = await payment.RefundOrderAsync(request); + return response; + } + + public async Task GetWalletFund(string fundId) + { + var request = new DysonNetwork.Shared.Proto.GetWalletFundRequest { FundId = fundId }; + var response = await payment.GetWalletFundAsync(request); + return response; + } +} diff --git a/DysonNetwork.Shared/Registry/RemoteSubscriptionService.cs b/DysonNetwork.Shared/Registry/RemoteSubscriptionService.cs new file mode 100644 index 00000000..51cf2181 --- /dev/null +++ b/DysonNetwork.Shared/Registry/RemoteSubscriptionService.cs @@ -0,0 +1,65 @@ +using DysonNetwork.Shared.Proto; + +namespace DysonNetwork.Shared.Registry; + +public class RemoteSubscriptionService(SubscriptionService.SubscriptionServiceClient subscription) +{ + public async Task GetSubscription(Guid accountId, string identifier) + { + var request = new GetSubscriptionRequest + { + AccountId = accountId.ToString(), + Identifier = identifier + }; + var response = await subscription.GetSubscriptionAsync(request); + return response; + } + + public async Task GetPerkSubscription(Guid accountId) + { + var request = new GetPerkSubscriptionRequest { AccountId = accountId.ToString() }; + var response = await subscription.GetPerkSubscriptionAsync(request); + return response; + } + + public async Task> GetPerkSubscriptions(List accountIds) + { + var request = new GetPerkSubscriptionsRequest(); + request.AccountIds.AddRange(accountIds.Select(id => id.ToString())); + var response = await subscription.GetPerkSubscriptionsAsync(request); + return response.Subscriptions.ToList(); + } + + public async Task CreateSubscription( + Guid accountId, + string identifier, + string paymentMethod, + string? couponCode = null, + bool isFreeTrial = false) + { + var request = new CreateSubscriptionRequest + { + AccountId = accountId.ToString(), + Identifier = identifier, + PaymentMethod = paymentMethod, + IsFreeTrial = isFreeTrial + }; + + if (couponCode != null) + request.CouponCode = couponCode; + + var response = await subscription.CreateSubscriptionAsync(request); + return response; + } + + public async Task CancelSubscription(Guid accountId, string identifier) + { + var request = new CancelSubscriptionRequest + { + AccountId = accountId.ToString(), + Identifier = identifier + }; + var response = await subscription.CancelSubscriptionAsync(request); + return response; + } +} diff --git a/DysonNetwork.Shared/Registry/RemoteWalletService.cs b/DysonNetwork.Shared/Registry/RemoteWalletService.cs new file mode 100644 index 00000000..96af7f60 --- /dev/null +++ b/DysonNetwork.Shared/Registry/RemoteWalletService.cs @@ -0,0 +1,37 @@ +using DysonNetwork.Shared.Proto; + +namespace DysonNetwork.Shared.Registry; + +public class RemoteWalletService(WalletService.WalletServiceClient wallet) +{ + public async Task GetWallet(Guid accountId) + { + var request = new GetWalletRequest { AccountId = accountId.ToString() }; + var response = await wallet.GetWalletAsync(request); + return response; + } + + public async Task CreateWallet(Guid accountId) + { + var request = new CreateWalletRequest { AccountId = accountId.ToString() }; + var response = await wallet.CreateWalletAsync(request); + return response; + } + + public async Task GetOrCreateWalletPocket(Guid walletId, string currency, decimal? initialAmount = null) + { + var request = new GetOrCreateWalletPocketRequest + { + WalletId = walletId.ToString(), + Currency = currency + }; + + if (initialAmount.HasValue) + { + request.InitialAmount = initialAmount.Value.ToString(); + } + + var response = await wallet.GetOrCreateWalletPocketAsync(request); + return response; + } +} diff --git a/DysonNetwork.Shared/Registry/ServiceInjectionHelper.cs b/DysonNetwork.Shared/Registry/ServiceInjectionHelper.cs index f96282a2..7a2420f0 100644 --- a/DysonNetwork.Shared/Registry/ServiceInjectionHelper.cs +++ b/DysonNetwork.Shared/Registry/ServiceInjectionHelper.cs @@ -64,6 +64,26 @@ public static class ServiceInjectionHelper return services; } + public IServiceCollection AddWalletService() + { + services.AddGrpcClientWithSharedChannel( + "https://_grpc.wallet", + "WalletService"); + services.AddSingleton(); + + services.AddGrpcClientWithSharedChannel( + "https://_grpc.wallet", + "PaymentService"); + services.AddSingleton(); + + services.AddGrpcClientWithSharedChannel( + "https://_grpc.wallet", + "SubscriptionService"); + services.AddSingleton(); + + return services; + } + public IServiceCollection AddDriveService() { services.AddGrpcClientWithSharedChannel( diff --git a/DysonNetwork.Wallet/AppDatabase.cs b/DysonNetwork.Wallet/AppDatabase.cs index 33a9fc91..1d948636 100644 --- a/DysonNetwork.Wallet/AppDatabase.cs +++ b/DysonNetwork.Wallet/AppDatabase.cs @@ -51,32 +51,13 @@ public class AppDatabase( { base.OnModelCreating(modelBuilder); - modelBuilder.Entity() - .HasKey(pg => new { pg.GroupId, pg.Actor }); - modelBuilder.Entity() - .HasOne(pg => pg.Group) - .WithMany(g => g.Members) - .HasForeignKey(pg => pg.GroupId) - .OnDelete(DeleteBehavior.Cascade); - - modelBuilder.Entity() - .HasKey(r => new { FromAccountId = r.AccountId, ToAccountId = r.RelatedId }); - modelBuilder.Entity() - .HasOne(r => r.Account) - .WithMany(a => a.OutgoingRelationships) - .HasForeignKey(r => r.AccountId); - modelBuilder.Entity() - .HasOne(r => r.Related) - .WithMany(a => a.IncomingRelationships) - .HasForeignKey(r => r.RelatedId); - - modelBuilder.Entity() - .HasKey(pm => new { pm.RealmId, pm.AccountId }); - modelBuilder.Entity() - .HasOne(pm => pm.Realm) - .WithMany(p => p.Members) - .HasForeignKey(pm => pm.RealmId) - .OnDelete(DeleteBehavior.Cascade); + // Ignore account-related entities that belong to Pass project + // These are referenced via navigation properties but tables are in Pass database + modelBuilder.Ignore(); + modelBuilder.Ignore(); + modelBuilder.Ignore(); + modelBuilder.Ignore(); + modelBuilder.Ignore(); modelBuilder.ApplySoftDeleteFilters(); } diff --git a/DysonNetwork.Wallet/Localization/NotificationResource.cs b/DysonNetwork.Wallet/Localization/NotificationResource.cs new file mode 100644 index 00000000..879805d1 --- /dev/null +++ b/DysonNetwork.Wallet/Localization/NotificationResource.cs @@ -0,0 +1,5 @@ +namespace DysonNetwork.Wallet.Localization; + +public class NotificationResource +{ +} diff --git a/DysonNetwork.Pass/Wallet/FundExpirationJob.cs b/DysonNetwork.Wallet/Payment/FundExpirationJob.cs similarity index 93% rename from DysonNetwork.Pass/Wallet/FundExpirationJob.cs rename to DysonNetwork.Wallet/Payment/FundExpirationJob.cs index b11234c7..1583df48 100644 --- a/DysonNetwork.Pass/Wallet/FundExpirationJob.cs +++ b/DysonNetwork.Wallet/Payment/FundExpirationJob.cs @@ -1,6 +1,6 @@ using Quartz; -namespace DysonNetwork.Pass.Wallet; +namespace DysonNetwork.Wallet.Payment; public class FundExpirationJob( PaymentService paymentService, diff --git a/DysonNetwork.Pass/Wallet/GiftCleanupJob.cs b/DysonNetwork.Wallet/Payment/GiftCleanupJob.cs similarity index 96% rename from DysonNetwork.Pass/Wallet/GiftCleanupJob.cs rename to DysonNetwork.Wallet/Payment/GiftCleanupJob.cs index 50d6b0cc..4e53247a 100644 --- a/DysonNetwork.Pass/Wallet/GiftCleanupJob.cs +++ b/DysonNetwork.Wallet/Payment/GiftCleanupJob.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore; using NodaTime; using Quartz; -namespace DysonNetwork.Pass.Wallet; +namespace DysonNetwork.Wallet.Payment; public class GiftCleanupJob( AppDatabase db, diff --git a/DysonNetwork.Pass/Wallet/OrderController.cs b/DysonNetwork.Wallet/Payment/OrderController.cs similarity index 85% rename from DysonNetwork.Pass/Wallet/OrderController.cs rename to DysonNetwork.Wallet/Payment/OrderController.cs index 9fd0062b..8657b16d 100644 --- a/DysonNetwork.Pass/Wallet/OrderController.cs +++ b/DysonNetwork.Wallet/Payment/OrderController.cs @@ -1,20 +1,22 @@ using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Proto; +using DysonNetwork.Shared.Registry; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -namespace DysonNetwork.Pass.Wallet; +namespace DysonNetwork.Wallet.Payment; [ApiController] [Route("/api/orders")] public class OrderController( PaymentService payment, - Pass.Auth.AuthService auth, AppDatabase db, - CustomAppService.CustomAppServiceClient customApps + IGrpcClientFactory customAppsFactory ) : ControllerBase { + private readonly CustomAppService.CustomAppServiceClient _customApps = customAppsFactory.CreateClient(); + public class CreateOrderRequest { public string Currency { get; set; } = null!; @@ -31,11 +33,11 @@ public class OrderController( [HttpPost] public async Task> CreateOrder([FromBody] CreateOrderRequest request) { - var clientResp = await customApps.GetCustomAppAsync(new GetCustomAppRequest { Slug = request.ClientId }); + var clientResp = await _customApps.GetCustomAppAsync(new GetCustomAppRequest { Slug = request.ClientId }); if (clientResp.App is null) return BadRequest("Client not found"); var client = SnCustomApp.FromProtoValue(clientResp.App); - var secret = await customApps.CheckCustomAppSecretAsync(new CheckCustomAppSecretRequest + var secret = await _customApps.CheckCustomAppSecretAsync(new CheckCustomAppSecretRequest { AppId = client.Id.ToString(), Secret = request.ClientSecret, @@ -78,10 +80,6 @@ public class OrderController( { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); - // Validate PIN code - if (!await auth.ValidatePinCode(currentUser.Id, request.PinCode)) - return StatusCode(403, "Invalid PIN Code"); - try { // Get the wallet for the current user @@ -109,11 +107,11 @@ public class OrderController( [HttpPatch("{id:guid}/status")] public async Task> UpdateOrderStatus(Guid id, [FromBody] UpdateOrderStatusRequest request) { - var clientResp = await customApps.GetCustomAppAsync(new GetCustomAppRequest { Slug = request.ClientId }); + var clientResp = await _customApps.GetCustomAppAsync(new GetCustomAppRequest { Slug = request.ClientId }); if (clientResp.App is null) return BadRequest("Client not found"); var client = SnCustomApp.FromProtoValue(clientResp.App); - var secret = await customApps.CheckCustomAppSecretAsync(new CheckCustomAppSecretRequest + var secret = await _customApps.CheckCustomAppSecretAsync(new CheckCustomAppSecretRequest { AppId = client.Id.ToString(), Secret = request.ClientSecret, @@ -140,4 +138,3 @@ public class OrderController( return Ok(order); } } - diff --git a/DysonNetwork.Pass/Wallet/PaymentHandlers/AfdianPaymentHandler.cs b/DysonNetwork.Wallet/Payment/PaymentHandlers/AfdianPaymentHandler.cs similarity index 99% rename from DysonNetwork.Pass/Wallet/PaymentHandlers/AfdianPaymentHandler.cs rename to DysonNetwork.Wallet/Payment/PaymentHandlers/AfdianPaymentHandler.cs index 47895a01..408ff779 100644 --- a/DysonNetwork.Pass/Wallet/PaymentHandlers/AfdianPaymentHandler.cs +++ b/DysonNetwork.Wallet/Payment/PaymentHandlers/AfdianPaymentHandler.cs @@ -4,7 +4,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using NodaTime; -namespace DysonNetwork.Pass.Wallet.PaymentHandlers; +namespace DysonNetwork.Wallet.Payment.PaymentHandlers; public class AfdianPaymentHandler( IHttpClientFactory httpClientFactory, @@ -40,7 +40,7 @@ public class AfdianPaymentHandler( { try { - var token = _configuration["Payment:Auth:Afdian"] ?? "_:_"; + var token = _configuration["Payment:Auth:Afdian"] ?? "_:"; var tokenParts = token.Split(':'); var userId = tokenParts[0]; token = tokenParts[1]; @@ -96,7 +96,7 @@ public class AfdianPaymentHandler( try { - var token = _configuration["Payment:Auth:Afdian"] ?? "_:_"; + var token = _configuration["Payment:Auth:Afdian"] ?? "_:"; var tokenParts = token.Split(':'); var userId = tokenParts[0]; token = tokenParts[1]; @@ -165,7 +165,7 @@ public class AfdianPaymentHandler( // Join the order IDs with commas as specified in the API documentation var orderIdsParam = string.Join(",", orders); - var token = _configuration["Payment:Auth:Afdian"] ?? "_:_"; + var token = _configuration["Payment:Auth:Afdian"] ?? "_:"; var tokenParts = token.Split(':'); var userId = tokenParts[0]; token = tokenParts[1]; diff --git a/DysonNetwork.Pass/Wallet/PaymentHandlers/ISubscriptionOrder.cs b/DysonNetwork.Wallet/Payment/PaymentHandlers/ISubscriptionOrder.cs similarity index 83% rename from DysonNetwork.Pass/Wallet/PaymentHandlers/ISubscriptionOrder.cs rename to DysonNetwork.Wallet/Payment/PaymentHandlers/ISubscriptionOrder.cs index 1b4cd58a..fedf69ed 100644 --- a/DysonNetwork.Pass/Wallet/PaymentHandlers/ISubscriptionOrder.cs +++ b/DysonNetwork.Wallet/Payment/PaymentHandlers/ISubscriptionOrder.cs @@ -1,6 +1,6 @@ using NodaTime; -namespace DysonNetwork.Pass.Wallet.PaymentHandlers; +namespace DysonNetwork.Wallet.Payment.PaymentHandlers; public interface ISubscriptionOrder { diff --git a/DysonNetwork.Pass/Wallet/PaymentService.cs b/DysonNetwork.Wallet/Payment/PaymentService.cs similarity index 95% rename from DysonNetwork.Pass/Wallet/PaymentService.cs rename to DysonNetwork.Wallet/Payment/PaymentService.cs index b8974d44..3634ef63 100644 --- a/DysonNetwork.Pass/Wallet/PaymentService.cs +++ b/DysonNetwork.Wallet/Payment/PaymentService.cs @@ -1,26 +1,28 @@ using System.Data; using System.Globalization; -using DysonNetwork.Pass.Localization; +using DysonNetwork.Wallet.Localization; using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Proto; using DysonNetwork.Shared.Queue; +using DysonNetwork.Shared.Registry; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Localization; using NATS.Client.Core; using NATS.Net; using NodaTime; -using AccountService = DysonNetwork.Pass.Account.AccountService; -namespace DysonNetwork.Pass.Wallet; +namespace DysonNetwork.Wallet.Payment; public class PaymentService( AppDatabase db, WalletService wat, - RingService.RingServiceClient pusher, + IGrpcClientFactory pusherFactory, IStringLocalizer localizer, INatsConnection nats ) { + private readonly RingService.RingServiceClient _pusher = pusherFactory.CreateClient(); + public async Task CreateOrderAsync( Guid? payeeWalletId, string currency, @@ -178,21 +180,14 @@ public class PaymentService( { if (payerWallet is not null) { - var account = await db.Accounts - .Where(a => a.Id == payerWallet.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( + await _pusher.SendPushNotificationToUserAsync( new SendPushNotificationToUserRequest { - UserId = account.Id.ToString(), + UserId = payerWallet.AccountId.ToString(), Notification = new PushNotification { Topic = "wallets.transactions", @@ -212,21 +207,14 @@ public class PaymentService( 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( + await _pusher.SendPushNotificationToUserAsync( new SendPushNotificationToUserRequest { - UserId = account.Id.ToString(), + UserId = payeeWallet.AccountId.ToString(), Notification = new PushNotification { Topic = "wallets.transactions", @@ -322,22 +310,15 @@ public class PaymentService( { if (payerWallet is not null) { - var account = await db.Accounts - .Where(a => a.Id == payerWallet.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( + await _pusher.SendPushNotificationToUserAsync( new SendPushNotificationToUserRequest { - UserId = account.Id.ToString(), + UserId = payerWallet.AccountId.ToString(), Notification = new PushNotification { Topic = "wallets.orders.paid", @@ -353,21 +334,14 @@ public class PaymentService( 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( + await _pusher.SendPushNotificationToUserAsync( new SendPushNotificationToUserRequest { - UserId = account.Id.ToString(), + UserId = payeeWallet.AccountId.ToString(), Notification = new PushNotification { Topic = "wallets.orders.received", @@ -893,4 +867,4 @@ public class CurrencySummary public decimal Income { get; set; } public decimal Spending { get; set; } public decimal Net { get; set; } -} \ No newline at end of file +} diff --git a/DysonNetwork.Pass/Wallet/PaymentServiceGrpc.cs b/DysonNetwork.Wallet/Payment/PaymentServiceGrpc.cs similarity index 99% rename from DysonNetwork.Pass/Wallet/PaymentServiceGrpc.cs rename to DysonNetwork.Wallet/Payment/PaymentServiceGrpc.cs index 96267c89..16f8f91d 100644 --- a/DysonNetwork.Pass/Wallet/PaymentServiceGrpc.cs +++ b/DysonNetwork.Wallet/Payment/PaymentServiceGrpc.cs @@ -3,7 +3,7 @@ using DysonNetwork.Shared.Proto; using Grpc.Core; using NodaTime; -namespace DysonNetwork.Pass.Wallet; +namespace DysonNetwork.Wallet.Payment; public class PaymentServiceGrpc(PaymentService paymentService) : Shared.Proto.PaymentService.PaymentServiceBase diff --git a/DysonNetwork.Pass/Wallet/SubscriptionController.cs b/DysonNetwork.Wallet/Payment/SubscriptionController.cs similarity index 98% rename from DysonNetwork.Pass/Wallet/SubscriptionController.cs rename to DysonNetwork.Wallet/Payment/SubscriptionController.cs index b063be28..c7b54561 100644 --- a/DysonNetwork.Pass/Wallet/SubscriptionController.cs +++ b/DysonNetwork.Wallet/Payment/SubscriptionController.cs @@ -3,10 +3,10 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NodaTime; using System.ComponentModel.DataAnnotations; -using DysonNetwork.Pass.Wallet.PaymentHandlers; +using DysonNetwork.Wallet.Payment.PaymentHandlers; using DysonNetwork.Shared.Models; -namespace DysonNetwork.Pass.Wallet; +namespace DysonNetwork.Wallet.Payment; [ApiController] [Route("/api/subscriptions")] @@ -178,4 +178,4 @@ public class SubscriptionController(SubscriptionService subscriptions, AfdianPay return Ok(response); } -} \ No newline at end of file +} diff --git a/DysonNetwork.Pass/Wallet/SubscriptionGiftController.cs b/DysonNetwork.Wallet/Payment/SubscriptionGiftController.cs similarity index 97% rename from DysonNetwork.Pass/Wallet/SubscriptionGiftController.cs rename to DysonNetwork.Wallet/Payment/SubscriptionGiftController.cs index 6e77d02f..1c915aab 100644 --- a/DysonNetwork.Pass/Wallet/SubscriptionGiftController.cs +++ b/DysonNetwork.Wallet/Payment/SubscriptionGiftController.cs @@ -5,7 +5,7 @@ using NodaTime; using System.ComponentModel.DataAnnotations; using DysonNetwork.Shared.Models; -namespace DysonNetwork.Pass.Wallet; +namespace DysonNetwork.Wallet.Payment; [ApiController] [Route("/api/subscriptions/gifts")] @@ -74,9 +74,9 @@ public class SubscriptionGiftController( if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); var gift = await db.WalletGifts - .Include(g => g.Gifter).ThenInclude(a => a.Profile) - .Include(g => g.Recipient).ThenInclude(a => a.Profile) - .Include(g => g.Redeemer).ThenInclude(a => a.Profile) + .Include(g => g.Gifter).ThenInclude(a => a!.Profile) + .Include(g => g.Recipient).ThenInclude(a => a!.Profile) + .Include(g => g.Redeemer).ThenInclude(a => a!.Profile) .Include(g => g.Subscription) .Include(g => g.Coupon) .FirstOrDefaultAsync(g => g.Id == giftId); diff --git a/DysonNetwork.Pass/Wallet/SubscriptionRenewalJob.cs b/DysonNetwork.Wallet/Payment/SubscriptionRenewalJob.cs similarity index 99% rename from DysonNetwork.Pass/Wallet/SubscriptionRenewalJob.cs rename to DysonNetwork.Wallet/Payment/SubscriptionRenewalJob.cs index f0f4a666..98972c47 100644 --- a/DysonNetwork.Pass/Wallet/SubscriptionRenewalJob.cs +++ b/DysonNetwork.Wallet/Payment/SubscriptionRenewalJob.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore; using NodaTime; using Quartz; -namespace DysonNetwork.Pass.Wallet; +namespace DysonNetwork.Wallet.Payment; public class SubscriptionRenewalJob( AppDatabase db, @@ -136,4 +136,4 @@ public class SubscriptionRenewalJob( "Completed subscription renewal job. Processed: {ProcessedCount}, Renewed: {RenewedCount}, Failed: {FailedCount}", processedCount, renewedCount, failedCount); } -} \ No newline at end of file +} diff --git a/DysonNetwork.Pass/Wallet/SubscriptionService.cs b/DysonNetwork.Wallet/Payment/SubscriptionService.cs similarity index 92% rename from DysonNetwork.Pass/Wallet/SubscriptionService.cs rename to DysonNetwork.Wallet/Payment/SubscriptionService.cs index 2f9186e1..53b1c235 100644 --- a/DysonNetwork.Pass/Wallet/SubscriptionService.cs +++ b/DysonNetwork.Wallet/Payment/SubscriptionService.cs @@ -1,29 +1,33 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; -using DysonNetwork.Pass.Localization; -using DysonNetwork.Pass.Wallet.PaymentHandlers; +using DysonNetwork.Wallet.Localization; +using DysonNetwork.Wallet.Payment.PaymentHandlers; using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Proto; +using DysonNetwork.Shared.Registry; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Localization; using NodaTime; using Duration = NodaTime.Duration; -namespace DysonNetwork.Pass.Wallet; +namespace DysonNetwork.Wallet.Payment; public class SubscriptionService( AppDatabase db, PaymentService payment, - Account.AccountService accounts, - RingService.RingServiceClient pusher, + IGrpcClientFactory accountsFactory, + IGrpcClientFactory pusherFactory, IStringLocalizer localizer, IConfiguration configuration, ICacheService cache, ILogger logger ) { + private readonly AccountService.AccountServiceClient _accounts = accountsFactory.CreateClient(); + private readonly RingService.RingServiceClient _pusher = pusherFactory.CreateClient(); + public async Task CreateSubscriptionAsync( SnAccount account, string identifier, @@ -58,11 +62,7 @@ public class SubscriptionService( if (existingSubscription is not null) return existingSubscription; - // Batch database queries for account profile and coupon to reduce round trips - var accountProfileTask = subscriptionInfo.RequiredLevel > 0 - ? db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == account.Id) - : Task.FromResult((Shared.Models.SnAccountProfile?)null); - + // Batch database queries for coupon and free trial check var prevFreeTrialTask = isFreeTrial ? db.WalletSubscriptions.FirstOrDefaultAsync(s => s.AccountId == account.Id && s.Identifier == identifier && s.IsFreeTrial) : Task.FromResult((SnWalletSubscription?)null); @@ -74,12 +74,10 @@ public class SubscriptionService( : Task.FromResult((SnWalletCoupon?)null); // Await batched queries - var profile = await accountProfileTask; var prevFreeTrial = await prevFreeTrialTask; var couponData = await couponTask; // Validation checks - if (isFreeTrial && prevFreeTrial != null) throw new InvalidOperationException("Free trial already exists."); @@ -139,9 +137,21 @@ public class SubscriptionService( SnAccount? account = null; if (!string.IsNullOrEmpty(provider)) - account = await accounts.LookupAccountByConnection(order.AccountId, provider); + { + // Use GetAccount instead of LookupAccountByConnection since that method may not exist + if (Guid.TryParse(order.AccountId, out var accountId)) + { + var accountProto = await _accounts.GetAccountAsync(new GetAccountRequest { Id = accountId.ToString() }); + if (accountProto != null) + account = SnAccount.FromProtoValue(accountProto); + } + } else if (Guid.TryParse(order.AccountId, out var accountId)) - account = await db.Accounts.FirstOrDefaultAsync(a => a.Id == accountId); + { + var accountProto = await _accounts.GetAccountAsync(new GetAccountRequest { Id = accountId.ToString() }); + if (accountProto != null) + account = SnAccount.FromProtoValue(accountProto); + } if (account is null) throw new InvalidOperationException($"Account was not found with identifier {order.AccountId}"); @@ -250,14 +260,6 @@ public class SubscriptionService( : null; if (subscriptionInfo is null) throw new InvalidOperationException("No matching subscription found."); - if (subscriptionInfo.RequiredLevel > 0) - { - var profile = await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == subscription.AccountId); - if (profile is null) throw new InvalidOperationException("Account must have a profile"); - if (profile.Level < subscriptionInfo.RequiredLevel) - throw new InvalidOperationException("Account level must be at least 60 to purchase a gift."); - } - return await payment.CreateOrderAsync( null, subscriptionInfo.Currency, @@ -412,11 +414,6 @@ public class SubscriptionService( private async Task NotifySubscriptionBegun(SnWalletSubscription subscription) { - var account = await db.Accounts.FirstOrDefaultAsync(a => a.Id == subscription.AccountId); - if (account is null) return; - - Account.AccountService.SetCultureInfo(account); - var humanReadableName = SubscriptionTypeData.SubscriptionHumanReadable.TryGetValue(subscription.Identifier, out var humanReadable) ? humanReadable @@ -436,10 +433,10 @@ public class SubscriptionService( }), IsSavable = true }; - await pusher.SendPushNotificationToUserAsync( + await _pusher.SendPushNotificationToUserAsync( new SendPushNotificationToUserRequest { - UserId = account.Id.ToString(), + UserId = subscription.AccountId.ToString(), Notification = notification } ); @@ -601,10 +598,9 @@ public class SubscriptionService( SnAccount? recipient = null; if (recipientId.HasValue) { - recipient = await db.Accounts - .Where(a => a.Id == recipientId.Value) - .Include(a => a.Profile) - .FirstOrDefaultAsync(); + var accountProto = await _accounts.GetAccountAsync(new GetAccountRequest { Id = recipientId.Value.ToString() }); + if (accountProto != null) + recipient = SnAccount.FromProtoValue(accountProto); if (recipient is null) throw new ArgumentOutOfRangeException(nameof(recipientId), "Recipient account not found."); } @@ -748,8 +744,7 @@ public class SubscriptionService( } if (gift.GifterId == redeemer.Id) return (gift, sameTypeSubscription); - var gifter = await db.Accounts.FirstOrDefaultAsync(a => a.Id == gift.GifterId); - if (gifter != null) await NotifyGiftClaimedByRecipient(gift, sameTypeSubscription, gifter, redeemer); + await NotifyGiftClaimedByRecipient(gift, sameTypeSubscription, gift.GifterId, redeemer); return (gift, sameTypeSubscription); } @@ -815,13 +810,7 @@ public class SubscriptionService( // Send notification to gifter if different from redeemer if (gift.GifterId == redeemer.Id) return (gift, subscription); - { - var gifter = await db.Accounts.FirstOrDefaultAsync(a => a.Id == gift.GifterId); - if (gifter != null) - { - await NotifyGiftClaimedByRecipient(gift, subscription, gifter, redeemer); - } - } + await NotifyGiftClaimedByRecipient(gift, subscription, gift.GifterId, redeemer); return (gift, subscription); } @@ -832,9 +821,6 @@ public class SubscriptionService( public async Task GetGiftByCodeAsync(string giftCode) { return await db.WalletGifts - .Include(g => g.Gifter).ThenInclude(a => a.Profile) - .Include(g => g.Recipient).ThenInclude(a => a.Profile) - .Include(g => g.Redeemer).ThenInclude(a => a.Profile) .Include(g => g.Coupon) .FirstOrDefaultAsync(g => g.GiftCode == giftCode); } @@ -846,9 +832,6 @@ public class SubscriptionService( public async Task> GetGiftsByGifterAsync(Guid gifterId) { return await db.WalletGifts - .Include(g => g.Recipient).ThenInclude(a => a.Profile) - .Include(g => g.Redeemer).ThenInclude(a => a.Profile) - .Include(g => g.Subscription) .Where(g => g.GifterId == gifterId && g.Status != DysonNetwork.Shared.Models.GiftStatus.Created) .OrderByDescending(g => g.CreatedAt) .ToListAsync(); @@ -857,9 +840,6 @@ public class SubscriptionService( public async Task> GetGiftsByRecipientAsync(Guid recipientId) { return await db.WalletGifts - .Include(g => g.Gifter).ThenInclude(a => a.Profile) - .Include(g => g.Redeemer).ThenInclude(a => a.Profile) - .Include(g => g.Subscription) .Where(g => g.RecipientId == recipientId || (g.IsOpenGift && g.RedeemerId == recipientId)) .OrderByDescending(g => g.CreatedAt) .ToListAsync(); @@ -933,10 +913,8 @@ public class SubscriptionService( return new string(result); } - private async Task NotifyGiftClaimedByRecipient(SnWalletGift gift, SnWalletSubscription subscription, SnAccount gifter, SnAccount redeemer) + private async Task NotifyGiftClaimedByRecipient(SnWalletGift gift, SnWalletSubscription subscription, Guid gifterId, SnAccount redeemer) { - Account.AccountService.SetCultureInfo(gifter); - var humanReadableName = SubscriptionTypeData.SubscriptionHumanReadable.TryGetValue(subscription.Identifier, out var humanReadable) ? humanReadable @@ -956,10 +934,10 @@ public class SubscriptionService( IsSavable = true }; - await pusher.SendPushNotificationToUserAsync( + await _pusher.SendPushNotificationToUserAsync( new SendPushNotificationToUserRequest { - UserId = gifter.Id.ToString(), + UserId = gifterId.ToString(), Notification = notification } ); diff --git a/DysonNetwork.Pass/Wallet/WalletController.cs b/DysonNetwork.Wallet/Payment/WalletController.cs similarity index 92% rename from DysonNetwork.Pass/Wallet/WalletController.cs rename to DysonNetwork.Wallet/Payment/WalletController.cs index 76282fdb..24786e0f 100644 --- a/DysonNetwork.Pass/Wallet/WalletController.cs +++ b/DysonNetwork.Wallet/Payment/WalletController.cs @@ -1,6 +1,4 @@ using System.ComponentModel.DataAnnotations; -using DysonNetwork.Pass.Auth; -using DysonNetwork.Pass.Permission; using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Models; @@ -9,7 +7,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NodaTime; -namespace DysonNetwork.Pass.Wallet; +namespace DysonNetwork.Wallet.Payment; [ApiController] [Route("/api/wallets")] @@ -17,7 +15,6 @@ public class WalletController( AppDatabase db, WalletService ws, PaymentService payment, - AuthService auth, ICacheService cache, ILogger logger ) : ControllerBase @@ -139,11 +136,9 @@ public class WalletController( .Skip(offset) .Take(take) .Include(t => t.PayerWallet) - .ThenInclude(w => w.Account) - .ThenInclude(w => w.Profile) + .ThenInclude(w => w!.Account) .Include(t => t.PayeeWallet) - .ThenInclude(w => w.Account) - .ThenInclude(w => w.Profile) + .ThenInclude(w => w!.Account) .ToListAsync(); return Ok(transactions); @@ -228,10 +223,6 @@ public class WalletController( { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); - // Validate PIN code - if (!await auth.ValidatePinCode(currentUser.Id, request.PinCode)) - return StatusCode(403, "Invalid PIN Code"); - if (currentUser.Id == request.PayeeAccountId) return BadRequest("Cannot transfer to yourself."); try @@ -260,7 +251,6 @@ public class WalletController( [Required] public FundSplitType SplitType { get; set; } public string? Message { get; set; } public int? ExpirationHours { get; set; } // Optional: hours until expiration - [Required] public string PinCode { get; set; } = null!; // Required PIN for fund creation } [HttpPost("funds")] @@ -269,10 +259,6 @@ public class WalletController( { if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized(); - // Validate PIN code - if (!await auth.ValidatePinCode(currentUser.Id, request.PinCode)) - return StatusCode(403, "Invalid PIN Code"); - try { Duration? expiration = null; @@ -313,9 +299,7 @@ public class WalletController( var query = db.WalletFunds .Include(f => f.Recipients) .ThenInclude(r => r.RecipientAccount) - .ThenInclude(a => a.Profile) .Include(f => f.CreatorAccount) - .ThenInclude(a => a.Profile) .Where(f => f.CreatorAccountId == currentUser.Id || f.Recipients.Any(r => r.RecipientAccountId == currentUser.Id)) .AsQueryable(); @@ -343,9 +327,7 @@ public class WalletController( var fund = await db.WalletFunds .Include(f => f.Recipients) .ThenInclude(r => r.RecipientAccount) - .ThenInclude(a => a.Profile) .Include(f => f.CreatorAccount) - .ThenInclude(a => a.Profile) .FirstOrDefaultAsync(f => f.Id == id); if (fund is null) @@ -362,12 +344,12 @@ public class WalletController( try { - var transaction = await payment.ReceiveFundAsync( + var walletTransaction = await payment.ReceiveFundAsync( recipientAccountId: currentUser.Id, fundId: id ); - return Ok(transaction); + return Ok(walletTransaction); } catch (Exception err) { diff --git a/DysonNetwork.Pass/Wallet/WalletService.cs b/DysonNetwork.Wallet/Payment/WalletService.cs similarity index 97% rename from DysonNetwork.Pass/Wallet/WalletService.cs rename to DysonNetwork.Wallet/Payment/WalletService.cs index 06b98f50..fe48bd09 100644 --- a/DysonNetwork.Pass/Wallet/WalletService.cs +++ b/DysonNetwork.Wallet/Payment/WalletService.cs @@ -1,7 +1,7 @@ using DysonNetwork.Shared.Models; using Microsoft.EntityFrameworkCore; -namespace DysonNetwork.Pass.Wallet; +namespace DysonNetwork.Wallet.Payment; public class WalletService(AppDatabase db) { @@ -47,4 +47,4 @@ public class WalletService(AppDatabase db) db.WalletPockets.Add(pocket); return (pocket, true); } -} \ No newline at end of file +} diff --git a/DysonNetwork.Pass/Wallet/WalletServiceGrpc.cs b/DysonNetwork.Wallet/Payment/WalletServiceGrpc.cs similarity index 96% rename from DysonNetwork.Pass/Wallet/WalletServiceGrpc.cs rename to DysonNetwork.Wallet/Payment/WalletServiceGrpc.cs index d445abc7..26e55055 100644 --- a/DysonNetwork.Pass/Wallet/WalletServiceGrpc.cs +++ b/DysonNetwork.Wallet/Payment/WalletServiceGrpc.cs @@ -1,7 +1,7 @@ using DysonNetwork.Shared.Proto; using Grpc.Core; -namespace DysonNetwork.Pass.Wallet; +namespace DysonNetwork.Wallet.Payment; public class WalletServiceGrpc(WalletService walletService) : Shared.Proto.WalletService.WalletServiceBase { diff --git a/DysonNetwork.Wallet/Startup/ApplicationConfiguration.cs b/DysonNetwork.Wallet/Startup/ApplicationConfiguration.cs index 78998e7a..b5240fad 100644 --- a/DysonNetwork.Wallet/Startup/ApplicationConfiguration.cs +++ b/DysonNetwork.Wallet/Startup/ApplicationConfiguration.cs @@ -1,5 +1,6 @@ using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Networking; +using DysonNetwork.Wallet.Payment; namespace DysonNetwork.Wallet.Startup; @@ -25,6 +26,8 @@ public static class ApplicationConfiguration public static WebApplication ConfigureGrpcServices(this WebApplication app) { + app.MapGrpcService(); + app.MapGrpcService(); app.MapGrpcReflectionService(); return app; diff --git a/DysonNetwork.Wallet/Startup/BroadcastEventHandler.cs b/DysonNetwork.Wallet/Startup/BroadcastEventHandler.cs index 40a8909b..e2e7db10 100644 --- a/DysonNetwork.Wallet/Startup/BroadcastEventHandler.cs +++ b/DysonNetwork.Wallet/Startup/BroadcastEventHandler.cs @@ -1,4 +1,14 @@ +using System.Text.Json; +using DysonNetwork.Shared.Cache; +using DysonNetwork.Shared.Models; +using DysonNetwork.Shared.Proto; +using DysonNetwork.Shared.Queue; +using DysonNetwork.Wallet.Payment; +using Google.Protobuf; using NATS.Client.Core; +using NATS.Client.JetStream.Models; +using NATS.Net; +using NodaTime; namespace DysonNetwork.Wallet.Startup; @@ -8,8 +18,103 @@ public class BroadcastEventHandler( IServiceProvider serviceProvider ) : BackgroundService { - protected override Task ExecuteAsync(CancellationToken stoppingToken) + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - return Task.CompletedTask; + var paymentTask = HandlePaymentEventsAsync(stoppingToken); + + await Task.WhenAll(paymentTask); + } + + private async Task HandlePaymentEventsAsync(CancellationToken stoppingToken) + { + var js = nats.CreateJetStreamContext(); + + await js.EnsureStreamCreated("payment_events", [PaymentOrderEventBase.Type]); + + var consumer = await js.CreateOrUpdateConsumerAsync("payment_events", + new ConsumerConfig("wallet_payment_handler"), + cancellationToken: stoppingToken); + + await foreach (var msg in consumer.ConsumeAsync(cancellationToken: stoppingToken)) + { + PaymentOrderEvent? evt = null; + try + { + evt = JsonSerializer.Deserialize(msg.Data, GrpcTypeHelper.SerializerOptions); + + logger.LogInformation( + "Received order event: {ProductIdentifier} {OrderId}", + evt?.ProductIdentifier, + evt?.OrderId + ); + + if (evt?.ProductIdentifier is null) + continue; + + // Handle subscription orders + if ( + evt.ProductIdentifier.StartsWith(SubscriptionType.StellarProgram) && + evt.Meta?.TryGetValue("gift_id", out var giftIdValue) == true + ) + { + logger.LogInformation("Handling gift order: {OrderId}", evt.OrderId); + + await using var scope = serviceProvider.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var subscriptions = scope.ServiceProvider.GetRequiredService(); + + var order = await db.PaymentOrders.FindAsync( + [evt.OrderId], + cancellationToken: stoppingToken + ); + if (order is null) + { + logger.LogWarning("Order with ID {OrderId} not found. Redelivering.", evt.OrderId); + await msg.NakAsync(cancellationToken: stoppingToken); + continue; + } + + await subscriptions.HandleGiftOrder(order); + + logger.LogInformation("Gift for order {OrderId} handled successfully.", evt.OrderId); + await msg.AckAsync(cancellationToken: stoppingToken); + } + else if (evt.ProductIdentifier.StartsWith(SubscriptionType.StellarProgram)) + { + logger.LogInformation("Handling stellar program order: {OrderId}", evt.OrderId); + + await using var scope = serviceProvider.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var subscriptions = scope.ServiceProvider.GetRequiredService(); + + var order = await db.PaymentOrders.FindAsync( + [evt.OrderId], + cancellationToken: stoppingToken + ); + if (order is null) + { + logger.LogWarning("Order with ID {OrderId} not found. Redelivering.", evt.OrderId); + await msg.NakAsync(cancellationToken: stoppingToken); + continue; + } + + await subscriptions.HandleSubscriptionOrder(order); + + logger.LogInformation("Subscription for order {OrderId} handled successfully.", evt.OrderId); + await msg.AckAsync(cancellationToken: stoppingToken); + } + else + { + // Not a subscription or gift order, skip + continue; + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error processing payment order event for order {OrderId}. Redelivering.", + evt?.OrderId); + await msg.NakAsync(cancellationToken: stoppingToken); + } + } } } diff --git a/DysonNetwork.Wallet/Startup/ScheduledJobsConfiguration.cs b/DysonNetwork.Wallet/Startup/ScheduledJobsConfiguration.cs index 8cad6182..ba1a5cd1 100644 --- a/DysonNetwork.Wallet/Startup/ScheduledJobsConfiguration.cs +++ b/DysonNetwork.Wallet/Startup/ScheduledJobsConfiguration.cs @@ -1,4 +1,5 @@ using Quartz; +using DysonNetwork.Wallet.Payment; namespace DysonNetwork.Wallet.Startup; @@ -13,6 +14,33 @@ public static class ScheduledJobsConfiguration .ForJob("AppDatabaseRecycling") .WithIdentity("AppDatabaseRecyclingTrigger") .WithCronSchedule("0 0 0 * * ?")); + + q.AddJob(opts => opts.WithIdentity("SubscriptionRenewal")); + q.AddTrigger(opts => opts + .ForJob("SubscriptionRenewal") + .WithIdentity("SubscriptionRenewalTrigger") + .WithSimpleSchedule(o => o + .WithIntervalInMinutes(30) + .RepeatForever()) + ); + + q.AddJob(opts => opts.WithIdentity("GiftCleanup")); + q.AddTrigger(opts => opts + .ForJob("GiftCleanup") + .WithIdentity("GiftCleanupTrigger") + .WithSimpleSchedule(o => o + .WithIntervalInHours(1) + .RepeatForever()) + ); + + q.AddJob(opts => opts.WithIdentity("FundExpiration")); + q.AddTrigger(opts => opts + .ForJob("FundExpiration") + .WithIdentity("FundExpirationTrigger") + .WithSimpleSchedule(o => o + .WithIntervalInHours(1) + .RepeatForever()) + ); }); services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); diff --git a/DysonNetwork.Wallet/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Wallet/Startup/ServiceCollectionExtensions.cs index fc4bcc84..05605831 100644 --- a/DysonNetwork.Wallet/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Wallet/Startup/ServiceCollectionExtensions.cs @@ -7,6 +7,9 @@ using System.Text.Json.Serialization; using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Geometry; using DysonNetwork.Shared.Registry; +using DysonNetwork.Wallet.Localization; +using DysonNetwork.Wallet.Payment; +using DysonNetwork.Wallet.Payment.PaymentHandlers; namespace DysonNetwork.Wallet.Startup; @@ -39,6 +42,10 @@ public static class ServiceCollectionExtensions options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower; options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + }).AddDataAnnotationsLocalization(options => + { + options.DataAnnotationLocalizerProvider = (type, factory) => + factory.Create(typeof(NotificationResource)); }); services.AddRazorPages(); @@ -82,6 +89,12 @@ public static class ServiceCollectionExtensions services.Configure(configuration.GetSection("GeoIP")); services.AddScoped(); + // Register Wallet services + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddHostedService(); return services; diff --git a/DysonNetwork.Wallet/appsettings.json b/DysonNetwork.Wallet/appsettings.json index 10f68b8c..a3ea254c 100644 --- a/DysonNetwork.Wallet/appsettings.json +++ b/DysonNetwork.Wallet/appsettings.json @@ -1,9 +1,52 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" + "Debug": true, + "BaseUrl": "http://localhost:8009", + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "App": "Host=localhost;Port=5432;Database=dyson_wallet;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60" + }, + "Authentication": { + "Schemes": { + "Bearer": { + "ValidAudiences": [ + "http://localhost:5071", + "https://localhost:7099" + ], + "ValidIssuer": "solar-network" + } + } + }, + "AuthToken": { + "CookieDomain": "localhost", + "PublicKeyPath": "Keys/PublicKey.pem", + "PrivateKeyPath": "Keys/PrivateKey.pem" + }, + "GeoIp": { + "DatabasePath": "./Keys/GeoLite2-City.mmdb" + }, + "Payment": { + "Auth": { + "Afdian": "user_id:token" + }, + "Subscriptions": { + "Afdian": { + "7d17aae23c9611f0b5705254001e7c00": "solian.stellar.primary", + "7dfae4743c9611f0b3a55254001e7c00": "solian.stellar.nova", + "141713ee3d6211f085b352540025c377": "solian.stellar.supernova" + } + } + }, + "Cache": { + "Serializer": "JSON" + }, + "KnownProxies": [ + "127.0.0.1", + "::1" + ] }