-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathScheduledTaskService.cs
More file actions
723 lines (628 loc) · 31.5 KB
/
ScheduledTaskService.cs
File metadata and controls
723 lines (628 loc) · 31.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
using Aquiis.Core.Constants;
using Aquiis.Core.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Aquiis.Application.Services.Workflows;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;
namespace Aquiis.Application.Services
{
public class ScheduledTaskService : BackgroundService
{
private readonly ILogger<ScheduledTaskService> _logger;
private readonly IServiceProvider _serviceProvider;
private Timer? _timer;
private Timer? _dailyTimer;
private Timer? _hourlyTimer;
private Timer? _weeklyTimer;
public ScheduledTaskService(
ILogger<ScheduledTaskService> logger,
IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Scheduled Task Service is starting.");
// Run immediately on startup
await DoWork(stoppingToken);
// Then run daily at 2 AM
_timer = new Timer(
async _ => await DoWork(stoppingToken),
null,
TimeSpan.FromMinutes(GetMinutesUntil2AM()),
TimeSpan.FromHours(24));
await Task.CompletedTask;
// Calculate time until next midnight for daily tasks
var now = DateTime.Now;
var nextMidnight = now.Date.AddDays(1);
var timeUntilMidnight = nextMidnight - now;
// Start daily timer (executes at midnight)
_dailyTimer = new Timer(
async _ => await ExecuteDailyTasks(),
null,
timeUntilMidnight,
TimeSpan.FromDays(1));
// Start hourly timer (executes every hour)
_hourlyTimer = new Timer(
async _ => await ExecuteHourlyTasks(),
null,
TimeSpan.Zero, // Start immediately
TimeSpan.FromHours(1));
// Calculate time until next Monday 6 AM for weekly tasks
var daysUntilMonday = ((int)DayOfWeek.Monday - (int)now.DayOfWeek + 7) % 7;
if (daysUntilMonday == 0 && now.Hour >= 6)
{
daysUntilMonday = 7; // If it's Monday and past 6 AM, schedule for next Monday
}
var nextMonday = now.Date.AddDays(daysUntilMonday).AddHours(6);
var timeUntilMonday = nextMonday - now;
// Start weekly timer (executes every Monday at 6 AM)
_weeklyTimer = new Timer(
async _ => await ExecuteWeeklyTasks(),
null,
timeUntilMonday,
TimeSpan.FromDays(7));
_logger.LogInformation("Scheduled Task Service started. Daily tasks will run at midnight, hourly tasks every hour, weekly tasks every Monday at 6 AM.");
// Keep the service running
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
private async Task DoWork(CancellationToken stoppingToken)
{
try
{
_logger.LogInformation("Running scheduled tasks at {time}", DateTimeOffset.Now);
using (var scope = _serviceProvider.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var organizationService = scope.ServiceProvider.GetRequiredService<OrganizationService>();
var leaseNotificationService = scope.ServiceProvider.GetRequiredService<LeaseNotificationService>();
// Get all distinct organization IDs from OrganizationSettings
var organizations = await dbContext.OrganizationSettings
.Where(s => !s.IsDeleted)
.Select(s => s.OrganizationId)
.Distinct()
.ToListAsync(stoppingToken);
foreach (var organizationId in organizations)
{
// Get settings for this organization
var settings = await organizationService.GetOrganizationSettingsByOrgIdAsync(organizationId);
if (settings == null)
{
_logger.LogWarning("No settings found for organization {OrganizationId}, skipping", organizationId);
continue;
}
// Task 1: Process overdue invoices (status update + late fees)
await ProcessOverdueInvoices(dbContext, organizationId, settings, stoppingToken);
// Task 2: Send payment reminders (if enabled)
if (settings.PaymentReminderEnabled)
{
await SendPaymentReminders(dbContext, organizationId, settings, stoppingToken);
}
// Task 3: Check for expiring leases and send renewal notifications
await leaseNotificationService.SendLeaseRenewalRemindersAsync(organizationId, stoppingToken);
// Task 4: Expire overdue leases using workflow service (with audit logging)
var expiredLeaseCount = await ExpireOverdueLeases(scope, organizationId);
if (expiredLeaseCount > 0)
{
_logger.LogInformation(
"Expired {Count} overdue lease(s) for organization {OrganizationId}",
expiredLeaseCount, organizationId);
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred executing scheduled tasks.");
}
}
/// <summary>
/// Process overdue invoices: Update status to Overdue and apply late fees in one atomic operation.
/// This prevents the race condition where status is updated but late fees are not applied.
/// </summary>
private async Task ProcessOverdueInvoices(
ApplicationDbContext dbContext,
Guid organizationId,
OrganizationSettings settings,
CancellationToken stoppingToken)
{
try
{
var today = DateTime.Today;
var gracePeriodCutoff = today.AddDays(-settings.LateFeeGracePeriodDays);
// Find ALL pending invoices that are past due
var overdueInvoices = await dbContext.Invoices
.Include(i => i.Lease)
.Where(i => !i.IsDeleted &&
i.OrganizationId == organizationId &&
i.Status == "Pending" &&
i.DueOn < today)
.ToListAsync(stoppingToken);
var statusUpdatedCount = 0;
var lateFeesAppliedCount = 0;
foreach (var invoice in overdueInvoices)
{
// Always update status to Overdue
invoice.Status = "Overdue";
invoice.LastModifiedOn = DateTime.UtcNow;
invoice.LastModifiedBy = ApplicationConstants.SystemUser.Id;
statusUpdatedCount++;
// Apply late fee if:
// 1. Late fees are enabled and auto-apply is on
// 2. Grace period has elapsed
// 3. Late fee hasn't been applied yet
if (settings.LateFeeEnabled &&
settings.LateFeeAutoApply &&
invoice.DueOn < gracePeriodCutoff &&
(invoice.LateFeeApplied == null || !invoice.LateFeeApplied.Value))
{
var lateFee = Math.Min(invoice.Amount * settings.LateFeePercentage, settings.MaxLateFeeAmount);
invoice.LateFeeAmount = lateFee;
invoice.LateFeeApplied = true;
invoice.LateFeeAppliedOn = DateTime.UtcNow;
invoice.Amount += lateFee;
invoice.Notes = string.IsNullOrEmpty(invoice.Notes)
? $"Late fee of {lateFee:C} applied on {DateTime.Now:MMM dd, yyyy}"
: $"{invoice.Notes}\nLate fee of {lateFee:C} applied on {DateTime.Now:MMM dd, yyyy}";
lateFeesAppliedCount++;
_logger.LogInformation(
"Applied late fee of {LateFee:C} to invoice {InvoiceNumber} (ID: {InvoiceId}) for organization {OrganizationId}",
lateFee, invoice.InvoiceNumber, invoice.Id, organizationId);
}
}
if (overdueInvoices.Any())
{
await dbContext.SaveChangesAsync(stoppingToken);
_logger.LogInformation(
"Processed {TotalCount} overdue invoice(s) for organization {OrganizationId}: {StatusUpdated} status updated, {LateFeesApplied} late fees applied",
overdueInvoices.Count, organizationId, statusUpdatedCount, lateFeesAppliedCount);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing overdue invoices for organization {OrganizationId}", organizationId);
}
}
private async Task SendPaymentReminders(
ApplicationDbContext dbContext,
Guid organizationId,
OrganizationSettings settings,
CancellationToken stoppingToken)
{
try
{
var today = DateTime.Today;
// Find invoices due soon
var upcomingInvoices = await dbContext.Invoices
.Include(i => i.Lease)
.ThenInclude(l => l.Tenant)
.Include(i => i.Lease)
.ThenInclude(l => l.Property)
.Where(i => !i.IsDeleted &&
i.OrganizationId == organizationId &&
i.Status == "Pending" &&
i.DueOn >= today &&
i.DueOn <= today.AddDays(settings.PaymentReminderDaysBefore) &&
(i.ReminderSent == null || !i.ReminderSent.Value))
.ToListAsync(stoppingToken);
foreach (var invoice in upcomingInvoices)
{
// TODO: Integrate with email service when implemented
// For now, just log the reminder
_logger.LogInformation(
"Payment reminder needed for invoice {InvoiceNumber} due {DueDate} for tenant {TenantName} in organization {OrganizationId}",
invoice.InvoiceNumber,
invoice.DueOn.ToString("MMM dd, yyyy"),
invoice.Lease?.Tenant?.FullName ?? "Unknown",
organizationId);
invoice.ReminderSent = true;
invoice.ReminderSentOn = DateTime.UtcNow;
invoice.LastModifiedOn = DateTime.UtcNow;
invoice.LastModifiedBy = ApplicationConstants.SystemUser.Id; // Automated task
}
if (upcomingInvoices.Any())
{
await dbContext.SaveChangesAsync(stoppingToken);
_logger.LogInformation("Marked {Count} invoices as reminder sent for organization {OrganizationId}",
upcomingInvoices.Count, organizationId);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error sending payment reminders for organization {OrganizationId}", organizationId);
}
}
// Lease renewal reminder logic moved to LeaseNotificationService
private async Task ExecuteDailyTasks()
{
_logger.LogInformation("Executing daily tasks at {Time}", DateTime.Now);
try
{
using var scope = _serviceProvider.CreateScope();
var paymentService = scope.ServiceProvider.GetRequiredService<PaymentService>();
var propertyService = scope.ServiceProvider.GetRequiredService<PropertyService>();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Calculate daily payment totals
var today = DateTime.Today;
var todayPayments = await paymentService.GetAllAsync();
var dailyTotal = todayPayments
.Where(p => p.PaidOn.Date == today && !p.IsDeleted)
.Sum(p => p.Amount);
_logger.LogInformation("Daily Payment Total for {Date}: ${Amount:N2}",
today.ToString("yyyy-MM-dd"),
dailyTotal);
// Check for overdue routine inspections
var overdueInspections = await propertyService.GetPropertiesWithOverdueInspectionsAsync();
if (overdueInspections.Any())
{
_logger.LogWarning("{Count} properties have overdue routine inspections",
overdueInspections.Count);
foreach (var property in overdueInspections.Take(5)) // Log first 5
{
var daysOverdue = (DateTime.Today - property.NextRoutineInspectionDueDate!.Value).Days;
_logger.LogWarning("Property {Address} - Inspection overdue by {Days} days (Due: {DueDate})",
property.Address,
daysOverdue,
property.NextRoutineInspectionDueDate.Value.ToString("yyyy-MM-dd"));
}
}
// Check for inspections due soon (within 30 days)
var dueSoonInspections = await propertyService.GetPropertiesWithInspectionsDueSoonAsync(30);
if (dueSoonInspections.Any())
{
_logger.LogInformation("{Count} propert(ies) have routine inspections due within 30 days",
dueSoonInspections.Count);
}
// Check for expired rental applications
var expiredApplicationsCount = await ExpireOldApplications(dbContext);
if (expiredApplicationsCount > 0)
{
_logger.LogInformation("Expired {Count} rental application(s) that passed their expiration date",
expiredApplicationsCount);
}
// Check for expired lease offers (uses workflow service for audit logging)
var expiredLeaseOffersCount = await ExpireOldLeaseOffers(scope);
if (expiredLeaseOffersCount > 0)
{
_logger.LogInformation("Expired {Count} lease offer(s) that passed their expiration date",
expiredLeaseOffersCount);
}
// Check for year-end dividend calculation (runs in first week of January)
if (today.Month == 1 && today.Day <= 7)
{
await ProcessYearEndDividends(scope, today.Year - 1);
}
// Send daily digest emails to users who have opted in
var digestService = scope.ServiceProvider.GetRequiredService<DigestService>();
await digestService.SendDailyDigestsAsync();
// Additional daily tasks:
// - Generate daily reports
// - Send payment reminders
// - Check for overdue invoices
// - Archive old records
}
catch (Exception ex)
{
_logger.LogError(ex, "Error executing daily tasks");
}
}
// Daily digest logic moved to DigestService
private async Task ExecuteWeeklyTasks()
{
_logger.LogInformation("Executing weekly tasks at {Time}", DateTime.Now);
try
{
using var scope = _serviceProvider.CreateScope();
var digestService = scope.ServiceProvider.GetRequiredService<DigestService>();
var documentNotificationService = scope.ServiceProvider.GetRequiredService<DocumentNotificationService>();
var maintenanceNotificationService = scope.ServiceProvider.GetRequiredService<MaintenanceNotificationService>();
await digestService.SendWeeklyDigestsAsync();
await documentNotificationService.CheckDocumentExpirationsAsync();
await maintenanceNotificationService.SendMaintenanceStatusSummaryAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error executing weekly tasks");
}
}
// Old SendDailyDigestsAsync removed - functionality in DigestService
private async Task ExecuteHourlyTasks()
{
_logger.LogInformation("Executing hourly tasks at {Time}", DateTime.Now);
try
{
using var scope = _serviceProvider.CreateScope();
var tourService = scope.ServiceProvider.GetRequiredService<TourService>();
var leaseService = scope.ServiceProvider.GetRequiredService<LeaseService>();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// Get all organizations
var organizations = await dbContext.OrganizationSettings
.Where(s => !s.IsDeleted)
.ToListAsync();
int totalMarkedNoShow = 0;
foreach (var orgSettings in organizations)
{
var organizationId = orgSettings.OrganizationId;
var gracePeriodHours = orgSettings.TourNoShowGracePeriodHours;
// Check for tours that should be marked as no-show
var cutoffTime = DateTime.Now.AddHours(-gracePeriodHours);
// Query tours directly for this organization (bypass user context)
var potentialNoShowTours = await dbContext.Tours
.Where(t => t.OrganizationId == organizationId && !t.IsDeleted)
.Include(t => t.ProspectiveTenant)
.Include(t => t.Property)
.ToListAsync();
var noShowTours = potentialNoShowTours
.Where(t => t.Status == ApplicationConstants.TourStatuses.Scheduled &&
t.ScheduledOn < cutoffTime)
.ToList();
foreach (var tour in noShowTours)
{
await tourService.MarkTourAsNoShowAsync(tour.Id);
totalMarkedNoShow++;
_logger.LogInformation(
"Marked tour {TourId} as No Show - Scheduled: {ScheduledTime}, Grace period: {Hours} hours",
tour.Id,
tour.ScheduledOn.ToString("yyyy-MM-dd HH:mm"),
gracePeriodHours);
}
}
if (totalMarkedNoShow > 0)
{
_logger.LogInformation("Marked {Count} tour(s) as No Show across all organizations", totalMarkedNoShow);
}
// Example hourly task: Check for upcoming lease expirations
var httpContextAccessor = scope.ServiceProvider.GetRequiredService<IHttpContextAccessor>();
var userId = httpContextAccessor.HttpContext?.User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (!string.IsNullOrEmpty(userId))
{
var upcomingLeases = await leaseService.GetAllAsync();
var expiringIn30Days = upcomingLeases
.Where(l => l.EndDate >= DateTime.Today &&
l.EndDate <= DateTime.Today.AddDays(30) &&
!l.IsDeleted)
.Count();
if (expiringIn30Days > 0)
{
_logger.LogInformation("{Count} lease(s) expiring in the next 30 days", expiringIn30Days);
}
}
// You can add more hourly tasks here:
// - Check for maintenance requests
// - Update lease statuses
// - Send notifications
}
catch (Exception ex)
{
_logger.LogError(ex, "Error executing hourly tasks");
}
}
private double GetMinutesUntil2AM()
{
var now = DateTime.Now;
var next2AM = DateTime.Today.AddDays(1).AddHours(2);
if (now.Hour < 2)
{
next2AM = DateTime.Today.AddHours(2);
}
return (next2AM - now).TotalMinutes;
}
private async Task<int> ExpireOldApplications(ApplicationDbContext dbContext)
{
try
{
// Find all applications that are expired but not yet marked as such
var expiredApplications = await dbContext.RentalApplications
.Where(a => !a.IsDeleted &&
(a.Status == ApplicationConstants.ApplicationStatuses.Submitted ||
a.Status == ApplicationConstants.ApplicationStatuses.UnderReview ||
a.Status == ApplicationConstants.ApplicationStatuses.Screening) &&
a.ExpiresOn.HasValue &&
a.ExpiresOn.Value < DateTime.UtcNow)
.ToListAsync();
foreach (var application in expiredApplications)
{
application.Status = ApplicationConstants.ApplicationStatuses.Expired;
application.LastModifiedOn = DateTime.UtcNow;
application.LastModifiedBy = ApplicationConstants.SystemUser.Id; // Automated task
_logger.LogInformation("Expired application {ApplicationId} for property {PropertyId} (Expired on: {ExpirationDate})",
application.Id,
application.PropertyId,
application.ExpiresOn!.Value.ToString("yyyy-MM-dd"));
}
if (expiredApplications.Any())
{
await dbContext.SaveChangesAsync();
}
return expiredApplications.Count;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error expiring old applications");
return 0;
}
}
/// <summary>
/// Expires lease offers that have passed their expiration date.
/// Uses ApplicationWorkflowService for proper audit logging.
/// </summary>
private async Task<int> ExpireOldLeaseOffers(IServiceScope scope)
{
try
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var workflowService = scope.ServiceProvider.GetRequiredService<ApplicationWorkflowService>();
// Find all pending lease offers that have expired
var expiredOffers = await dbContext.LeaseOffers
.Where(lo => !lo.IsDeleted &&
lo.Status == "Pending" &&
lo.ExpiresOn < DateTime.UtcNow)
.ToListAsync();
var expiredCount = 0;
foreach (var offer in expiredOffers)
{
try
{
var result = await workflowService.ExpireLeaseOfferAsync(offer.Id);
if (result.Success)
{
expiredCount++;
_logger.LogInformation(
"Expired lease offer {LeaseOfferId} for property {PropertyId} (Expired on: {ExpirationDate})",
offer.Id,
offer.PropertyId,
offer.ExpiresOn.ToString("yyyy-MM-dd"));
}
else
{
_logger.LogWarning(
"Failed to expire lease offer {LeaseOfferId}: {Errors}",
offer.Id,
string.Join(", ", result.Errors));
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error expiring lease offer {LeaseOfferId}", offer.Id);
}
}
return expiredCount;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error expiring old lease offers");
return 0;
}
}
/// <summary>
/// Processes year-end security deposit dividend calculations.
/// Runs in the first week of January for the previous year.
/// </summary>
private async Task ProcessYearEndDividends(IServiceScope scope, int year)
{
try
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var securityDepositService = scope.ServiceProvider.GetRequiredService<SecurityDepositService>();
// Get all organizations that have security deposit investment enabled
var organizations = await dbContext.OrganizationSettings
.Where(s => !s.IsDeleted && s.SecurityDepositInvestmentEnabled)
.Select(s => s.OrganizationId)
.Distinct()
.ToListAsync();
foreach (var organizationId in organizations)
{
try
{
// Check if pool exists and has performance recorded
var pool = await dbContext.SecurityDepositInvestmentPools
.FirstOrDefaultAsync(p => p.OrganizationId == organizationId &&
p.Year == year &&
!p.IsDeleted);
if (pool == null)
{
_logger.LogInformation(
"No investment pool found for organization {OrganizationId} for year {Year}",
organizationId, year);
continue;
}
if (pool.Status == "Distributed" || pool.Status == "Closed")
{
_logger.LogInformation(
"Dividends already processed for organization {OrganizationId} for year {Year}",
organizationId, year);
continue;
}
if (pool.TotalEarnings == 0)
{
_logger.LogInformation(
"No earnings recorded for organization {OrganizationId} for year {Year}. " +
"Please record investment performance before dividend calculation.",
organizationId, year);
continue;
}
// Calculate dividends
var dividends = await securityDepositService.CalculateDividendsAsync(year);
if (dividends.Any())
{
_logger.LogInformation(
"Calculated {Count} dividend(s) for organization {OrganizationId} for year {Year}. " +
"Total tenant share: ${TenantShare:N2}",
dividends.Count,
organizationId,
year,
dividends.Sum(d => d.DividendAmount));
}
else
{
_logger.LogInformation(
"No dividends to calculate for organization {OrganizationId} for year {Year}",
organizationId, year);
}
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error processing dividends for organization {OrganizationId} for year {Year}",
organizationId, year);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing year-end dividends for year {Year}", year);
}
}
/// <summary>
/// Expires leases that have passed their end date using LeaseWorkflowService.
/// This provides proper audit logging for lease expiration.
/// </summary>
private async Task<int> ExpireOverdueLeases(IServiceScope scope, Guid organizationId)
{
try
{
var leaseWorkflowService = scope.ServiceProvider.GetRequiredService<LeaseWorkflowService>();
var result = await leaseWorkflowService.ExpireOverdueLeaseAsync(organizationId);
if (result.Success)
{
return result.Data;
}
else
{
_logger.LogWarning(
"Failed to expire overdue leases for organization {OrganizationId}: {Errors}",
organizationId,
string.Join(", ", result.Errors));
return 0;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error expiring overdue leases for organization {OrganizationId}", organizationId);
return 0;
}
}
public override Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Scheduled Task Service is stopping.");
_timer?.Dispose();
_dailyTimer?.Change(Timeout.Infinite, 0);
_hourlyTimer?.Change(Timeout.Infinite, 0);
_weeklyTimer?.Change(Timeout.Infinite, 0);
return base.StopAsync(stoppingToken);
}
public override void Dispose()
{
_timer?.Dispose();
_dailyTimer?.Dispose();
_hourlyTimer?.Dispose();
base.Dispose();
}
}
}