Skip to content
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
1 change: 1 addition & 0 deletions api/.jest/setEnvVars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ process.env.PAY_ACCOUNT_TRANSACTIONS_URL = "https://selfservice.payments.service
process.env.SUBMISSION_EMAIL_ADDRESS = "pye@cautionyourblast.com";
process.env.POST_EMAILS = '{"the British Embassy Tirana": "pye+albania@cautionyourblast.com", "the British Embassy Rome": "pye+rome@cautionyourblast.com"}';
process.env.NOTIFY_TEMPLATE_AFFIRMATION_USER_CONFIRMATION = "ABC";
process.env.NOTIFY_TEMPLATE_AFFIRMATION_USER_CONFIRMATION_SIMPLIFIED = "ABC";
process.env.NOTIFY_TEMPLATE_CNI_USER_CONFIRMATION = "ABC";
process.env.NOTIFY_TEMPLATE_CNI_USER_POSTAL_CONFIRMATION = "ABC";
process.env.NOTIFY_TEMPLATE_EXCHANGE_USER_CONFIRMATION = "ABC";
Expand Down
1 change: 1 addition & 0 deletions api/config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"Notify": {
"Template": {
"affirmationUserConfirmation": "NOTIFY_TEMPLATE_AFFIRMATION_USER_CONFIRMATION",
"affirmationUserConfirmationSimplified": "NOTIFY_TEMPLATE_AFFIRMATION_USER_CONFIRMATION_SIMPLIFIED",
"cniUserConfirmation": "NOTIFY_TEMPLATE_CNI_USER_CONFIRMATION",
"cniUserPostalConfirmation": "NOTIFY_TEMPLATE_CNI_USER_POSTAL_CONFIRMATION",
"exchangeUserConfirmation": "NOTIFY_TEMPLATE_EXCHANGE_USER_CONFIRMATION",
Expand Down
31 changes: 28 additions & 3 deletions api/src/middlewares/services/QueueService/QueueService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import config from "config";
import { ApplicationError } from "../../../ApplicationError";
import { QueueConfig, QueueName } from "./QueueConfig";

const DEADLOCK_ERROR_CODE = "40P01";
const CREATE_QUEUE_ATTEMPTS = 5;

type JobWithMetadata<T = {}> = T & {
reference?: string;
metadata?: { reference: string };
Expand All @@ -27,17 +30,39 @@ export class QueueService {
NOTIFY_SEND: new QueueConfig("NOTIFY_SEND"),
NOTIFY_PROCESS: new QueueConfig("NOTIFY_PROCESS"),
};
boss.start().then(() => {
boss.start().then(async () => {
this.logger.info("Creating queues");
this.createQueues();
await this.createQueues();
}).catch((err) => {
this.logger.error({ err }, "Queue startup failed");
throw err;
});
}

async createQueueWithRetry(queueName: string) {
for (let attempt = 1; attempt <= CREATE_QUEUE_ATTEMPTS; attempt++) {
try {
await this.boss.createQueue(queueName);
return;
} catch (err: any) {
const isDeadlock = err?.code === DEADLOCK_ERROR_CODE;
const isLastAttempt = attempt === CREATE_QUEUE_ATTEMPTS;

if (!isDeadlock || isLastAttempt) {
throw err;
}

this.logger.warn({ queueName, attempt, err }, "Deadlock detected while creating queue. Retrying.");
await new Promise((resolve) => setTimeout(resolve, attempt * 500));
}
}
}

async createQueues() {
const configs = Object.keys(this.configs);
for (const key of configs) {
this.logger.info(`Creating queue ${key}`);
await this.boss.createQueue(key);
await this.createQueueWithRetry(key);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class SubmitService {
this.logger.info({reference, caseProcessJob}, `SES_PROCESS job queued successfully for ${reference}`);
}

const userProcessJob = await this.userService.sendToProcessQueue(answers, { reference, payment: metadata.pay, type, postal: metadata.postal });
const userProcessJob = await this.userService.sendToProcessQueue(answers, { reference, payment: metadata.pay, type, postal: metadata.postal, source: metadata.source });

this.logger.info({ reference, userProcessJob }, `NOTIFY_PROCESS job queued successfully for ${reference}`);
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,24 @@ import { UserTemplateGroup } from "./UserTemplateGroup";

export class MarriageUserTemplates implements UserTemplateGroup {
templates: {
affirmation: Record<PostalVariant, string>;
affirmation: {
simplified: Record<PostalVariant, string>;
legacy: Record<PostalVariant, string>;
};
exchange: Record<PostalVariant, string>;
cni: Record<CNISubGroup, Record<PostalVariant, string>>;
};
constructor() {
this.templates = {
affirmation: {
inPerson: config.get<string>("Notify.Template.affirmationUserConfirmation"),
postal: config.get<string>("Notify.Template.affirmationUserConfirmation"),
simplified: {
inPerson: config.get<string>("Notify.Template.affirmationUserConfirmationSimplified"),
postal: config.get<string>("Notify.Template.affirmationUserConfirmationSimplified"),
},
legacy: {
inPerson: config.get<string>("Notify.Template.affirmationUserConfirmation"),
postal: config.get<string>("Notify.Template.affirmationUserConfirmation"),
},
},
cni: {
cni: {
Expand All @@ -39,7 +48,7 @@ export class MarriageUserTemplates implements UserTemplateGroup {
};
}

getTemplate(data: { answers: AnswersHashMap; metadata: { reference: string; payment?: PayMetadata; type: FormType; postal?: boolean } }) {
getTemplate(data: { answers: AnswersHashMap; metadata: { reference: string; payment?: PayMetadata; type: FormType; postal?: boolean; source?: string } }) {
const { answers, metadata } = data;
const { type } = data.metadata;

Expand All @@ -51,13 +60,34 @@ export class MarriageUserTemplates implements UserTemplateGroup {
if (type === "cni") {
const serviceSubtype = (answers.service ?? "cni") as MarriageFormType;
template = this.templates.cni[serviceSubtype][postalVariant];
} else if (type === "affirmation") {
const isSimplified = metadata.source === "simplified-marriage-v1";
template = this.templates.affirmation[isSimplified ? "simplified" : "legacy"][postalVariant];
} else {
template = this.templates.exchange[postalVariant];
}

template ??= this.templates[type][postalVariant];

const personalisationBuilder = getPersonalisationBuilder(type);

builder ??= personalisationBuilder[postalVariant];
builder = personalisationBuilder[postalVariant];

// Only the simplified affirmation template uses a `duration` placeholder.
// Wrap the shared builder to append it without affecting other templates.
if (type === "affirmation" && metadata.source === "simplified-marriage-v1") {
const baseBuilder = builder;
builder = (answers: AnswersHashMap, meta: { reference: string; payment?: PayMetadata; type?: FormType }) => {
const personalisation = baseBuilder(answers, meta);
const country = answers.country as string;
const countryContext = additionalContexts.marriage.countries[country];
const hasPreviousNameByDeedPoll = this.hasSimplifiedPreviousNameStatus(answers.nameChangedByDeedPoll);
const hasPreviousNameByMarriage = this.hasSimplifiedPreviousNameStatus(answers.nameChangedByMarriage);
return {
...personalisation,
duration: countryContext?.duration || "3 months",
previousNames: hasPreviousNameByDeedPoll || hasPreviousNameByMarriage,
};
};
}

return {
template,
Expand All @@ -76,4 +106,17 @@ export class MarriageUserTemplates implements UserTemplateGroup {

return postalSupport ? "postal" : "inPerson";
}

private hasSimplifiedPreviousNameStatus(value: string | boolean | undefined) {
if (typeof value !== "string") {
return false;
}

const normalised = value.trim().toLowerCase();
return (
normalised === "once" ||
normalised === "name changed more than once" ||
normalised === "yesmorethanonce"
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FormType, PayMetadata } from "../../../../types/FormDataBody";

type getTemplateParams = {
answers: AnswersHashMap;
metadata: { reference: string; payment?: PayMetadata; type: FormType; postal?: boolean };
metadata: { reference: string; payment?: PayMetadata; type: FormType; postal?: boolean; source?: string };
};

type Builder = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class UserTemplates {
}
}

getTemplate(data: { answers: AnswersHashMap; metadata: { reference: string; payment?: PayMetadata; type: FormType; postal?: boolean } }) {
getTemplate(data: { answers: AnswersHashMap; metadata: { reference: string; payment?: PayMetadata; type: FormType; postal?: boolean; source?: string } }) {
const { type } = data.metadata;

if (this.isMarriageFormType(type)) {
Expand Down
4 changes: 2 additions & 2 deletions api/src/middlewares/services/UserService/UserService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ export class UserService {
/**
* Stores the user's answers in the queue for processing.
*/
async sendToProcessQueue(answers: AnswersHashMap, metadata: { reference: string; payment?: PayMetadata; type: FormType; postal?: boolean }) {
async sendToProcessQueue(answers: AnswersHashMap, metadata: { reference: string; payment?: PayMetadata; type: FormType; postal?: boolean; source?: string }) {
return await this.queueService.sendToQueue("NOTIFY_PROCESS", { answers, metadata });
}

async sendEmailToUser(data: { answers: AnswersHashMap; metadata: { reference: string; payment?: PayMetadata; type: FormType; postal?: boolean } }) {
async sendEmailToUser(data: { answers: AnswersHashMap; metadata: { reference: string; payment?: PayMetadata; type: FormType; postal?: boolean; source?: string } }) {
const { answers, metadata } = data;
const { reference } = metadata;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ jest.mock("config", () => ({
get(setting) {
const templates = {
"Notify.Template.affirmationUserConfirmation": "affirmation-template",
"Notify.Template.affirmationUserConfirmationSimplified": "affirmation-simplified-template",
"Notify.Template.cniUserConfirmation": "cni-template",
"Notify.Template.cniUserPostalConfirmation": "cni-postal-template",
"Notify.Template.mscUserConfirmation": "msc-template",
Expand Down Expand Up @@ -47,8 +48,9 @@ beforeEach(() => {

describe("sendEmailToUser - Marriage templates", () => {
test.each`
label | answers | metadata | template
${"affirmation"} | ${{}} | ${{ type: "affirmation" }} | ${"affirmation-template"}
label | answers | metadata | template
${"affirmation - legacy"} | ${{}} | ${{ type: "affirmation" }} | ${"affirmation-template"}
${"affirmation - simplified"} | ${{}} | ${{ type: "affirmation", source: "simplified-marriage-v1" }} | ${"affirmation-simplified-template"}
${"exchange - inPerson"} | ${{}} | ${{ type: "exchange" }} | ${"exchange-template"}
${"exchange - postal"} | ${{}} | ${{ type: "exchange", postal: true }} | ${"exchange-postal-template"}
${"exchange - croatia"} | ${{ country: "Croatia" }} | ${{ type: "exchange" }} | ${"exchange-template"}
Expand All @@ -66,6 +68,91 @@ describe("sendEmailToUser - Marriage templates", () => {
})
);
});

test("affirmation - simplified adds previousNames when either name-change path exists", async () => {
await userService.sendEmailToUser({
answers: {
firstName: "test",
emailAddress: "test@example.com",
country: "Italy",
nameChangedByMarriage: "name changed more than once",
nameChangedByDeedPoll: "false",
previousNameByMarriage: "No",
previousNameByDeedPoll: "No",
},
metadata: {
type: "affirmation",
source: "simplified-marriage-v1",
reference: "ref",
},
});

expect(sendEmailSpy).toHaveBeenCalledWith(
expect.objectContaining({
options: expect.objectContaining({
personalisation: expect.objectContaining({
previousNames: true,
}),
}),
})
);
});

test("affirmation - simplified does not add previousNames for explicit negative values", async () => {
await userService.sendEmailToUser({
answers: {
firstName: "test",
emailAddress: "test@example.com",
country: "Italy",
nameChangedByMarriage: "No",
nameChangedByDeedPoll: "false",
previousNameByMarriage: "No",
previousNameByDeedPoll: "Old Name",
},
metadata: {
type: "affirmation",
source: "simplified-marriage-v1",
reference: "ref",
},
});

expect(sendEmailSpy).toHaveBeenCalledWith(
expect.objectContaining({
options: expect.objectContaining({
personalisation: expect.objectContaining({
previousNames: false,
}),
}),
})
);
});

test("affirmation - simplified adds previousNames for deed poll YesMoreThanOnce", async () => {
await userService.sendEmailToUser({
answers: {
firstName: "test",
emailAddress: "test@example.com",
country: "Italy",
nameChangedByMarriage: "no",
nameChangedByDeedPoll: "YesMoreThanOnce",
},
metadata: {
type: "affirmation",
source: "simplified-marriage-v1",
reference: "ref",
},
});

expect(sendEmailSpy).toHaveBeenCalledWith(
expect.objectContaining({
options: expect.objectContaining({
personalisation: expect.objectContaining({
previousNames: true,
}),
}),
})
);
});
});

describe("sendEmailToUser - certifyCopy", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ export const personalisationTypeMap: Record<MarriageFormType, PersonalisationFun
exchange: getExchangePersonalisations,
};

function hasNoPreviousMarriage(maritalStatus: unknown) {
return maritalStatus === "Never married" || maritalStatus === "Single";
}

export function getAffirmationPersonalisations(fields: AnswersHashMap) {
return {
previouslyMarried: fields.maritalStatus !== "Never married",
previouslyMarried: !hasNoPreviousMarriage(fields.maritalStatus),
religious: fields.oathType === "Religious",
};
}
Expand All @@ -20,7 +24,7 @@ export function getCNIPersonalisations(fields: AnswersHashMap) {
return {
livesInCountry: fields.livesInCountry === true,
livesAbroad: !fields.livesInCountry,
previouslyMarried: fields.maritalStatus !== "Never married",
previouslyMarried: !hasNoPreviousMarriage(fields.maritalStatus),
religious: fields.oathType === "Religious",
croatiaCertNeeded: fields.certRequired === true,
countryIsItaly: fields.country === "Italy",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function buildPostalPersonalisation(answers: AnswersHashMap, metadata: {
const isSuccessfulPayment = metadata.payment?.state?.status === "success";
const country = answers["country"] as string;
const post = answers["post"] as string;
const userHadPreviousMarriage = answers.maritalStatus !== "Never married";
const userHadPreviousMarriage = answers.maritalStatus !== "Never married" && answers.maritalStatus !== "Single";

const additionalContext = getPostalAdditionalContext(country, post);
const personalisationType = getAdditionalPersonalisationsType(answers, metadata.type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,18 @@ test("getAffirmationPersonalisations returns the correct personalisations given
});
});

test("getAffirmationPersonalisations treats simplified 'Single' as not previously married", () => {
const answers = {
maritalStatus: "Single",
oathType: "Non-religious",
};

expect(getAffirmationPersonalisations(answers)).toStrictEqual({
previouslyMarried: false,
religious: false,
});
});

test("getCNIPersonalisations returns the correct personalisations given all positive answers", () => {
const answers = {
livesInCountry: true,
Expand Down Expand Up @@ -210,6 +222,17 @@ test("getCNIPersonalisations returns the correct personalisations given all nega
});
});

test("buildPostalPersonalisation treats simplified 'Single' as no previous marriage", () => {
const answers = {
...marriageAnswers,
maritalStatus: "Single",
};

const personalisation = buildPostalPersonalisation(answers, { reference: "1234", type: "cni" });

expect(personalisation.userHadPreviousMarriage).toBe(false);
});

test("buildJobData should return the correct personalisation for a certify a copy in-person email", () => {
const personalisation = CertifyCopyPersonalisationBuilder.inPerson(certifyCopyAnswers, { type: "certifyCopy", reference: "1234" });

Expand Down
Loading