Skip to content

Commit 42282e5

Browse files
authored
Merge pull request #25 from xnodeoncode/development
v1.0.1: Hotfix Release
2 parents 94f5d1d + 9d3911f commit 42282e5

10 files changed

Lines changed: 272 additions & 434 deletions

File tree

2-Aquiis.Application/Services/PaymentService.cs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@ public async Task<string> GeneratePaymentNumberAsync()
402402

403403
/// <summary>
404404
/// Updates the invoice status and paid amount after a payment change.
405+
/// Also applies late fees if invoice becomes overdue and fees haven't been applied yet.
405406
/// </summary>
406407
private async Task UpdateInvoiceAfterPaymentChangeAsync(Guid invoiceId)
407408
{
@@ -420,9 +421,12 @@ private async Task UpdateInvoiceAfterPaymentChangeAsync(Guid invoiceId)
420421

421422
invoice.AmountPaid = totalPaid;
422423

423-
// Total due is the invoice amount (which includes late fees already added by ScheduledTaskService)
424+
// Total due is the invoice amount (which includes late fees if already applied)
424425
var totalDue = invoice.Amount;
425426

427+
var previousStatus = invoice.Status;
428+
var statusChangedToOverdue = false;
429+
426430
// Update invoice status based on payment
427431
// Don't change status if invoice is Cancelled or Voided
428432
if (invoice.Status != ApplicationConstants.InvoiceStatuses.Cancelled
@@ -442,6 +446,7 @@ private async Task UpdateInvoiceAfterPaymentChangeAsync(Guid invoiceId)
442446
if (invoice.DueOn < DateTime.Today)
443447
{
444448
invoice.Status = ApplicationConstants.InvoiceStatuses.Overdue;
449+
statusChangedToOverdue = (previousStatus != ApplicationConstants.InvoiceStatuses.Overdue);
445450
}
446451
else
447452
{
@@ -454,6 +459,7 @@ private async Task UpdateInvoiceAfterPaymentChangeAsync(Guid invoiceId)
454459
if (invoice.DueOn < DateTime.Today)
455460
{
456461
invoice.Status = ApplicationConstants.InvoiceStatuses.Overdue;
462+
statusChangedToOverdue = (previousStatus != ApplicationConstants.InvoiceStatuses.Overdue);
457463
}
458464
else
459465
{
@@ -462,6 +468,13 @@ private async Task UpdateInvoiceAfterPaymentChangeAsync(Guid invoiceId)
462468
}
463469
}
464470

471+
// If invoice just became overdue, check if late fee should be applied
472+
if (statusChangedToOverdue &&
473+
(invoice.LateFeeApplied == null || !invoice.LateFeeApplied.Value))
474+
{
475+
await ApplyLateFeeIfEligibleAsync(invoice, organizationId);
476+
}
477+
465478
var userId = await _userContext.GetUserIdAsync();
466479
invoice.LastModifiedBy = userId ?? "system";
467480
invoice.LastModifiedOn = DateTime.UtcNow;
@@ -475,5 +488,55 @@ private async Task UpdateInvoiceAfterPaymentChangeAsync(Guid invoiceId)
475488
throw;
476489
}
477490
}
491+
492+
/// <summary>
493+
/// Applies late fee to an invoice if eligible based on organization settings.
494+
/// Uses the same logic as the scheduled task to ensure consistency.
495+
/// </summary>
496+
private async Task ApplyLateFeeIfEligibleAsync(Invoice invoice, Guid? organizationId)
497+
{
498+
try
499+
{
500+
// Get organization settings
501+
var settings = await _context.OrganizationSettings
502+
.FirstOrDefaultAsync(s => s.OrganizationId == organizationId);
503+
504+
if (settings == null || !settings.LateFeeEnabled || !settings.LateFeeAutoApply)
505+
{
506+
// Late fees not enabled or not set to auto-apply
507+
return;
508+
}
509+
510+
var today = DateTime.Today;
511+
var gracePeriodCutoff = today.AddDays(-settings.LateFeeGracePeriodDays);
512+
513+
// Check if invoice is past grace period
514+
if (invoice.DueOn >= gracePeriodCutoff)
515+
{
516+
// Still within grace period
517+
return;
518+
}
519+
520+
// Calculate and apply late fee
521+
var lateFee = Math.Min(invoice.Amount * settings.LateFeePercentage, settings.MaxLateFeeAmount);
522+
523+
invoice.LateFeeAmount = lateFee;
524+
invoice.LateFeeApplied = true;
525+
invoice.LateFeeAppliedOn = DateTime.UtcNow;
526+
invoice.Amount += lateFee;
527+
invoice.Notes = string.IsNullOrEmpty(invoice.Notes)
528+
? $"Late fee of {lateFee:C} applied on {DateTime.Now:MMM dd, yyyy}"
529+
: $"{invoice.Notes}\nLate fee of {lateFee:C} applied on {DateTime.Now:MMM dd, yyyy}";
530+
531+
_logger.LogInformation(
532+
"Late fee of {LateFee:C} auto-applied to invoice {InvoiceNumber} (ID: {InvoiceId}) during payment processing",
533+
lateFee, invoice.InvoiceNumber, invoice.Id);
534+
}
535+
catch (Exception ex)
536+
{
537+
_logger.LogError(ex, "Error applying late fee to invoice {InvoiceId}", invoice.Id);
538+
// Don't throw - we don't want to fail the payment processing if late fee fails
539+
}
540+
}
478541
}
479542
}

2-Aquiis.Application/Services/ScheduledTaskService.cs

Lines changed: 48 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -117,25 +117,19 @@ private async Task DoWork(CancellationToken stoppingToken)
117117
continue;
118118
}
119119

120-
// Task 1: Apply late fees to overdue invoices (if enabled)
121-
if (settings.LateFeeEnabled && settings.LateFeeAutoApply)
122-
{
123-
await ApplyLateFees(dbContext, organizationId, settings, stoppingToken);
124-
}
125-
126-
// Task 2: Update invoice statuses
127-
await UpdateInvoiceStatuses(dbContext, organizationId, stoppingToken);
120+
// Task 1: Process overdue invoices (status update + late fees)
121+
await ProcessOverdueInvoices(dbContext, organizationId, settings, stoppingToken);
128122

129-
// Task 3: Send payment reminders (if enabled)
123+
// Task 2: Send payment reminders (if enabled)
130124
if (settings.PaymentReminderEnabled)
131125
{
132126
await SendPaymentReminders(dbContext, organizationId, settings, stoppingToken);
133127
}
134128

135-
// Task 4: Check for expiring leases and send renewal notifications
129+
// Task 3: Check for expiring leases and send renewal notifications
136130
await leaseNotificationService.SendLeaseRenewalRemindersAsync(organizationId, stoppingToken);
137131

138-
// Task 5: Expire overdue leases using workflow service (with audit logging)
132+
// Task 4: Expire overdue leases using workflow service (with audit logging)
139133
var expiredLeaseCount = await ExpireOverdueLeases(scope, organizationId);
140134
if (expiredLeaseCount > 0)
141135
{
@@ -152,7 +146,11 @@ private async Task DoWork(CancellationToken stoppingToken)
152146
}
153147
}
154148

155-
private async Task ApplyLateFees(
149+
/// <summary>
150+
/// Process overdue invoices: Update status to Overdue and apply late fees in one atomic operation.
151+
/// This prevents the race condition where status is updated but late fees are not applied.
152+
/// </summary>
153+
private async Task ProcessOverdueInvoices(
156154
ApplicationDbContext dbContext,
157155
Guid organizationId,
158156
OrganizationSettings settings,
@@ -161,82 +159,66 @@ private async Task ApplyLateFees(
161159
try
162160
{
163161
var today = DateTime.Today;
162+
var gracePeriodCutoff = today.AddDays(-settings.LateFeeGracePeriodDays);
164163

165-
// Find overdue invoices that haven't been charged a late fee yet
164+
// Find ALL pending invoices that are past due
166165
var overdueInvoices = await dbContext.Invoices
167166
.Include(i => i.Lease)
168167
.Where(i => !i.IsDeleted &&
169168
i.OrganizationId == organizationId &&
170169
i.Status == "Pending" &&
171-
i.DueOn < today.AddDays(-settings.LateFeeGracePeriodDays) &&
172-
(i.LateFeeApplied == null || !i.LateFeeApplied.Value))
170+
i.DueOn < today)
173171
.ToListAsync(stoppingToken);
174172

173+
var statusUpdatedCount = 0;
174+
var lateFeesAppliedCount = 0;
175+
175176
foreach (var invoice in overdueInvoices)
176177
{
177-
var lateFee = Math.Min(invoice.Amount * settings.LateFeePercentage, settings.MaxLateFeeAmount);
178-
179-
invoice.LateFeeAmount = lateFee;
180-
invoice.LateFeeApplied = true;
181-
invoice.LateFeeAppliedOn = DateTime.UtcNow;
182-
invoice.Amount += lateFee;
178+
// Always update status to Overdue
183179
invoice.Status = "Overdue";
184180
invoice.LastModifiedOn = DateTime.UtcNow;
185-
invoice.LastModifiedBy = ApplicationConstants.SystemUser.Id; // Automated task
186-
invoice.Notes = string.IsNullOrEmpty(invoice.Notes)
187-
? $"Late fee of {lateFee:C} applied on {DateTime.Now:MMM dd, yyyy}"
188-
: $"{invoice.Notes}\nLate fee of {lateFee:C} applied on {DateTime.Now:MMM dd, yyyy}";
189-
190-
_logger.LogInformation(
191-
"Applied late fee of {LateFee:C} to invoice {InvoiceNumber} (ID: {InvoiceId}) for organization {OrganizationId}",
192-
lateFee, invoice.InvoiceNumber, invoice.Id, organizationId);
193-
}
194-
195-
if (overdueInvoices.Any())
196-
{
197-
await dbContext.SaveChangesAsync(stoppingToken);
198-
_logger.LogInformation("Applied late fees to {Count} invoices for organization {OrganizationId}",
199-
overdueInvoices.Count, organizationId);
200-
}
201-
}
202-
catch (Exception ex)
203-
{
204-
_logger.LogError(ex, "Error applying late fees for organization {OrganizationId}", organizationId);
205-
}
206-
}
207-
208-
private async Task UpdateInvoiceStatuses(ApplicationDbContext dbContext, Guid organizationId, CancellationToken stoppingToken)
209-
{
210-
try
211-
{
212-
var today = DateTime.Today;
181+
invoice.LastModifiedBy = ApplicationConstants.SystemUser.Id;
182+
statusUpdatedCount++;
183+
184+
// Apply late fee if:
185+
// 1. Late fees are enabled and auto-apply is on
186+
// 2. Grace period has elapsed
187+
// 3. Late fee hasn't been applied yet
188+
if (settings.LateFeeEnabled &&
189+
settings.LateFeeAutoApply &&
190+
invoice.DueOn < gracePeriodCutoff &&
191+
(invoice.LateFeeApplied == null || !invoice.LateFeeApplied.Value))
192+
{
193+
var lateFee = Math.Min(invoice.Amount * settings.LateFeePercentage, settings.MaxLateFeeAmount);
194+
195+
invoice.LateFeeAmount = lateFee;
196+
invoice.LateFeeApplied = true;
197+
invoice.LateFeeAppliedOn = DateTime.UtcNow;
198+
invoice.Amount += lateFee;
199+
invoice.Notes = string.IsNullOrEmpty(invoice.Notes)
200+
? $"Late fee of {lateFee:C} applied on {DateTime.Now:MMM dd, yyyy}"
201+
: $"{invoice.Notes}\nLate fee of {lateFee:C} applied on {DateTime.Now:MMM dd, yyyy}";
213202

214-
// Update pending invoices that are now overdue (and haven't had late fees applied)
215-
var newlyOverdueInvoices = await dbContext.Invoices
216-
.Where(i => !i.IsDeleted &&
217-
i.OrganizationId == organizationId &&
218-
i.Status == "Pending" &&
219-
i.DueOn < today &&
220-
(i.LateFeeApplied == null || !i.LateFeeApplied.Value))
221-
.ToListAsync(stoppingToken);
203+
lateFeesAppliedCount++;
222204

223-
foreach (var invoice in newlyOverdueInvoices)
224-
{
225-
invoice.Status = "Overdue";
226-
invoice.LastModifiedOn = DateTime.UtcNow;
227-
invoice.LastModifiedBy = ApplicationConstants.SystemUser.Id; // Automated task
205+
_logger.LogInformation(
206+
"Applied late fee of {LateFee:C} to invoice {InvoiceNumber} (ID: {InvoiceId}) for organization {OrganizationId}",
207+
lateFee, invoice.InvoiceNumber, invoice.Id, organizationId);
208+
}
228209
}
229210

230-
if (newlyOverdueInvoices.Any())
211+
if (overdueInvoices.Any())
231212
{
232213
await dbContext.SaveChangesAsync(stoppingToken);
233-
_logger.LogInformation("Updated {Count} invoices to Overdue status for organization {OrganizationId}",
234-
newlyOverdueInvoices.Count, organizationId);
214+
_logger.LogInformation(
215+
"Processed {TotalCount} overdue invoice(s) for organization {OrganizationId}: {StatusUpdated} status updated, {LateFeesApplied} late fees applied",
216+
overdueInvoices.Count, organizationId, statusUpdatedCount, lateFeesAppliedCount);
235217
}
236218
}
237219
catch (Exception ex)
238220
{
239-
_logger.LogError(ex, "Error updating invoice statuses for organization {OrganizationId}", organizationId);
221+
_logger.LogError(ex, "Error processing overdue invoices for organization {OrganizationId}", organizationId);
240222
}
241223
}
242224

3-Aquiis.UI.Shared/Components/Notifications/NotificationBell.razor

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,50 +9,60 @@
99
<span class="spinner-border spinner-border-sm" role="status"></span>
1010
</div>
1111
}
12-
else if (notifications.Count > 0)
12+
else
1313
{
1414
<div class="dropdown notification-dropdown">
1515
<div class="notification-bell">
1616
<button class="notification-bell-button dropdown-toggle" @onclick="ToggleDropdown">
1717
<i class="@((notificationCount > 0) ? "bi bi-bell-fill" : "bi bi-bell-slash")"></i> Notifications
18-
<span class="notification-bell-badge">@notificationCount</span>
18+
@if (notificationCount > 0)
19+
{
20+
<span class="notification-bell-badge">@notificationCount</span>
21+
}
22+
else
23+
{
24+
<span class="notification-bell-badge-inactive">0</span>
25+
}
1926
</button>
2027
</div>
2128
@if (isDropdownOpen)
2229
{
2330
<ul class="dropdown-menu show">
24-
@foreach (var notification in notifications)
31+
@if (notifications.Count > 0)
2532
{
26-
if(notification.IsRead)
27-
{
28-
<li class="dropdown-item">
29-
<i class="bi bi-envelope-open"></i><a @onclick="() => ShowNotification(notification)">@notification.Title</a>
30-
</li>
31-
}
32-
else
33+
@foreach (var notification in notifications)
3334
{
34-
<li class="dropdown-item">
35-
<i class="bi bi-envelope"></i><a @onclick="() => ShowNotification(notification)">@notification.Title</a>
36-
</li>
35+
if(notification.IsRead)
36+
{
37+
<li class="dropdown-item">
38+
<i class="bi bi-envelope-open"></i><a @onclick="() => ShowNotification(notification)">@notification.Title</a>
39+
</li>
40+
}
41+
else
42+
{
43+
<li class="dropdown-item">
44+
<i class="bi bi-envelope"></i><a @onclick="() => ShowNotification(notification)">@notification.Title</a>
45+
</li>
46+
}
3747
}
48+
<hr />
49+
<li class="dropdown-item text-center">
50+
<a @onclick="MarkAllAsRead">Mark all as read</a>
51+
</li>
52+
}
53+
else
54+
{
55+
<li class="dropdown-item text-center text-muted">
56+
<i class="bi bi-inbox me-2"></i>No new notifications
57+
</li>
58+
<hr />
3859
}
39-
<hr />
40-
<li class="dropdown-item text-center">
41-
<a @onclick="MarkAllAsRead">Mark all as read</a>
42-
</li>
4360
<li class="dropdown-item text-center">
4461
<a @onclick="GoToNotificationCenter">View all</a>
4562
</li>
4663
</ul>
4764
}
4865
</div>
49-
} else {
50-
<div class="notification-bell">
51-
<button class="notification-bell-button">
52-
<i class="bi bi-bell-slash"></i> Notifications
53-
<span class="notification-bell-badge-inactive">@notificationCount</span>
54-
</button>
55-
</div>
5666
}
5767

5868
<!-- Notification Modal -->

0 commit comments

Comments
 (0)