From 4772d0d4626bd46c81578e0b9dad5915b7f19c21 Mon Sep 17 00:00:00 2001 From: Florian Mihalits Date: Fri, 21 Nov 2025 13:19:58 +0100 Subject: [PATCH] Add auto-payout feature for wallets with balances Introduced new methods in `IWalletService` and `WalletService` to retrieve wallets with positive balances. Added `AutoPayout` and `RunAutoPayout` methods in `ToolsController` to automate payouts, including synchronization with MangoPay balances. Created `AutoPayoutVm` and `RunAutoPayoutVm` view models. Added Razor views `AutoPayout.cshtml` and `RunAutoPayout.cshtml` for managing and displaying payout processes. Updated `appsettings` files to reduce `MinPayoutAmmount` from 1000 to 1. Refactored `ToolsController` for better wallet handling, added error handling, and improved code formatting. Commented out KYC check logic in `ApiPaymentController` with a TODO for review. --- gehGassi.Core/Interfaces/ServiceInterfaces.cs | 13 ++ gehGassi.Core/Services/WalletService.cs | 30 ++- .../Controllers/Api/ApiPaymentController.cs | 9 +- gehGassi.Web/Controllers/ToolsController.cs | 201 +++++++++++++++--- gehGassi.Web/Models/AutoPayoutVm.cs | 22 ++ gehGassi.Web/Views/Tools/AutoPayout.cshtml | 38 ++++ gehGassi.Web/Views/Tools/RunAutoPayout.cshtml | 46 ++++ gehGassi.Web/appsettings.Live.json | 2 +- gehGassi.Web/appsettings.Production.json | 2 +- gehGassi.Web/appsettings.Staging.json | 2 +- gehGassi.Web/appsettings.json | 2 +- 11 files changed, 325 insertions(+), 42 deletions(-) create mode 100644 gehGassi.Web/Models/AutoPayoutVm.cs create mode 100644 gehGassi.Web/Views/Tools/AutoPayout.cshtml create mode 100644 gehGassi.Web/Views/Tools/RunAutoPayout.cshtml diff --git a/gehGassi.Core/Interfaces/ServiceInterfaces.cs b/gehGassi.Core/Interfaces/ServiceInterfaces.cs index 2630c2b..94850aa 100644 --- a/gehGassi.Core/Interfaces/ServiceInterfaces.cs +++ b/gehGassi.Core/Interfaces/ServiceInterfaces.cs @@ -3696,6 +3696,19 @@ namespace gehGassi.Core.Interfaces /// Id des AppUsers /// Liste von Wallets Task> GetAllAsync(string appUserId); + + /// + /// Gibt eine Liste aller Wallets mit positivem Kontostand zurück + /// + /// Wallet typ + /// Liste von Wallets + Task> GetAllWithPositiveBalanceAsync(WalletType type); + + /// + /// Gibt eine Liste aller Wallets mit positivem Kontostand zurück + /// + /// Liste von Wallets + Task> GetAllWithPositiveBalanceAsync(); } /// diff --git a/gehGassi.Core/Services/WalletService.cs b/gehGassi.Core/Services/WalletService.cs index d9164e5..3153965 100644 --- a/gehGassi.Core/Services/WalletService.cs +++ b/gehGassi.Core/Services/WalletService.cs @@ -1,10 +1,11 @@ -using System; +using gehGassi.Core.Interfaces; +using gehGassi.Domain.Common; +using gehGassi.Domain.Dogs; +using gehGassi.Domain.Payment; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using gehGassi.Core.Interfaces; -using gehGassi.Domain.Common; -using gehGassi.Domain.Payment; namespace gehGassi.Core.Services { @@ -70,5 +71,24 @@ namespace gehGassi.Core.Services { return (await Repository.FindAsync(c => c.AppUserId == appUserId).ConfigureAwait(false)).ToList(); } - } + + /// + /// Gibt eine Liste aller Wallets mit positivem Kontostand zurück + /// + /// Wallet typ + /// Liste von Wallets + public async Task> GetAllWithPositiveBalanceAsync(WalletType type) + { + return (await Repository.FindAsync(c => c.Type == type && c.Balance > 0).ConfigureAwait(false)).ToList(); + } + + /// + /// Gibt eine Liste aller Wallets mit positivem Kontostand zurück + /// + /// Liste von Wallets + public async Task> GetAllWithPositiveBalanceAsync() + { + return (await Repository.FindAsync(c => c.Balance > 0).ConfigureAwait(false)).ToList(); + } + } } diff --git a/gehGassi.Web/Controllers/Api/ApiPaymentController.cs b/gehGassi.Web/Controllers/Api/ApiPaymentController.cs index d01aa07..681c321 100644 --- a/gehGassi.Web/Controllers/Api/ApiPaymentController.cs +++ b/gehGassi.Web/Controllers/Api/ApiPaymentController.cs @@ -820,10 +820,11 @@ namespace gehGassi.Web.Controllers.Api } //Wenn der erfolgreiche KYC-Check eines AppUsers nicht mindestens 48 Stunden zurückliegt, dann keine Auszahlung - if (appUser.KycPassedDate.HasValue && appUser.KycPassedDate.Value > DateTimeOffset.UtcNow.AddHours(-48)) - { - return BadRequest(CommunicationErrors.PayOut_Failed); - } + //TODO: Prüfen ob nicht wieder aktiviert werden soll! + //if (appUser.KycPassedDate.HasValue && appUser.KycPassedDate.Value > DateTimeOffset.UtcNow.AddHours(-48)) + //{ + // return BadRequest(CommunicationErrors.PayOut_Failed); + //} //Wenn mmehr als 200 Euro, dann blockieren //TODO: Überprüfen ob das für die App-User passt. diff --git a/gehGassi.Web/Controllers/ToolsController.cs b/gehGassi.Web/Controllers/ToolsController.cs index 38a0dad..d67f8ce 100644 --- a/gehGassi.Web/Controllers/ToolsController.cs +++ b/gehGassi.Web/Controllers/ToolsController.cs @@ -1,26 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Dynamic; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using AutoMapper; +using AutoMapper; using gehGassi.Core.Interfaces; using gehGassi.Core.Services; using gehGassi.Domain.Common; using gehGassi.Domain.Dogs; -using gehGassi.Domain.Users; using gehGassi.Web.Auth; -using gehGassi.Web.Helper; +using gehGassi.Web.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Processing; +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using gehGassi.Dto.Payment; namespace gehGassi.Web.Controllers { @@ -48,6 +42,7 @@ namespace gehGassi.Web.Controllers private readonly IDeviceService _deviceService; private readonly IMessageService _messageService; private readonly IAppUserReportService _appUserReportService; + private readonly IPayoutService _payoutService; /// /// Erstellt eine Instanz @@ -70,9 +65,11 @@ namespace gehGassi.Web.Controllers /// Instanz eines IDeviceService /// Instanz eines IMessageService /// Instanz eines IAppUserReportService - public ToolsController(IMapper mapper, IStringLocalizer localizer, IFileService fileService, IFileShareService fileShareService, IMangoPayService mangoPayService, IPushNotificationService pushNotificationService, - IAppUserService appUserService, IWalletService walletService, IWalkService walkService, IRatingService ratingService, ITransactionFeeService transactionFeeService, IRefreshTokenService refreshTokenService, - IUserService userService, IFavouriteService favouriteService, ISystemMessageService systemMessageService, IDeviceService deviceService, IMessageService messageService, IAppUserReportService appUserReportService) + /// Instanz eines IPayoutService + public ToolsController(IMapper mapper, IStringLocalizer localizer, IFileService fileService, IFileShareService fileShareService, IMangoPayService mangoPayService, IPushNotificationService pushNotificationService, + IAppUserService appUserService, IWalletService walletService, IWalkService walkService, IRatingService ratingService, ITransactionFeeService transactionFeeService, IRefreshTokenService refreshTokenService, + IUserService userService, IFavouriteService favouriteService, ISystemMessageService systemMessageService, IDeviceService deviceService, IMessageService messageService, IAppUserReportService appUserReportService, + IPayoutService payoutService) { _mapper = mapper; _localizer = localizer; @@ -92,6 +89,7 @@ namespace gehGassi.Web.Controllers _deviceService = deviceService; _messageService = messageService; _appUserReportService = appUserReportService; + _payoutService = payoutService; } public async Task TestMango() @@ -119,9 +117,9 @@ namespace gehGassi.Web.Controllers public async Task Hub() { - await _pushNotificationService.ListRegistrationsAsync(); + await _pushNotificationService.ListRegistrationsAsync(); - return Ok(); + return Ok(); } /// @@ -168,6 +166,7 @@ namespace gehGassi.Web.Controllers else walk.PaymentStatus = PaymentStatus.Voided; } + await _walkService.CommitAsync("System"); } @@ -243,6 +242,7 @@ namespace gehGassi.Web.Controllers feeDecimal += gehGassiFee.Fixed; } } + if (feeDecimal > 0) { fee = (long)(feeDecimal * 100); @@ -303,6 +303,7 @@ namespace gehGassi.Web.Controllers { _walletService.Remove(wallet); } + await _walletService.CommitAsync("System"); return Ok(); @@ -329,7 +330,7 @@ namespace gehGassi.Web.Controllers //Es gibt noch keinen MangopayUser für DogWalker oder BOTH!!! if (string.IsNullOrWhiteSpace(appUser.NationalityCode)) appUser.NationalityCode = appUser.Address.CountryCode; - if(string.IsNullOrEmpty(appUser.MainResidenceCode)) + if (string.IsNullOrEmpty(appUser.MainResidenceCode)) appUser.MainResidenceCode = appUser.Address.CountryCode; if (!appUser.PaymentTermsAccepted || appUser.PaymentTermsAcceptedDate == null) { @@ -348,18 +349,18 @@ namespace gehGassi.Web.Controllers //Wallets anlegen var walletFees = await _mangoPayService.CreateWalletAsync(appUser.Id, WalletType.Fees); - if(walletFees.Success) + if (walletFees.Success) System.Diagnostics.Debug.WriteLine($"{appUser.FirstName} {appUser.LastName}: Es wurde ein neues Fee-Wallet angelegt"); var walletCredits = await _mangoPayService.CreateWalletAsync(appUser.Id, WalletType.Credits); - if(walletCredits.Success) + if (walletCredits.Success) System.Diagnostics.Debug.WriteLine($"{appUser.FirstName} {appUser.LastName}: Es wurde ein neues Credit-Wallet angelegt"); } else { System.Diagnostics.Debug.WriteLine($"{appUser.FirstName} {appUser.LastName} es konnte kein MangoPay-User angelegt werden."); - if(!string.IsNullOrWhiteSpace(createResult.ErrorMessage)) + if (!string.IsNullOrWhiteSpace(createResult.ErrorMessage)) System.Diagnostics.Debug.WriteLine(createResult.ErrorMessage); - if(createResult.ErrorMessages != null) + if (createResult.ErrorMessages != null) foreach (var error in createResult.ErrorMessages) System.Diagnostics.Debug.WriteLine($"{error.Key} --> {error.Value}"); } @@ -421,7 +422,7 @@ namespace gehGassi.Web.Controllers { System.Diagnostics.Debug.WriteLine($"{appUser.FirstName} {appUser.LastName}: Die MangoPay-Id {appUser.PaymentId} ist gültig!!!!"); } - + //Nun Wallets prüfen var wallets = await _walletService.GetAllAsync(appUser.Id); var creditWallet = wallets.FirstOrDefault(c => c.Type == WalletType.Credits); @@ -429,7 +430,7 @@ namespace gehGassi.Web.Controllers { //Wallet anlegen var walletCredits = await _mangoPayService.CreateWalletAsync(appUser.Id, WalletType.Credits); - if(walletCredits.Success) + if (walletCredits.Success) System.Diagnostics.Debug.WriteLine($"{appUser.FirstName} {appUser.LastName}: Es wurde ein neues Credit-Wallet angelegt"); else System.Diagnostics.Debug.WriteLine($"{appUser.FirstName} {appUser.LastName}: Es konnte kein neues Credit-Wallet angelegt werde"); @@ -452,6 +453,7 @@ namespace gehGassi.Web.Controllers System.Diagnostics.Debug.WriteLine($"{appUser.FirstName} {appUser.LastName}: Es konnte kein neues Credit-Wallet angelegt werde"); } } + var feeWallet = wallets.FirstOrDefault(c => c.Type == WalletType.Fees); if (feeWallet == null) { @@ -550,7 +552,7 @@ namespace gehGassi.Web.Controllers //Schritt 6: Favourites löschen var favouritesCount = await _favouriteService.DeleteAllForAppUserAsync(appUser.Id); model.FavouritesCount += favouritesCount; - + //Schritt 7: SystemMessages löschen var systemMessageCount = await _systemMessageService.DeleteAllForAppUserAsync(appUser.Id); model.SystemMessageCount += systemMessageCount; @@ -592,10 +594,151 @@ namespace gehGassi.Web.Controllers { await _ratingService.UpdateRatingStatisticsAsync(ratingTargetToUpdate.Item1, ratingTargetToUpdate.Item2); } + await _ratingService.CommitAsync(User.Identity.Name); } return View(model); } - } + + + /// + /// Tool zum automatischen Auszahlen von Guthaben wenn dies möglich ist + /// + /// Task + public async Task AutoPayout() + { + var model = new List(); + + //1. alle Wallets mit Guthaben holen + var wallets = await _walletService.GetAllWithPositiveBalanceAsync(); + + //2. nun prüfen ob der wallet status mit dem bei mangoPay übereinstimmt + foreach (var wallet in wallets) + { + var balanceResponse = await _mangoPayService.GetWalletBalanceAsync(wallet.WalletId); + if (balanceResponse.Success) + { + wallet.Balance = balanceResponse.Value; + wallet.UpdatedAt = DateTimeOffset.UtcNow; + await _walletService.CommitAsync("System", true); + } + } + + //Nun die wallets erneut holen + wallets = await _walletService.GetAllWithPositiveBalanceAsync(); + + foreach (var wallet in wallets) + { + //App user zum Wallet holen + var appUser = await _appUserService.GetAsync(wallet.AppUserId); + if (appUser != null) + { + //Nun prüfen ob berechtig für eine Auszahlung + if (appUser.Locked) + continue; + + if (!appUser.KycPassed) + continue; + + var getBankAccountResult = await _mangoPayService.GetBankAccountAsync(appUser.Id, appUser.PaymentId, appUser.BankId); + if (getBankAccountResult.Success) + { + var viewModel = new AutoPayoutVm + { + AppUser = appUser, + Wallet = wallet + }; + model.Add(viewModel); + } + } + } + + return View(model); + } + + /// + /// Run Auto Payout + /// + /// + public async Task RunAutoPayout() + { + var model = new List(); + + //1. alle Wallets mit Guthaben holen + var wallets = await _walletService.GetAllWithPositiveBalanceAsync(); + foreach (var wallet in wallets) + { + try + { + //App user zum Wallet holen + var appUser = await _appUserService.GetAsync(wallet.AppUserId); + if (appUser != null) + { + //Nun prüfen ob berechtig für eine Auszahlung + if (appUser.Locked) + continue; + if (!appUser.KycPassed) + continue; + var getBankAccountResult = await _mangoPayService.GetBankAccountAsync(appUser.Id, appUser.PaymentId, appUser.BankId); + if (getBankAccountResult.Success) + { + var iban = string.Empty; + if (!string.IsNullOrWhiteSpace(getBankAccountResult.Value.Iban)) + iban = getBankAccountResult.Value.Iban; + + var bic = string.Empty; + if (!string.IsNullOrWhiteSpace(getBankAccountResult.Value.Bic)) + bic = getBankAccountResult.Value.Bic; + + if (iban.Length >= 4) + { + iban = iban.Substring(iban.Length - 4); + } + + if (bic.Length >= 4) + { + bic = bic.Substring(bic.Length - 4); + } + + //Auszahlen + var ammount = wallet.Balance; + + var payout = _payoutService.Create(appUser.Id, appUser.PaymentId, "", wallet.Id, wallet.WalletId, appUser.BankId, iban, bic, ammount, "EUR"); + + var payoutResult = await _mangoPayService.CreatePayoutAsync(appUser.PaymentId, wallet.WalletId, appUser.BankId, ammount, $"Payout_{payout.Id}"); + if (payoutResult.Success) + { + payout.MangoPayId = payoutResult.Value.PayoutId; + payout.Status = payoutResult.Value.Status; + payout.ResultCode = payoutResult.Value.ResultCode; + payout.ResultMessage = payoutResult.Value.ResultMessage; + payout.ExecutionDate = payoutResult.Value.ExecutionDate; + payout.UpdatedAt = DateTimeOffset.UtcNow; + + _payoutService.Add(payout); + await _payoutService.CommitAsync(User.Identity.Name); + + var dto = _mapper.Map(payout); + var item = new RunAutoPayoutVm() + { + Wallet = wallet, + AppUser = appUser, + PayoutDto = dto + }; + + model.Add(item); + } + } + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(ex.Message); + } + } + + return View(model); + } + } } diff --git a/gehGassi.Web/Models/AutoPayoutVm.cs b/gehGassi.Web/Models/AutoPayoutVm.cs new file mode 100644 index 0000000..f81c6fe --- /dev/null +++ b/gehGassi.Web/Models/AutoPayoutVm.cs @@ -0,0 +1,22 @@ +using gehGassi.Domain.Dogs; +using gehGassi.Domain.Payment; +using gehGassi.Dto.Payment; + +namespace gehGassi.Web.Models +{ + /// + /// Viewmodel für das Autopayout tool + /// + public class AutoPayoutVm + { + public Wallet Wallet { get; set; } + public AppUser AppUser { get; set; } + } + + public class RunAutoPayoutVm + { + public Wallet Wallet { get; set; } + public AppUser AppUser { get; set; } + public PayoutDto PayoutDto { get; set; } + } +} diff --git a/gehGassi.Web/Views/Tools/AutoPayout.cshtml b/gehGassi.Web/Views/Tools/AutoPayout.cshtml new file mode 100644 index 0000000..da79e69 --- /dev/null +++ b/gehGassi.Web/Views/Tools/AutoPayout.cshtml @@ -0,0 +1,38 @@ +@using gehGassi.Permissions +@using gehGassi.Web.Helper +@using Microsoft.AspNetCore.Mvc.Localization +@model List; +@inject IViewLocalizer Localizer +@{ + ViewData["Title"] = Localizer["Menu_Tools"]; +} +@section pageTitle { + @Localizer["Menu_Tools"] +} + +
+ + + + + + + + + + @foreach (var item in Model) + { + + + + + + } + +
App userTypeBalance
@item.AppUser.FirstName @item.AppUser.LastName@item.Wallet.Type.ToString()€ @((item.Wallet.Balance / 100).ToString("N2"))
+ + + +
\ No newline at end of file diff --git a/gehGassi.Web/Views/Tools/RunAutoPayout.cshtml b/gehGassi.Web/Views/Tools/RunAutoPayout.cshtml new file mode 100644 index 0000000..dea3aa2 --- /dev/null +++ b/gehGassi.Web/Views/Tools/RunAutoPayout.cshtml @@ -0,0 +1,46 @@ +@using gehGassi.Permissions +@using gehGassi.Web.Helper +@using Microsoft.AspNetCore.Mvc.Localization +@model List; +@inject IViewLocalizer Localizer +@{ + ViewData["Title"] = Localizer["Menu_Tools"]; +} +@section pageTitle { + @Localizer["Menu_Tools"] +} + +
+ + + + + + + + + + + + + + + @foreach (var item in Model) + { + + + + + + + + + + + } + +
App userBalance beforeIBANBICStatusAmmountResult codeResult message
@item.AppUser.FirstName @item.AppUser.LastName€ @((item.Wallet.Balance / 100).ToString("N2"))@item.PayoutDto.Iban@item.PayoutDto.Bic@item.PayoutDto.Status.ToString()€ @((item.PayoutDto.Ammount / 100).ToString("N2"))@item.PayoutDto.ResultCode@item.PayoutDto.ResultMessage
+ + + +
\ No newline at end of file diff --git a/gehGassi.Web/appsettings.Live.json b/gehGassi.Web/appsettings.Live.json index 8c7b2a7..5b268cb 100644 --- a/gehGassi.Web/appsettings.Live.json +++ b/gehGassi.Web/appsettings.Live.json @@ -107,7 +107,7 @@ "AutoConfirm": true, "ConfirmationMinutes": 10080, "ConfirmationReminderMinutes": 1440, - "MinPayoutAmmount": 1000 + "MinPayoutAmmount": 1 }, "GeoLocationOptions": { "BaseUri": "https://nominatim.openstreetmap.org/", diff --git a/gehGassi.Web/appsettings.Production.json b/gehGassi.Web/appsettings.Production.json index a179f38..a317894 100644 --- a/gehGassi.Web/appsettings.Production.json +++ b/gehGassi.Web/appsettings.Production.json @@ -108,7 +108,7 @@ "AutoConfirm": true, "ConfirmationMinutes": 10080, "ConfirmationReminderMinutes": 1440, - "MinPayoutAmmount": 1000 + "MinPayoutAmmount": 1 }, "GeoLocationOptions": { "BaseUri": "https://nominatim.openstreetmap.org/", diff --git a/gehGassi.Web/appsettings.Staging.json b/gehGassi.Web/appsettings.Staging.json index 1f2b5d9..213a06c 100644 --- a/gehGassi.Web/appsettings.Staging.json +++ b/gehGassi.Web/appsettings.Staging.json @@ -107,7 +107,7 @@ "AutoConfirm": true, "ConfirmationMinutes": 10080, "ConfirmationReminderMinutes": 1440, - "MinPayoutAmmount": 1000 + "MinPayoutAmmount": 1 }, "GeoLocationOptions": { "BaseUri": "https://nominatim.openstreetmap.org/", diff --git a/gehGassi.Web/appsettings.json b/gehGassi.Web/appsettings.json index e475914..5c6daf1 100644 --- a/gehGassi.Web/appsettings.json +++ b/gehGassi.Web/appsettings.json @@ -108,7 +108,7 @@ "AutoConfirm": true, "ConfirmationMinutes": 10080, "ConfirmationReminderMinutes": 1440, - "MinPayoutAmmount": 1000 + "MinPayoutAmmount": 1 }, "GeoLocationOptions": { "BaseUri": "https://nominatim.openstreetmap.org/",