diff --git a/api/src/services/email.service.ts b/api/src/services/email.service.ts
index 2dab3f2604..e1eab9692a 100644
--- a/api/src/services/email.service.ts
+++ b/api/src/services/email.service.ts
@@ -411,12 +411,14 @@ export class EmailService {
listingInfo: IdDTO,
emails: string[],
appUrl: string,
+ jurisEmail: string,
) {
const jurisdiction = await this.getJurisdiction([jurisdictionId]);
void (await this.loadTranslations(jurisdiction));
await this.sendSES({
- to: emails,
+ to: jurisEmail,
+ bcc: emails,
subject: this.polyglot.t('requestApproval.header'),
html: this.template('request-approval')({
appOptions: { listingName: listingInfo.name },
@@ -431,6 +433,7 @@ export class EmailService {
listingInfo: listingInfo,
emails: string[],
appUrl: string,
+ jurisEmail: string,
) {
const jurisdiction = listingInfo.juris
? await this.getJurisdiction([{ id: listingInfo.juris }])
@@ -438,7 +441,8 @@ export class EmailService {
void (await this.loadTranslations(jurisdiction));
await this.sendSES({
- to: emails,
+ to: jurisEmail,
+ bcc: emails,
subject: this.polyglot.t('changesRequested.header'),
html: this.template('changes-requested')({
appOptions: { listingName: listingInfo.name },
@@ -453,12 +457,14 @@ export class EmailService {
listingInfo: IdDTO,
emails: string[],
publicUrl: string,
+ jurisEmail: string,
) {
const jurisdiction = await this.getJurisdiction([jurisdictionId]);
void (await this.loadTranslations(jurisdiction));
await this.sendSES({
- to: emails,
+ to: jurisEmail,
+ bcc: emails,
subject: this.polyglot.t('listingApproved.header'),
html: this.template('listing-approved')({
appOptions: { listingName: listingInfo.name },
@@ -615,9 +621,11 @@ export class EmailService {
listingInfo: listingInfo,
emails: string[],
appUrl: string,
+ jurisEmail: string,
) {
await this.sendSES({
- to: emails,
+ to: jurisEmail,
+ bcc: emails,
subject: this.polyglot.t('lotteryReleased.header', {
listingName: listingInfo.name,
}),
@@ -633,13 +641,15 @@ export class EmailService {
listingInfo: listingInfo,
emails: string[],
appUrl: string,
+ jurisEmail: string,
) {
const jurisdiction = await this.getJurisdiction([
{ id: listingInfo.juris },
]);
void (await this.loadTranslations(jurisdiction));
await this.sendSES({
- to: emails,
+ to: jurisEmail,
+ bcc: emails,
subject: this.polyglot.t('lotteryPublished.header', {
listingName: listingInfo.name,
}),
@@ -664,8 +674,8 @@ export class EmailService {
for (const language in emails) {
void (await this.loadTranslations(null, language as LanguagesEnum));
await this.sendSES({
- to: emails[language],
-
+ to: jurisdiction.emailFromAddress,
+ bcc: emails[language],
subject: this.polyglot.t('lotteryAvailable.header', {
listingName: listingInfo.name,
}),
diff --git a/api/src/services/listing.service.ts b/api/src/services/listing.service.ts
index 3f1f2d8465..a6653bc38f 100644
--- a/api/src/services/listing.service.ts
+++ b/api/src/services/listing.service.ts
@@ -364,7 +364,12 @@ export class ListingService implements OnModuleInit {
listingId?: string,
jurisId?: string,
getPublicUrl = false,
- ): Promise<{ emails: string[]; publicUrl?: string | null }> {
+ getEmailFromAddress = false,
+ ): Promise<{
+ emails: string[];
+ publicUrl?: string | null;
+ emailFromAddress?: string | null;
+ }> {
// determine where clause(s)
const userRolesWhere: Prisma.UserAccountsWhereInput[] = [];
if (userRoles.includes(UserRoleEnum.admin))
@@ -381,28 +386,37 @@ export class ListingService implements OnModuleInit {
});
}
- const userResults = await this.prisma.userAccounts.findMany({
- include: {
- jurisdictions: {
- select: {
- id: true,
- publicUrl: getPublicUrl,
- },
- },
+ const rawUsers = await this.prisma.userAccounts.findMany({
+ select: {
+ id: true,
+ email: true,
},
where: {
OR: userRolesWhere,
},
});
- // account for users having access to multiple jurisdictions
- const publicUrl = getPublicUrl
- ? userResults[0]?.jurisdictions?.find((juris) => juris.id === jurisId)
- ?.publicUrl
+ const rawJuris = await this.prisma.jurisdictions.findFirst({
+ select: {
+ id: true,
+ publicUrl: getPublicUrl,
+ emailFromAddress: getEmailFromAddress,
+ },
+ where: {
+ id: jurisId,
+ },
+ });
+
+ const publicUrl = getPublicUrl ? rawJuris?.publicUrl : null;
+ const emailFromAddress = getEmailFromAddress
+ ? rawJuris?.emailFromAddress
: null;
- const userEmails: string[] = [];
- userResults?.forEach((user) => user?.email && userEmails.push(user.email));
- return { emails: userEmails, publicUrl };
+
+ const userEmails: string[] = rawUsers?.reduce((userEmails, user) => {
+ if (user?.email) userEmails.push(user.email);
+ return userEmails;
+ }, []);
+ return { emails: userEmails, publicUrl, emailFromAddress };
}
public async listingApprovalNotify(params: {
@@ -424,12 +438,15 @@ export class ListingService implements OnModuleInit {
params.approvingRoles,
params.listingInfo.id,
params.jurisId,
+ false,
+ true,
);
await this.emailService.requestApproval(
{ id: params.jurisId },
{ id: params.listingInfo.id, name: params.listingInfo.name },
userInfo.emails,
this.configService.get('PARTNERS_PORTAL_URL'),
+ userInfo.emailFromAddress,
);
}
// admin updates status to changes requested when approval requires partner changes
@@ -441,6 +458,8 @@ export class ListingService implements OnModuleInit {
nonApprovingRoles,
params.listingInfo.id,
params.jurisId,
+ false,
+ true,
);
await this.emailService.changesRequested(
params.user,
@@ -451,6 +470,7 @@ export class ListingService implements OnModuleInit {
},
userInfo.emails,
this.configService.get('PARTNERS_PORTAL_URL'),
+ userInfo.emailFromAddress,
);
}
// check if status of active requires notification
@@ -466,12 +486,14 @@ export class ListingService implements OnModuleInit {
params.listingInfo.id,
params.jurisId,
true,
+ true,
);
await this.emailService.listingApproved(
{ id: params.jurisId },
{ id: params.listingInfo.id, name: params.listingInfo.name },
userInfo.emails,
userInfo.publicUrl,
+ userInfo.emailFromAddress,
);
}
}
diff --git a/api/src/services/lottery.service.ts b/api/src/services/lottery.service.ts
index 6414b64e13..8ee935c99d 100644
--- a/api/src/services/lottery.service.ts
+++ b/api/src/services/lottery.service.ts
@@ -406,6 +406,8 @@ export class LotteryService {
],
listing.id,
listing.jurisdictions?.id,
+ false,
+ true,
);
const publicUserEmailInfo = await this.getPublicUserEmailInfo(listing.id);
@@ -423,6 +425,7 @@ export class LotteryService {
},
partnerUserEmailInfo.emails,
this.configService.get('PARTNERS_PORTAL_URL'),
+ partnerUserEmailInfo.emailFromAddress,
);
await this.emailService.lotteryPublishedApplicant(
@@ -508,6 +511,8 @@ export class LotteryService {
],
storedListing.id,
storedListing.jurisdictionId,
+ false,
+ true,
);
await this.emailService.lotteryReleased(
@@ -518,6 +523,7 @@ export class LotteryService {
},
partnerUserEmailInfo.emails,
this.configService.get('PARTNERS_PORTAL_URL'),
+ partnerUserEmailInfo.emailFromAddress,
);
break;
}
diff --git a/api/test/integration/listing.e2e-spec.ts b/api/test/integration/listing.e2e-spec.ts
index b5a14bf62a..513c3f95fa 100644
--- a/api/test/integration/listing.e2e-spec.ts
+++ b/api/test/integration/listing.e2e-spec.ts
@@ -51,6 +51,7 @@ describe('Listing Controller Tests', () => {
let app: INestApplication;
let prisma: PrismaService;
let jurisdictionAId: string;
+ let jurisdictionAEmail: string;
let adminAccessToken: string;
const testEmailService = {
@@ -87,6 +88,7 @@ describe('Listing Controller Tests', () => {
data: jurisdictionFactory(),
});
jurisdictionAId = jurisdiction.id;
+ jurisdictionAEmail = jurisdiction.emailFromAddress;
await reservedCommunityTypeFactoryAll(jurisdictionAId, prisma);
await unitAccessibilityPriorityTypeFactoryAll(prisma);
const adminUser = await prisma.userAccounts.create({
@@ -940,6 +942,7 @@ describe('Listing Controller Tests', () => {
{ id: listing.id, name: val.name },
expect.arrayContaining([adminUser.email, jurisAdmin.email]),
process.env.PARTNERS_PORTAL_URL,
+ jurisdictionAEmail,
);
//ensure juris admin is not included since don't have approver permissions in alameda seed
expect(mockRequestApproval.mock.calls[0]['emails']).toEqual(
@@ -980,6 +983,7 @@ describe('Listing Controller Tests', () => {
{ id: listing.id, name: val.name },
expect.arrayContaining([partnerUser.email]),
jurisdictionA.publicUrl,
+ jurisdictionA.emailFromAddress,
);
expect(mockListingOpportunity).toBeCalledWith(
expect.objectContaining({
@@ -1022,6 +1026,7 @@ describe('Listing Controller Tests', () => {
{ id: listing.id, name: val.name, juris: expect.anything() },
expect.arrayContaining([partnerUser.email]),
process.env.PARTNERS_PORTAL_URL,
+ jurisdictionAEmail,
);
});
});
diff --git a/api/test/integration/lottery.e2e-spec.ts b/api/test/integration/lottery.e2e-spec.ts
index acd005132d..cca9da797d 100644
--- a/api/test/integration/lottery.e2e-spec.ts
+++ b/api/test/integration/lottery.e2e-spec.ts
@@ -44,6 +44,7 @@ describe('Lottery Controller Tests', () => {
let cookies = '';
let adminAccessToken: string;
let jurisdictionAId: string;
+ let jurisdictionAEmail: string;
let unitTypeA: UnitTypes;
const testEmailService = {
@@ -101,6 +102,7 @@ describe('Lottery Controller Tests', () => {
data: jurisdictionFactory(),
});
jurisdictionAId = jurisdiction.id;
+ jurisdictionAEmail = jurisdiction.emailFromAddress;
await reservedCommunityTypeFactoryAll(jurisdictionAId, prisma);
await unitTypeFactoryAll(prisma);
unitTypeA = await unitTypeFactorySingle(prisma, UnitTypeEnum.oneBdrm);
@@ -895,6 +897,7 @@ describe('Lottery Controller Tests', () => {
},
expect.arrayContaining([partnerUser.email, adminUser.email]),
process.env.PARTNERS_PORTAL_URL,
+ jurisdictionAEmail,
);
const activityLogResult = await prisma.activityLog.findFirst({
@@ -1078,6 +1081,7 @@ describe('Lottery Controller Tests', () => {
},
expect.arrayContaining([partnerUser.email, adminUser.email]),
process.env.PARTNERS_PORTAL_URL,
+ jurisdictionAEmail,
);
expect(mockLotteryPublishedApplicant).toBeCalledWith(
diff --git a/api/test/unit/services/email.service.spec.ts b/api/test/unit/services/email.service.spec.ts
index 3caf02eec6..07ea3cef24 100644
--- a/api/test/unit/services/email.service.spec.ts
+++ b/api/test/unit/services/email.service.spec.ts
@@ -336,7 +336,7 @@ describe('Testing email service', () => {
expect(sendMock).toHaveBeenCalled();
const emailMock = sendMock.mock.calls[0][0];
- expect(emailMock.to).toEqual(emailArr);
+ expect(emailMock.bcc).toEqual(emailArr);
expect(emailMock.subject).toEqual('Listing approval requested');
expect(emailMock.html).toMatch(
``,
@@ -378,7 +378,7 @@ describe('Testing email service', () => {
expect(sendMock).toHaveBeenCalled();
const emailMock = sendMock.mock.calls[0][0];
- expect(emailMock.to).toEqual(emailArr);
+ expect(emailMock.bcc).toEqual(emailArr);
expect(emailMock.subject).toEqual('Listing changes requested');
expect(emailMock.html).toMatch(
``,
@@ -423,7 +423,7 @@ describe('Testing email service', () => {
expect(sendMock).toHaveBeenCalled();
const emailMock = sendMock.mock.calls[0][0];
- expect(emailMock.to).toEqual(emailArr);
+ expect(emailMock.bcc).toEqual(emailArr);
expect(emailMock.subject).toEqual('New published listing');
expect(emailMock.html).toMatch(
``,
diff --git a/api/test/unit/services/listing.service.spec.ts b/api/test/unit/services/listing.service.spec.ts
index 4dc1defbf0..f219671ab2 100644
--- a/api/test/unit/services/listing.service.spec.ts
+++ b/api/test/unit/services/listing.service.spec.ts
@@ -3131,9 +3131,10 @@ describe('Testing listing service', () => {
describe('Test listingApprovalNotify endpoint', () => {
it('listingApprovalNotify request approval email', async () => {
- jest
- .spyOn(service, 'getUserEmailInfo')
- .mockResolvedValueOnce({ emails: ['admin@email.com'] });
+ jest.spyOn(service, 'getUserEmailInfo').mockResolvedValueOnce({
+ emails: ['admin@email.com'],
+ emailFromAddress: 'no-reply@housingbayarea.org',
+ });
await service.listingApprovalNotify({
user,
listingInfo: { id: 'id', name: 'name' },
@@ -3146,18 +3147,22 @@ describe('Testing listing service', () => {
['admin'],
'id',
'jurisId',
+ false,
+ true,
);
expect(requestApprovalMock).toBeCalledWith(
{ id: 'jurisId' },
{ id: 'id', name: 'name' },
['admin@email.com'],
config.get('PARTNERS_PORTAL_URL'),
+ 'no-reply@housingbayarea.org',
);
});
it('listingApprovalNotify changes requested email', async () => {
jest.spyOn(service, 'getUserEmailInfo').mockResolvedValueOnce({
emails: ['jurisAdmin@email.com', 'partner@email.com'],
+ emailFromAddress: 'no-reply@housingbayarea.org',
});
await service.listingApprovalNotify({
user,
@@ -3171,12 +3176,15 @@ describe('Testing listing service', () => {
['partner', 'jurisdictionAdmin'],
'id',
'jurisId',
+ false,
+ true,
);
expect(changesRequestedMock).toBeCalledWith(
user,
{ id: 'id', name: 'name', juris: 'jurisId' },
['jurisAdmin@email.com', 'partner@email.com'],
config.get('PARTNERS_PORTAL_URL'),
+ 'no-reply@housingbayarea.org',
);
});
@@ -3184,6 +3192,7 @@ describe('Testing listing service', () => {
jest.spyOn(service, 'getUserEmailInfo').mockResolvedValueOnce({
emails: ['jurisAdmin@email.com', 'partner@email.com'],
publicUrl: 'public.housing.gov',
+ emailFromAddress: 'no-reply@housingbayarea.org',
});
await service.listingApprovalNotify({
user,
@@ -3199,12 +3208,14 @@ describe('Testing listing service', () => {
'id',
'jurisId',
true,
+ true,
);
expect(listingApprovedMock).toBeCalledWith(
expect.objectContaining({ id: 'jurisId' }),
{ id: 'id', name: 'name' },
['jurisAdmin@email.com', 'partner@email.com'],
'public.housing.gov',
+ 'no-reply@housingbayarea.org',
);
});
diff --git a/api/test/unit/services/lottery.service.spec.ts b/api/test/unit/services/lottery.service.spec.ts
index 026231c996..7a959d5867 100644
--- a/api/test/unit/services/lottery.service.spec.ts
+++ b/api/test/unit/services/lottery.service.spec.ts
@@ -87,14 +87,6 @@ describe('Testing lottery service', () => {
prisma = module.get(PrismaService);
listingService = module.get(ListingService);
config = module.get(ConfigService);
-
- jest.spyOn(listingService, 'getUserEmailInfo').mockResolvedValueOnce({
- emails: ['admin@email.com', 'partner@email.com'],
- });
-
- jest.spyOn(service, 'getPublicUserEmailInfo').mockResolvedValueOnce({
- en: ['applicant@email.com'],
- });
});
describe('Testing lotteryRandomizerHelper()', () => {
@@ -685,6 +677,7 @@ describe('Testing lottery service', () => {
jest.spyOn(listingService, 'getUserEmailInfo').mockResolvedValueOnce({
emails: ['admin@email.com', 'partner@email.com'],
+ emailFromAddress: 'no-reply@housingbayarea.org',
});
jest.spyOn(service, 'getPublicUserEmailInfo').mockResolvedValueOnce({
@@ -721,12 +714,15 @@ describe('Testing lottery service', () => {
['admin', 'jurisdictionAdmin', 'partner'],
'example id',
'jurisId',
+ false,
+ true,
);
expect(lotteryReleasedMock).toBeCalledWith(
{ id: 'example id', juris: 'jurisId', name: 'example name' },
['admin@email.com', 'partner@email.com'],
config.get('PARTNERS_PORTAL_URL'),
+ 'no-reply@housingbayarea.org',
);
});
diff --git a/yarn.lock b/yarn.lock
index 0e514ae6a4..67b4bb1305 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -14631,13 +14631,6 @@ rfdc@^1.3.0:
resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz"
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
-rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
- integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
- dependencies:
- glob "^7.1.3"
-
rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz"
@@ -14645,6 +14638,13 @@ rimraf@^2.6.3:
dependencies:
glob "^7.1.3"
+rimraf@^3.0.0, rimraf@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+ integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+ dependencies:
+ glob "^7.1.3"
+
rollup@2.78.0:
version "2.78.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.78.0.tgz#00995deae70c0f712ea79ad904d5f6b033209d9e"