Skip to content
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

chore: update translations and sign in link for lottery email #4506

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/prisma/seed-helpers/translation-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ const translations = (jurisdictionName?: string, language?: LanguagesEnum) => {
signIn: 'Sign In to View Your Results',
whatHappensHeader: 'What happens next?',
whatHappensContent:
'The property manager will begin to contact applicants by their preferred contact method. They will do so in the order of lottery rank, within each lottery preference. When the units are all filled, the property manager will stop contacting applicants. All the units could be filled before the property manager reaches your rank. If this happens, you will not be contacted.',
'The property manager will begin to contact applicants in the order of lottery rank, within each lottery preference. When the units are all filled, the property manager will stop contacting applicants. All the units could be filled before the property manager reaches your rank. If this happens, you will not be contacted.',
otherOpportunities1:
'To view other housing opportunities, please visit %{appUrl}. You can sign up to receive notifications of new application opportunities',
otherOpportunities2: 'here',
Expand Down Expand Up @@ -231,7 +231,7 @@ const translations = (jurisdictionName?: string, language?: LanguagesEnum) => {
signIn: 'Inicie sesión para ver sus resultados',
whatHappensHeader: '¿Qué pasa después?',
whatHappensContent:
'El administrador de la propiedad comenzará a comunicarse con los solicitantes mediante su método de contacto preferido. Lo harán en el orden de clasificación de la lotería, dentro de cada preferencia de lotería. Cuando todas las unidades estén ocupadas, el administrador de la propiedad dejará de comunicarse con los solicitantes. Todas las unidades podrían llenarse antes de que el administrador de la propiedad alcance su rango. Si esto sucede, no lo contactaremos.',
'El administrador de la propiedad comenzará a comunicarse con los solicitantes en el orden de clasificación de la lotería, dentro de cada preferencia de la lotería. Cuando todas las unidades estén ocupadas, el administrador de la propiedad dejará de comunicarse con los solicitantes. Es posible que todas las unidades estén ocupadas antes de que el administrador de la propiedad alcance su clasificación. Si esto sucede, no se comunicarán con usted.',
otherOpportunities1:
'Para ver otras oportunidades de vivienda, visite %{appUrl}. Puede registrarse para recibir notificaciones de nuevas oportunidades de solicitud',
otherOpportunities2: 'aquí',
Expand Down
13 changes: 13 additions & 0 deletions api/src/controllers/script-runner.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,19 @@ export class ScirptRunnerController {
return await this.scriptRunnerService.hideProgramsFromListings(req);
}

@Put('updatesWhatHappensInLotteryEmail')
@ApiOperation({
summary:
'A script that updates the "what happens next" content in lottery email',
operationId: 'updatesWhatHappensInLotteryEmail',
})
@ApiOkResponse({ type: SuccessDTO })
async updatesWhatHappensInLotteryEmail(
@Request() req: ExpressRequest,
): Promise<SuccessDTO> {
return await this.scriptRunnerService.updatesWhatHappensInLotteryEmail(req);
}

@Put('addFeatureFlags')
@ApiOperation({
summary:
Expand Down
2 changes: 1 addition & 1 deletion api/src/services/email.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ export class EmailService {
listingName: listingInfo.name,
appUrl: jurisdiction.publicUrl,
},
appUrl: jurisdiction.publicUrl,
signInUrl: `${jurisdiction.publicUrl}/${language}/sign-in`,
// These two URLs are placeholders and must be updated per jurisdiction
notificationsUrl: 'https://www.exygy.com',
helpCenterUrl: 'https://www.exygy.com',
Expand Down
154 changes: 124 additions & 30 deletions api/src/services/script-runner.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ export class ScriptRunnerService {
async addLotteryTranslations(req: ExpressRequest): Promise<SuccessDTO> {
const requestingUser = mapTo(User, req['user']);
await this.markScriptAsRunStart('add lottery translations', requestingUser);
this.addLotteryTranslationsHelper();
this.addLotteryTranslationsHelper(true);
await this.markScriptAsComplete('add lottery translations', requestingUser);

return { success: true };
Expand All @@ -295,7 +295,7 @@ export class ScriptRunnerService {
'add lottery translations create if empty',
requestingUser,
);
this.addLotteryTranslationsHelper();
this.addLotteryTranslationsHelper(true);
await this.markScriptAsComplete(
'add lottery translations create if empty',
requestingUser,
Expand Down Expand Up @@ -435,8 +435,64 @@ export class ScriptRunnerService {
}

/**
Adds all existing feature flags across Bloom to the database
*/
*
* @param req incoming request object
* @returns successDTO
* @description updates the "what happens next" content in lottery email
*/
async updatesWhatHappensInLotteryEmail(
req: ExpressRequest,
): Promise<SuccessDTO> {
const requestingUser = mapTo(User, req['user']);
await this.markScriptAsRunStart(
'update what happens next content in lottery email',
requestingUser,
);

await this.updateTranslationsForLanguage(LanguagesEnum.en, {
lotteryAvailable: {
whatHappensContent:
'The property manager will begin to contact applicants in the order of lottery rank, within each lottery preference. When the units are all filled, the property manager will stop contacting applicants. All the units could be filled before the property manager reaches your rank. If this happens, you will not be contacted.',
},
});
await this.updateTranslationsForLanguage(LanguagesEnum.es, {
lotteryAvailable: {
whatHappensContent:
'El administrador de la propiedad comenzará a comunicarse con los solicitantes en el orden de clasificación de la lotería, dentro de cada preferencia de la lotería. Cuando todas las unidades estén ocupadas, el administrador de la propiedad dejará de comunicarse con los solicitantes. Es posible que todas las unidades estén ocupadas antes de que el administrador de la propiedad alcance su clasificación. Si esto sucede, no se comunicarán con usted.',
},
});
await this.updateTranslationsForLanguage(LanguagesEnum.tl, {
lotteryAvailable: {
whatHappensContent:
'Ang tagapamahala ng ari-arian ay magsisimulang makipag-ugnayan sa mga aplikante sa pagkakasunud-sunod ng ranggo ng lottery, sa loob ng bawat kagustuhan sa lottery. Kapag napuno na ang lahat ng unit, hihinto na ang property manager sa pakikipag-ugnayan sa mga aplikante. Maaaring mapunan ang lahat ng unit bago maabot ng property manager ang iyong ranggo. Kung mangyari ito, hindi ka makontak.',
},
});
await this.updateTranslationsForLanguage(LanguagesEnum.vi, {
lotteryAvailable: {
whatHappensContent:
'Người quản lý bất động sản sẽ bắt đầu liên hệ với người nộp đơn theo thứ hạng xổ số, trong mỗi sở thích xổ số. Khi tất cả các đơn vị đã được lấp đầy, người quản lý bất động sản sẽ ngừng liên hệ với người nộp đơn. Tất cả các đơn vị có thể được lấp đầy trước khi người quản lý bất động sản đạt đến thứ hạng của bạn. Nếu điều này xảy ra, bạn sẽ không được liên hệ.',
},
});
await this.updateTranslationsForLanguage(LanguagesEnum.zh, {
lotteryAvailable: {
whatHappensContent:
'物业经理将按照抽签顺序开始联系申请人,每个抽签偏好内都是如此。当所有单元都已满时,物业经理将停止联系申请人。在物业经理达到您的排名之前,所有单元都可能已满。如果发生这种情况,您将不会被联系。',
},
});

await this.markScriptAsComplete(
'update what happens next content in lottery email',
requestingUser,
);
return { success: true };
}

/**
*
* @param req incoming request object
* @returns successDTO
* @description Adds all existing feature flags across Bloom to the database
*/
async addFeatureFlags(req: ExpressRequest): Promise<SuccessDTO> {
const requestingUser = mapTo(User, req['user']);
await this.markScriptAsRunStart('add feature flags', requestingUser);
Expand Down Expand Up @@ -531,40 +587,58 @@ export class ScriptRunnerService {
});
}

async addLotteryTranslationsHelper() {
const updateForLanguage = async (
language: LanguagesEnum,
translationKeys: Record<string, Record<string, string>>,
) => {
let translations;
translations = await this.prisma.translations.findFirst({
where: { language, jurisdictionId: null },
});
async updateTranslationsForLanguage(
Copy link
Collaborator

@ColinBuyck ColinBuyck Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: This is all looking good but do you mind sharing more on this refactor? Specifically the changes around findMany? Is this just a code improvement or would the previous helper not work for this case?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I lost track of this thread! Some of that code is a backport to Core from Doorway, so everything related to translations helpers is now aligned across the two repos. That's the reason for there being more changes than just addressing what's detailed in the issue descriptions.

language: LanguagesEnum,
newTranslations: Record<string, any>,
createIfMissing?: boolean,
) {
let translations;
translations = await this.prisma.translations.findMany({
where: { language },
});

if (!translations) {
translations = await this.prisma.translations.create({
if (!translations?.length) {
if (createIfMissing) {
const createdTranslations = await this.prisma.translations.create({
data: {
language: language,
translations: {},
jurisdictions: undefined,
},
});
translations = [createdTranslations];
} else {
console.log(
`Translations for ${language} don't exist in Bloom database`,
);
return;
}
}

const translationsJSON =
translations.translations as unknown as Prisma.JsonArray;
for (const translation of translations) {
const translationsJSON = translation.translations as Prisma.JsonObject;

Object.keys(newTranslations).forEach((key) => {
translationsJSON[key] = {
...((translationsJSON[key] || {}) as Prisma.JsonObject),
...newTranslations[key],
};
});

// technique taken from
// https://www.prisma.io/docs/orm/prisma-client/special-fields-and-types/working-with-json-fields#advanced-example-update-a-nested-json-key-value
const dataClause = Prisma.validator<Prisma.TranslationsUpdateInput>()({
translations: translationsJSON,
});

await this.prisma.translations.update({
where: { id: translations.id },
data: {
translations: {
...translationsJSON,
...translationKeys,
},
},
where: { id: translation.id },
data: dataClause,
});
};
}
}

async addLotteryTranslationsHelper(createIfMissing?: boolean) {
const enKeys = {
lotteryReleased: {
header: 'Lottery results for %{listingName} are ready to be published',
Expand Down Expand Up @@ -665,11 +739,31 @@ export class ScriptRunnerService {
otherOpportunities4: 'Housing Portal 幫助中心',
},
};
await updateForLanguage(LanguagesEnum.en, enKeys);
await updateForLanguage(LanguagesEnum.es, esKeys);
await updateForLanguage(LanguagesEnum.tl, tlKeys);
await updateForLanguage(LanguagesEnum.vi, viKeys);
await updateForLanguage(LanguagesEnum.zh, zhKeys);
await this.updateTranslationsForLanguage(
LanguagesEnum.en,
enKeys,
createIfMissing,
);
await this.updateTranslationsForLanguage(
LanguagesEnum.es,
esKeys,
createIfMissing,
);
await this.updateTranslationsForLanguage(
LanguagesEnum.tl,
tlKeys,
createIfMissing,
);
await this.updateTranslationsForLanguage(
LanguagesEnum.vi,
viKeys,
createIfMissing,
);
await this.updateTranslationsForLanguage(
LanguagesEnum.zh,
zhKeys,
createIfMissing,
);
}

featureFlags = [
Expand Down
2 changes: 1 addition & 1 deletion api/src/views/lottery-published-applicant.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<td align="center">
<a
class="btn btn-primary"
href="{{ appUrl }}"
href="{{ signInUrl }}"
target="_blank"
style="color:white"
>
Expand Down
24 changes: 23 additions & 1 deletion api/test/unit/services/email.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const translationServiceMock = {

const jurisdictionServiceMock = {
findOne: () => {
return { name: 'Jurisdiction 1' };
return { name: 'Jurisdiction 1', publicUrl: 'https://example.com' };
},
};

Expand Down Expand Up @@ -465,4 +465,26 @@ describe('Testing email service', () => {
expect(emailMock.html).toMatch('Bloom Housing Portal');
});
});

describe('lottery published for applicant', () => {
it('should generate html body', async () => {
const emailArr = ['[email protected]', '[email protected]'];
const service = await module.resolve(EmailService);
await service.lotteryPublishedApplicant(
{ name: 'listing name', id: 'listingId', juris: 'jurisdictionId' },
{ en: emailArr },
);

expect(sendMock).toHaveBeenCalled();
const emailMock = sendMock.mock.calls[0][0];
expect(emailMock.to).toEqual(emailArr);
expect(emailMock.subject).toEqual(
'New Housing Lottery Results Available',
);
expect(emailMock.html).toMatch(
/href="https:\/\/example\.com\/en\/sign-in"/,
);
expect(emailMock.html).toMatch(/please visit https:\/\/example\.com/);
});
});
});
2 changes: 1 addition & 1 deletion api/test/unit/services/script-runner.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ describe('Testing script runner service', () => {
prisma.scriptRuns.findUnique = jest.fn().mockResolvedValue(null);
prisma.scriptRuns.create = jest.fn().mockResolvedValue(null);
prisma.scriptRuns.update = jest.fn().mockResolvedValue(null);
prisma.translations.findFirst = jest.fn().mockResolvedValue(undefined);
prisma.translations.findMany = jest.fn().mockResolvedValue(undefined);
prisma.translations.update = jest.fn().mockResolvedValue(null);
prisma.translations.create = jest.fn().mockReturnValue({
language: LanguagesEnum.en,
Expand Down
16 changes: 16 additions & 0 deletions shared-helpers/src/types/backend-swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2327,6 +2327,22 @@ export class ScriptRunnerService {
axios(configs, resolve, reject)
})
}
/**
* A script that updates the "what happens next" content in lottery email
*/
updatesWhatHappensInLotteryEmail(options: IRequestOptions = {}): Promise<SuccessDTO> {
return new Promise((resolve, reject) => {
let url = basePath + "/scriptRunner/updatesWhatHappensInLotteryEmail"

const configs: IRequestConfig = getConfigs("put", "application/json", url, options)

let data = null

configs.data = data

axios(configs, resolve, reject)
})
}
/**
* A script that adds existing feature flags into the feature flag table
*/
Expand Down