@@ -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 } \n Late 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 } \n Late 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
0 commit comments