using System; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using gehGassi.Core.Interfaces; using gehGassi.Core.Services; using gehGassi.Domain.Common; using gehGassi.Domain.Dogs; using gehGassi.Domain.Messages; using gehGassi.Dto.Messages; using gehGassi.External.Services; using gehGassi.Web.Helper; using gehGassi.Web.Hubs; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NetTopologySuite.Geometries; using NetTopologySuite; namespace gehGassi.Web.BackgroundServices { /// /// Hintergrundservice der regelmäßige Aufgaben übernimmt /// public class BackgroundService : IHostedService, IDisposable { private Timer _timer; private int _syncPoint = 0; private readonly IServiceProvider _services; private readonly ILogger _logger; private readonly IOptions _backgroundServiceOptions; private readonly IDistributedCache _cache; private readonly IServiceScopeFactory _resolver; private readonly IWebHostEnvironment _webHostEnvironment; /// /// Erstellt eine Instanz /// /// Instanz eine IServiceProvider /// Instanz eines ILogger /// Instanz eines IOptions BackgroundServiceOptions /// Instanz eines IDistributedCache /// Instanz eines IServiceScopeFactory /// Instanz eines IWebHostEnvironment public BackgroundService(IServiceProvider services, ILogger logger, IOptions backgroundServiceOptions, IDistributedCache cache, IServiceScopeFactory resolver, IWebHostEnvironment webHostEnvironment) { _services = services; _logger = logger; _backgroundServiceOptions = backgroundServiceOptions; _cache = cache; _resolver = resolver; _webHostEnvironment = webHostEnvironment; } /// public Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("BackgroundService started"); _timer = new Timer(async o => await DoWorkAsync(cancellationToken), null, TimeSpan.Zero, TimeSpan.FromSeconds(_backgroundServiceOptions.Value.TimerIntervalInSeconds)); return Task.CompletedTask; } /// /// Task der die Abarbeitung übernimmt /// /// CancellationToken /// Task private async Task DoWorkAsync(CancellationToken stoppingToken) { //Achtung: in Azure muss in den Slots eine Variable "Slot" angelegt sein und in der Production-Slot muss diese auf "Production" stehen var slotName = Environment.GetEnvironmentVariable("Slot"); var runTasks = true; if (!string.IsNullOrWhiteSpace(slotName) && slotName.ToLower() != "production") runTasks = false; int sync = Interlocked.CompareExchange(ref _syncPoint, 1, 0); if (sync == 0) { try { if (runTasks) { _timer.Change(Timeout.Infinite, Timeout.Infinite); _logger.LogInformation("BackgroundService - Do Work"); await DeleteOldTempFilesAsync(); await RemoveExpiredRefreshTokensAsync(); await RemovePersistedTicketsAsync(); await InvalidateExpiredAppUserSubscription(); await ResetReservedListingsAsync(); await HandleKlarnaOrdersShipmentAsync(); await StartAndStopSpecialEntitiesAsync(); await AutoUnlockAppUsersAsync(); await CorrectInvalidAppUserLocationsAsync(); await TimeoutPublicWalkRequestsAsync(); await CancelTimedOutWalksAsync(); await CancelPendingWalksAsync(); if (_backgroundServiceOptions.Value.AutoComplete) await CompleteWalksAsync(); if (_backgroundServiceOptions.Value.AutoConfirm) { await ConfirmCompletedWalksAsync(); await ConfirmCompletedWalksReminderAsync(); } await SendSystemMessagesAsync(); await RemoveExpiredSystemMessagesAsync(); await RemoveExpiredBlocksAsync(); await RemoveExpiredMessagesAsync(); await SendPublicWalkRequestNotificationsAsync(); if (DateTimeOffset.UtcNow.DayOfWeek == DayOfWeek.Monday && DateTimeOffset.UtcNow.Hour == 3) { //Jeden Montag um 3 Uhr Auszahlungen durchführen //Vorher noch prüfen ob diese bereits durchgeführt wurden var lastPayout = await _cache.GetStringAsync("LastPayout"); if (lastPayout == null || string.IsNullOrWhiteSpace(lastPayout)) { var options = new DistributedCacheEntryOptions(); options.SetAbsoluteExpiration(DateTimeOffset.UtcNow.AddHours(2)); await _cache.SetStringAsync("LastPayout", DateTimeOffset.UtcNow.ToString(), options); //Jetzt Payout starten _ = Task.Run(() => DoAutoPayoutAsync(_backgroundServiceOptions.Value.MinPayoutAmmount, _resolver)); } } _logger.LogInformation("BackgroundService - Do Work done"); } } catch (Exception ex) { _logger.LogError("BackgroundService Exception: {ex}", ex); } finally { _syncPoint = 0; _timer.Change(TimeSpan.FromSeconds(_backgroundServiceOptions.Value.TimerIntervalInSeconds), TimeSpan.FromSeconds(_backgroundServiceOptions.Value.TimerIntervalInSeconds)); } } } /// /// Löschen alter Dateien im Temp-Ordner /// /// Task private async Task DeleteOldTempFilesAsync() { using var scope = _services.CreateScope(); var fileService = scope.ServiceProvider.GetRequiredService(); await fileService.DeleteOldFilesAsync(FileServiceHelper.TempContainer, "", DateTimeOffset.UtcNow.AddMinutes(-60)); } /// /// Löschen abgelaufener Refreshtokens /// /// private async Task RemoveExpiredRefreshTokensAsync() { using var scope = _services.CreateScope(); var refreshTokenService = scope.ServiceProvider.GetRequiredService(); await refreshTokenService.RemoveExpiredAsync("System"); await refreshTokenService.CommitAsync("System"); } /// /// Löschen abgelaufener PersistedTickets /// /// private async Task RemovePersistedTicketsAsync() { using var scope = _services.CreateScope(); var persistedTicketService = scope.ServiceProvider.GetRequiredService(); await persistedTicketService.RemoveExpiredAsync(); await persistedTicketService.CommitAsync("System"); } /// /// Invalidierung abgelaufener AppUser-Abonnements /// /// Task private async Task InvalidateExpiredAppUserSubscription() { using var scope = _services.CreateScope(); var subscriptionService = scope.ServiceProvider.GetRequiredService(); var count = await subscriptionService.InvalidateExipredAppUserSubscriptionsAsync(DateTimeOffset.UtcNow); await subscriptionService.CommitAsync("System"); } /// public Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("BackgroundService stopped"); _timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } #region Tasks die dann in Webjobs ausgelagert werden sollten //1. Reservierung für Listungen /// /// Zurücksetzen von reservierten Listungen wenn keine Zahlung erfolgt ist /// /// Task private async Task ResetReservedListingsAsync() { using var scope = _services.CreateScope(); var listingService = scope.ServiceProvider.GetRequiredService(); await listingService.ResetReservedAsync(DateTimeOffset.UtcNow); await listingService.CommitAsync("System"); } //2. Klarna-Versand wenn nur digitale Produkte //Besteht aus 2 Teilen - setzen der Bestellungen welche nur digitale Produkte beinhalten auf "Shipped" //Dann noch für alle Bestellungen die "Shipped" wurden das an Klarna melden /// /// Klarna-Versand wenn nur digitale Produkte /// /// Task private async Task HandleKlarnaOrdersShipmentAsync() { using var scope = _services.CreateScope(); var orderService = scope.ServiceProvider.GetRequiredService(); await orderService.SetShipmentForKlarnaDigitalOnlyAsync(); await orderService.CommitAsync("System"); var orders = await orderService.GetOrdersKlarnaToShipAsync(); if (orders.Any()) { try { var klarnaService = scope.ServiceProvider.GetRequiredService(); var shopSettingsService = scope.ServiceProvider.GetRequiredService(); var shopSettings = await shopSettingsService.GetAsync(); int ordersHandled = 0; foreach (var order in orders) { if (order.KlarnaShipmentError) continue; var result = await klarnaService.CaptureOrderAsync(order.Id, shopSettings.KlarnaClientId, shopSettings.KlarnaSecret); switch (result) { case CaptureOrderResult.Success: order.KlarnaShipmentSentDate = DateTime.UtcNow; order.PaymentInfo = $"{DateTime.UtcNow.ToString()} Shipment success" + Environment.NewLine + order.PaymentInfo; break; case CaptureOrderResult.CaptureNotAllowed: order.KlarnaShipmentError = true; order.PaymentInfo = $"{DateTime.UtcNow.ToString()} Error: Capture not allowed" + Environment.NewLine + order.PaymentInfo; break; case CaptureOrderResult.RemainingAuthorizedAmount: order.KlarnaShipmentError = true; order.PaymentInfo = $"{DateTime.UtcNow.ToString()} Error: RemainingAuthorizedAmount 0" + Environment.NewLine + order.PaymentInfo; break; case CaptureOrderResult.OrderNotFound: order.KlarnaShipmentError = true; order.PaymentInfo = $"{DateTime.UtcNow.ToString()} Error: Order not found" + Environment.NewLine + order.PaymentInfo; break; case CaptureOrderResult.Timeout: //Nichts tun, soll nochmal probiert werden order.PaymentInfo = $"{DateTime.UtcNow.ToString()} Error: Timeout" + Environment.NewLine + order.PaymentInfo; break; case CaptureOrderResult.AuthorizationFailed: //Nichts tun, soll nochmal probiert werden order.PaymentInfo = $"{DateTime.UtcNow.ToString()} Error: Authorization failed" + Environment.NewLine + order.PaymentInfo; break; //Auth failed default: //Nichts tun, soll nochmal probiert werden order.PaymentInfo = $"{DateTime.UtcNow.ToString()} Error: Unknown Error" + Environment.NewLine + order.PaymentInfo; break; } } await orderService.CommitAsync("klarna shipment service"); } catch{} } } //3. Starten und Stoppen von Listungen, Werbungen, Bannern, News und Gutscheinkampagnen und Pins nach Datum /// /// Starten und Stoppen von Listungen, Werbungen, Bannern und Pins nach Datum /// /// Task private async Task StartAndStopSpecialEntitiesAsync() { using var scope = _services.CreateScope(); var listingService = scope.ServiceProvider.GetRequiredService(); var advertisementService = scope.ServiceProvider.GetRequiredService(); var bannerService = scope.ServiceProvider.GetRequiredService(); var pinService = scope.ServiceProvider.GetRequiredService(); var newsService = scope.ServiceProvider.GetRequiredService(); var voucherCampaignService = scope.ServiceProvider.GetRequiredService(); await listingService.StartBookedAsnyc(); await listingService.StopRunningAsync(); await listingService.CommitAsync("System"); await advertisementService.StartBookedAsnyc(); await advertisementService.StopRunningAsync(); await advertisementService.CommitAsync("System"); await bannerService.StartBookedAsnyc(); await bannerService.StopRunningAsync(); await bannerService.CommitAsync("System"); await pinService.StartBookedAsnyc(); await pinService.StopRunningAsync(); await pinService.CommitAsync("System"); await newsService.StartApprovedAsnyc(); await newsService.StopRunningAsync(); await newsService.CommitAsync("System"); await voucherCampaignService.StartBookedAsnyc(); await voucherCampaignService.StopRunningAsync(); await voucherCampaignService.CommitAsync("System"); } //4. Clear (das sind Log-Dateien hier konkret Audits die älter als z.B. ein Jahr sind //5. Auto-Entsperren von App-Usern /// /// Automatisches Entsperren von App-Usern deren Sperrdatum erreicht wurde /// /// Task private async Task AutoUnlockAppUsersAsync() { using var scope = _services.CreateScope(); var appUserService = scope.ServiceProvider.GetRequiredService(); await appUserService.AutoUnlockAsync(); await appUserService.CommitAsync("System"); } /// /// Bei AppUsern deren Location nicht korrekt eingetragen wurde, diese korrigieren wenn möglich /// /// Task private async Task CorrectInvalidAppUserLocationsAsync() { using var scope = _services.CreateScope(); var appUserService = scope.ServiceProvider.GetRequiredService(); var geoLocationService = scope.ServiceProvider.GetRequiredService(); var appUsers = await appUserService.GetWithInvalidLocationAsync(); if (appUsers.Any()) { foreach (var appUser in appUsers) { var location = await geoLocationService.GetLocationAsync(appUser.Address, CultureInfo.CurrentCulture.TwoLetterISOLanguageName); if (location.Success) { var geometryFactory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); var geoLocation = geometryFactory.CreatePoint(new Coordinate(location.Longitude, location.Latitude)); appUser.Location = geoLocation; appUser.UpdatedAt = DateTimeOffset.UtcNow; } } await appUserService.CommitAsync("System"); } } /// /// Timeout von abgelaufenen öffentlichen Anfragen setzen /// /// Task private async Task TimeoutPublicWalkRequestsAsync() { using var scope = _services.CreateScope(); var publicWalkRequestService = scope.ServiceProvider.GetRequiredService(); await publicWalkRequestService.SetTimedOutAsync(DateTimeOffset.UtcNow); await publicWalkRequestService.CommitAsync("System"); } /// /// Abgelaufene Walks stornieren. /// Diese sind im Status "Request" und das Startdatum wurde bereits erreicht /// /// Task private async Task CancelTimedOutWalksAsync() { using var scope = _services.CreateScope(); var walkService = scope.ServiceProvider.GetRequiredService(); await walkService.CancelTimedOutAsync(DateTimeOffset.UtcNow); await walkService.CommitAsync("System"); } /// /// Walks stornieren die direkt gebucht wurden und nach X Minuten nicht bezahlt wurden /// /// Task private async Task CancelPendingWalksAsync() { using var scope = _services.CreateScope(); var walkService = scope.ServiceProvider.GetRequiredService(); await walkService.CancelPendingAsync(DateTimeOffset.UtcNow, _backgroundServiceOptions.Value.CancelPendingMinutes); await walkService.CommitAsync("System"); } /// /// Walks abschliessen, welche nicht innerhalb von X Minuten abgeschlossen wurden /// /// Task private async Task CompleteWalksAsync() { using var scope = _services.CreateScope(); var walkService = scope.ServiceProvider.GetRequiredService(); await walkService.CompleteAutoAsync(DateTimeOffset.UtcNow, _backgroundServiceOptions.Value.CompleteMinutes); await walkService.CommitAsync("System"); } /// /// Walks bestätigen die abgeschlossen, aber innerhalb von X Minuten nicht bestätigt wurden /// /// Task private async Task ConfirmCompletedWalksAsync() { using var scope = _services.CreateScope(); var walkService = scope.ServiceProvider.GetRequiredService(); await walkService.ConfirmCompletedAsync(DateTimeOffset.UtcNow, _backgroundServiceOptions.Value.ConfirmationMinutes); await walkService.CommitAsync("System"); } /// /// Walks bestätigen die abgeschlossen Erinnerung senden /// /// Task private async Task ConfirmCompletedWalksReminderAsync() { using var scope = _services.CreateScope(); var walkService = scope.ServiceProvider.GetRequiredService(); await walkService.ConfirmCompletedReminderAsync(DateTimeOffset.UtcNow, _backgroundServiceOptions.Value.ConfirmationReminderMinutes); await walkService.CommitAsync("System"); } /// /// Löschen abgelaufener Systemnachrichten /// /// private async Task RemoveExpiredSystemMessagesAsync() { using var scope = _services.CreateScope(); var systemMessageService = scope.ServiceProvider.GetRequiredService(); await systemMessageService.RemoveExpiredAsync(DateTimeOffset.UtcNow); await systemMessageService.CommitAsync("System"); } /// /// Löschen abgelaufener Blockierungen /// /// private async Task RemoveExpiredBlocksAsync() { using var scope = _services.CreateScope(); var applicationUserService = scope.ServiceProvider.GetRequiredService(); await applicationUserService.RemoveExpiredBlocksAsync(DateTimeOffset.UtcNow); await applicationUserService.CommitAsync("System"); } /// /// Löschen von Nachrichten die nicht abgehlot wurden /// /// private async Task RemoveExpiredMessagesAsync() { using var scope = _services.CreateScope(); var messageService = scope.ServiceProvider.GetRequiredService(); var removedCount = await messageService.RemoveUnreadMessagesAsync(DateTimeOffset.UtcNow.AddMonths(-1)); if(removedCount > 0) await messageService.CommitAsync("System"); removedCount = await messageService.RemoveMessageCopiesAsync(DateTimeOffset.UtcNow.AddDays(-7)); if (removedCount > 0) await messageService.CommitAsync("System"); } /// /// Senden von Systemnachtichten die gesendet werden sollen /// /// Task private async Task SendSystemMessagesAsync() { using var scope = _services.CreateScope(); var systemMessageService = scope.ServiceProvider.GetRequiredService(); var appUserService = scope.ServiceProvider.GetRequiredService(); var pushNotificationService = scope.ServiceProvider.GetRequiredService(); var messagesToSend = await systemMessageService.GetToSendAsync(DateTimeOffset.UtcNow); if (messagesToSend != null && messagesToSend.Any()) { foreach (var message in messagesToSend) { message.AppUserCount = await appUserService.CountForSystemMessageAsync(new SystemMessageAppUserQuery() { AppUserType = message.AppUserType, City = message.City, Country = message.Country, Sex = message.Sex, State = message.State, VerifiedOnly = message.VerifiedOnly, Zip = message.Zip }); message.SentCount = message.AppUserCount; message.HasBeenSent = true; message.SentDate = DateTimeOffset.UtcNow; message.SentBy = "System"; await systemMessageService.CommitAsync("System"); var appUsers = await appUserService.GetForSystemMessageAsync(new SystemMessageAppUserQuery() {AppUserType = message.AppUserType, City = message.City, Country = message.Country, Sex = message.Sex, State = message.State, VerifiedOnly = message.VerifiedOnly, Zip = message.Zip }); var sentCount = 0; foreach (var appUser in appUsers) { sentCount += 1; systemMessageService.Add(appUser.Id, appUser.Type, message.Id.ToString(), DateTimeOffset.UtcNow.AddDays(14), message.Message); if (message.SendPushNotification) { await pushNotificationService.SendNewTextMessageAsync(appUser.Id, message.Id.ToString()); } } if (sentCount > 0) { await systemMessageService.CommitAsync("System"); } } } } /// /// Senden von Benachrichtigungen wenn eine öffentliche Anfrage erstellt wurde und Walker in der Nähe benachrichtigt werden sollen /// /// Task private async Task SendPublicWalkRequestNotificationsAsync() { using var scope = _services.CreateScope(); var publicWalkRequestService = scope.ServiceProvider.GetRequiredService(); var pushNotificationService = scope.ServiceProvider.GetRequiredService(); var appUserService = scope.ServiceProvider.GetRequiredService(); var notificationsToSend = await publicWalkRequestService.GetNotificationsToSendAsync(); if (notificationsToSend != null && notificationsToSend.Any()) { foreach (var notification in notificationsToSend) { var walkers = await appUserService.GetWalkersForNotificationAsync(notification.DogOwnerId, notification.Location); int sentCount = 0; if (walkers != null && walkers.Any()) sentCount = walkers.Count; notification.HasBeenSent = true; notification.SentCount = sentCount; notification.SentDate = DateTimeOffset.UtcNow; await publicWalkRequestService.CommitAsync("System"); //Senden wenn geht if (walkers != null && walkers.Any()) { foreach (var walker in walkers) { await pushNotificationService.SendPublicWalkRequestAvailableAsync(walker.Id, notification.PublicWalkRequestId); } } } } } #endregion #region Payout /// /// Task der für alle App-User eine Autoauszahlung durchführt, sofern der Benutzer dies aktiviert hat /// /// Mindestbetrag für Auszahlung /// IServiceScopeFactory /// Task private async Task DoAutoPayoutAsync(long minPayoutAmmount, IServiceScopeFactory resolver) { try { using var scope = resolver.CreateScope(); var svcProvider = scope.ServiceProvider; var appUserService = svcProvider.GetRequiredService(); var walletService = svcProvider.GetRequiredService(); var mangoPayService = svcProvider.GetRequiredService(); var payoutService = svcProvider.GetRequiredService(); var systemMessageService = svcProvider.GetRequiredService(); var appHubSender = svcProvider.GetRequiredService(); var pushnotificationService = svcProvider.GetRequiredService(); if (appUserService != null && walletService != null && mangoPayService != null && payoutService != null && pushnotificationService != null) { var appUsers = await appUserService.GetForAutoPayoutAsync(); if (appUsers != null && appUsers.Any()) { foreach (var appUser in appUsers) { if(appUser.Locked) continue; try { var creditWallet = await walletService.GetAsync(appUser.Id, WalletType.Credits); if (creditWallet != null) { if (creditWallet.Balance >= minPayoutAmmount) { 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); } var ammount = creditWallet.Balance; var payout = payoutService.Create(appUser.Id, appUser.PaymentId, "", creditWallet.Id, creditWallet.WalletId, appUser.BankId, iban, bic, ammount, "EUR"); var payoutResult = await mangoPayService.CreatePayoutAsync(appUser.PaymentId, creditWallet.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("System"); //Wallet aktualisieren await mangoPayService.GetWalletBalanceAsync(appUser.Id, WalletType.Credits); //Benachrichtigung if (systemMessageService != null && appHubSender != null) { if (appUser.Type == AppUserType.DogOwner || appUser.Type == AppUserType.Both) { systemMessageService.Add(appUser.Id, AppUserType.DogOwner, payout.Id, SystemMessageTables.Payout, SystemMessageType.PayoutUpdate, DateTimeOffset.UtcNow.AddDays(14)); await systemMessageService.CommitAsync("System"); await appHubSender.SystemMessageAddedAsync(appUser.Id); await pushnotificationService.SendPayoutUpdateAsync(appUser.Id, payout.Id); } else if (appUser.Type == AppUserType.DogWalker || appUser.Type == AppUserType.Both) { systemMessageService.Add(appUser.Id, AppUserType.DogWalker, payout.Id, SystemMessageTables.Payout, SystemMessageType.PayoutUpdate, DateTimeOffset.UtcNow.AddDays(14)); await systemMessageService.CommitAsync("System"); await appHubSender.SystemMessageAddedAsync(appUser.Id); await pushnotificationService.SendPayoutUpdateAsync(appUser.Id, payout.Id); } } } } } } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.Message); } } } } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.Message); } } #endregion /// public void Dispose() { _timer?.Dispose(); } } }