diff --git a/api/src/errors.ts b/api/src/errors.ts index 2ecb4461..4634bcc3 100644 --- a/api/src/errors.ts +++ b/api/src/errors.ts @@ -17,7 +17,7 @@ /** * Category of the error - this is likely to match the service it came from */ -export type ErrorTypes = "WEBHOOK" | "SES" | "NOTIFY" | "QUEUE" | "GENERIC"; +export type ErrorTypes = "WEBHOOK" | "SES" | "NOTIFY" | "QUEUE" | "ORBIT" | "GENERIC"; /** * Error code for the matching ErrorType. @@ -26,13 +26,14 @@ type WebhookErrorCode = "VALIDATION"; type SESErrorCode = "MISSING_ANSWER" | "PROCESS_VALIDATION" | "UNKNOWN"; type NotifyErrorCode = "PROCESS_VALIDATION" | "UNKNOWN"; type QueueErrorCode = "SES_PROCESS_ERROR" | "NOTIFY_PROCESS_ERROR" | "NOTIFY_SEND_ERROR" | "SES_SEND_ERROR"; +type OrbitErrorCode = "ORBIT_ERROR"; type GenericErrorCode = "UNKNOWN" | "RATE_LIMIT_EXCEEDED"; /** * Union of all the different ErrorCode. */ -export type ErrorCode = WebhookErrorCode | SESErrorCode | NotifyErrorCode | QueueErrorCode | GenericErrorCode; +export type ErrorCode = WebhookErrorCode | SESErrorCode | NotifyErrorCode | QueueErrorCode | OrbitErrorCode | GenericErrorCode; /** * {@ErrorRecord} uses `Record`, which means every key passed into the generic, must be implemented @@ -68,12 +69,17 @@ const QUEUE: ErrorRecord = { SES_SEND_ERROR: "unable to queue SES_SEND_ERROR", }; +const ORBIT: ErrorRecord = { + ORBIT_ERROR: "orbit error from case mgmt", +}; + type ErrorRecords = { WEBHOOK: typeof WEBHOOK; SES: typeof SES; NOTIFY: typeof NOTIFY; QUEUE: typeof QUEUE; GENERIC: typeof GENERIC; + ORBIT: typeof ORBIT; }; export const ERRORS: ErrorRecords = { WEBHOOK, @@ -81,4 +87,5 @@ export const ERRORS: ErrorRecords = { NOTIFY, QUEUE, GENERIC, + ORBIT, }; diff --git a/api/src/middlewares/services/CaseService/certifyCopy/CertifyCopyCaseService.ts b/api/src/middlewares/services/CaseService/certifyCopy/CertifyCopyCaseService.ts index bc87840f..3a14acd4 100644 --- a/api/src/middlewares/services/CaseService/certifyCopy/CertifyCopyCaseService.ts +++ b/api/src/middlewares/services/CaseService/certifyCopy/CertifyCopyCaseService.ts @@ -8,7 +8,6 @@ import { isFieldType } from "../../../../utils"; import { getPostForCertifyCopy } from "../../utils/getPost"; import { CertifyCopyProcessQueueData, PaymentData } from "../types"; import { PaymentViewModel } from "../utils/PaymentViewModel"; -import { CaseService } from "../types"; import { reorderSectionsWithNewName } from "../utils/reorderSectionsWithNewName"; import { order, remap } from "./mappings"; import { createRemapper } from "../utils/createRemapper"; @@ -16,8 +15,9 @@ import { CertifyCopyProcessQueueDataInput } from "../types"; import { getPostEmailAddress } from "../../utils/getPostEmailAddress"; import { QueueService } from "../../QueueService"; import logger, { Logger } from "pino"; +import { SESCaseService } from "../types/CaseService"; -export class CertifyCopyCaseService implements CaseService { +export class CertifyCopyCaseService implements SESCaseService { logger: Logger; queueService: QueueService; templates: { diff --git a/api/src/middlewares/services/CaseService/consularLetter/ConsularLetterCaseService.ts b/api/src/middlewares/services/CaseService/consularLetter/ConsularLetterCaseService.ts index 322194bc..251544d5 100644 --- a/api/src/middlewares/services/CaseService/consularLetter/ConsularLetterCaseService.ts +++ b/api/src/middlewares/services/CaseService/consularLetter/ConsularLetterCaseService.ts @@ -5,7 +5,6 @@ import * as handlebars from "handlebars"; import { isFieldType } from "../../../../utils"; import { ConsularLetterProcessQueueData, PaymentData } from "../types"; import { PaymentViewModel } from "../utils/PaymentViewModel"; -import { CaseService } from "../types"; import { reorderSectionsWithNewName } from "../utils/reorderSectionsWithNewName"; import { order, remap } from "./mappings"; import { createRemapper } from "../utils/createRemapper"; @@ -13,8 +12,9 @@ import { ConsularLetterProcessQueueDataInput } from "../types"; import { getPostEmailAddress } from "../../utils/getPostEmailAddress"; import { QueueService } from "../../QueueService"; import logger, { Logger } from "pino"; +import { SESCaseService } from "../types/CaseService"; -export class ConsularLetterCaseService implements CaseService { +export class ConsularLetterCaseService implements SESCaseService { logger: Logger; queueService: QueueService; templates: { diff --git a/api/src/middlewares/services/CaseService/marriage/MarriageCaseService.ts b/api/src/middlewares/services/CaseService/marriage/MarriageCaseService.ts index 49558284..77c7def1 100644 --- a/api/src/middlewares/services/CaseService/marriage/MarriageCaseService.ts +++ b/api/src/middlewares/services/CaseService/marriage/MarriageCaseService.ts @@ -1,165 +1,37 @@ -import { FormField } from "../../../../types/FormField"; -import marriageSubmissionTemplate from "./marriageSubmissionTemplate"; -import { MarriageFormType } from "../../../../types/FormDataBody"; -import { remappers } from "./remappers"; -import { getAnswerOrThrow } from "../utils/getAnswerOrThrow"; -import { reorderers } from "./reorderers"; -import { getApplicationTypeName } from "../utils/getApplicationTypeName"; -import { answersHashMap } from "../../helpers"; -import config from "config"; -import * as handlebars from "handlebars"; -import { isFieldType } from "../../../../utils"; -import { getPostForMarriage } from "../../utils/getPost"; -import { MarriageProcessQueueData, PaymentData, CaseService } from "../types"; -import { PaymentViewModel } from "../utils/PaymentViewModel"; -import { MarriageProcessQueueDataInput } from "../types"; -import { getPostEmailAddress } from "../../utils/getPostEmailAddress"; import logger, { Logger } from "pino"; import { QueueService } from "../../QueueService"; -import { AnswersHashMap } from "../../../../types/AnswersHashMap"; -import { MarriageTemplateType } from "../../utils/types"; +import { OrbitCaseService } from "../types/CaseService"; +import { ApplicationError } from "../../../../ApplicationError"; -export class MarriageCaseService implements CaseService { +export class MarriageCaseService implements OrbitCaseService { logger: Logger; queueService: QueueService; - templates: { - SES: HandlebarsTemplateDelegate; - Notify: Record<"postAlert", string>; - }; + constructor({ queueService }) { this.queueService = queueService; - this.templates = { - SES: MarriageCaseService.createTemplate(marriageSubmissionTemplate), - Notify: { - postAlert: config.get("Notify.Template.postNotification"), - }, - }; - this.logger = logger().child({ service: "SES" }); - } - - buildProcessQueueData(input: MarriageProcessQueueDataInput): MarriageProcessQueueData { - const { fields, reference, metadata, type } = input; - return { - fields, - metadata: { - reference, - payment: metadata.pay, - type: type, - postal: metadata.postal, - }, - }; - } - - /** - * This will add all the parameters needed to process the email to the queue. The NOTIFY_PROCESS queue will pick up - * this message and make a post request to notarial-api/forms/emails/staff - */ - async sendToProcessQueue(data: MarriageProcessQueueData) { - return await this.queueService.sendToQueue("SES_PROCESS", data); - } - - async sendEmail(data: MarriageProcessQueueData) { - const jobData = this.buildJobData(data); - return await this.queueService.sendToQueue("SES_SEND", jobData); - } - - getEmailBody(data: { fields: FormField[]; payment?: PaymentData; reference: string; postal?: boolean }, type: MarriageTemplateType) { - const { fields, payment, reference, postal } = data; - const remapperName = postal ? `${type}Postal` : type; - - const remapFields = remappers[remapperName]; - const remapped = remapFields(fields); - - const { information } = remapped; - - const reorderer = reorderers[remapperName]; - const reordered = reorderer(remapped); - const country = getAnswerOrThrow(information, "country"); - const post = getPostForMarriage(country, information.post?.answer); - let oathType, jurats; - if ((type === "affirmation" || type === "cni") && !postal) { - oathType = getAnswerOrThrow(information, "oathType"); - jurats = getAnswerOrThrow(information, "jurats"); - } - return this.templates.SES({ - post, - type: getApplicationTypeName(type), - reference, - payment, - country, - oathType, - jurats, - certifyPassport: information.certifyPassport?.answer ?? false, - questions: reordered, - }); + this.logger = logger().child({ service: "MarriageCaseService" }); } - buildJobData(data: MarriageProcessQueueData) { - const { fields, metadata } = data; - const { reference, payment, type, postal } = metadata; - const answers = answersHashMap(fields); - let paymentViewModel: PaymentData | undefined; - + async send(data): Promise { + /** + * TODO: This can be implemented to POST to Case Management Service, or handled by the form runner directly. + * In which case this method should be a "no-op", or unimplemented entirely. + */ try { - paymentViewModel = PaymentViewModel(payment, answers.country as string); - } catch (e) { - this.logger.warn(`Payment details for ${reference} could not be parsed. Payment details will not be shown on the email.`); - } - - const country = answers.country as string; - const templateType = this.getTemplateType(answers, type); - const emailBody = this.getEmailBody({ fields, payment: paymentViewModel, reference, postal }, templateType); - const post = getPostForMarriage(country, answers.post as string); - const onCompleteJob = this.getPostAlertData(country, post, reference); - return { - subject: `Local marriage application - ${post} – ${reference}`, - body: emailBody, - attachments: fields.filter(isFieldType("file")), - reference, - metadata: { - reference, - type, - }, - onComplete: { - queue: "NOTIFY_SEND", - ...(onCompleteJob && { job: onCompleteJob }), - }, - }; - } - - getPostAlertData(country: string, post: string, reference: string) { - const emailAddress = getPostEmailAddress(post); - if (!emailAddress) { - this.logger.error( - { code: "UNRECOGNISED_SERVICE_APPLICATION" }, - `No email address found for the specified post – ${country} - ${post} – reference ${reference}.` - ); - return; + // const { reference } = postRequest(data); + // This reference will be shown to the user on the application complete page. + // return reference; + } catch (err) { + // handle response errors etc. + if (err.response) { + // add additional logging: + this.logger.error({ err }, "Case management api responded with an error"); + // This error will be logged, ensure that `ORBIT_ERROR` has been added to alerting. + throw new ApplicationError("ORBIT", "ORBIT_ERROR", err.response.statusCode); + } + + throw new ApplicationError("ORBIT", "ORBIT_ERROR", 500); } - - return { - template: this.templates.Notify.postAlert, - emailAddress, - reference, - options: { - personalisation: { - post, - reference, - }, - reference, - }, - }; - } - - getTemplateType(answers: AnswersHashMap, type: MarriageFormType): MarriageTemplateType { - if (answers.service) { - return answers.service as MarriageTemplateType; - } - return type; - } - - private static createTemplate(template: string) { - return handlebars.compile(template); } } diff --git a/api/src/middlewares/services/CaseService/requestDocument/requestDocumentCaseService.ts b/api/src/middlewares/services/CaseService/requestDocument/requestDocumentCaseService.ts index 14643425..aae38b02 100644 --- a/api/src/middlewares/services/CaseService/requestDocument/requestDocumentCaseService.ts +++ b/api/src/middlewares/services/CaseService/requestDocument/requestDocumentCaseService.ts @@ -7,7 +7,6 @@ import { isFieldType } from "../../../../utils"; import { getPostForRequestDocument } from "../../utils/getPost"; import { CertifyCopyProcessQueueData, PaymentData } from "../types"; import { PaymentViewModel } from "../utils/PaymentViewModel"; -import { CaseService } from "../types"; import { reorderSectionsWithNewName } from "../utils/reorderSectionsWithNewName"; import { order, remap } from "./mappings"; import { createRemapper } from "../utils/createRemapper"; @@ -16,8 +15,9 @@ import { getPostEmailAddress } from "../../utils/getPostEmailAddress"; import { QueueService } from "../../QueueService"; import logger, { Logger } from "pino"; import { RequestDocumentAnswersHashmap } from "../../../../types/AnswersHashMap"; +import { SESCaseService } from "../types/CaseService"; -export class RequestDocumentCaseService implements CaseService { +export class RequestDocumentCaseService implements SESCaseService { logger: Logger; queueService: QueueService; templates: { diff --git a/api/src/middlewares/services/CaseService/types/CaseService.ts b/api/src/middlewares/services/CaseService/types/CaseService.ts index 965bbbb9..6fa50719 100644 --- a/api/src/middlewares/services/CaseService/types/CaseService.ts +++ b/api/src/middlewares/services/CaseService/types/CaseService.ts @@ -7,7 +7,16 @@ import { PaymentData } from "./PaymentData"; import { ProcessQueueDataInput } from "./ProcessQueueDataInput"; import { Logger } from "pino"; -export interface CaseService { +export type CaseService = OrbitCaseService | SESCaseService; + +export interface OrbitCaseService { + logger: Logger; + queueService: QueueService; + + send(data): Promise; +} + +export interface SESCaseService { logger: Logger; queueService: QueueService; diff --git a/api/src/middlewares/services/SubmitService/SubmitService.ts b/api/src/middlewares/services/SubmitService/SubmitService.ts index 8d650861..ab7026fb 100644 --- a/api/src/middlewares/services/SubmitService/SubmitService.ts +++ b/api/src/middlewares/services/SubmitService/SubmitService.ts @@ -3,8 +3,9 @@ import { FormDataBody } from "../../../types"; import { answersHashMap, flattenQuestions } from "../helpers"; import { UserService } from "../UserService"; import { MarriageCaseService, RequestDocumentCaseService, CertifyCopyCaseService, ConsularLetterCaseService } from "../CaseService"; -import { getCaseServiceName } from "../utils/getCaseServiceName"; +import { ExpressCaseServices, getCaseServiceName } from "../utils/getCaseServiceName"; import { CaseService } from "../CaseService/types"; +import { OrbitCaseService, SESCaseService } from "../CaseService/types/CaseService"; const { customAlphabet } = require("nanoid"); const nanoid = customAlphabet("1234567890ABCDEFGHIJKLMNPQRSTUVWXYZ-_", 10); @@ -47,27 +48,49 @@ export class SubmitService { const formFields = flattenQuestions(questions); const answers = answersHashMap(formFields); const { pay, type } = metadata; - const reference = metadata?.pay?.reference ?? this.generateId(); + let reference = metadata?.pay?.reference ?? this.generateId(); + let orbitReference; const caseServiceName = getCaseServiceName(type); + if (pay) { pay.total = fees?.total; } try { - const caseService: CaseService = this[caseServiceName]; - const processQueueData = caseService.buildProcessQueueData({ - fields: formFields, - reference, - type, - metadata, - }); - const caseProcessJob = await caseService.sendToProcessQueue(processQueueData); - this.logger.info({ reference, caseProcessJob }, `SES_PROCESS job queued successfully for ${reference}`); + const caseService = this.getCaseService(caseServiceName); + + if (this.caseServiceIsOrbitCaseService(caseService)) { + orbitReference = await caseService.send(formData); + + this.logger.info({ reference, orbitReference }, `Orbit responded with ${orbitReference}`); + } + + if (this.caseServiceIsSESCaseService(caseService)) { + const processQueueData = caseService.buildProcessQueueData({ + fields: formFields, + reference, + type, + metadata, + }); + + const caseProcessJob = await caseService.sendToProcessQueue(processQueueData); + 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 }); this.logger.info({ reference, userProcessJob }, `NOTIFY_PROCESS job queued successfully for ${reference}`); } catch (e) { + if (e.name === "ORBIT") { + /** + * Rethrown application errors will be caught by {@link errorHandler}, which will respond to + * the client (forms-worker) with a http error, and will keep the job in an error or retry state. + * It cannot be handled like SES Cases, since we are relying on Orbit's case reference. + * + */ + throw e; + } + /** * Even though the data did not queue correctly, the user's data is safe in the /queue database, so we can respond with the reference number. * The reference number is returned so that the user can get support / get refunds etc. The user's submission should be retried. @@ -78,8 +101,25 @@ export class SubmitService { ); } + /** + * This reference is shown to the user + */ return { reference, }; } + + getCaseService(name: keyof ExpressCaseServices) { + return this[name]; + } + + caseServiceIsOrbitCaseService(caseService: CaseService): caseService is OrbitCaseService { + return caseService instanceof MarriageCaseService; + } + + caseServiceIsSESCaseService(caseService: CaseService): caseService is SESCaseService { + return ( + caseService instanceof CertifyCopyCaseService || caseService instanceof RequestDocumentCaseService || caseService instanceof ConsularLetterCaseService + ); + } } diff --git a/api/src/middlewares/services/utils/getCaseServiceName.ts b/api/src/middlewares/services/utils/getCaseServiceName.ts index 0307c824..c0d26dc1 100644 --- a/api/src/middlewares/services/utils/getCaseServiceName.ts +++ b/api/src/middlewares/services/utils/getCaseServiceName.ts @@ -1,6 +1,6 @@ import { FormType } from "../../../types/FormDataBody"; -type ExpressCaseServices = Pick< +export type ExpressCaseServices = Pick< Express.Application["services"], "marriageCaseService" | "certifyCopyCaseService" | "requestDocumentCaseService" | "consularLetterCaseService" >;