@@ -93,8 +93,16 @@ describe("GrantService", () => {
9393 providers : [ GrantService ] ,
9494 } ) . compile ( ) ;
9595
96+ grantService = Object . assign ( module . get < GrantService > ( GrantService ) , {
97+ notificationService : {
98+ createNotification : vi . fn ( ) ,
99+ updateNotification : vi . fn ( )
100+ }
101+ } ) ;
102+
96103 controller = module . get < GrantController > ( GrantController ) ;
97104 grantService = module . get < GrantService > ( GrantService ) ;
105+
98106 } ) ;
99107
100108 it ( "should be defined" , ( ) => {
@@ -392,4 +400,161 @@ describe('deleteGrantById', () => {
392400 . rejects . toThrow ( / F a i l e d t o d e l e t e / ) ;
393401 } ) ;
394402} ) ;
403+ describe ( 'Notification helpers' , ( ) => {
404+ let notificationServiceMock : any ;
405+ let grantServiceWithMockNotif : GrantService ;
406+
407+ beforeEach ( ( ) => {
408+ // mock notification service with spy functions
409+ notificationServiceMock = {
410+ createNotification : vi . fn ( ) . mockResolvedValue ( undefined ) ,
411+ updateNotification : vi . fn ( ) . mockResolvedValue ( undefined ) ,
412+ } ;
413+
414+ grantServiceWithMockNotif = new GrantService ( notificationServiceMock ) ;
415+ } ) ;
416+
417+ describe ( 'getNotificationTimes' , ( ) => {
418+ it ( 'should return ISO strings for 14, 7, and 3 days before deadline' , ( ) => {
419+ const deadline = '2025-12-25T00:00:00.000Z' ;
420+ const result = ( grantServiceWithMockNotif as any ) . getNotificationTimes ( deadline ) ;
421+
422+ expect ( result ) . toHaveLength ( 3 ) ;
423+ result . forEach ( ( date : any ) => expect ( date ) . toMatch ( / ^ \d { 4 } - \d { 2 } - \d { 2 } T / ) ) ;
424+
425+ const parsed = result . map ( ( r : string | number | Date ) => new Date ( r ) ) ;
426+ const main = new Date ( deadline ) ;
427+ const diffs = parsed . map ( ( d : string | number ) => Math . round ( ( + main - + d ) / ( 1000 * 60 * 60 * 24 ) ) ) ;
428+
429+ expect ( diffs ) . toEqual ( [ 14 , 7 , 3 ] ) ;
430+ } ) ;
431+ } ) ;
432+
433+ describe ( 'createGrantNotifications' , ( ) => {
434+ it ( 'should create notifications for application and report deadlines' , async ( ) => {
435+ const mockGrant : Grant = {
436+ grantId : 100 ,
437+ organization : 'Boston Cares' ,
438+ does_bcan_qualify : true ,
439+ status : Status . Active ,
440+ amount : 10000 ,
441+ grant_start_date : '2025-01-01' ,
442+ application_deadline : '2025-12-31T00:00:00.000Z' ,
443+ report_deadlines : [ '2026-01-31T00:00:00.000Z' ] ,
444+ description : 'Helping local communities' ,
445+ timeline : 12 ,
446+ estimated_completion_time : 365 ,
447+ grantmaker_poc :
{ POC_name :
'Sarah' , POC_email :
'[email protected] ' } , 448+ bcan_poc :
{ POC_name :
'Tom' , POC_email :
'[email protected] ' } , 449+ attachments : [ ] ,
450+ isRestricted : false ,
451+ } ;
452+
453+ await ( grantServiceWithMockNotif as any ) . createGrantNotifications ( mockGrant , 'user123' ) ;
454+
455+ // application_deadline => 3 notifications (14,7,3 days)
456+ // one report_deadline => 3 more
457+ expect ( notificationServiceMock . createNotification ) . toHaveBeenCalledTimes ( 6 ) ;
458+ expect ( notificationServiceMock . createNotification ) . toHaveBeenCalledWith (
459+ expect . objectContaining ( {
460+ userId : 'user123' ,
461+ notificationId : expect . stringContaining ( '-app' ) ,
462+ message : expect . stringContaining ( 'Application due in' ) ,
463+ alertTime : expect . any ( String ) ,
464+ } )
465+ ) ;
466+ expect ( notificationServiceMock . createNotification ) . toHaveBeenCalledWith (
467+ expect . objectContaining ( {
468+ notificationId : expect . stringContaining ( '-report' ) ,
469+ message : expect . stringContaining ( 'Report due in' ) ,
470+ } )
471+ ) ;
472+ } ) ;
473+
474+ it ( 'should handle missing deadlines gracefully' , async ( ) => {
475+ const mockGrant = {
476+ grantId : 55 ,
477+ organization : 'No Deadline Org' ,
478+ does_bcan_qualify : true ,
479+ status : Status . Active ,
480+ amount : 5000 ,
481+ grant_start_date : '2025-01-01' ,
482+ description : '' ,
483+ timeline : 1 ,
484+ estimated_completion_time : 10 ,
485+ grantmaker_poc :
{ POC_name :
'A' , POC_email :
'[email protected] ' } , 486+ bcan_poc :
{ POC_name :
'B' , POC_email :
'[email protected] ' } , 487+ attachments : [ ] ,
488+ isRestricted : false ,
489+ } as unknown as Grant ;
490+
491+ await ( grantServiceWithMockNotif as any ) . createGrantNotifications ( mockGrant , 'userX' ) ;
492+
493+ expect ( notificationServiceMock . createNotification ) . not . toHaveBeenCalled ( ) ;
494+ } ) ;
495+ } ) ;
496+
497+ describe ( 'updateGrantNotifications' , ( ) => {
498+ it ( 'should call updateNotification for all alert times' , async ( ) => {
499+ const mockGrant : Grant = {
500+ grantId : 123 ,
501+ organization : 'Grant Org' ,
502+ does_bcan_qualify : true ,
503+ status : Status . Pending ,
504+ amount : 5000 ,
505+ grant_start_date : '2025-01-01' ,
506+ application_deadline : '2025-06-30T00:00:00.000Z' ,
507+ report_deadlines : [ '2025-07-15T00:00:00.000Z' ] ,
508+ description : 'Test desc' ,
509+ timeline : 1 ,
510+ estimated_completion_time : 100 ,
511+ grantmaker_poc :
{ POC_name :
'Alice' , POC_email :
'[email protected] ' } , 512+ bcan_poc :
{ POC_name :
'Bob' , POC_email :
'[email protected] ' } , 513+ attachments : [ ] ,
514+ isRestricted : false ,
515+ } ;
516+
517+ await ( grantServiceWithMockNotif as any ) . updateGrantNotifications ( mockGrant ) ;
518+
519+ // Expect 6 updateNotification calls (3 per deadline)
520+ expect ( notificationServiceMock . updateNotification ) . toHaveBeenCalledTimes ( 6 ) ;
521+ expect ( notificationServiceMock . updateNotification ) . toHaveBeenCalledWith (
522+ expect . stringContaining ( '-app' ) ,
523+ expect . objectContaining ( {
524+ message : expect . stringContaining ( 'Application due in' ) ,
525+ alertTime : expect . any ( String ) ,
526+ } )
527+ ) ;
528+ expect ( notificationServiceMock . updateNotification ) . toHaveBeenCalledWith (
529+ expect . stringContaining ( '-report' ) ,
530+ expect . objectContaining ( {
531+ message : expect . stringContaining ( 'Report due in' ) ,
532+ } )
533+ ) ;
534+ } ) ;
535+
536+ it ( 'should not crash when no deadlines exist' , async ( ) => {
537+ const mockGrant = {
538+ grantId : 321 ,
539+ organization : 'No deadlines' ,
540+ does_bcan_qualify : false ,
541+ status : Status . Inactive ,
542+ amount : 0 ,
543+ grant_start_date : '2025-01-01' ,
544+ report_deadlines : [ ] ,
545+ description : '' ,
546+ timeline : 0 ,
547+ estimated_completion_time : 0 ,
548+ grantmaker_poc :
{ POC_name :
'X' , POC_email :
'[email protected] ' } , 549+ bcan_poc :
{ POC_name :
'Y' , POC_email :
'[email protected] ' } , 550+ attachments : [ ] ,
551+ isRestricted : false ,
552+ } as unknown as Grant ;
553+
554+ await ( grantServiceWithMockNotif as any ) . updateGrantNotifications ( mockGrant ) ;
555+
556+ expect ( notificationServiceMock . updateNotification ) . not . toHaveBeenCalled ( ) ;
557+ } ) ;
558+ } ) ;
559+ } ) ;
395560} ) ;
0 commit comments