1+ using MediatR ;
12using Microsoft . Extensions . Logging ;
23using Yoma . Core . Domain . Core . Exceptions ;
34using Yoma . Core . Domain . Core . Helpers ;
45using Yoma . Core . Domain . Core . Interfaces ;
6+ using Yoma . Core . Domain . Referral . Events ;
57using Yoma . Core . Domain . Referral . Interfaces ;
68using Yoma . Core . Domain . Referral . Interfaces . Lookups ;
79using Yoma . Core . Domain . Referral . Models ;
@@ -14,17 +16,22 @@ public class LinkMaintenanceService : ILinkMaintenanceService
1416 private readonly ILinkStatusService _linkStatusService ;
1517 private readonly ILinkUsageStatusService _linkUsageStatusService ;
1618
19+ private readonly IMediator _mediator ;
1720 private readonly IExecutionStrategyService _executionStrategyService ;
1821
1922 private readonly IRepositoryBatchedValueContainsWithNavigation < ReferralLink > _linkRepository ;
2023 private readonly IRepositoryBatched < ReferralLinkUsage > _linkUsageRepository ;
24+
25+ private const int Processing_BatchSize = 1000 ;
26+ private const int Processing_Parallelism = 10 ;
2127 #endregion
2228
2329 #region Constructor
2430 public LinkMaintenanceService (
2531 ILinkStatusService linkStatusService ,
2632 ILinkUsageStatusService linkUsageStatusService ,
2733
34+ IMediator mediator ,
2835 IExecutionStrategyService executionStrategyService ,
2936
3037 IRepositoryBatchedValueContainsWithNavigation < ReferralLink > linkRepository ,
@@ -33,6 +40,7 @@ public LinkMaintenanceService(
3340 _linkStatusService = linkStatusService ?? throw new ArgumentNullException ( nameof ( linkStatusService ) ) ;
3441 _linkUsageStatusService = linkUsageStatusService ?? throw new ArgumentNullException ( nameof ( linkUsageStatusService ) ) ;
3542
43+ _mediator = mediator ?? throw new ArgumentNullException ( nameof ( mediator ) ) ;
3644 _executionStrategyService = executionStrategyService ?? throw new ArgumentNullException ( nameof ( executionStrategyService ) ) ;
3745
3846 _linkRepository = linkRepository ?? throw new ArgumentNullException ( nameof ( linkRepository ) ) ;
@@ -214,6 +222,61 @@ await _executionStrategyService.ExecuteInExecutionStrategyAsync(async () =>
214222
215223 logger . LogInformation ( "Expired {Total} link(s) across {ProgramCount} program(s)" , items . Count , byProgram . Count ) ;
216224 }
225+
226+ /// <summary>
227+ /// Trigger sweep: reprocesses all pending link usages for a program when completion requirements are reduced
228+ /// (e.g. Proof of Personhood or Pathway removed), ensuring users are re-evaluated against updated rules.
229+ /// </summary>
230+ public async Task ProcessUsageProgressByProgramId ( Guid programId , ILogger ? logger = null )
231+ {
232+ if ( programId == Guid . Empty ) throw new ArgumentNullException ( nameof ( programId ) ) ;
233+
234+ var statusPendingId = _linkUsageStatusService . GetByName ( ReferralLinkUsageStatus . Pending . ToString ( ) ) . Id ;
235+ var totalProcessed = 0 ;
236+ var page = 0 ;
237+
238+ using var throttler = new SemaphoreSlim ( Processing_Parallelism , Processing_Parallelism ) ;
239+
240+ while ( true )
241+ {
242+ var items = _linkUsageRepository . Query ( )
243+ . Where ( o => o . ProgramId == programId && o . StatusId == statusPendingId )
244+ . OrderBy ( o => o . Id )
245+ . Take ( Processing_BatchSize )
246+ . Select ( o => new { o . UserId , o . Username , o . UserDisplayName } )
247+ . ToList ( ) ;
248+
249+ if ( items . Count == 0 ) break ;
250+
251+ var tasks = items . Select ( async item =>
252+ {
253+ await throttler . WaitAsync ( ) ;
254+ try
255+ {
256+ await _mediator . Publish ( new ReferralProgressTriggerEvent ( new ReferralProgressTriggerMessage
257+ {
258+ Source = ReferralTriggerSource . ProgramUpdated ,
259+ UserId = item . UserId ,
260+ Username = item . Username ,
261+ UserDisplayName = item . UserDisplayName
262+ } ) ) ;
263+ }
264+ finally { throttler . Release ( ) ; }
265+ } ) ;
266+
267+ await Task . WhenAll ( tasks ) ;
268+
269+ totalProcessed += items . Count ;
270+ page ++ ;
271+ }
272+
273+ if ( logger == null || ! logger . IsEnabled ( LogLevel . Information ) ) return ;
274+
275+ if ( totalProcessed == 0 )
276+ logger . LogInformation ( "No pending link usages found for program {ProgramId}" , programId ) ;
277+ else
278+ logger . LogInformation ( "Processed {Count} pending link usage(s) for program {ProgramId}" , totalProcessed , programId ) ;
279+ }
217280 #endregion
218281
219282 #region Private Members
@@ -224,25 +287,36 @@ private async Task ExpireLinkUsagesByLinkId(List<Guid> linkIds, ILogger? logger
224287 {
225288 if ( linkIds == null || linkIds . Count == 0 || linkIds . Any ( o => o == Guid . Empty ) )
226289 throw new ArgumentNullException ( nameof ( linkIds ) ) ;
290+
227291 linkIds = [ .. linkIds . Distinct ( ) ] ;
228292
229293 var statusExpiredId = _linkUsageStatusService . GetByName ( ReferralLinkUsageStatus . Expired . ToString ( ) ) . Id ;
230294 var statusExpirableIds = LinkUsageBackgroundService . Statuses_Expirable . Select ( o => _linkUsageStatusService . GetByName ( o . ToString ( ) ) . Id ) . ToList ( ) ;
231295
232- var items = _linkUsageRepository . Query ( )
233- . Where ( o => linkIds . Contains ( o . LinkId ) && statusExpirableIds . Contains ( o . StatusId ) )
234- . ToList ( ) ;
296+ var totalProcessed = 0 ;
235297
236- if ( items . Count == 0 )
298+ while ( true )
237299 {
238- if ( logger ? . IsEnabled ( LogLevel . Information ) == true ) logger . LogInformation ( "No expirable link usages found for {LinkCount} link(s)" , linkIds . Count ) ;
239- return ;
300+ var items = _linkUsageRepository . Query ( )
301+ . Where ( o => linkIds . Contains ( o . LinkId ) && statusExpirableIds . Contains ( o . StatusId ) )
302+ . OrderBy ( o => o . Id )
303+ . Take ( Processing_BatchSize )
304+ . ToList ( ) ;
305+
306+ if ( items . Count == 0 ) break ;
307+
308+ items . ForEach ( o => { o . StatusId = statusExpiredId ; o . Status = ReferralLinkUsageStatus . Expired ; } ) ;
309+ await _linkUsageRepository . Update ( items ) ;
310+
311+ totalProcessed += items . Count ;
240312 }
241313
242- items . ForEach ( o => { o . StatusId = statusExpiredId ; o . Status = ReferralLinkUsageStatus . Expired ; } ) ;
243- await _linkUsageRepository . Update ( items ) ;
314+ if ( logger == null || ! logger . IsEnabled ( LogLevel . Information ) ) return ;
244315
245- if ( logger ? . IsEnabled ( LogLevel . Information ) == true ) logger . LogInformation ( "Expired {Count} link usage(s) across {LinkCount} link(s)" , items . Count , linkIds . Count ) ;
316+ if ( totalProcessed == 0 )
317+ logger . LogInformation ( "No expirable link usages found for {LinkCount} link(s)" , linkIds . Count ) ;
318+ else
319+ logger . LogInformation ( "Expired {Count} link usage(s) across {LinkCount} link(s)" , totalProcessed , linkIds . Count ) ;
246320 }
247321 #endregion
248322 }
0 commit comments