From 29c485bd181d4cba2e3e283d63cb7446eadc0adb Mon Sep 17 00:00:00 2001 From: Jen Duong Date: Fri, 5 Apr 2024 16:33:00 +0100 Subject: [PATCH] chore: documentation and startup scripts (#71) * fix: remove rethrow * chore: add init.db for docker startup * Add docs for postgres * chore: add env var docs * chore: add troubleshooting guide, add monitorStateIntervalInSeconds * chore: switch to using /notarial db * chore: add dotenv and make notify api key non-required docker compose env var * fix: URLs and refer to jobs on this project --- README.md | 35 +++- TROUBLESHOOTING.md | 151 ++++++++++++++++++ api/README.md | 52 +++++- api/config/custom-environment-variables.json | 9 -- api/config/default.js | 3 +- docker-compose.yml | 21 ++- init.sql | 1 + .../config/custom-environment-variables.json | 7 +- worker/config/default.js | 9 +- worker/package.json | 1 + worker/src/Consumer/getConsumer.ts | 13 +- worker/src/README.md | 149 ++++++++++++----- .../notify/workers/notifySendHandler.ts | 4 +- worker/src/queues/ses/helpers/FileService.ts | 3 - yarn.lock | 8 + 15 files changed, 391 insertions(+), 75 deletions(-) create mode 100644 TROUBLESHOOTING.md create mode 100644 init.sql diff --git a/README.md b/README.md index 834d1bf4..311af1c1 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ This ia a monorepo used for the notarial services apis, which power the [prove y ## Workspaces -Currently, there is only workspace in this project: * [api](./api/README.md) +* [worker](./worker/README.md) ### Getting started with Docker You may use docker and docker compose to build and start the project with the right components (e.g. database, microservices), but will not be able to run the application(s) in dev mode. @@ -30,12 +30,43 @@ docker compose down docker compose -d --build ``` +### Running the applications locally (without docker) + +You may still need docker to start the databases. + +From the root directory, +`docker compose up postgres` + +This will start the postgres container running on port 5432; with username `user` and password `root`, +and a database created called `notarial` (`postgres://user:root@localhost:5432/notarial`). +[`init.sql`](init.sql) is also loaded as a volume into the container, and will create a database named `queue` +(`postgres://user:root@localhost:5432/queue`). This is useful if you are going to be running [forms-worker](https://github.com/UKForeignOffice/forms-queue) +at the same time. If you are running postgres from this repo, you do not need to run it in `forms-queue`. + + +1. If you wish to send emails locally, you will need to authenticate your terminal with AWS. (`formsawsauth prod`) +2. Start the api `yarn api start:local` +3. start the worker `NOTIFY_API_KEY=".." yarn worker start:local` +4. Send a post request to `http://localhost:9000/forms`, use the payload found in `notarial-api/api/src/middlewares/services/UserService/personalisation/__tests__/fixtures/testData.ts`, or run the form runner locally, with the webhook configured to `http://localhost:9000/forms`. + + ### Formatting This project uses ESLint and Prettier to ensure consistent formatting. It is recommended that you add and turn on the prettier plugin for your IDE, and reformat on save. ## CI/CD -There is a CI/CD pipeline currently set up for deploying new versions of the project to test environments. For more information, please refer to the [CI/CD docs](https://github.com/UKForeignOffice/notarial-api/blob/main/docs/ci.md) +There is a CI/CD pipeline currently set up for deploying new versions of the project to test environments. For more information, please refer to the [CI/CD docs](docs/ci.md) + +This project uses [semantic-release](https://github.com/semantic-release/semantic-release). This allows for automatic versioning of the project based on the commit messages when merging. + +When merging, prefix the pull request subject with +- `chore:` for changes which should not increment the version number (like documentation changes) +- `fix:` for bug fixes, (increments the patch version) +- `feat:` for new features (increment the minor version) +- `BREAKING:` for major changes (increments the major version) + +[Releases](https://github.com/UKForeignOffice/notarial-api/releases) are automatically generated from commit messages. Prefix each commit with one of the above prefixes to include the commit message in the generated message. + ## Testing Currently, there is unit testing and integration testing set up for the api workspace. For more information, refer to the [testing docs](./docs/testing.md). diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 00000000..61958cb8 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,151 @@ +# Troubleshooting + +Use this guide to troubleshoot issues and resolve errors that may occur when notarial-api or notarial-worker is deployed. + +Connect to the database: +```sh +kubectl run -it --rm --env PGPASSWORD='' --env PAGER= --image=postgres:16 --restart=Never postgres-client -- psql -h -U master -d notarial +``` + +Replace PASSWORD with the password for the database, ENDPOINT_URL with the endpoint URL for the database. + +## pgboss + +[pgboss](https://github.com/timgit/pg-boss) is used to manage queueing jobs. On application start, pgboss will automatically create necessary tables in the database. + +### Jobs table +The jobs table `pgboss.job` is where all the current jobs are stored. Jobs will remain here, until they are completed or failed. Then they will move to `pgboss.archive` + +The jobs table has the following columns: + +``` + Column | Type | Collation | Nullable | Default +--------------+-----------------------------+-----------+----------+----------------------------- + id | uuid | | not null | gen_random_uuid() + name | text | | not null | + priority | integer | | not null | 0 + data | jsonb | | | + state | pgboss.job_state | | not null | 'created'::pgboss.job_state + retrylimit | integer | | not null | 0 + retrycount | integer | | not null | 0 + retrydelay | integer | | not null | 0 + retrybackoff | boolean | | not null | false + startafter | timestamp with time zone | | not null | now() + startedon | timestamp with time zone | | | + singletonkey | text | | | + singletonon | timestamp without time zone | | | + expirein | interval | | not null | '00:15:00'::interval + createdon | timestamp with time zone | | not null | now() + completedon | timestamp with time zone | | | + keepuntil | timestamp with time zone | | not null | now() + '14 days'::interval + on_complete | boolean | | not null | false + output | jsonb | | | +``` + +Columns/values to note are +- `name`: the name of the job. It can be one of SES_PROCESS, NOTIFY_PROCESS, SES_SEND, NOTIFY_SEND. More detail can be found in [worker/src/README.md](worker/src/README.md). PgBoss will also create some of its own. +- `state`: the state of the job. Read more about them in [pgboss documentation](https://github.com/timgit/pg-boss/blob/master/docs/readme.md#job-states) + - `created`: the job has been created + - `failed`: the job has failed + - `completed`: the job has been completed (successfully) + - `active`: the job is currently being processed +- `data`: the data associated with the job. +- `output`: the output of the job. This will contain the reference number, or the error message if the job has failed +- `keepuntil`: the time until the job will be kept in the table. After this time, the job will be moved to `pgboss.archive`. If you need more time to resolve the issue, you can update this value to a later time. + + + +## Finding jobs +To find jobs that have failed, run the following query: + +```postgresql + select id, output from pgboss.job where state = 'failed'; +``` + +## Fixing data +If the retrylimit has not been hit (retrylimit > retrycount) and the retrylimit is not 0, the job will be automatically retried. + +It is recommended you run every query in a transaction, so that you can abort the changes if they are incorrect. + +```postgresql + begin; + -- First run a query to print the current state of the job you are trying to change + select data from pgboss.job where id = ''; + + update pgboss.job + set state = 'created', + completedon = null, + retrycount = 0, + state = 'created' + where id = ''; + + -- Run the query again, to see if you've made the correct changes + select data from pgboss.job where state = 'failed' and id = ''; + + -- Run the following query to commit the changes + -- commit; + -- Run the following to abort the changes + -- rollback; +``` + +The following queries will assume that you are running them in a transaction. + +## Incorrect data +If the data is incorrect, you can update the data in the database. All data is stored as jsonb, so you can use [postgresql's jsonb functions](https://www.postgresql.org/docs/current/functions-json.html) to update the data + +```postgresql + update pgboss.job + set data = jsonb_set( + data, + '{data, keyToChange}', + '""' + ) + where id = ''; +``` + +You may find it easier to copy the data to a text editor, make the changes, and then update the data in the database. + +```postgresql + update pgboss.job + set data = '' + where id = ''; +``` + +### Retry a job +If a job has failed, and you want to retry it, you can update the job to `created` state, and reset the `retrycount` to 0. + +```postgresql + update pgboss.job + set state = 'created', + completedon = null, + retrycount = 0, + state = 'created' +-- output = null + where id = ''; +``` +You may also want to update output to null, to clear the error message. + +## Creating a new job +If the job does not seem to be retrying, or it is easier to just create a new job you need to create a new job, you can do so by running the following query: + +```postgresql + insert into pgboss.job (name, data) + values ('NOTIFY_PROCESS', '{"answers": {}, "metadata": {}, "reference": "123456"}'); +``` + +Alternatively, you can copy the data from the failed job, and create a new job with the same data. + +```postgresql + insert into pgboss.job (name, data) + SELECT name, data + from pgboss.job where id = ''; +``` + +### Moving a job from archive to job +If a job has been moved to the archive, and you want to retry it, you can move it back to the jobs table. + +```postgresql + insert into pgboss.job (name, data) + SELECT name, data + from pgboss.archive where id = ''; +``` \ No newline at end of file diff --git a/api/README.md b/api/README.md index 0130e617..a33fa0bb 100644 --- a/api/README.md +++ b/api/README.md @@ -1,5 +1,8 @@ # API workspace -This workspace is used to run the API server, which is called at various parts during the form and triggers email submissions after the form has been submitter. +This workspace is used to run the API server, which is called at various parts during the form and triggers email submissions after the form has been submitted. + +Use the API to house all the business logic. This will allow the worker to be a simple service that just listens for messages and sends them to an API. +The API should parse user's data, render email templates etc. ## Prerequisites 1. A node version manager, like [nvm](https://formulae.brew.sh/formula/nvm), or [n](https://github.com/tj/n) @@ -19,7 +22,7 @@ yarn install yarn api build yarn api start:local ``` -These commands will build the project dependencies, cmpile the initial build of the api workspace, and run the workspace in local mode (allowing watching for changes). +These commands will build the project dependencies, compile the initial build of the api workspace, and run the workspace in local mode (allowing watching for changes). ### Getting started with Docker You may use docker and docker compose to build and start the project with the right components (e.g. database, microservices), but will not be able to run the application(s) in dev mode. @@ -41,5 +44,46 @@ docker compose -d --build This will cause docker to tear down the current container, and force a new build image for the server, allowing you to test your most recent changes. ## Routes -There is currently one route set up, which will be used to test the Optical Character Recognition (OCR) capabilities of the server. This route is: - * /ocr-email \ No newline at end of file + +### POST `/forms` +This route is used to submit a form. It will then create two new jobs, "SES_PROCESS" and "NOTIFY_PROCESS". It will then return with a reference number. +The reference number is the GOV.UK Pay reference number. If a GOV.UK pay reference number could not be found, it will generate a random one. Use this reference number to track the user across the application. + + +### POST `/forms/emails/ses` +This route is used to parse user's data and prepare an email that will be sent to FCDO. The request will come from the SES_PROCESS job. +It will generate an email body to be sent via SES. The attachments are not added to the email body at this point. + + +### POST `/forms/emails/notify` +This route is used to parse the user's data to generate the confirmation email for the user. The request will come from the NOTIFY_PROCESS job. + + + +### Environment variables + +| Env var | Description | default | +|-----------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------| +| `PORT` | Port to start the application on | 9000 | +| `NODE_ENV` | Environment the application is running in. If the environment has a matching [config](./config) file, it will use those as the defaults | development | +| `NOTIFY_TEMPLATE_AFFIRMATION_USER_CONFIRMATION` | Notify template which sends a confirmation email to the user for the /affirmation form | 7 | +| `NOTIFY_TEMPLATE_EXCHANGE_USER_CONFIRMATION` | Notify template which sends a confirmation email to the user for the /exchange-uk-cni form | | +| `NOTIFY_TEMPLATE_EXCHANGE_USER_POSTAL_CONFIRMATION` | Notify template which sends a confirmation email to the user for the /cni | | +| `NOTIFY_TEMPLATE_POST_NOTIFICATION` | Notify template which alerts embassies that they have an application | | +| `QUEUE_URL` | The connection string to the database, including username and password | postgres://user:root@localhost:5432/notarial | +| `ARCHIVE_FAILED_AFTER_DAYS` | How long to keep failed jobs in the pgboss.job before moving it to pgboss.archive | 30 | +| `DELETE_ARCHIVED_AFTER_DAYS` | How long to keep jobs in pgboss.archive before deleting it | 7 | +| `SES_SEND_RETRY_BACKOFF` | Whether or not to retry SES_SEND jobs if they failed with exponential backoff (i.e. each retry is delayed longer than the last attempt) | true | +| `SES_SEND_RETRY_LIMIT` | Number of times the SES_SEND job is allowed to be retried | 50 | +| `SES_SEND_ON_COMPLETE` | If SES_SEND has a completion handler (i.e. a job that should be triggered if this job becomes completed), and should be turned on | true | +| `SES_PROCESS_RETRY_BACKOFF` | Whether or not to retry SES_PROCESS if they failed with exponential backoff (i.e. each retry is delayed longer than the last attempt) | true | +| `SES_PROCESS_RETRY_LIMIT` | Number of times the SES_PROCESS job is allowed to be retried | 50 | +| `SES_PROCESS_ON_COMPLETE` | If SES_PROCESS has a completion handler (i.e. a job that should be triggered if this job becomes completed), and should be turned on | true | +| `NOTIFY_SEND_RETRY_BACKOFF` | Whether or not to retry NOTIFY_SEND jobs if they failed with exponential backoff (i.e. each retry is delayed longer than the last attempt) | true | +| `NOTIFY_SEND_RETRY_LIMIT` | Number of times the NOTIFY_SEND job is allowed to be retried | 50 | +| `NOTIFY_SEND_ON_COMPLETE` | If NOTIFY_SENd has a completion handler (i.e. a job that should be triggered if this job becomes completed) | true | +| `NOTIFY_PROCESS_RETRY_BACKOFF` | Whether or not to retry NOTIFY_PROCESS jobs if they failed with exponential backoff (i.e. each retry is delayed longer than the last attempt) | true | +| `NOTIFY_PROCESS_RETRY_LIMIT` | Number of times the NOTIFY_PROCESS job is allowed to be retried | 50 | +| `NOTIFY_PRCESS_ON_COMPLETE` | If the NOTIFY_PROCESS job has a completion handler (i.e. a job that should be triggered if this job becomes completed), and should be turned on | true | + + diff --git a/api/config/custom-environment-variables.json b/api/config/custom-environment-variables.json index 1f5175a6..46b966e9 100644 --- a/api/config/custom-environment-variables.json +++ b/api/config/custom-environment-variables.json @@ -1,11 +1,6 @@ { "port": "PORT", "env": "NODE_ENV", - "documentPassword": "DOCUMENT_PASSWORD", - "affirmationTemplate": "AFFIRMATION_TEMPLATE_ID", - "cniTemplate": "CNI_TEMPLATE_ID", - "submissionEmail": "SUBMISSION_EMAIL_ADDRESS", - "senderEmail": "SENDER_EMAIL_ADDRESS", "Notify": { "Template": { "affirmationUserConfirmation": "NOTIFY_TEMPLATE_AFFIRMATION_USER_CONFIRMATION", @@ -13,10 +8,6 @@ "exchangeUserConfirmation": "NOTIFY_TEMPLATE_EXCHANGE_USER_CONFIRMATION", "exchangeUserPostalConfirmation": "NOTIFY_TEMPLATE_EXCHANGE_USER_POSTAL_CONFIRMATION", "postNotification": "NOTIFY_TEMPLATE_POST_NOTIFICATION" - }, - "Retry": { - "backoff": "NOTIFY_RETRY_BACKOFF", - "limit": "NOTIFY_RETRY_LIMIT" } }, "Queue": { diff --git a/api/config/default.js b/api/config/default.js index 00a79112..172338af 100644 --- a/api/config/default.js +++ b/api/config/default.js @@ -7,11 +7,10 @@ if (process.env.NODE_ENV !== "test") { module.exports = { port: "9000", env: "development", - documentPassword: "Sup3rS3cr3tP4ssw0rd", submissionAddress: "pye@cautionyourblast.com", senderEmail: "pye@cautionyourblast.com", Queue: { - url: "postgresql://user:root@localhost:5432/queue", + url: "postgresql://user:root@localhost:5432/notarial", defaultOptions: { retryBackoff: "true", retryLimit: "50", diff --git a/docker-compose.yml b/docker-compose.yml index 8a40e639..ba19fa42 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,31 @@ version: "3.9" services: api: + depends_on: [postgres] env_file: - api/.env + environment: + - QUEUE_DATABASE_URL=postgres://user:root@postgres:5432/notarial build: context: . dockerfile: api/Dockerfile worker: + depends_on: [postgres] environment: - - NOTIFY_API_KEY=${NOTIFY_API_KEY:?} + - NOTIFY_API_KEY=${NOTIFY_API_KEY:-} + - QUEUE_URL=postgres://user:root@postgres:5432/notarial build: context: . - dockerfile: worker/Dockerfile \ No newline at end of file + dockerfile: worker/Dockerfile + + postgres: + container_name: postgres + image: "postgres:16" + volumes: + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + ports: + - "5432:5432" + environment: + POSTGRES_DB: notarial + POSTGRES_PASSWORD: root + POSTGRES_USER: user \ No newline at end of file diff --git a/init.sql b/init.sql new file mode 100644 index 00000000..c47d5c4f --- /dev/null +++ b/init.sql @@ -0,0 +1 @@ +create database queue; \ No newline at end of file diff --git a/worker/config/custom-environment-variables.json b/worker/config/custom-environment-variables.json index 9e7e428d..9b3e7ab1 100644 --- a/worker/config/custom-environment-variables.json +++ b/worker/config/custom-environment-variables.json @@ -2,7 +2,8 @@ "Queue": { "url": "QUEUE_URL", "archiveFailedAfterDays": "ARCHIVE_FAILED_AFTER_DAYS", - "deleteArchivedAfterDays": "DELETE_ARCHIVED_IN_DAYS" + "deleteArchivedAfterDays": "DELETE_ARCHIVED_IN_DAYS", + "monitorStateIntervalInSeconds": "MONITOR_STATE_INTERVAL_IN_SECONDS" }, "Notify": { "apiKey": "NOTIFY_API_KEY" @@ -14,10 +15,6 @@ }, "Recipient": { "emailAddress": "SUBMISSION_ADDRESS" - }, - "Retry": { - "backoff": "SES_RETRY_BACKOFF", - "limit": "SES_RETRY_LIMIT" } }, "NotarialApi": { diff --git a/worker/config/default.js b/worker/config/default.js index f43e05ef..222c95cf 100644 --- a/worker/config/default.js +++ b/worker/config/default.js @@ -1,8 +1,15 @@ +const dotEnv = require("dotenv"); + +if (process.env.NODE_ENV !== "test") { + dotEnv.config({ path: ".env" }); +} + module.exports = { Queue: { - url: "postgres://user:root@localhost:5432/queue", + url: "postgres://user:root@localhost:5432/notarial", archiveFailedInDays: 30, deleteArchivedAfterDays: 7, + monitorStateIntervalSeconds: 10, }, SES: { Sender: { diff --git a/worker/package.json b/worker/package.json index 9bec4833..e16bbd18 100644 --- a/worker/package.json +++ b/worker/package.json @@ -44,6 +44,7 @@ "@aws-sdk/client-ses": "^3.427.0", "axios": "^1.6.7", "config": "^3.3.9", + "dotenv": "^16.4.5", "mimetext": "^3.0.16", "notifications-node-client": "^8.0.0", "pg-boss": "^9.0.3", diff --git a/worker/src/Consumer/getConsumer.ts b/worker/src/Consumer/getConsumer.ts index ed48f34d..d50bfa84 100644 --- a/worker/src/Consumer/getConsumer.ts +++ b/worker/src/Consumer/getConsumer.ts @@ -5,7 +5,7 @@ import pino from "pino"; const DEFAULT_URL = config.get("Queue.url"); const logger = pino().child({ - method: "Consumer.create", + method: "Consumer", }); const MINUTE_IN_S = 60; const HOUR_IN_S = MINUTE_IN_S * 60; @@ -13,8 +13,11 @@ const DAY_IN_S = HOUR_IN_S * 24; const archiveFailedAfterDays = parseInt(config.get("Queue.archiveFailedInDays")); const deleteAfterDays = parseInt(config.get("Queue.deleteArchivedAfterDays")); +const monitorStateIntervalSeconds = parseInt(config.get("Queue.monitorStateIntervalSeconds")); -logger.info(`archiveFailedAfterDays: ${archiveFailedAfterDays}, deleteAfterDays: ${deleteAfterDays}`); +logger.info( + `archiveFailedAfterDays: ${archiveFailedAfterDays}, deleteAfterDays: ${deleteAfterDays}, monitorStateIntervalSeconds: ${monitorStateIntervalSeconds}` +); let consumer; @@ -24,12 +27,18 @@ export async function create(url: string = DEFAULT_URL) { connectionString: url, archiveFailedAfterSeconds: archiveFailedAfterDays * DAY_IN_S, deleteAfterDays, + monitorStateIntervalSeconds, }); boss.on("error", (error) => { + logger.error(error); throw error; }); + boss.on("monitor-states", (states) => { + logger.info({ states }, "STATUS_UPDATE"); + }); + try { await boss.start(); } catch (e: any) { diff --git a/worker/src/README.md b/worker/src/README.md index 59b5d1b5..fc09c926 100644 --- a/worker/src/README.md +++ b/worker/src/README.md @@ -5,12 +5,13 @@ Handles emails via GOV.UK Notify or AWS SES. The queues are managed by [pg-boss](https://github.com/timgit/pg-boss) The current naming scheme is `NOTIFY_*` and `SES_*` to handle GOV.UK Notify and AWS SES respectively. -The suffixes `_PROCESS` and `_SEND` are currently used. +The suffixes `_PROCESS` and `_SEND` are currently used. + - `_PROCESS` is used to store the parameters, and allow retrying of the message. Minimal business logic is used for these workers, they simply send the request to `notarial-api/forms/emails*` where the actual parsing of data and business logic is held - `_SEND` is used to send the email -The general flow is +The general flow is 1. POST to /forms (handled by forms-worker) 1. creates two messages (`.sendToProcessQueue`) which just takes the data and adds it to `*_PROCESS` queues @@ -23,13 +24,17 @@ The general flow is This allows all stages to be retried individually. If errors are thrown, or there are erroneous responses (4xx or 5xx errors), these will be stored in the database, in the output column. - - `NOTIFY_PROCESS` is handled by [notifyProcessHandler](queues/notify/workers/notifyProcessHandler.ts) - `NOTIFY_SEND` is handled by [notifySendHandler](queues/notify/workers/notifySendHandler.ts) - `SES_PROCESS` is handled by [sesProcessHandler](queues/ses/workers/sesProcessHandler.ts) - `SES_SEND` is handled by [sesSendHandler](queues/ses/workers/sesSendHandler.ts) +Generally all jobs will be added to the queue with the data required for the operation, as well as metadata to allow for easy tracking of the job. +All jobs will have a metadata property, which will contain the reference number. This will match with their GOV.UK Pay reference number (not to be confused wit their payment ID). + + ## `notifyProcessHandler` + [notifyProcessHandler](queues/notify/workers/notifyProcessHandler.ts) When a message on the `NOTIFY_PROCESS` queue is detected this worker will send a request to `notarial-api/forms/emails/notify`. @@ -39,10 +44,35 @@ The source of this event is notarial-api, after a user has submitted a form (POS select * from pgboss.job where name = 'NOTIFY_PROCESS'; ``` +The data stored in this job will be the user's answers, and the metadata of the form submission. For example: + +```json5 +{ + "answers": { + "firstName": "test", + "middleName": null, + "dateOfBirth": "2000-01-01" + /** etc **/ + }, + "metadata": { + "type": "affirmation", + "payment": { + "payId": "usfetplth9aqfm0ft598eigpkm", + "state": { + "status": "created", + "finished": false + }, + "reference": "B-6FYZIU1M" + }, + "reference": "B-6FYZIU1M" + } +} +``` + ## `notifySendHandler` -[notifySendHandler](queues/notify/workers/notifySendHandler.ts) +[notifySendHandler](queues/notify/workers/notifySendHandler.ts) When a message on the "NOTIFY_SEND" queue is detected, this worker sends a GOV.UK notify request. The source of this event is notarial-api/forms/emails/notify, which processes the user's data. @@ -51,8 +81,30 @@ The source of this event is notarial-api/forms/emails/notify, which processes th select * from pgboss.job where name = 'NOTIFY_SEND'; ``` +The data stored in this job will be GOV.UK Notify API options, which include personalisations, reference, template ID and email address. + +For example: +```json5 + { + "options": { + "reference": "PF3N8EGP9L", + "personalisation": { + "post": "British Embassy Rome", + "country": "Italy", + "firstName": "test", + /** etc **/ + }, + "metadata": { + "reference": "PF3N8EGP9L" + }, + "template": "ABC-DEF", + "emailAddress": "pye@cautionyourblast.com" + } +} +``` ## `sesProcessHandler` + [sesProcessHandler](./queues/ses/workers/sesProcessHandler.ts) When a message on the `SES_PROCESS` queue is detected this worker will send a request to `notarial-api/forms/emails/ses`. @@ -62,8 +114,8 @@ The source of this event is notarial-api, after a user has submitted a form (POS select * from pgboss.job where name = 'SES_PROCESS'; ``` - ## `sesSendHandler` + [sesSendHandler](./queues/ses/workers/sesSendHandler.ts) When a message on the "NOTIFY_SEND" queue is detected, this worker sends a GOV.UK notify request. @@ -73,49 +125,60 @@ The source of this event is notarial-api/forms/emails/ses, which processes the u select * from pgboss.job where name = 'SES_SEND'; ``` -### Troubleshooting - -When tasks fail, the error emitted will automatically be added to the jobs, and the error is logged. - -If the logs are incomplete, further logging may be found on the database in the `output` column. - -To see all failed events - -```postgresql - -select * from pgboss.job where name = 'notify' and state = 'failed'; - +The data stored in this job will be the email body and attachments. + +```json5 +{ + "body": "\n

Dear British Embassy Rome, Use them to create a new case in Casebook and prepare the affirmation document.\n

...", + "subject": "cni application, British Embassy Rome – PF3N8EGP9L", + "metadata": { + "reference": "PF3N8EGP9L" + }, + "reference": "PF3N8EGP9L", + "onComplete": { + "job": { + "options": { // This will match the NOTIFY_SEND job + "personalisation": {}, + "template": "ABCDEF_EG", + "reference": "PF3N8EGP9L", + "emailAddress": "pye@cautionyourblast.com" + }, + "queue": "NOTIFY_SEND" + }, + "attachments": [ + { + "key": "ukPassportFile", + "type": "file", + "title": "UK passport", + "answer": "http://documentupload:9000/v1/files/511ffde6-ea44-4d72-967c-a5581f73fb8e.png", + "category": "applicantDetails" + } + ] + } +} ``` +The attachments will be fetched, then added to the email body in memory (i.e. not updated in the database) before sending to SES. -```postgresql - select data, output from pgboss.job where id = '6aa3b250-4bc8-4fcb-9a15-7ca56551d04b'; -``` +### Troubleshooting -Events can easily be retried by setting completedon = null, retrycount = 0, state = 'created' -```postgresql - update pgboss.job - set data = jsonb_set( - data, - '{webhook_url}', - '"https://b4bf0fcd-1dd3-4650-92fe-d1f83885a447.mock.pstmn.io"' - ), - completedon = null, - retrycount = 0, - state = 'created' - where id = '4aad27dc-db53-48e4-824b-612a4b3d9fa7'; -``` +When tasks fail, the error emitted will automatically be added to the jobs, and the error is logged. If the logs are incomplete, further logging may be found on the database in the `output` column. -If you cannot find a job in `pgboss.job`, it may be in the archive table, `pgboss.archive`. +See [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) for more information. -```postgresql - select * from pgboss.archive where id = '4aad27dc-db53-48e4-824b-612a4b3d9fa7;' -``` +### Environment variables -If you wish to keep a record of the failed event, create a new event using the failed events details. -```postgresql - insert into pgboss.job (name, data) - SELECT name, data - from pgboss.job where id = '4aad27dc-db53-48e4-824b-612a4b3d9fa7'; -``` \ No newline at end of file +| Env var | Description | default | +|----------------------------------------|---------------------------------------------------------------------------------------------------------|----------------------------------------------| +| `QUEUE_URL` | Connection string of the db | postgres://user:root@localhost:5432/notarial | +| `ARCHIVE_FAILED_AFTER_DAYS` | In days, how long to keep failed jobs in the table `pgboss.jobs`, before sending it to `pgboss.archive` | 30 | +| `DELETE_ARCHIVED_AFTER_DAYS` | In days, how long to keep any jobs in `pgboss.archive` before deleting | 7 | +| `MONITOR_STATE_INTERVAL_SECONDS` | In seconds, how often to log the statuses of each queue | 10 | +| `NOTIFY_API_KEY` | Notify API key to send emails from | | +| `SES_SENDER_NAME` | The name to display when sending an email via SES | Getting Married Abroad Service | +| `SENDER_EMAIL_ADDRESS` | Where the email should be sent from. There must be an SES domain identity matching this email address | pye@cautionyourblast.com | +| `SUBMISSION_ADDRESS` | Where to send the emails to | pye@cautionyourblast.com | +| `NOTARIAL_API_CREATE_SES_EMAIL_URL` | URL on the notarial-api where SES emails can be created | http://localhost:9000/forms/emails/ses | +| `NOTARIAL_API_CREATE_NOTIFY_EMAIL_URL` | URL on the notarial-api where Notify emails can be created | http://localhost:9000/forms/emails/notify | +| `FILES_ALLOWED_ORIGINS` | Allowed origins where files can be downloaded from | ["http://localhost:9000"] | diff --git a/worker/src/queues/notify/workers/notifySendHandler.ts b/worker/src/queues/notify/workers/notifySendHandler.ts index 16267eb5..8921868e 100644 --- a/worker/src/queues/notify/workers/notifySendHandler.ts +++ b/worker/src/queues/notify/workers/notifySendHandler.ts @@ -32,12 +32,12 @@ export async function notifySendHandler(job: Job) { return id; } catch (e: any) { if (e.response) { - logger.error({ jobId, err: e.response.data.errors, emailAddress }, "Notify responded with an error"); + logger.error({ jobId, err: e.response.data.errors, emailAddress, errorCode: "NOTIFY_RESPONSE_ERROR" }, "Notify responded with an error"); throw e.response.data; } if (e.request) { - logger.error(jobId, `Request could not be sent to Notify`); + logger.error({ jobId, errorCode: "NOTIFY_REQUEST_ERROR" }, `Request could not be sent to Notify`); } throw e; } diff --git a/worker/src/queues/ses/helpers/FileService.ts b/worker/src/queues/ses/helpers/FileService.ts index eb5bffc1..6fe276aa 100644 --- a/worker/src/queues/ses/helpers/FileService.ts +++ b/worker/src/queues/ses/helpers/FileService.ts @@ -46,9 +46,6 @@ export default class FileService { throw new ApplicationError("FILE", "NOT_FOUND", `Requested file could not be found at ${err.response?.config.url}`); } } - if (err instanceof ApplicationError) { - throw err; - } throw new ApplicationError("FILE", "UNKNOWN", err.message); } } diff --git a/yarn.lock b/yarn.lock index b7288902..4faadf33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3633,6 +3633,7 @@ __metadata: axios: ^1.6.7 babel-jest: ^29.5.0 config: ^3.3.9 + dotenv: ^16.4.5 eslint: ^8.56.0 eslint-config-prettier: ^9.0.0 eslint-plugin-prettier: ^5.0.0 @@ -6704,6 +6705,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^16.4.5": + version: 16.4.5 + resolution: "dotenv@npm:16.4.5" + checksum: 301a12c3d44fd49888b74eb9ccf9f07a1f5df43f489e7fcb89647a2edcd84c42d6bc349dc8df099cd18f07c35c7b04685c1a4f3e6a6a9e6b30f8d48c15b7f49c + languageName: node + linkType: hard + "duplexer2@npm:~0.1.0": version: 0.1.4 resolution: "duplexer2@npm:0.1.4"