-
Notifications
You must be signed in to change notification settings - Fork 0
SSF-188 Automated Order Lifecycle Emails #162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ddbe3fb
4058b03
61f6859
83baa1c
ff7d8e6
5deb81b
6006842
dd564f4
721c0c4
25664af
ef55069
6ef0e51
2470ddb
aa7871a
0c8f55a
8446636
0701342
1b7750f
f658e67
0a285e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,12 @@ import { mock } from 'jest-mock-extended'; | |
| import { emailTemplates } from '../emails/emailTemplates'; | ||
| import { Allocation } from '../allocations/allocations.entity'; | ||
| import { ApplicationStatus } from '../shared/types'; | ||
| import { User } from '../users/users.entity'; | ||
| import { UsersService } from '../users/users.service'; | ||
| import { Donation } from '../donations/donations.entity'; | ||
| import { AuthService } from '../auth/auth.service'; | ||
| import { PantriesService } from '../pantries/pantries.service'; | ||
| import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service'; | ||
|
|
||
| jest.setTimeout(60000); | ||
|
|
||
|
|
@@ -38,6 +44,9 @@ describe('RequestsService', () => { | |
| const module = await Test.createTestingModule({ | ||
| providers: [ | ||
| RequestsService, | ||
| UsersService, | ||
| PantriesService, | ||
| FoodManufacturersService, | ||
| { | ||
| provide: getRepositoryToken(FoodRequest), | ||
| useValue: testDataSource.getRepository(FoodRequest), | ||
|
|
@@ -62,6 +71,20 @@ describe('RequestsService', () => { | |
| provide: getRepositoryToken(Allocation), | ||
| useValue: testDataSource.getRepository(Allocation), | ||
| }, | ||
| { | ||
| provide: getRepositoryToken(User), | ||
| useValue: testDataSource.getRepository(User), | ||
| }, | ||
| { | ||
| provide: getRepositoryToken(Donation), | ||
| useValue: testDataSource.getRepository(Donation), | ||
| }, | ||
| { | ||
| provide: AuthService, | ||
| useValue: { | ||
| adminCreateUser: jest.fn().mockResolvedValue('test-sub'), | ||
| }, | ||
| }, | ||
| { | ||
| provide: EmailsService, | ||
| useValue: mockEmailsService, | ||
|
|
@@ -381,6 +404,64 @@ describe('RequestsService', () => { | |
| new NotFoundException('Request 999 not found'), | ||
| ); | ||
| }); | ||
|
|
||
| it('sends pantry closed email with last delivered order assignee on auto-close', async () => { | ||
| const requestId = 1; | ||
| const pantry = await testDataSource.getRepository(Pantry).findOne({ | ||
| where: { pantryId: 1 }, | ||
| relations: ['pantryUser'], | ||
| }); | ||
| const lastDeliveredOrder = await testDataSource | ||
| .getRepository(Order) | ||
| .findOne({ | ||
| where: { requestId, status: OrderStatus.DELIVERED }, | ||
| order: { deliveredAt: 'DESC' }, | ||
| relations: ['assignee'], | ||
| }); | ||
|
|
||
| await service.updateRequestStatus(requestId); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same thing here, can we check that the request is still active? |
||
|
|
||
| const assignee = lastDeliveredOrder!.assignee; | ||
| const expectedMessage = emailTemplates.pantryRequestClosed({ | ||
| pantryName: pantry!.pantryName, | ||
| volunteerName: `${assignee.firstName} ${assignee.lastName}`, | ||
| volunteerEmail: assignee.email, | ||
| }); | ||
|
|
||
| expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(1); | ||
| expect(mockEmailsService.sendEmails).toHaveBeenCalledWith( | ||
| [pantry!.pantryUser.email], | ||
| expectedMessage.subject, | ||
| expectedMessage.bodyHTML, | ||
| ); | ||
| }); | ||
|
|
||
| it('does not send email when not all orders are delivered (request stays active)', async () => { | ||
| await service.updateRequestStatus(3); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we check beforehand that one of the orders for this are not delivered (its okay to hardcode, we just want to be sure so we know thats why an email does not send). |
||
|
|
||
| expect(mockEmailsService.sendEmails).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('does not send email when request was already closed before updateRequestStatus', async () => { | ||
| await testDataSource.query( | ||
| `UPDATE food_requests SET status = 'closed' WHERE request_id = 1`, | ||
| ); | ||
|
|
||
| await service.updateRequestStatus(1); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we make sure the request was closed before this call? |
||
|
|
||
| expect(mockEmailsService.sendEmails).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('still auto-closes request when email fails', async () => { | ||
| mockEmailsService.sendEmails.mockRejectedValueOnce( | ||
| new Error('SMTP error'), | ||
| ); | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we make sure before this update call, that the request beforehand was marked as active? |
||
| await service.updateRequestStatus(1); | ||
|
|
||
| const request = await service.findOne(1); | ||
| expect(request.status).toBe(FoodRequestStatus.CLOSED); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we either add to this test or create another test that, in this event, in addition to the request still closing, that the error message is also logged? |
||
| }); | ||
| }); | ||
|
|
||
| describe('getMatchingManufacturers', () => { | ||
|
|
@@ -746,8 +827,14 @@ describe('RequestsService', () => { | |
| }); | ||
|
|
||
| describe('closeRequest', () => { | ||
| let volunteerId: number; | ||
|
|
||
| beforeEach(() => { | ||
| volunteerId = 6; | ||
| }); | ||
|
|
||
| it('should close an active request', async () => { | ||
| const result = await service.closeRequest(3); | ||
| const result = await service.closeRequest(3, volunteerId); | ||
|
|
||
| expect(result.status).toBe(FoodRequestStatus.CLOSED); | ||
|
|
||
|
|
@@ -756,15 +843,15 @@ describe('RequestsService', () => { | |
| }); | ||
|
|
||
| it('should throw BadRequestException when request is already closed', async () => { | ||
| await service.closeRequest(3); | ||
| await service.closeRequest(3, volunteerId); | ||
|
|
||
| await expect(service.closeRequest(3)).rejects.toThrow( | ||
| await expect(service.closeRequest(3, volunteerId)).rejects.toThrow( | ||
| new BadRequestException('Cannot close a request with status: closed'), | ||
| ); | ||
| }); | ||
|
|
||
| it('should throw NotFoundException for non-existent request', async () => { | ||
| await expect(service.closeRequest(999)).rejects.toThrow( | ||
| await expect(service.closeRequest(999, volunteerId)).rejects.toThrow( | ||
| new NotFoundException('Request 999 not found'), | ||
| ); | ||
| }); | ||
|
|
@@ -774,7 +861,7 @@ describe('RequestsService', () => { | |
| .getRepository(Order) | ||
| .find({ where: { requestId: 3 } }); | ||
|
|
||
| await service.closeRequest(3); | ||
| await service.closeRequest(3, volunteerId); | ||
|
|
||
| const ordersAfter = await testDataSource | ||
| .getRepository(Order) | ||
|
|
@@ -786,11 +873,50 @@ describe('RequestsService', () => { | |
| }); | ||
|
|
||
| it('should not reopen a closed request when updateRequestStatus is called', async () => { | ||
| await service.closeRequest(1); | ||
| await service.closeRequest(1, volunteerId); | ||
| await service.updateRequestStatus(1); | ||
|
|
||
| const fromDb = await service.findOne(1); | ||
| expect(fromDb.status).toBe(FoodRequestStatus.CLOSED); | ||
| }); | ||
|
|
||
| it('sends pantry closed email with acting volunteer info on successful close', async () => { | ||
| const pantry = await testDataSource.getRepository(Pantry).findOne({ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: cast here |
||
| where: { pantryId: 3 }, | ||
| relations: ['pantryUser'], | ||
| }); | ||
|
|
||
| await service.closeRequest(3, volunteerId); | ||
|
|
||
| const expectedMessage = emailTemplates.pantryRequestClosed({ | ||
| pantryName: pantry!.pantryName, | ||
| volunteerName: `James Thomas`, | ||
| volunteerEmail: `james.t@volunteer.org`, | ||
| }); | ||
|
|
||
| expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(1); | ||
| expect(mockEmailsService.sendEmails).toHaveBeenCalledWith( | ||
| [pantry!.pantryUser.email], | ||
| expectedMessage.subject, | ||
| expectedMessage.bodyHTML, | ||
| ); | ||
| }); | ||
|
|
||
| it('still closes request when email fails (manual close)', async () => { | ||
| mockEmailsService.sendEmails.mockRejectedValueOnce( | ||
| new Error('SMTP error'), | ||
| ); | ||
|
|
||
| await expect(service.closeRequest(3, volunteerId)).rejects.toThrow( | ||
| new InternalServerErrorException( | ||
| 'Failed to send food request closed email to pantry', | ||
| ), | ||
| ); | ||
|
|
||
| const request = await service.findOne(3); | ||
|
|
||
| expect(request.status).toBe(FoodRequestStatus.CLOSED); | ||
| expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(1); | ||
| }); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can we cast both of these rather than a !