diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9e6b67e1ae..9598810657 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,7 +55,7 @@ jobs: run: npm ci - name: Extract branch name shell: bash - run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" + run: echo "branch=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_OUTPUT id: extract_branch - name: Run versioning script run: . ./scripts/github/deploy.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index ea5c82dae7..7634ab3e7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,412 @@ +## v1.57.0-rc.12 (2024-12-13) + +#### :bug: Bug Fix +* `gafl-webapp-service` + * [#2095](https://github.com/DEFRA/rod-licensing/pull/2095) Missing inline error on renewals id page ([@jaucourt](https://github.com/jaucourt)) + +#### Committers: 1 +- Phil Benson ([@jaucourt](https://github.com/jaucourt)) + +## v1.57.0-rc.11 (2024-12-13) + +#### :rocket: Enhancement +* `dynamics-lib` + * [#2091](https://github.com/DEFRA/rod-licensing/pull/2091) Update activity in CRM ([@nabeelamir-defra](https://github.com/nabeelamir-defra)) + +#### Committers: 1 +- Nabeel Amir ([@nabeelamir-defra](https://github.com/nabeelamir-defra)) + +## v1.57.0-rc.10 (2024-12-13) + +#### :bug: Bug Fix +* `gafl-webapp-service` + * [#2094](https://github.com/DEFRA/rod-licensing/pull/2094) Licence to start error not in error summary ([@jaucourt](https://github.com/jaucourt)) + +#### Committers: 1 +- Phil Benson ([@jaucourt](https://github.com/jaucourt)) + +## v1.57.0-rc.9 (2024-12-12) + +#### :bug: Bug Fix +* `gafl-webapp-service` + * [#2093](https://github.com/DEFRA/rod-licensing/pull/2093) Fix DOB message in Welsh ([@jaucourt](https://github.com/jaucourt)) + +#### Committers: 1 +- Phil Benson ([@jaucourt](https://github.com/jaucourt)) + +## v1.57.0-rc.8 (2024-12-12) + +#### :bug: Bug Fix +* `sales-api-service` + * [#2092](https://github.com/DEFRA/rod-licensing/pull/2092) Fix RP creation bug ([@jaucourt](https://github.com/jaucourt)) + +#### Committers: 1 +- Phil Benson ([@jaucourt](https://github.com/jaucourt)) + + +## v1.57.0-rc.6 (2024-12-10) + +#### :rocket: Enhancement +* `dynamics-lib`, `sales-api-service` + * [#2081](https://github.com/DEFRA/rod-licensing/pull/2081) Create RP Record in CRM ([@jaucourt](https://github.com/jaucourt)) + +#### Committers: 1 +- Phil Benson ([@jaucourt](https://github.com/jaucourt)) + +## v1.57.0-rc.5 (2024-12-10) + +#### :bug: Bug Fix +* `gafl-webapp-service` + * [#2088](https://github.com/DEFRA/rod-licensing/pull/2088) Change length link junior licence ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + +## v1.57.0-rc.4 (2024-12-09) + +#### :rocket: Enhancement +* `gafl-webapp-service` + * [#2022](https://github.com/DEFRA/rod-licensing/pull/2022) Apply more specific date error messages ([@irisfaraway](https://github.com/irisfaraway)) + +#### Committers: 1 +- Iris Faraway ([@irisfaraway](https://github.com/irisfaraway)) + +## v1.57.0-rc.3 (2024-12-06) + +#### :rocket: Enhancement +* `connectors-lib`, `gafl-webapp-service` + * [#2082](https://github.com/DEFRA/rod-licensing/pull/2082) Rename createRecurringPayment to clarify it creates agreements ([@irisfaraway](https://github.com/irisfaraway)) + +#### Committers: 1 +- Iris Faraway ([@irisfaraway](https://github.com/irisfaraway)) + +## v1.57.0-rc.2 (2024-12-04) + +#### :rocket: Enhancement +* `pocl-job` + * [#2085](https://github.com/DEFRA/rod-licensing/pull/2085) FTP POCL references ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + +## v1.57.0-rc.1 (2024-11-29) + +#### :rocket: Enhancement +* `business-rules-lib`, `connectors-lib`, `dynamics-lib`, `fulfilment-job`, `gafl-webapp-service`, `payment-mop-up-job`, `pocl-job`, `recurring-payments-job`, `sales-api-service`, `sqs-receiver-service` + * [#2086](https://github.com/DEFRA/rod-licensing/pull/2086) RCP fails ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + +## v1.57.0-rc.0 (2024-11-29) + +#### :rocket: Enhancement +* `business-rules-lib`, `connectors-lib`, `dynamics-lib`, `fulfilment-job`, `gafl-webapp-service`, `payment-mop-up-job`, `pocl-job`, `recurring-payments-job`, `sales-api-service`, `sqs-receiver-service` + * [#2075](https://github.com/DEFRA/rod-licensing/pull/2075) RCP job fails locally ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + +## v1.55.0-rc.6 (2024-11-25) + +#### :rocket: Enhancement +* `fulfilment-job` + * [#2070](https://github.com/DEFRA/rod-licensing/pull/2070) Remove ftp image and build ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + + +## v1.55.0-rc.5 (2024-11-25) + +#### :rocket: Enhancement +* `fulfilment-job`, `pocl-job` + * [#2064](https://github.com/DEFRA/rod-licensing/pull/2064) Remove ftp functionality ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + + +## v1.55.0-rc.4 (2024-11-21) + +#### :rocket: Enhancement +* `connectors-lib`, `gafl-webapp-service`, `sales-api-service` + * [#2074](https://github.com/DEFRA/rod-licensing/pull/2074) Add agreement_id to existing GOV.UK Pay request ([@irisfaraway](https://github.com/irisfaraway)) + +#### Committers: 1 +- Iris Faraway ([@irisfaraway](https://github.com/irisfaraway)) + + +## v1.55.0-rc.3 (2024-11-21) + +#### :rocket: Enhancement +* [#2078](https://github.com/DEFRA/rod-licensing/pull/2078) Set-output removed from versioning script ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + + +## v1.55.0-rc.2 (2024-11-19) + +#### :rocket: Enhancement +* `gafl-webapp-service` + * [#2077](https://github.com/DEFRA/rod-licensing/pull/2077) Change links for three rod licence ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + + +## v1.55.0-rc.1 (2024-11-19) + +#### :rocket: Enhancement +* `gafl-webapp-service` + * [#2079](https://github.com/DEFRA/rod-licensing/pull/2079) Licence summary licence type capitals ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + + +## v1.55.0-rc.0 (2024-11-13) + +#### :rocket: Enhancement +* `dynamics-lib` + * [#2072](https://github.com/DEFRA/rod-licensing/pull/2072) Create activity in CRM ([@nabeelamir-defra](https://github.com/nabeelamir-defra)) + +#### Committers: 1 +- Nabeel Amir ([@nabeelamir-defra](https://github.com/nabeelamir-defra)) + +## v1.55.0-rc.6 (2024-11-25) + +#### :rocket: Enhancement +* `fulfilment-job` + * [#2070](https://github.com/DEFRA/rod-licensing/pull/2070) Remove ftp image and build ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + +## v1.55.0-rc.5 (2024-11-25) + +#### :rocket: Enhancement +* `fulfilment-job`, `pocl-job` + * [#2064](https://github.com/DEFRA/rod-licensing/pull/2064) Remove ftp functionality ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + +## v1.55.0-rc.4 (2024-11-21) + +#### :rocket: Enhancement +* `connectors-lib`, `gafl-webapp-service`, `sales-api-service` + * [#2074](https://github.com/DEFRA/rod-licensing/pull/2074) Add agreement_id to existing GOV.UK Pay request ([@irisfaraway](https://github.com/irisfaraway)) + +#### Committers: 1 +- Iris Faraway ([@irisfaraway](https://github.com/irisfaraway)) + +## v1.55.0-rc.3 (2024-11-21) + +#### :rocket: Enhancement +* [#2078](https://github.com/DEFRA/rod-licensing/pull/2078) Set-output removed from versioning script ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + +## v1.55.0-rc.2 (2024-11-19) + +#### :rocket: Enhancement +* `gafl-webapp-service` + * [#2077](https://github.com/DEFRA/rod-licensing/pull/2077) Change links for three rod licence ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + +## v1.55.0-rc.1 (2024-11-19) + +#### :rocket: Enhancement +* `gafl-webapp-service` + * [#2079](https://github.com/DEFRA/rod-licensing/pull/2079) Licence summary licence type capitals ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + +## v1.55.0-rc.0 (2024-11-13) + +#### :rocket: Enhancement +* `dynamics-lib` + * [#2072](https://github.com/DEFRA/rod-licensing/pull/2072) Create activity in CRM ([@nabeelamir-defra](https://github.com/nabeelamir-defra)) + +#### Committers: 1 +- Nabeel Amir ([@nabeelamir-defra](https://github.com/nabeelamir-defra)) + + + + + + +## v1.51.0-rc.5 (2024-11-11) + +#### :rocket: Enhancement +* `gafl-webapp-service` + * [#2073](https://github.com/DEFRA/rod-licensing/pull/2073) Building tag error ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + +## v1.51.0-rc.4 (2024-11-07) + +#### :rocket: Enhancement +* `gafl-webapp-service`, `recurring-payments-job`, `sales-api-service` + * [#2069](https://github.com/DEFRA/rod-licensing/pull/2069) Change docker to not install dev dependency ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + + +## v1.51.0-rc.2 (2024-11-07) + +#### :bug: Bug Fix +* `gafl-webapp-service` + * [#2071](https://github.com/DEFRA/rod-licensing/pull/2071) Fix bug where transaction id is not recognised ([@jaucourt](https://github.com/jaucourt)) + +#### Committers: 1 +- Phil Benson ([@jaucourt](https://github.com/jaucourt)) + +## v1.51.0-rc.1 (2024-10-29) + +#### :rocket: Enhancement +* `connectors-lib`, `fulfilment-job`, `gafl-webapp-service`, `payment-mop-up-job`, `pocl-job`, `recurring-payments-job`, `sales-api-service`, `sqs-receiver-service` + * [#2050](https://github.com/DEFRA/rod-licensing/pull/2050) Create rcp agreement with GOV.UK pay ([@ScottDormand96](https://github.com/ScottDormand96)) +* `gafl-webapp-service` + * [#2030](https://github.com/DEFRA/rod-licensing/pull/2030) Content amendments to payment summary ([@lailien3](https://github.com/lailien3)) +* Other + * [#2060](https://github.com/DEFRA/rod-licensing/pull/2060) Update dependencies in package-lock ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 2 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) +- laila aleissa ([@lailien3](https://github.com/lailien3)) + +## v1.51.0-rc.0 (2024-10-21) + +#### :rocket: Enhancement +* `gafl-webapp-service` + * [#2067](https://github.com/DEFRA/rod-licensing/pull/2067) Payment complete 10 days ([@ScottDormand96](https://github.com/ScottDormand96)) + * [#2066](https://github.com/DEFRA/rod-licensing/pull/2066) Update content to 10 days ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + + +## v1.50.0-rc.10 (2024-10-09) + +#### :rocket: Enhancement +* `gafl-webapp-service` + * [#2061](https://github.com/DEFRA/rod-licensing/pull/2061) Uninstall pdfmake ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + +## v1.50.0-rc.9 (2024-10-09) + +#### :bug: Bug Fix +* [#2063](https://github.com/DEFRA/rod-licensing/pull/2063) Fix localstack config ([@jaucourt](https://github.com/jaucourt)) + +#### Committers: 1 +- Phil Benson ([@jaucourt](https://github.com/jaucourt)) + +## v1.50.0-rc.8 (2024-09-25) + +#### :bug: Bug Fix +* `gafl-webapp-service` + * [#2054](https://github.com/DEFRA/rod-licensing/pull/2054) Correcting styling for RCP T&C headers ([@lailien3](https://github.com/lailien3)) + +#### Committers: 2 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) +- laila aleissa ([@lailien3](https://github.com/lailien3)) + +## v1.50.0-rc.7 (2024-09-18) + +#### :rocket: Enhancement +* `dynamics-lib` + * [#2051](https://github.com/DEFRA/rod-licensing/pull/2051) Add missing export for RCR angler login ([@nabeelamir-defra](https://github.com/nabeelamir-defra)) + +#### Committers: 1 +- [@nabeelamir-defra](https://github.com/nabeelamir-defra) + +## v1.50.0-rc.6 (2024-09-18) + +#### :rocket: Enhancement +* `dynamics-lib` + * [#2049](https://github.com/DEFRA/rod-licensing/pull/2049) Add query to allow angler to login in to RCR ([@nabeelamir-defra](https://github.com/nabeelamir-defra)) + +#### Committers: 1 +- [@nabeelamir-defra](https://github.com/nabeelamir-defra) + +## v1.50.0-rc.5 (2024-09-17) + +#### :rocket: Enhancement +* `gafl-webapp-service` + * [#2045](https://github.com/DEFRA/rod-licensing/pull/2045) Display Recurring payment terms and conditions ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + +## v1.50.0-rc.4 (2024-09-16) + +#### :bug: Bug Fix +* `gafl-webapp-service` + * [#2047](https://github.com/DEFRA/rod-licensing/pull/2047) Fulfilment page deselecting post ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + +## v1.50.0-rc.3 (2024-09-16) + +#### :rocket: Enhancement +* `gafl-webapp-service` + * [#2046](https://github.com/DEFRA/rod-licensing/pull/2046) Change link on address page ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + +## v1.50.0-rc.2 (2024-09-13) + +#### :rocket: Enhancement +* `gafl-webapp-service` + * [#2040](https://github.com/DEFRA/rod-licensing/pull/2040) Amend Change link in Easy Renew Journey ([@lailien3](https://github.com/lailien3)) + +#### Committers: 1 +- laila aleissa ([@lailien3](https://github.com/lailien3)) + +## v1.50.0-rc.1 (2024-09-13) + +#### :bug: Bug Fix +* `gafl-webapp-service` + * [#2039](https://github.com/DEFRA/rod-licensing/pull/2039) RCP T&C - Shrink Subheadings ([@lailien3](https://github.com/lailien3)) + +#### Committers: 1 +- laila aleissa ([@lailien3](https://github.com/lailien3)) + +## v1.50.0-rc.0 (2024-09-13) + +#### :rocket: Enhancement +* `gafl-webapp-service` + * [#2043](https://github.com/DEFRA/rod-licensing/pull/2043) Update postal fulfilment information banner ([@lailien3](https://github.com/lailien3)) + +#### Committers: 1 +- laila aleissa ([@lailien3](https://github.com/lailien3)) + + +## v1.49.0-rc.11 (2024-09-05) + +#### :rocket: Enhancement +* `gafl-webapp-service` + * [#2036](https://github.com/DEFRA/rod-licensing/pull/2036) Save multiple times cookies page ([@ScottDormand96](https://github.com/ScottDormand96)) + +#### Committers: 1 +- Scott Dormand ([@ScottDormand96](https://github.com/ScottDormand96)) + + ## v1.49.0-rc.9 (2024-09-02) #### :bug: Bug Fix diff --git a/docker/env/fulfilment_job.env.example b/docker/env/fulfilment_job.env.example index 36356b4102..a616195603 100644 --- a/docker/env/fulfilment_job.env.example +++ b/docker/env/fulfilment_job.env.example @@ -20,11 +20,6 @@ FULFILMENT_S3_BUCKET=fulfilment-audit FULFILMENT_FILE_SIZE=5000 # FTP Settings -FULFILMENT_FTP_HOST=host.docker.internal -FULFILMENT_FTP_PORT=2222 -FULFILMENT_FTP_PATH=/share/fulfilment -FULFILMENT_FTP_USERNAME=test_sftp_user -FULFILMENT_FTP_KEY_SECRET_ID=/dev/fsh/local/sftp/ssh_ed25519_host_key FULFILMENT_SEND_UNENCRYPTED_FILE=true FULFILMENT_PGP_PUBLIC_KEY_SECRET_ID=/dev/fsh/local/ diff --git a/docker/env/gafl_webapp.env.example b/docker/env/gafl_webapp.env.example index 37a340bd62..436f8417f6 100644 --- a/docker/env/gafl_webapp.env.example +++ b/docker/env/gafl_webapp.env.example @@ -18,6 +18,7 @@ FEEDBACK_URI=https://www.smartsurvey.co.uk/s/0L205/ GOV_PAY_API_URL=https://publicapi.payments.service.gov.uk/v1/payments GOV_PAY_HTTPS_REDIRECT=false GOV_PAY_REQUEST_TIMEOUT_MS=10000 +GOV_PAY_RCP_API_URL=https://publicapi.payments.service.gov.uk/v1/agreements # Sales API SALES_API_URL=http://host.docker.internal:4000 diff --git a/docker/env/gafl_webapp.secrets.env.example b/docker/env/gafl_webapp.secrets.env.example index 4b8818c26c..4075008506 100644 --- a/docker/env/gafl_webapp.secrets.env.example +++ b/docker/env/gafl_webapp.secrets.env.example @@ -3,5 +3,6 @@ #################################################################################### GOV_PAY_APIKEY= +GOV_PAY_RECURRING_APIKEY= SESSION_COOKIE_PASSWORD= ANALYTICS_PROPERTY_API= diff --git a/docker/env/pocl_job.env.example b/docker/env/pocl_job.env.example index 5b98081fb6..73604b3125 100644 --- a/docker/env/pocl_job.env.example +++ b/docker/env/pocl_job.env.example @@ -23,12 +23,5 @@ SALES_API_TIMEOUT_MS=120000 POCL_FILE_STAGING_TABLE=PoclFileStaging POCL_RECORD_STAGING_TABLE=PoclRecordStaging -# FTP Settings -POCL_FTP_HOST=host.docker.internal -POCL_FTP_PORT=2222 -POCL_FTP_PATH=/share/pocl -POCL_FTP_USERNAME=test_sftp_user -POCL_FTP_KEY_SECRET_ID=/dev/fsh/local/sftp/ssh_ed25519_host_key - # Debug settings DEBUG=pocl:*,-pocl:ftp diff --git a/docker/infrastructure.yml b/docker/infrastructure.yml index 94c1ff47ba..acaf6a6ec2 100644 --- a/docker/infrastructure.yml +++ b/docker/infrastructure.yml @@ -16,14 +16,13 @@ services: environment: - SERVICES=s3,dynamodb,secretsmanager,cloudformation - DEFAULT_REGION=eu-west-2 - - DATA_DIR=/tmp/localstack/data - DOCKER_HOST=unix:///var/run/docker.sock - LAMBDA_EXECUTOR=docker - LAMBDA_REMOTE_DOCKER=false - DYNAMODB_HEAP_SIZE=1G volumes: - /var/run/docker.sock:/var/run/docker.sock - - ./volumes/localstack:/tmp/localstack + - ./volumes/localstack:/var/lib/localstack - ./resources/infrastructure/localstack/localstack-init.sh:/docker-entrypoint-initaws.d/init.sh - ./resources/infrastructure/localstack/localstack-cfn.yml:/docker-entrypoint-initaws.d/localstack-cfn.yml deploy: @@ -108,30 +107,3 @@ services: deploy: restart_policy: condition: on-failure - - ####################################################### - # Test SFTP server - ####################################################### - ftp: - image: rod_licensing/ftp:${TAG:-latest} - build: - dockerfile: Dockerfile - context: ./resources/infrastructure/sftp - depends_on: - - localstack - ports: - - '2222:22' - volumes: - - ./volumes/sftp:/home/test_sftp_user/share - environment: - SFTP_USER: test_sftp_user - SFTP_FOLDERS: share/pocl;share/fulfilment - AWS_SECRETSMANAGER_ENDPOINT: http://host.docker.internal:4566 - AWS_DEFAULT_REGION: eu-west-2 - AWS_ACCESS_KEY_ID: local - AWS_SECRET_ACCESS_KEY: local - SSH_HOST_ED25519_SECRET_ID: /dev/fsh/local/sftp/ssh_ed25519_host_key - SSH_HOST_RSA_SECRET_ID: /dev/fsh/local/sftp/ssh_rsa_host_key - deploy: - restart_policy: - condition: on-failure diff --git a/docker/resources/infrastructure/sftp/Dockerfile b/docker/resources/infrastructure/sftp/Dockerfile deleted file mode 100644 index 2c9f4ff96b..0000000000 --- a/docker/resources/infrastructure/sftp/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM alpine:latest - -RUN apk --no-cache -U -v upgrade \ - && apk --no-cache -U -v add bash openssh openssh-sftp-server aws-cli \ - && mkdir -p /var/run/sshd \ - && rm -f /etc/ssh/ssh_host_*key* - -COPY ./files/sshd_config /etc/ssh/sshd_config -COPY ./files/entrypoint.sh / -EXPOSE 22 -ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/resources/infrastructure/sftp/files/entrypoint.sh b/docker/resources/infrastructure/sftp/files/entrypoint.sh deleted file mode 100755 index 7e853335a0..0000000000 --- a/docker/resources/infrastructure/sftp/files/entrypoint.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/bin/bash -############################################################################### -# SFTP Initialisation script -############################################################################### -set -e -trap 'exit 1' INT - -ED25519_KEY_PATH="/etc/ssh/ssh_host_ed25519_key" -RSA_KEY_PATH="/etc/ssh/ssh_host_rsa_key" -SFTP_USER=${SFTP_USER:=test} -SFTP_FOLDERS=${SFTP_FOLDERS:=share} - -AWS_CLI_ARGS=() -if [ -n "${AWS_SECRETSMANAGER_ENDPOINT}" ]; then - AWS_CLI_ARGS+=('--endpoint' "${AWS_SECRETSMANAGER_ENDPOINT}") -fi - -######################### -# Create SSH keys -######################### -if [ -n "${SSH_HOST_ED25519_SECRET_ID}" ]; then - echo "Retrieving SSH_HOST_ED25519_KEY from aws secrets manager" - aws "${AWS_CLI_ARGS[@]}" secretsmanager get-secret-value --secret-id "${SSH_HOST_ED25519_SECRET_ID}" --query SecretString --output text | (umask 177; cat > "${ED25519_KEY_PATH}") -elif [ -n "${SSH_HOST_ED25519_KEY}" ]; then - echo "Using SSH_HOST_ED25519_KEY defined in environment" - echo "${SSH_HOST_ED25519_KEY}" | (umask 177; cat > "${ED25519_KEY_PATH}") -else - echo "Generating new SSH_HOST_ED25519_KEY" - ssh-keygen -t ed25519 -f "${ED25519_KEY_PATH}" -N '' - cat "${ED25519_KEY_PATH}" -fi -ssh-keygen -y -f "${ED25519_KEY_PATH}" > "${ED25519_KEY_PATH}.pub" -ssh-keygen -lvf "${ED25519_KEY_PATH}" - -if [ -n "${SSH_HOST_RSA_SECRET_ID}" ]; then - echo "Retrieving SSH_HOST_RSA_SECRET_ID from aws secrets manager" - aws "${AWS_CLI_ARGS[@]}" secretsmanager get-secret-value --secret-id "${SSH_HOST_RSA_SECRET_ID}" --query SecretString --output text | (umask 177; cat > "${RSA_KEY_PATH}") -elif [ -n "${SSH_HOST_RSA_KEY}" ]; then - echo "Using SSH_HOST_RSA_KEY defined in environment" - echo "${SSH_HOST_RSA_KEY}" | (umask 177; cat > "${RSA_KEY_PATH}") -else - echo "Generating new SSH_HOST_RSA_KEY" - ssh-keygen -t rsa -b 4096 -f "${RSA_KEY_PATH}" -N '' - cat "${RSA_KEY_PATH}" -fi -ssh-keygen -y -f "${RSA_KEY_PATH}" > "${RSA_KEY_PATH}.pub" -ssh-keygen -lvf "${RSA_KEY_PATH}" - -######################### -# Create test user -######################### -echo "Creating user ${SFTP_USER} with random password" -adduser "${SFTP_USER}" > /dev/null 2>&1 || true -echo "${SFTP_USER}:$(base64 /dev/urandom | tr -d '/+' | fold -w 32 | head -n1)" | chpasswd -e > /dev/null 2>&1 - -######################### -# Add authorised keys -######################### -echo "Adding authorised keys" -mkdir -p "/home/${SFTP_USER}/.ssh/keys/" -cp "${ED25519_KEY_PATH}.pub" "/home/${SFTP_USER}/.ssh/keys/id_ed25519.pub" -cp "${RSA_KEY_PATH}.pub" "/home/${SFTP_USER}/.ssh/keys/id_rsa.pub" -for publickey in "/home/${SFTP_USER}/.ssh/keys"/*; do - cat "${publickey}" >> "/home/${SFTP_USER}/.ssh/authorized_keys" -done - - -######################### -# Create default folders -######################### -IFS=';' -read -ra FOLDERS <<< "${SFTP_FOLDERS}" -for folder in "${FOLDERS[@]}"; do - echo "Creating folder /home/${SFTP_USER}/${folder}" - mkdir -p "/home/${SFTP_USER}/${folder}" -done - -######################### -# Set permissions -######################### -chown -R "${SFTP_USER}" "/home/${SFTP_USER}/" -chown root:root "/home/${SFTP_USER}" -chmod 755 "/home/${SFTP_USER}" - -exec /usr/sbin/sshd -D -e diff --git a/docker/resources/infrastructure/sftp/files/sshd_config b/docker/resources/infrastructure/sftp/files/sshd_config deleted file mode 100644 index 96e7861832..0000000000 --- a/docker/resources/infrastructure/sftp/files/sshd_config +++ /dev/null @@ -1,24 +0,0 @@ -# Secure defaults -Protocol 2 -HostKey /etc/ssh/ssh_host_ed25519_key -HostKey /etc/ssh/ssh_host_rsa_key - -# IPv4 Only -AddressFamily inet - -# Faster connection -UseDNS no - -# Limit access -PermitRootLogin no -X11Forwarding no -AllowTcpForwarding no -PasswordAuthentication no - -# Force sftp and chroot jail -Subsystem sftp internal-sftp -ForceCommand internal-sftp -ChrootDirectory %h - -# Enable this for more logs -LogLevel VERBOSE diff --git a/docker/volumes/sftp/fulfilment/README.md b/docker/volumes/sftp/fulfilment/README.md deleted file mode 100644 index c919ee9d42..0000000000 --- a/docker/volumes/sftp/fulfilment/README.md +++ /dev/null @@ -1 +0,0 @@ -> Used to provide a remote FTP server folder for fulfilment - do not remove diff --git a/docker/volumes/sftp/pocl/README.md b/docker/volumes/sftp/pocl/README.md deleted file mode 100644 index fa717a9d65..0000000000 --- a/docker/volumes/sftp/pocl/README.md +++ /dev/null @@ -1 +0,0 @@ -> Used to provide a remote FTP server folder for POCL - do not remove diff --git a/lerna.json b/lerna.json index 815da80386..068f183f88 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "1.49.0-rc.10", + "version": "1.57.0", "npmClient": "npm", "publishConfig": { "registry": "http://registry.npmjs.org/" diff --git a/package-lock.json b/package-lock.json index 26dcd51408..a4ba7b8591 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "husky": "^7.0.4", "jest": "^27.5.1", "jest-circus": "^27.5.1", - "lerna": "^5.0.0", + "lerna": "^5.6.2", "lerna-changelog": "^2.2.0", "lerna-update-wizard": "^1.1.1", "lint-staged": "^11.2.6", diff --git a/package.json b/package.json index 3a03085abf..3e3771b729 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "husky": "^7.0.4", "jest": "^27.5.1", "jest-circus": "^27.5.1", - "lerna": "^5.0.0", + "lerna": "^5.6.2", "lerna-changelog": "^2.2.0", "lerna-update-wizard": "^1.1.1", "lint-staged": "^11.2.6", diff --git a/packages/business-rules-lib/package-lock.json b/packages/business-rules-lib/package-lock.json index 19e8c0354f..e6ad334366 100644 --- a/packages/business-rules-lib/package-lock.json +++ b/packages/business-rules-lib/package-lock.json @@ -1,12 +1,12 @@ { "name": "@defra-fish/business-rules-lib", - "version": "1.49.0-rc.10", + "version": "1.57.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@defra-fish/business-rules-lib", - "version": "1.49.0-rc.10", + "version": "1.57.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { "joi": "^17.6.0", diff --git a/packages/business-rules-lib/package.json b/packages/business-rules-lib/package.json index 1767a0fe38..c61fc223cc 100644 --- a/packages/business-rules-lib/package.json +++ b/packages/business-rules-lib/package.json @@ -1,6 +1,6 @@ { "name": "@defra-fish/business-rules-lib", - "version": "1.49.0-rc.10", + "version": "1.57.0", "description": "Shared business rules for the rod licensing digital services", "type": "module", "engines": { diff --git a/packages/connectors-lib/README.md b/packages/connectors-lib/README.md index 1a09877969..40a6b3ef18 100644 --- a/packages/connectors-lib/README.md +++ b/packages/connectors-lib/README.md @@ -18,7 +18,9 @@ Provides connectivity to the resources/infrastructure used in the rod licensing | SALES_API_TIMEOUT_MS | Request timeout for the requests to the sales API | no | 20000 (20s) | | | | GOV_PAY_API_URL | The GOV.UK Pay API base url | yes | | | | | GOV_PAY_APIKEY | GOV pay access identifier | yes | | | | +| GOV_PAY_RECURRING_APIKEY | GOV pay access identifier for recurring payments | yes | | | | | GOV_PAY_REQUEST_TIMEOUT_MS | Timeout in milliseconds for API requests | no | 10000 | | | +| GOV_PAY_RCP_API_URL | The GOV.UK Pay API url for agreements | yes | | # Prerequisites diff --git a/packages/connectors-lib/package-lock.json b/packages/connectors-lib/package-lock.json index 7d51142f95..a2a35ced64 100644 --- a/packages/connectors-lib/package-lock.json +++ b/packages/connectors-lib/package-lock.json @@ -1,12 +1,12 @@ { "name": "@defra-fish/connectors-lib", - "version": "1.49.0-rc.10", + "version": "1.57.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@defra-fish/connectors-lib", - "version": "1.49.0-rc.10", + "version": "1.57.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@airbrake/node": "^2.1.7", diff --git a/packages/connectors-lib/package.json b/packages/connectors-lib/package.json index 3d9d67ffc7..890dea8b22 100644 --- a/packages/connectors-lib/package.json +++ b/packages/connectors-lib/package.json @@ -1,6 +1,6 @@ { "name": "@defra-fish/connectors-lib", - "version": "1.49.0-rc.10", + "version": "1.57.0", "description": "Shared connectors", "type": "module", "engines": { diff --git a/packages/connectors-lib/src/__tests__/govuk-pay-api.spec.js b/packages/connectors-lib/src/__tests__/govuk-pay-api.spec.js index 0f7fbd9b74..4c1832c70e 100644 --- a/packages/connectors-lib/src/__tests__/govuk-pay-api.spec.js +++ b/packages/connectors-lib/src/__tests__/govuk-pay-api.spec.js @@ -3,7 +3,9 @@ jest.mock('node-fetch') const fetch = require('node-fetch') process.env.GOV_PAY_API_URL = 'http://0.0.0.0/payment' +process.env.GOV_PAY_RCP_API_URL = 'http://0.0.0.0/agreement' process.env.GOV_PAY_APIKEY = 'key' +process.env.GOV_PAY_RECURRING_APIKEY = 'recurringkey' const headers = { accept: 'application/json', @@ -11,6 +13,12 @@ const headers = { 'content-type': 'application/json' } +const recurringHeaders = { + accept: 'application/json', + authorization: `Bearer ${process.env.GOV_PAY_RECURRING_APIKEY}`, + 'content-type': 'application/json' +} + describe('govuk-pay-api-connector', () => { beforeEach(jest.clearAllMocks) @@ -40,7 +48,19 @@ describe('govuk-pay-api-connector', () => { }) expect(consoleErrorSpy).toHaveBeenCalled() }) + + it('uses the correct API key if recurring arg is set to true', async () => { + fetch.mockReturnValue({ ok: true, status: 200 }) + await expect(govUkPayApi.createPayment({ cost: 0 }, true)).resolves.toEqual({ ok: true, status: 200 }) + expect(fetch).toHaveBeenCalledWith('http://0.0.0.0/payment', { + body: JSON.stringify({ cost: 0 }), + headers: recurringHeaders, + method: 'post', + timeout: 10000 + }) + }) }) + describe('fetchPaymentStatus', () => { it('retrieves payment status', async () => { fetch.mockReturnValue({ ok: true, status: 200, json: () => {} }) @@ -61,6 +81,16 @@ describe('govuk-pay-api-connector', () => { expect(fetch).toHaveBeenCalledWith('http://0.0.0.0/payment/123', { headers, method: 'get', timeout: 10000 }) expect(consoleErrorSpy).toHaveBeenCalled() }) + + it('uses the correct API key if recurring arg is set to true', async () => { + fetch.mockReturnValue({ ok: true, status: 200, json: () => {} }) + await expect(govUkPayApi.fetchPaymentStatus(123, true)).resolves.toEqual(expect.objectContaining({ ok: true, status: 200 })) + expect(fetch).toHaveBeenCalledWith('http://0.0.0.0/payment/123', { + headers: recurringHeaders, + method: 'get', + timeout: 10000 + }) + }) }) describe('fetchPaymentEvents', () => { @@ -78,5 +108,43 @@ describe('govuk-pay-api-connector', () => { await expect(govUkPayApi.fetchPaymentEvents(123)).rejects.toEqual(Error('test event error')) expect(consoleErrorSpy).toHaveBeenCalled() }) + + it('uses the correct API key if recurring arg is set to true', async () => { + fetch.mockReturnValue({ ok: true, status: 200, json: () => {} }) + await expect(govUkPayApi.fetchPaymentEvents(123, true)).resolves.toEqual(expect.objectContaining({ ok: true, status: 200 })) + expect(fetch).toHaveBeenCalledWith('http://0.0.0.0/payment/123/events', { + headers: recurringHeaders, + method: 'get', + timeout: 10000 + }) + }) + }) + + describe('createRecurringPaymentAgreement', () => { + it('creates new payments', async () => { + fetch.mockReturnValue({ ok: true, status: 200 }) + await expect(govUkPayApi.createRecurringPaymentAgreement({ cost: 0 })).resolves.toEqual({ ok: true, status: 200 }) + expect(fetch).toHaveBeenCalledWith('http://0.0.0.0/agreement', { + body: JSON.stringify({ cost: 0 }), + headers: recurringHeaders, + method: 'post', + timeout: 10000 + }) + }) + + it('logs and throws errors', async () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn()) + fetch.mockImplementation(() => { + throw new Error('') + }) + expect(govUkPayApi.createRecurringPaymentAgreement({ reference: '123' })).rejects.toEqual(Error('')) + expect(fetch).toHaveBeenCalledWith('http://0.0.0.0/agreement', { + body: JSON.stringify({ reference: '123' }), + headers: recurringHeaders, + method: 'post', + timeout: 10000 + }) + expect(consoleErrorSpy).toHaveBeenCalled() + }) }) }) diff --git a/packages/connectors-lib/src/govuk-pay-api.js b/packages/connectors-lib/src/govuk-pay-api.js index 1e55f6ad6d..b3295dec3b 100644 --- a/packages/connectors-lib/src/govuk-pay-api.js +++ b/packages/connectors-lib/src/govuk-pay-api.js @@ -4,21 +4,43 @@ import fetch from 'node-fetch' const GOV_PAY_REQUEST_TIMEOUT_MS_DEFAULT = 10000 -const headers = () => ({ +const headers = recurring => ({ accept: 'application/json', - authorization: `Bearer ${process.env.GOV_PAY_APIKEY}`, + authorization: `Bearer ${recurring ? process.env.GOV_PAY_RECURRING_APIKEY : process.env.GOV_PAY_APIKEY}`, 'content-type': 'application/json' }) +/** + * Create a new recurring payment + * @param preparedPayment - see the GOV.UK pay API reference for details + * @returns {Promise<*>} + */ +export const createRecurringPaymentAgreement = async preparedPayment => { + try { + return fetch(process.env.GOV_PAY_RCP_API_URL, { + headers: headers(true), + method: 'post', + body: JSON.stringify(preparedPayment), + timeout: process.env.GOV_PAY_REQUEST_TIMEOUT_MS || GOV_PAY_REQUEST_TIMEOUT_MS_DEFAULT + }) + } catch (err) { + console.error( + `Error creating recurring payment agreement in the GOV.UK API service - agreement: ${JSON.stringify(preparedPayment, null, 4)}`, + err + ) + throw err + } +} + /** * Create a new payment * @param preparedPayment - see the GOV.UK pay API reference for details * @returns {Promise<*>} */ -export const createPayment = async preparedPayment => { +export const createPayment = async (preparedPayment, recurring = false) => { try { return fetch(process.env.GOV_PAY_API_URL, { - headers: headers(), + headers: headers(recurring), method: 'post', body: JSON.stringify(preparedPayment), timeout: process.env.GOV_PAY_REQUEST_TIMEOUT_MS || GOV_PAY_REQUEST_TIMEOUT_MS_DEFAULT @@ -34,10 +56,10 @@ export const createPayment = async preparedPayment => { * @param paymentId * @returns {Promise} */ -export const fetchPaymentStatus = async paymentId => { +export const fetchPaymentStatus = async (paymentId, recurring = false) => { try { return fetch(`${process.env.GOV_PAY_API_URL}/${paymentId}`, { - headers: headers(), + headers: headers(recurring), method: 'get', timeout: process.env.GOV_PAY_REQUEST_TIMEOUT_MS || GOV_PAY_REQUEST_TIMEOUT_MS_DEFAULT }) @@ -52,10 +74,10 @@ export const fetchPaymentStatus = async paymentId => { * @param paymentId * @returns {Promise} */ -export const fetchPaymentEvents = async paymentId => { +export const fetchPaymentEvents = async (paymentId, recurring = false) => { try { return fetch(`${process.env.GOV_PAY_API_URL}/${paymentId}/events`, { - headers: headers(), + headers: headers(recurring), method: 'get', timeout: process.env.GOV_PAY_REQUEST_TIMEOUT_MS || GOV_PAY_REQUEST_TIMEOUT_MS_DEFAULT }) diff --git a/packages/dynamics-lib/package-lock.json b/packages/dynamics-lib/package-lock.json index e955de3df1..91e50b63e6 100644 --- a/packages/dynamics-lib/package-lock.json +++ b/packages/dynamics-lib/package-lock.json @@ -1,12 +1,12 @@ { "name": "@defra-fish/dynamics-lib", - "version": "1.49.0-rc.10", + "version": "1.57.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@defra-fish/dynamics-lib", - "version": "1.49.0-rc.10", + "version": "1.57.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { "cache-manager": "^3.6.0", diff --git a/packages/dynamics-lib/package.json b/packages/dynamics-lib/package.json index d9f3dd2e13..aae89eef6a 100644 --- a/packages/dynamics-lib/package.json +++ b/packages/dynamics-lib/package.json @@ -1,6 +1,6 @@ { "name": "@defra-fish/dynamics-lib", - "version": "1.49.0-rc.10", + "version": "1.57.0", "description": "Framework to support integration with dynamics", "type": "module", "engines": { diff --git a/packages/dynamics-lib/src/entities/__tests__/recurring-payment.entity.spec.js b/packages/dynamics-lib/src/entities/__tests__/recurring-payment.entity.spec.js index 8ca272c0ee..3f26d9f94d 100644 --- a/packages/dynamics-lib/src/entities/__tests__/recurring-payment.entity.spec.js +++ b/packages/dynamics-lib/src/entities/__tests__/recurring-payment.entity.spec.js @@ -17,8 +17,6 @@ describe('recurring payment entity', () => { recurringPayment.agreementId = 'c9267c6e-573d-488b-99ab-ea18431fc472' recurringPayment.publicId = '649-213' recurringPayment.status = 1 - recurringPayment.contactId = 'b3d33cln-2e83-ea11-a811-000d3a649213' - recurringPayment.activePermission = 'a5b24adf-2e83-ea11-a811-000d3a649213' recurringPayment.bindToEntity(RecurringPayment.definition.relationships.contact, contact) recurringPayment.bindToEntity(RecurringPayment.definition.relationships.activePermission, permission) @@ -66,9 +64,7 @@ describe('recurring payment entity', () => { endDate: '2019-12-15T00:00:00Z', agreementId: 'c9267c6e-573d-488b-99ab-ea18431fc472', publicId: '649-213', - status: 1, - activePermission: 'a5b24adf-2e83-ea11-a811-000d3a649213', - contactId: 'b3d33cln-2e83-ea11-a811-000d3a649213' + status: 1 }) ) }) @@ -93,8 +89,6 @@ describe('recurring payment entity', () => { defra_agreementid: 'c9267c6e-573d-488b-99ab-ea18431fc472', defra_publicid: '649-213', statecode: 1, - _defra_activepermission_value: 'a5b24adf-2e83-ea11-a811-000d3a649213', - _defra_contact_value: 'b3d33cln-2e83-ea11-a811-000d3a649213', 'defra_Contact@odata.bind': `$${contact.uniqueContentId}`, 'defra_ActivePermission@odata.bind': `$${permission.uniqueContentId}` }) @@ -121,9 +115,7 @@ describe('recurring payment entity', () => { endDate: '2019-12-15T00:00:00Z', agreementId: 'c9267c6e-573d-488b-99ab-ea18431fc472', publicId: '649-213', - status: 1, - activePermission: 'a5b24adf-2e83-ea11-a811-000d3a649213', - contactId: 'b3d33cln-2e83-ea11-a811-000d3a649213' + status: 1 }) ) }) @@ -143,8 +135,6 @@ describe('recurring payment entity', () => { defra_agreementid: 'c9267c6e-573d-488b-99ab-ea18431fc472', defra_publicid: '649-213', statecode: 1, - _defra_activepermission_value: 'a5b24adf-2e83-ea11-a811-000d3a649213', - _defra_contact_value: 'b3d33cln-2e83-ea11-a811-000d3a649213', 'defra_Contact@odata.bind': `$${contact.uniqueContentId}`, 'defra_ActivePermission@odata.bind': `$${permission.uniqueContentId}` }) diff --git a/packages/dynamics-lib/src/entities/recurring-payment.entity.js b/packages/dynamics-lib/src/entities/recurring-payment.entity.js index 12e1e4fa6c..2b07f6698e 100755 --- a/packages/dynamics-lib/src/entities/recurring-payment.entity.js +++ b/packages/dynamics-lib/src/entities/recurring-payment.entity.js @@ -134,28 +134,4 @@ export class RecurringPayment extends BaseEntity { set status (status) { super._setState('status', status) } - - /** - * The ID of the associated contact - * @type {string} - */ - get contactId () { - return super._getState('contactId') - } - - set contactId (contactId) { - super._setState('contactId', contactId) - } - - /** - * The ID of the associated active permission - * @type {string} - */ - get activePermission () { - return super._getState('activePermission') - } - - set activePermission (activePermission) { - super._setState('activePermission', activePermission) - } } diff --git a/packages/dynamics-lib/src/index.js b/packages/dynamics-lib/src/index.js index c501bdc23e..e896f46651 100644 --- a/packages/dynamics-lib/src/index.js +++ b/packages/dynamics-lib/src/index.js @@ -27,6 +27,8 @@ export * from './queries/fulfilment.queries.js' export * from './queries/concession-proof.queries.js' export * from './queries/pocl-validation-error.queries.js' export * from './queries/recurring-payments.queries.js' +export * from './queries/contact.queries.js' +export * from './queries/activity.queries.js' // Framework functionality export * from './client/util.js' diff --git a/packages/dynamics-lib/src/queries/__tests__/activity.queries.spec.js b/packages/dynamics-lib/src/queries/__tests__/activity.queries.spec.js new file mode 100644 index 0000000000..85788234e9 --- /dev/null +++ b/packages/dynamics-lib/src/queries/__tests__/activity.queries.spec.js @@ -0,0 +1,133 @@ +import { createActivity, updateActivity } from '../activity.queries.js' +import { dynamicsClient } from '../../client/dynamics-client.js' + +jest.mock('dynamics-web-api', () => { + return jest.fn().mockImplementation(() => { + return { + executeUnboundAction: jest.fn() + } + }) +}) + +describe('Activity Service', () => { + describe('createActivity', () => { + const getSuccessResponse = () => ({ + '@odata.context': 'https://dynamics.com/api/data/v9.1/defra_CreateRCRActivityResponse', + RCRActivityId: 'abc123', + ReturnStatus: 'success', + SuccessMessage: 'RCR Activity - created successfully', + ErrorMessage: null, + oDataContext: 'https://dynamics.com/api/data/v9.1/defra_CreateRCRActivityResponse' + }) + + const getErrorResponse = () => ({ + '@odata.context': 'https://dynamics.com/api/data/v9.1/.defra_CreateRCRActivityResponse', + RCRActivityId: null, + ReturnStatus: 'error', + SuccessMessage: '', + ErrorMessage: 'Failed to create activity', + oDataContext: 'https://dynamics.com/api/data/v9.1/$metadata#Microsoft.Dynamics.CRM.defra_CreateRCRActivityResponse' + }) + + it('should call dynamicsClient with correct parameters', async () => { + dynamicsClient.executeUnboundAction.mockResolvedValue(getSuccessResponse()) + + await createActivity('contact-identifier-123', 2023) + + expect(dynamicsClient.executeUnboundAction).toHaveBeenCalledWith('defra_CreateRCRActivity', { + ContactId: 'contact-identifier-123', + ActivityStatus: 'STARTED', + Season: 2023 + }) + }) + + it('should return the CRM response correctly', async () => { + const successResponse = getSuccessResponse() + dynamicsClient.executeUnboundAction.mockResolvedValue(successResponse) + + const result = await createActivity('contact-identifier-123', 2024) + + expect(result).toEqual(successResponse) + }) + + it('should handle error in dynamicsClient response', async () => { + const error = new Error('Failed to create activity') + dynamicsClient.executeUnboundAction.mockRejectedValue(error) + + await expect(createActivity('contact-identifier-123', 2024)).rejects.toThrow('Failed to create activity') + }) + + it('should handle the case where activity creation fails', async () => { + dynamicsClient.executeUnboundAction.mockResolvedValue(getErrorResponse()) + + const result = await createActivity('invalid-contact-id', 2024) + + expect(result).toMatchObject({ + RCRActivityId: null, + ReturnStatus: 'error', + SuccessMessage: '', + ErrorMessage: 'Failed to create activity' + }) + }) + }) + + describe('updateActivity', () => { + const getSuccessResponse = () => ({ + '@odata.context': 'https://dynamics.om/api/data/v9.1/defra_UpdateRCRActivityResponse', + ReturnStatus: 'success', + SuccessMessage: 'RCR Activity - updated successfully', + ErrorMessage: null, + oDataContext: 'https://dynamics.com/api/data/v9.1/defra_UpdateRCRActivityResponse' + }) + + const getErrorResponse = () => ({ + '@odata.context': 'https://dynamics.om/api/data/v9.1/defra_UpdateRCRActivityResponse', + RCRActivityId: null, + ReturnStatus: 'error', + SuccessMessage: '', + ErrorMessage: 'Failed to update activity', + oDataContext: 'https://dynamics.com/api/data/v9.1/defra_UpdateRCRActivityResponse' + }) + + it('should call dynamicsClient with correct parameters', async () => { + dynamicsClient.executeUnboundAction.mockResolvedValue(getSuccessResponse()) + + await updateActivity('contact-identifier-123', 2023) + + expect(dynamicsClient.executeUnboundAction).toHaveBeenCalledWith('defra_UpdateRCRActivity', { + ContactId: 'contact-identifier-123', + ActivityStatus: 'SUBMITTED', + Season: 2023 + }) + }) + + it('should return the CRM response correctly', async () => { + const successResponse = getSuccessResponse() + dynamicsClient.executeUnboundAction.mockResolvedValue(successResponse) + + const result = await updateActivity('contact-identifier-123', 2024) + + expect(result).toEqual(successResponse) + }) + + it('should handle error in dynamicsClient response', async () => { + const error = new Error('Failed to update activity') + dynamicsClient.executeUnboundAction.mockRejectedValue(error) + + await expect(updateActivity('contact-identifier-123', 2024)).rejects.toThrow('Failed to update activity') + }) + + it('should handle the case where activity creation fails', async () => { + dynamicsClient.executeUnboundAction.mockResolvedValue(getErrorResponse()) + + const result = await updateActivity('invalid-contact-id', 2024) + + expect(result).toMatchObject({ + RCRActivityId: null, + ReturnStatus: 'error', + SuccessMessage: '', + ErrorMessage: 'Failed to update activity' + }) + }) + }) +}) diff --git a/packages/dynamics-lib/src/queries/__tests__/contact.queries.spec.js b/packages/dynamics-lib/src/queries/__tests__/contact.queries.spec.js new file mode 100644 index 0000000000..ff7418b4d7 --- /dev/null +++ b/packages/dynamics-lib/src/queries/__tests__/contact.queries.spec.js @@ -0,0 +1,100 @@ +import { contactForLicensee } from '../contact.queries.js' +import { dynamicsClient } from '../../client/dynamics-client.js' + +jest.mock('dynamics-web-api', () => { + return jest.fn().mockImplementation(() => { + return { + executeUnboundAction: jest.fn() + } + }) +}) + +describe('Contact Queries', () => { + describe('contactForLicensee', () => { + const mockResponse = { + ContactId: 'f1bb733e-3b1e-ea11-a810-000d3a25c5d6', + FirstName: 'Fester', + LastName: 'Tester', + DateOfBirth: '9/13/1946 12:00:00 AM', + Premises: '47', + Street: null, + Town: 'Testerton', + Locality: null, + Postcode: 'AB12 3CD', + ReturnStatus: 'success', + SuccessMessage: 'contact found successfully', + ErrorMessage: null, + ReturnPermissionNumber: '11100420-2WT1SFT-KPMW2C', + oDataContext: 'https://api.com/api/data/v9.1/$metadata#Microsoft.Dynamics.CRM.defra_GetContactByLicenceAndPostcodeResponse' + } + + const noContactResponse = { + ContactId: null, + FirstName: null, + LastName: null, + DateOfBirth: null, + Premises: null, + Street: null, + Town: null, + Locality: null, + Postcode: null, + ReturnStatus: 'error', + SuccessMessage: '', + ErrorMessage: 'contact does not exists', + ReturnPermissionNumber: null, + oDataContext: + 'https://api.crm4.dynamics.com/api/data/v9.1/$metadata#Microsoft.Dynamics.CRM.defra_GetContactByLicenceAndPostcodeResponse' + } + + it('should call dynamicsClient with correct parameters', async () => { + dynamicsClient.executeUnboundAction.mockResolvedValue(mockResponse) + + const permissionNumber = 'KPMW2C' + const postcode = 'AB12 3CD' + + await contactForLicensee(permissionNumber, postcode) + + expect(dynamicsClient.executeUnboundAction).toHaveBeenCalledWith('defra_GetContactByLicenceAndPostcode', { + PermissionNumber: permissionNumber, + InputPostCode: postcode + }) + }) + + it('should return the CRM response correctly', async () => { + dynamicsClient.executeUnboundAction.mockResolvedValue(mockResponse) + + const result = await contactForLicensee('KPMW2C', 'AB12 3CD') + + expect(result).toEqual(mockResponse) + }) + + it('should handle error in dynamicsClient response', async () => { + const error = new Error('Failed to fetch data') + dynamicsClient.executeUnboundAction.mockRejectedValue(error) + + await expect(contactForLicensee('KPMW2C', 'AB12 3CD')).rejects.toThrow('Failed to fetch data') + }) + + it('should handle the case where contact does not exist', async () => { + dynamicsClient.executeUnboundAction.mockResolvedValue(noContactResponse) + + const result = await contactForLicensee('654321', 'ZZ1 1ZZ') + + expect(result).toMatchObject({ + ContactId: null, + FirstName: null, + LastName: null, + DateOfBirth: null, + Premises: null, + Street: null, + Town: null, + Locality: null, + Postcode: null, + ReturnStatus: 'error', + SuccessMessage: '', + ErrorMessage: 'contact does not exists', + ReturnPermissionNumber: null + }) + }) + }) +}) diff --git a/packages/dynamics-lib/src/queries/activity.queries.js b/packages/dynamics-lib/src/queries/activity.queries.js new file mode 100644 index 0000000000..efbbb339fb --- /dev/null +++ b/packages/dynamics-lib/src/queries/activity.queries.js @@ -0,0 +1,47 @@ +import { dynamicsClient } from '../client/dynamics-client.js' + +/** + * Creates an RCR Activity in Microsoft Dynamics CRM. + * + * @param {string} contactId - The ID of the contact associated with the activity. + * @param {number} season - The season year for which the activity is being created. + * @returns {Promise} - A promise that resolves to the response from Dynamics CRM. + * @property {string} response.@odata.context - The OData context URL of the response. + * @property {string} response.RCRActivityId - The unique identifier of the created RCR activity. + * @property {string} response.ReturnStatus - The status of the activity creation operation (e.g., 'success'). + * @property {string} response.SuccessMessage - A message indicating successful creation of the activity. + * @property {string|null} response.ErrorMessage - An error message if the activity creation failed, otherwise null. + * @property {string} response.oDataContext - The OData context URL of the response. + */ +export const createActivity = (contactId, season) => { + const request = { + ContactId: contactId, + ActivityStatus: 'STARTED', + Season: season + } + + return dynamicsClient.executeUnboundAction('defra_CreateRCRActivity', request) +} + +/** + * Updates an RCR Activity in Microsoft Dynamics CRM. + * + * @param {string} contactId - The ID of the contact associated with the activity. + * @param {number} season - The season year for which the activity is being created. + * @returns {Promise} - A promise that resolves to the response from Dynamics CRM. + * @property {string} response.@odata.context - The OData context URL of the response. + * @property {string} response.RCRActivityId - The unique identifier of the created RCR activity. + * @property {string} response.ReturnStatus - The status of the activity creation operation (e.g., 'success'). + * @property {string} response.SuccessMessage - A message indicating successful creation of the activity. + * @property {string|null} response.ErrorMessage - An error message if the activity creation failed, otherwise null. + * @property {string} response.oDataContext - The OData context URL of the response. + */ +export const updateActivity = (contactId, season) => { + const request = { + ContactId: contactId, + ActivityStatus: 'SUBMITTED', + Season: season + } + + return dynamicsClient.executeUnboundAction('defra_UpdateRCRActivity', request) +} diff --git a/packages/dynamics-lib/src/queries/contact.queries.js b/packages/dynamics-lib/src/queries/contact.queries.js new file mode 100644 index 0000000000..12c1c72bed --- /dev/null +++ b/packages/dynamics-lib/src/queries/contact.queries.js @@ -0,0 +1,35 @@ +import { dynamicsClient } from '../client/dynamics-client.js' + +/** + * @typedef {Object} ContactByLicenceAndPostcode + * @property {string|null} ContactId - The contact's unique identifier + * @property {string|null} FirstName - The contact's first name + * @property {string|null} LastName - The contact's last name + * @property {string|null} DateOfBirth - The contact's date of birth + * @property {string|null} Street - The contact's street + * @property {string|null} Town - The contact's town + * @property {string|null} Locality - The contact's locality + * @property {string|null} Postcode - The contact's postcode + * @property {string} ReturnStatus - The status of the request (e.g., "success" or "error") + * @property {string|null} SuccessMessage - A success message if the contact is found + * @property {string|null} ErrorMessage - An error message if the contact is not found + * @property {string|null} ReturnPermissionNumber - The full permission number of the contact + * @property {string} oDataContext - The OData context URL + */ + +/** + * Calls the defra_GetContactByLicenceAndPostcode CRM plugin to retrieve a contact by the last 6 characters if their license number and postcode + * + * @param permissionReferenceNumberLast6Characters the last 6 characters of the permission reference number + * @param licenseePostcode the postcode of the contact associated with the permission + * @returns {Promise} + */ + +export const contactForLicensee = (permissionReferenceNumberLast6Characters, licenseePostcode) => { + const request = { + PermissionNumber: permissionReferenceNumberLast6Characters, + InputPostCode: licenseePostcode + } + + return dynamicsClient.executeUnboundAction('defra_GetContactByLicenceAndPostcode', request) +} diff --git a/packages/fulfilment-job/README.md b/packages/fulfilment-job/README.md index 566f9b5ff7..393cc1eca7 100644 --- a/packages/fulfilment-job/README.md +++ b/packages/fulfilment-job/README.md @@ -19,21 +19,16 @@ provider. # Environment variables -| name | description | required | default | valid | notes | -| ----------------------------------- | ----------------------------------------------------------------------------------------- | :------: | ------- | ----------------------------------------------------------------------- | ----- | -| NODE_ENV | Node environment | no | | development, test, production | | -| FULFILMENT_FILE_SIZE | The maximum number of records written to an aggregated fulfilment file | yes | | | | -| FULFILMENT_FTP_HOST | The hostname of the target FTP server | yes | | | | -| FULFILMENT_FTP_PORT | The port of the FTP service on the target server | yes | | | | -| FULFILMENT_FTP_PATH | The base path under which files should be written to the FTP server | yes | | | | -| FULFILMENT_FTP_USERNAME | The username used to authenticate with the FTP server | yes | | | | -| FULFILMENT_FTP_KEY_SECRET_ID | The ID of the secret in AWS secrets manager which contains the SSH key for authentication | yes | | | | -| FULFILMENT_S3_BUCKET | The name of the AWS S3 bucket in which to stage and aggregate fulfilment data | yes | | | | -| FULFILMENT_SEND_UNENCRYPTED_FILE | Flag for whether to send the unencrypted fulfilment file | no | false | true, false, 0, 1 | | -| FULFILMENT_PGP_PUBLIC_KEY_SECRET_ID | The secret id for the file encryption public key | yes | | | | -| DEBUG | Use to enable output of debug information to the console | yes | | fulfilment:\*, fulfilment:staging, fulfilment:transport, fulfilment:ftp | | -| AIRBRAKE_HOST | URL of airbrake host | no | | | | -| AIRBRAKE_PROJECT_KEY | Project key for airbrake logging | no | | | | +| name | description | required | default | valid | notes | +| ----------------------------------- | ----------------------------------------------------------------------------- | :------: | ------- | ----------------------------------------------------------------------- | ----- | +| NODE_ENV | Node environment | no | | development, test, production | | +| FULFILMENT_FILE_SIZE | The maximum number of records written to an aggregated fulfilment file | yes | | | | +| FULFILMENT_S3_BUCKET | The name of the AWS S3 bucket in which to stage and aggregate fulfilment data | yes | | | | +| FULFILMENT_SEND_UNENCRYPTED_FILE | Flag for whether to send the unencrypted fulfilment file | no | false | true, false, 0, 1 | | +| FULFILMENT_PGP_PUBLIC_KEY_SECRET_ID | The secret id for the file encryption public key | yes | | | | +| DEBUG | Use to enable output of debug information to the console | yes | | fulfilment:\*, fulfilment:staging, fulfilment:transport, fulfilment:ftp | | +| AIRBRAKE_HOST | URL of airbrake host | no | | | | +| AIRBRAKE_PROJECT_KEY | Project key for airbrake logging | no | | | | ### See also: diff --git a/packages/fulfilment-job/package-lock.json b/packages/fulfilment-job/package-lock.json index 05b1a7e470..c9885d9a96 100644 --- a/packages/fulfilment-job/package-lock.json +++ b/packages/fulfilment-job/package-lock.json @@ -1,44 +1,27 @@ { "name": "@defra-fish/fulfilment-job", - "version": "1.49.0-rc.10", + "version": "1.57.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@defra-fish/fulfilment-job", - "version": "1.49.0-rc.10", + "version": "1.57.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { + "@defra-fish/connectors-lib": "1.57.0", + "@defra-fish/dynamics-lib": "1.57.0", "commander": "^7.2.0", "debug": "^4.3.3", "merge2": "^1.4.1", "moment": "^2.29.1", "openpgp": "^5.0.0-1", - "pluralize": "^8.0.0", - "ssh2-sftp-client": "^6.0.1" + "pluralize": "^8.0.0" }, "engines": { "node": ">=18.17" } }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", - "integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==", - "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "node_modules/asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, "node_modules/asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -50,77 +33,11 @@ "safer-buffer": "^2.1.0" } }, - "node_modules/async": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", - "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, "node_modules/bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, - "node_modules/buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "node_modules/color": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", - "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", - "dependencies": { - "color-convert": "^1.9.1", - "color-string": "^1.5.2" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "node_modules/color-string": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz", - "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/colorspace": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", - "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", - "dependencies": { - "color": "3.0.x", - "text-hex": "1.0.x" - } - }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -129,25 +46,6 @@ "node": ">= 10" } }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, "node_modules/debug": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", @@ -164,71 +62,11 @@ } } }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" - }, - "node_modules/fast-safe-stringify": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", - "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" - }, - "node_modules/fecha": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz", - "integrity": "sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg==" - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - }, - "node_modules/is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" - }, - "node_modules/logform": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz", - "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==", - "dependencies": { - "colors": "^1.2.1", - "fast-safe-stringify": "^2.0.4", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "triple-beam": "^1.3.0" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -255,14 +93,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "dependencies": { - "fn.name": "1.x.x" - } - }, "node_modules/openpgp": { "version": "5.0.0-1", "resolved": "https://registry.npmjs.org/openpgp/-/openpgp-5.0.0-1.tgz", @@ -282,217 +112,10 @@ "node": ">=4" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", - "engines": { - "node": ">= 4" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/ssh2": { - "version": "0.8.9", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz", - "integrity": "sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==", - "dependencies": { - "ssh2-streams": "~0.4.10" - }, - "engines": { - "node": ">=5.2.0" - } - }, - "node_modules/ssh2-sftp-client": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssh2-sftp-client/-/ssh2-sftp-client-6.0.1.tgz", - "integrity": "sha512-Glut2SmK/XpNOBiEuzqlKZGKkIyha2XMbuWVXR2hFUJkNsbyl/wmlZSeUEPxKFp/dC9UEvUKzanKydgLmNdfkw==", - "dependencies": { - "concat-stream": "^2.0.0", - "promise-retry": "^2.0.1", - "ssh2": "^0.8.9", - "winston": "^3.3.3" - } - }, - "node_modules/ssh2-streams": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz", - "integrity": "sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==", - "dependencies": { - "asn1": "~0.2.0", - "bcrypt-pbkdf": "^1.0.2", - "streamsearch": "~0.1.2" - }, - "engines": { - "node": ">=5.2.0" - } - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", - "engines": { - "node": "*" - } - }, - "node_modules/streamsearch": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" - }, - "node_modules/triple-beam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", - "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "node_modules/winston": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", - "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==", - "dependencies": { - "@dabh/diagnostics": "^2.0.2", - "async": "^3.1.0", - "is-stream": "^2.0.0", - "logform": "^2.2.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.4.0" - }, - "engines": { - "node": ">= 6.4.0" - } - }, - "node_modules/winston-transport": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", - "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", - "dependencies": { - "readable-stream": "^2.3.7", - "triple-beam": "^1.2.0" - }, - "engines": { - "node": ">= 6.4.0" - } - }, - "node_modules/winston-transport/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/winston-transport/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/winston-transport/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } } } } \ No newline at end of file diff --git a/packages/fulfilment-job/package.json b/packages/fulfilment-job/package.json index 0d4dbc4fd9..ee465fb261 100644 --- a/packages/fulfilment-job/package.json +++ b/packages/fulfilment-job/package.json @@ -1,6 +1,6 @@ { "name": "@defra-fish/fulfilment-job", - "version": "1.49.0-rc.10", + "version": "1.57.0", "description": "Rod Licensing Sales Fulfilment Job", "type": "module", "engines": { @@ -35,14 +35,13 @@ "test": "echo \"Error: run tests from root\" && exit 1" }, "dependencies": { - "@defra-fish/connectors-lib": "1.49.0-rc.10", - "@defra-fish/dynamics-lib": "1.49.0-rc.10", + "@defra-fish/connectors-lib": "1.57.0", + "@defra-fish/dynamics-lib": "1.57.0", "commander": "^7.2.0", "debug": "^4.3.3", "merge2": "^1.4.1", "moment": "^2.29.1", "openpgp": "^5.0.0-1", - "pluralize": "^8.0.0", - "ssh2-sftp-client": "^6.0.1" + "pluralize": "^8.0.0" } } diff --git a/packages/fulfilment-job/src/__mocks__/ssh2-sftp-client.js b/packages/fulfilment-job/src/__mocks__/ssh2-sftp-client.js deleted file mode 100644 index 124d4777bb..0000000000 --- a/packages/fulfilment-job/src/__mocks__/ssh2-sftp-client.js +++ /dev/null @@ -1,9 +0,0 @@ -const ssh2sftpClient = jest.genMockFromModule('ssh2-sftp-client') - -export const mockedFtpMethods = { - connect: jest.fn(async () => {}), - put: jest.fn(async () => {}), - end: jest.fn() -} -ssh2sftpClient.mockImplementation(() => mockedFtpMethods) -export default ssh2sftpClient diff --git a/packages/fulfilment-job/src/__tests__/config.spec.js b/packages/fulfilment-job/src/__tests__/config.spec.js index 1cbecefe04..883b6c86c6 100644 --- a/packages/fulfilment-job/src/__tests__/config.spec.js +++ b/packages/fulfilment-job/src/__tests__/config.spec.js @@ -17,11 +17,6 @@ const clearEnvVars = () => { const envVars = Object.freeze({ FULFILMENT_FILE_SIZE: 1234, - FULFILMENT_FTP_HOST: 'test-host', - FULFILMENT_FTP_PORT: 2222, - FULFILMENT_FTP_PATH: '/remote/share', - FULFILMENT_FTP_USERNAME: 'test-user', - FULFILMENT_FTP_KEY_SECRET_ID: 'test-secret-id', FULFILMENT_S3_BUCKET: 'test-bucket', FULFILMENT_PGP_PUBLIC_KEY_SECRET_ID: 'pgp-key-secret-id', FULFILMENT_SEND_UNENCRYPTED_FILE: 'false' @@ -44,32 +39,6 @@ describe('config', () => { }) }) - describe('ftp', () => { - it('provides properties relating the use of SFTP', async () => { - expect(config.ftp).toEqual( - expect.objectContaining({ - host: 'test-host', - port: '2222', - path: '/remote/share', - username: 'test-user', - privateKey: 'test-ssh-key', - algorithms: { cipher: expect.any(Array), kex: expect.any(Array) }, - // Wait up to 60 seconds for the SSH handshake - readyTimeout: expect.any(Number), - // Retry 5 times over a minute - retries: expect.any(Number), - retry_minTimeout: expect.any(Number), - debug: expect.any(Function) - }) - ) - }) - it('defaults the sftp port to 22 if the environment variable is not configured', async () => { - delete process.env.FULFILMENT_FTP_PORT - await config.initialise() - expect(config.ftp.port).toEqual('22') - }) - }) - describe('s3', () => { it('provides properties relating the use of Amazon S3', async () => { expect(config.s3.bucket).toEqual('test-bucket') @@ -79,7 +48,7 @@ describe('config', () => { describe('pgp config', () => { const init = async (samplePublicKey = 'sample-pgp-key') => { - AwsMock.SecretsManager.__setNextResponses('getSecretValue', { SecretString: 'test-ssh-key' }, { SecretString: samplePublicKey }) + AwsMock.SecretsManager.__setNextResponses('getSecretValue', { SecretString: samplePublicKey }) await config.initialise() } beforeAll(setEnvVars) diff --git a/packages/fulfilment-job/src/config.js b/packages/fulfilment-job/src/config.js index bc0620fe45..e0f9c01315 100644 --- a/packages/fulfilment-job/src/config.js +++ b/packages/fulfilment-job/src/config.js @@ -1,45 +1,6 @@ import { AWS } from '@defra-fish/connectors-lib' -import db from 'debug' -const { secretsManager } = AWS() -/** - * Key exchange algorithms for public key authentication - in descending order of priority - * @type {string[]} - */ -export const SFTP_KEY_EXCHANGE_ALGORITHMS = [ - 'curve25519-sha256@libssh.org', - 'curve25519-sha256', - 'ecdh-sha2-nistp521', - 'ecdh-sha2-nistp384', - 'ecdh-sha2-nistp256', - 'diffie-hellman-group-exchange-sha256', - 'diffie-hellman-group14-sha256', - 'diffie-hellman-group16-sha512', - 'diffie-hellman-group18-sha512', - 'diffie-hellman-group14-sha1', - 'diffie-hellman-group-exchange-sha1', - 'diffie-hellman-group1-sha1' -] -/** - * Ciphers for SFTP support - in descending order of priority - * @type {string[]} - */ -export const SFTP_CIPHERS = [ - // http://tools.ietf.org/html/rfc4344#section-4 - 'aes256-ctr', - 'aes192-ctr', - 'aes128-ctr', - 'aes256-gcm', - 'aes256-gcm@openssh.com', - 'aes128-gcm', - 'aes128-gcm@openssh.com', - 'aes256-cbc', - 'aes192-cbc', - 'aes128-cbc', - 'blowfish-cbc', - '3des-cbc', - 'cast128-cbc' -] +const { secretsManager } = AWS() const falseRegEx = /(false|0)/i const trueRegEx = /(true|1)/i const toBoolean = val => { @@ -54,7 +15,6 @@ const toBoolean = val => { class Config { _file - _ftp _s3 _pgp @@ -68,20 +28,6 @@ class Config { */ partFileSize: Math.min(Number.parseInt(process.env.FULFILMENT_FILE_SIZE), 999) } - this.ftp = { - host: process.env.FULFILMENT_FTP_HOST, - port: process.env.FULFILMENT_FTP_PORT || '22', - path: process.env.FULFILMENT_FTP_PATH, - username: process.env.FULFILMENT_FTP_USERNAME, - privateKey: (await secretsManager.getSecretValue({ SecretId: process.env.FULFILMENT_FTP_KEY_SECRET_ID }).promise()).SecretString, - algorithms: { cipher: SFTP_CIPHERS, kex: SFTP_KEY_EXCHANGE_ALGORITHMS }, - // Wait up to 60 seconds for the SSH handshake - readyTimeout: 60000, - // Retry 5 times over a minute - retries: 5, - retry_minTimeout: 12000, - debug: db('fulfilment:ftp') - } this.s3 = { bucket: process.env.FULFILMENT_S3_BUCKET } @@ -104,18 +50,6 @@ class Config { this._file = cfg } - /** - * FTP configuration settings - * @type {object} - */ - get ftp () { - return this._ftp - } - - set ftp (cfg) { - this._ftp = cfg - } - /** * S3 configuration settings * @type {object} diff --git a/packages/fulfilment-job/src/staging/__tests__/deliver-fulfilment-files.spec.js b/packages/fulfilment-job/src/staging/__tests__/deliver-fulfilment-files.spec.js index 83e2145335..ca157ec115 100644 --- a/packages/fulfilment-job/src/staging/__tests__/deliver-fulfilment-files.spec.js +++ b/packages/fulfilment-job/src/staging/__tests__/deliver-fulfilment-files.spec.js @@ -1,7 +1,6 @@ import { Readable, PassThrough, Writable } from 'stream' import { deliverFulfilmentFiles } from '../deliver-fulfilment-files.js' import { createS3WriteStream, readS3PartFiles } from '../../transport/s3.js' -import { createFtpWriteStream } from '../../transport/ftp.js' import { FULFILMENT_FILE_STATUS_OPTIONSET, getOptionSetEntry } from '../staging-common.js' import { FulfilmentRequestFile, executeQuery, persist } from '@defra-fish/dynamics-lib' import openpgp from 'openpgp' @@ -10,7 +9,6 @@ import streamHelper from '../streamHelper.js' import merge2 from 'merge2' jest.mock('../../transport/s3.js') -jest.mock('../../transport/ftp.js') jest.mock('openpgp', () => ({ readKey: jest.fn(() => ({})), encrypt: jest.fn(({ message: readableStream }) => readableStream), @@ -46,20 +44,10 @@ describe('deliverFulfilmentFiles', () => { executeQuery.mockResolvedValue([{ entity: mockFulfilmentRequestFile2 }, { entity: mockFulfilmentRequestFile1 }]) // Streams for file1 - const { - s3DataStreamFile: s3DataStreamFile1, - ftpDataStreamFile: ftpDataStreamFile1, - s3HashStreamFile: s3HashStreamFile1, - ftpHashStreamFile: ftpHashStreamFile1 - } = createMockFileStreams() + const { s3DataStreamFile: s3DataStreamFile1, s3HashStreamFile: s3HashStreamFile1 } = createMockFileStreams() // Streams for file2 - const { - s3DataStreamFile: s3DataStreamFile2, - ftpDataStreamFile: ftpDataStreamFile2, - s3HashStreamFile: s3HashStreamFile2, - ftpHashStreamFile: ftpHashStreamFile2 - } = createMockFileStreams() + const { s3DataStreamFile: s3DataStreamFile2, s3HashStreamFile: s3HashStreamFile2 } = createMockFileStreams() // Run the delivery await expect(deliverFulfilmentFiles()).resolves.toBeUndefined() @@ -69,22 +57,14 @@ describe('deliverFulfilmentFiles', () => { // File 1 expectations expect(createS3WriteStream).toHaveBeenNthCalledWith(1, 'EAFF202006180001.json') expect(createS3WriteStream).toHaveBeenNthCalledWith(3, 'EAFF202006180001.json.sha256') - expect(createFtpWriteStream).toHaveBeenNthCalledWith(1, 'EAFF202006180001.json') - expect(createFtpWriteStream).toHaveBeenNthCalledWith(3, 'EAFF202006180001.json.sha256') expect(JSON.parse(s3DataStreamFile1.dataProcessed)).toEqual({ licences: [{ part: 0 }, { part: 1 }] }) - expect(JSON.parse(ftpDataStreamFile1.dataProcessed)).toEqual({ licences: [{ part: 0 }, { part: 1 }] }) expect(s3HashStreamFile1.dataProcessed).toEqual(fileShaHash) // validated - expect(ftpHashStreamFile1.dataProcessed).toEqual(fileShaHash) // validated // File 2 expectations expect(createS3WriteStream).toHaveBeenNthCalledWith(4, 'EAFF202006180002.json') expect(createS3WriteStream).toHaveBeenNthCalledWith(6, 'EAFF202006180002.json.sha256') - expect(createFtpWriteStream).toHaveBeenNthCalledWith(4, 'EAFF202006180002.json') - expect(createFtpWriteStream).toHaveBeenNthCalledWith(6, 'EAFF202006180002.json.sha256') expect(JSON.parse(s3DataStreamFile2.dataProcessed)).toEqual({ licences: [{ part: 0 }, { part: 1 }] }) - expect(JSON.parse(ftpDataStreamFile2.dataProcessed)).toEqual({ licences: [{ part: 0 }, { part: 1 }] }) expect(s3HashStreamFile2.dataProcessed).toEqual(fileShaHash) // validated - expect(ftpHashStreamFile2.dataProcessed).toEqual(fileShaHash) // validated // Persist to dynamics for file 1 expect(persist).toHaveBeenNthCalledWith(1, [ @@ -197,10 +177,7 @@ describe('deliverFulfilmentFiles', () => { const s3 = createTestableStream() streamHelper.pipelinePromise.mockResolvedValue() openpgp.encrypt.mockResolvedValue(s2) - merge2 - .mockReturnValueOnce(s1) - .mockReturnValueOnce(s2) - .mockReturnValueOnce(s3) + merge2.mockReturnValueOnce(s1).mockReturnValueOnce(s2).mockReturnValueOnce(s3) await mockExecuteQuery() createMockFileStreams() @@ -227,27 +204,18 @@ const createMockFulfilmentRequestFile = async (fileName, date) => const createMockFileStreams = () => { const s3DataStreamFile = createTestableStream() - const ftpDataStreamFile = createTestableStream() createS3WriteStream.mockReturnValueOnce({ s3WriteStream: s3DataStreamFile, managedUpload: Promise.resolve() }) - createFtpWriteStream.mockReturnValueOnce({ ftpWriteStream: ftpDataStreamFile, managedUpload: Promise.resolve() }) const s3EncryptedDataStreamFile = createTestableStream() - const ftpEncryptedDataStreamFile = createTestableStream() createS3WriteStream.mockReturnValueOnce({ s3WriteStream: s3EncryptedDataStreamFile, managedUpload: Promise.resolve() }) - createFtpWriteStream.mockReturnValueOnce({ ftpWriteStream: ftpEncryptedDataStreamFile, managedUpload: Promise.resolve() }) const s3HashStreamFile = createTestableStream() - const ftpHashStreamFile = createTestableStream() createS3WriteStream.mockReturnValueOnce({ s3WriteStream: s3HashStreamFile, managedUpload: Promise.resolve() }) - createFtpWriteStream.mockReturnValueOnce({ ftpWriteStream: ftpHashStreamFile, managedUpload: Promise.resolve() }) return { s3DataStreamFile, - ftpDataStreamFile, s3EncryptedDataStreamFile, - ftpEncryptedDataStreamFile, - s3HashStreamFile, - ftpHashStreamFile + s3HashStreamFile } } diff --git a/packages/fulfilment-job/src/staging/deliver-fulfilment-files.js b/packages/fulfilment-job/src/staging/deliver-fulfilment-files.js index 09c24ceb80..25036678dc 100644 --- a/packages/fulfilment-job/src/staging/deliver-fulfilment-files.js +++ b/packages/fulfilment-job/src/staging/deliver-fulfilment-files.js @@ -4,7 +4,6 @@ import merge2 from 'merge2' import moment from 'moment' import { executeQuery, persist, findFulfilmentFiles } from '@defra-fish/dynamics-lib' import { createS3WriteStream, readS3PartFiles } from '../transport/s3.js' -import { createFtpWriteStream } from '../transport/ftp.js' import { FULFILMENT_FILE_STATUS_OPTIONSET, getOptionSetEntry } from './staging-common.js' import db from 'debug' import openpgp from 'openpgp' @@ -69,11 +68,6 @@ const createEncryptedDataReadStream = async file => { */ const deliver = async (targetFileName, readableStream, ...transforms) => { const { s3WriteStream: s3DataStream, managedUpload: s3DataManagedUpload } = createS3WriteStream(targetFileName) - const { ftpWriteStream: ftpDataStream, managedUpload: ftpDataManagedUpload } = createFtpWriteStream(targetFileName) - await Promise.all([ - streamHelper.pipelinePromise([readableStream, ...transforms, s3DataStream, ftpDataStream]), - s3DataManagedUpload, - ftpDataManagedUpload - ]) + await Promise.all([streamHelper.pipelinePromise([readableStream, ...transforms, s3DataStream]), s3DataManagedUpload]) } diff --git a/packages/fulfilment-job/src/transport/__tests__/ftp.spec.js b/packages/fulfilment-job/src/transport/__tests__/ftp.spec.js deleted file mode 100644 index bf6e868ac4..0000000000 --- a/packages/fulfilment-job/src/transport/__tests__/ftp.spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import { createFtpWriteStream } from '../ftp.js' -import { mockedFtpMethods } from 'ssh2-sftp-client' - -jest.mock('stream') -jest.mock('../../config.js', () => ({ - ftp: { - host: 'testhost', - port: 2222, - path: 'testpath/', - username: 'testusername', - privateKey: 'testprivatekey' - } -})) - -describe('ftp', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - - describe('createFtpWriteStream', () => { - it('creates a stream to write to the configured FTP server', async () => { - const { ftpWriteStream, managedUpload } = createFtpWriteStream('testfile.json') - ftpWriteStream.write('Some data') - ftpWriteStream.end() - await managedUpload - expect(mockedFtpMethods.connect).toHaveBeenCalledWith( - expect.objectContaining({ - host: 'testhost', - port: 2222, - username: 'testusername', - privateKey: 'testprivatekey' - }) - ) - expect(mockedFtpMethods.put).toHaveBeenCalledWith(ftpWriteStream, 'testpath/testfile.json', { - flags: 'w', - encoding: 'UTF-8', - autoClose: false - }) - expect(mockedFtpMethods.end).toHaveBeenCalled() - }) - - it('rejects the managed upload promise if an FTP upload error occurs', async () => { - const testError = new Error('Test error') - mockedFtpMethods.put.mockImplementationOnce(() => Promise.reject(testError)) - const { ftpWriteStream, managedUpload } = createFtpWriteStream('testfile.json') - await expect(managedUpload).rejects.toThrow('Test error') - expect(mockedFtpMethods.connect).toHaveBeenCalledWith( - expect.objectContaining({ - host: 'testhost', - port: 2222, - username: 'testusername', - privateKey: 'testprivatekey' - }) - ) - expect(mockedFtpMethods.put).toHaveBeenCalledWith(ftpWriteStream, 'testpath/testfile.json', { - flags: 'w', - encoding: 'UTF-8', - autoClose: false - }) - expect(mockedFtpMethods.end).toHaveBeenCalled() - }) - }) -}) diff --git a/packages/fulfilment-job/src/transport/ftp.js b/packages/fulfilment-job/src/transport/ftp.js deleted file mode 100644 index 0ce3f41c9c..0000000000 --- a/packages/fulfilment-job/src/transport/ftp.js +++ /dev/null @@ -1,28 +0,0 @@ -import FtpClient from 'ssh2-sftp-client' -import Path from 'path' -import { PassThrough } from 'stream' -import config from '../config.js' -import db from 'debug' -const debug = db('fulfilment:transport') - -/** - * Create a stream to write to the configured FTP server - * - * @param {string} filename The name of the file to be written to the remote server - * @returns {{ftpWriteStream: module:stream.internal.PassThrough, managedUpload: Promise<*>}} - */ -export const createFtpWriteStream = filename => { - const sftp = new FtpClient() - const passThrough = new PassThrough() - const remoteFilePath = Path.join(config.ftp.path, filename) - return { - ftpWriteStream: passThrough, - managedUpload: sftp - .connect(config.ftp) - .then(() => sftp.put(passThrough, remoteFilePath, { flags: 'w', encoding: 'UTF-8', autoClose: false })) - .then(() => - debug('File successfully uploaded to fulfilment provider at sftp://%s:%s%s', config.ftp.host, config.ftp.port, remoteFilePath) - ) - .finally(() => sftp.end()) - } -} diff --git a/packages/gafl-webapp-service/Dockerfile b/packages/gafl-webapp-service/Dockerfile index c85bfa5b81..fa494d74d4 100644 --- a/packages/gafl-webapp-service/Dockerfile +++ b/packages/gafl-webapp-service/Dockerfile @@ -8,10 +8,8 @@ WORKDIR /app COPY packages/gafl-webapp-service/package*.json /app/ COPY packages/gafl-webapp-service/assets /app/assets COPY packages/gafl-webapp-service/build /app/build -RUN npm install \ - && npm run prepare \ - && npm prune --production \ - && npm cache clean --force > /dev/null 2>&1 + +RUN npm install && npm run prepare # Bundle app source COPY packages/gafl-webapp-service/ /app @@ -23,7 +21,9 @@ RUN rm -Rf /app/build /app/assets #################################################################################################################################### FROM rod_licensing/base WORKDIR /app -COPY --from=builder /app/ /app/ +COPY --from=builder --chown=node:node /app/ /app/ + +RUN npm prune --production && npm cache clean --force > /dev/null 2>&1 # Default service port ARG PORT=3000 diff --git a/packages/gafl-webapp-service/package-lock.json b/packages/gafl-webapp-service/package-lock.json index dd5a4212ea..b74aa746d5 100644 --- a/packages/gafl-webapp-service/package-lock.json +++ b/packages/gafl-webapp-service/package-lock.json @@ -1,14 +1,16 @@ { "name": "@defra-fish/gafl-webapp-service", - "version": "1.49.0-rc.10", + "version": "1.57.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@defra-fish/gafl-webapp-service", - "version": "1.49.0-rc.10", + "version": "1.57.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { + "@defra-fish/business-rules-lib": "1.57.0", + "@defra-fish/connectors-lib": "1.57.0", "@defra/hapi-gapi": "^2.0.0", "@hapi/boom": "^9.1.2", "@hapi/catbox-redis": "^6.0.2", @@ -32,7 +34,6 @@ "node-fetch": "^2.6.7", "nunjucks": "^3.2.3", "openid-client": "^4.9.1", - "pdfmake": "^0.1.72", "semver": "^7.3.5", "uuid": "^8.3.2" }, @@ -581,14 +582,6 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@swc/helpers": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.3.17.tgz", - "integrity": "sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==", - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -974,18 +967,6 @@ "node": ">=0.10.0" } }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", - "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array-each": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", @@ -1157,17 +1138,6 @@ "node": ">= 4.5.0" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/axios": { "version": "1.6.5", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", @@ -1281,14 +1251,6 @@ "node": ">=0.10.0" } }, - "node_modules/base64-js": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", - "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1351,33 +1313,6 @@ "node": ">=0.10.0" } }, - "node_modules/brotli": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", - "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", - "dependencies": { - "base64-js": "^1.1.2" - } - }, - "node_modules/brotli/node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/buffer-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", @@ -1442,12 +1377,20 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1615,6 +1558,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, "engines": { "node": ">=0.8" } @@ -1795,11 +1739,6 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, - "node_modules/crypto-js": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", - "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" - }, "node_modules/css": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", @@ -1900,34 +1839,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-equal": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", - "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.1", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.0", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/default-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", @@ -1958,22 +1869,28 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -2054,11 +1971,6 @@ "node": ">=0.10.0" } }, - "node_modules/dfa": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", - "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2150,23 +2062,27 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" + "get-intrinsic": "^1.2.4" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" } }, "node_modules/es5-ext": { @@ -2688,30 +2604,6 @@ } } }, - "node_modules/fontkit": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.9.0.tgz", - "integrity": "sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==", - "dependencies": { - "@swc/helpers": "^0.3.13", - "brotli": "^1.3.2", - "clone": "^2.1.2", - "deep-equal": "^2.0.5", - "dfa": "^1.2.0", - "restructure": "^2.0.1", - "tiny-inflate": "^1.0.3", - "unicode-properties": "^1.3.1", - "unicode-trie": "^2.0.0" - } - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dependencies": { - "is-callable": "^1.1.3" - } - }, "node_modules/for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -2795,14 +2687,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2814,14 +2699,20 @@ "dev": true }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3238,6 +3129,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -3541,24 +3433,19 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true, "engines": { "node": ">= 0.4.0" } }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3568,6 +3455,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -3579,20 +3467,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dependencies": { - "has-symbols": "^1.0.2" - }, + "dev": true, "engines": { "node": ">= 0.4" }, @@ -3639,6 +3514,19 @@ "node": ">=0.10.0" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hoek": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", @@ -3702,17 +3590,6 @@ "url": "https://github.com/sponsors/mashpie" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -3758,19 +3635,6 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, - "node_modules/internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", - "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/interpret": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", @@ -3859,51 +3723,12 @@ "node": ">=0.10.0" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3916,38 +3741,12 @@ "node": ">=8" } }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-core-module": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", @@ -3984,20 +3783,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", @@ -4054,14 +3839,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-negated-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", @@ -4083,20 +3860,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-number/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -4142,21 +3905,6 @@ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", "dev": true }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", @@ -4169,67 +3917,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dependencies": { - "which-typed-array": "^1.1.11" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-unc-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", @@ -4257,26 +3944,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -4287,9 +3954,11 @@ } }, "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" }, "node_modules/isemail": { "version": "3.2.0", @@ -4457,15 +4126,6 @@ "node": ">=0.10.0" } }, - "node_modules/linebreak": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", - "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", - "dependencies": { - "base64-js": "0.0.8", - "unicode-trie": "^2.0.0" - } - }, "node_modules/load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -5298,33 +4958,11 @@ "node": ">= 6" } }, - "node_modules/object-inspect": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.0.tgz", - "integrity": "sha512-HQ4J+ic8hKrgIt3mqk6cVOVrW2ozL4KdvHlqpBv9vDYWx9ysAgENAdvy4FoGF+KFdhR7nQTNm5J0ctAeOwn+3g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, "engines": { "node": ">= 0.4" } @@ -5345,6 +4983,7 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -5499,11 +5138,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pako": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" - }, "node_modules/parse-filepath": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", @@ -5620,32 +5254,6 @@ "node": ">=8" } }, - "node_modules/pdfkit": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.12.3.tgz", - "integrity": "sha512-+qDLgm2yq6WOKcxTb43lDeo3EtMIDQs0CK1RNqhHC9iT6u0KOmgwAClkYh9xFw2ATbmUZzt4f7KMwDCOfPDluA==", - "dependencies": { - "crypto-js": "^4.0.0", - "fontkit": "^1.8.1", - "linebreak": "^1.0.2", - "png-js": "^1.0.0" - } - }, - "node_modules/pdfmake": { - "version": "0.1.72", - "resolved": "https://registry.npmjs.org/pdfmake/-/pdfmake-0.1.72.tgz", - "integrity": "sha512-xZrPS+Safjf1I8ZYtMoXX83E6C6Pd1zFwa168yNTeeJWHclqf1z9DoYajjlY2uviN7gGyxwVZeou39uSk1oh1g==", - "dependencies": { - "iconv-lite": "^0.6.2", - "linebreak": "^1.0.2", - "pdfkit": "^0.12.0", - "svg-to-pdfkit": "^0.1.8", - "xmldoc": "^1.1.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -5731,11 +5339,6 @@ "node": ">=0.10.0" } }, - "node_modules/png-js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", - "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" - }, "node_modules/posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -5918,12 +5521,6 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/readable-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6022,22 +5619,6 @@ "node": ">=0.10.0" } }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/remove-bom-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", @@ -6192,11 +5773,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restructure": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/restructure/-/restructure-2.0.1.tgz", - "integrity": "sha512-e0dOpjm5DseomnXx2M5lpdZ5zoHqF1+bqdMJUohoYVVQa7cBdnk7fdmeI6byNWP/kiME72EeTiSypTCVnpLiDg==" - }, "node_modules/ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -6269,11 +5845,6 @@ "ret": "~0.1.10" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, "node_modules/sanitizer": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/sanitizer/-/sanitizer-0.1.3.tgz", @@ -6296,11 +5867,6 @@ "node": ">=14.0.0" } }, - "node_modules/sax": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", - "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" - }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -6333,14 +5899,19 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true }, - "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", "dependencies": { - "define-data-property": "^1.0.1", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -6373,19 +5944,6 @@ "node": ">=0.10.0" } }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -6717,17 +6275,6 @@ "node": ">=0.10.0" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/stream-exhaust": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", @@ -6839,14 +6386,6 @@ "es6-symbol": "^3.1.1" } }, - "node_modules/svg-to-pdfkit": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/svg-to-pdfkit/-/svg-to-pdfkit-0.1.8.tgz", - "integrity": "sha512-QItiGZBy5TstGy+q8mjQTMGRlDDOARXLxH+sgVm1n/LYeo0zFcQlcCh8m4zi8QxctrxB9Kue/lStc/RD5iLadQ==", - "dependencies": { - "pdfkit": ">=0.8.1" - } - }, "node_modules/terser": { "version": "3.17.0", "resolved": "https://registry.npmjs.org/terser/-/terser-3.17.0.tgz", @@ -6909,11 +6448,6 @@ "next-tick": "1" } }, - "node_modules/tiny-inflate": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", - "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" - }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -7124,11 +6658,6 @@ "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", "integrity": "sha512-up6Yvai4PYKhpNp5PkYtx50m3KbwQrqDwbuZP/ItyL64YEWHAvH6Md83LFLV/GRSk/BoUVwwgUzX6SOQSbsfAg==" }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/type": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", @@ -7185,43 +6714,6 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==" }, - "node_modules/unicode-properties": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", - "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", - "dependencies": { - "base64-js": "^1.3.0", - "unicode-trie": "^2.0.0" - } - }, - "node_modules/unicode-properties/node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/unicode-trie": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", - "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", - "dependencies": { - "pako": "^0.2.5", - "tiny-inflate": "^1.0.0" - } - }, "node_modules/union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -7304,12 +6796,6 @@ "node": ">=0.10.0" } }, - "node_modules/unset-value/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, "node_modules/upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", @@ -7532,59 +7018,12 @@ "which": "bin/which" } }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/which-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==", "dev": true }, - "node_modules/which-typed-array": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", - "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", @@ -7624,14 +7063,6 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, - "node_modules/xmldoc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/xmldoc/-/xmldoc-1.3.0.tgz", - "integrity": "sha512-y7IRWW6PvEnYQZNZFMRLNJw+p3pezM4nKYPfr15g4OOW9i8VpeydycFuipE2297OvZnh3jSb2pxOt9QpkZUVng==", - "dependencies": { - "sax": "^1.2.4" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/packages/gafl-webapp-service/package.json b/packages/gafl-webapp-service/package.json index 4164b83d49..72a43c1d49 100644 --- a/packages/gafl-webapp-service/package.json +++ b/packages/gafl-webapp-service/package.json @@ -1,6 +1,6 @@ { "name": "@defra-fish/gafl-webapp-service", - "version": "1.49.0-rc.10", + "version": "1.57.0", "description": "The websales frontend for the GAFL service", "type": "module", "engines": { @@ -36,8 +36,8 @@ "prepare": "gulp --gulpfile build/gulpfile.cjs" }, "dependencies": { - "@defra-fish/business-rules-lib": "1.49.0-rc.10", - "@defra-fish/connectors-lib": "1.49.0-rc.10", + "@defra-fish/business-rules-lib": "1.57.0", + "@defra-fish/connectors-lib": "1.57.0", "@defra/hapi-gapi": "^2.0.0", "@hapi/boom": "^9.1.2", "@hapi/catbox-redis": "^6.0.2", @@ -61,7 +61,6 @@ "node-fetch": "^2.6.7", "nunjucks": "^3.2.3", "openid-client": "^4.9.1", - "pdfmake": "^0.1.72", "semver": "^7.3.5", "uuid": "^8.3.2" }, diff --git a/packages/gafl-webapp-service/src/__mocks__/mock-journeys.js b/packages/gafl-webapp-service/src/__mocks__/mock-journeys.js index c0c0c31641..340114d082 100644 --- a/packages/gafl-webapp-service/src/__mocks__/mock-journeys.js +++ b/packages/gafl-webapp-service/src/__mocks__/mock-journeys.js @@ -365,3 +365,10 @@ export const MOCK_PAYMENT_RESPONSE = { } } } + +export const MOCK_RECURRING_PAYMENT_RESPONSE = { + state: { status: 'created', finished: false }, + reference: 'transaction-id', + description: 'recurring card payments', + user_identifier: 'permission-id' +} diff --git a/packages/gafl-webapp-service/src/__tests__/server.spec.js b/packages/gafl-webapp-service/src/__tests__/server.spec.js index 4e242792a2..bec30f188f 100644 --- a/packages/gafl-webapp-service/src/__tests__/server.spec.js +++ b/packages/gafl-webapp-service/src/__tests__/server.spec.js @@ -15,7 +15,8 @@ jest.mock('../uri.js', () => ({ PRIVACY_POLICY: { uri: '/PRIVACY_POLICY' }, REFUND_POLICY: { uri: '/REFUND_POLICY' }, NEW_TRANSACTION: { uri: '/NEW_TRANSACTION' }, - NEW_PRICES: { uri: '/NEW_PRICES' } + NEW_PRICES: { uri: '/NEW_PRICES' }, + RECURRING_TERMS_CONDITIONS: { uri: '/RECURRING_TERMS_CONDITIONS' } })) export const catboxOptions = { @@ -52,31 +53,37 @@ describe('The server', () => { expect(serverDecorateSpy).toHaveBeenCalledWith('toolkit', 'redirectWithLanguageCode', expect.any(Function)) }) - it('addLanguageCodeToUri is called with request but not new prices', async () => { - createServer(catboxOptions) - const serverDecorateSpy = jest.spyOn(server, 'decorate').mockImplementation(() => {}) + it.each([[uris.REFUND_POLICY.uri], [uris.COOKIES.uri], [uris.PRIVACY_POLICY.uri]])( + 'addLanguageCodeToUri is not called with %s which is not in redirect exception array', + async uri => { + createServer(catboxOptions) + const serverDecorateSpy = jest.spyOn(server, 'decorate').mockImplementation(() => {}) - await init() - const redirect = serverDecorateSpy.mock.calls[1][2] - const mockRequest = { url: { pathname: '/buy' } } - const mockRedirect = () => {} - await redirect.call({ request: mockRequest, redirect: mockRedirect }) + await init() + const redirect = serverDecorateSpy.mock.calls[1][2] + const mockRequest = { url: { pathname: uri } } + const mockRedirect = () => {} + await redirect.call({ request: mockRequest, redirect: mockRedirect }) - expect(addLanguageCodeToUri).not.toHaveBeenCalledWith(expect.any(Object), uris.NEW_PRICES.uri) - }) + expect(addLanguageCodeToUri).not.toHaveBeenCalledWith(expect.any(Object), uri) + } + ) - it('addLanguageCodeToUri is called with request and new prices', async () => { - createServer(catboxOptions) - const serverDecorateSpy = jest.spyOn(server, 'decorate').mockImplementation(() => {}) + it.each([[uris.NEW_PRICES.uri], [uris.RECURRING_TERMS_CONDITIONS.uri]])( + 'addLanguageCodeToUri is called with request and %s', + async uri => { + createServer(catboxOptions) + const serverDecorateSpy = jest.spyOn(server, 'decorate').mockImplementation(() => {}) - await init() - const redirect = serverDecorateSpy.mock.calls[1][2] - const mockRequest = { url: { pathname: uris.NEW_PRICES.uri } } - const mockRedirect = () => {} - await redirect.call({ request: mockRequest, redirect: mockRedirect }) + await init() + const redirect = serverDecorateSpy.mock.calls[1][2] + const mockRequest = { url: { pathname: uri } } + const mockRedirect = () => {} + await redirect.call({ request: mockRequest, redirect: mockRedirect }) - expect(addLanguageCodeToUri).toHaveBeenCalledWith(mockRequest, uris.NEW_PRICES.uri) - }) + expect(addLanguageCodeToUri).toHaveBeenCalledWith(mockRequest, uri) + } + ) it('configures session handling in redis by default', async () => { process.env.REDIS_HOST = '0.0.0.0' diff --git a/packages/gafl-webapp-service/src/constants.js b/packages/gafl-webapp-service/src/constants.js index 6e4993814d..ccdd0ed695 100644 --- a/packages/gafl-webapp-service/src/constants.js +++ b/packages/gafl-webapp-service/src/constants.js @@ -31,7 +31,8 @@ export const COMPLETION_STATUS = { paymentFailed: 'payment-failed', paymentCompleted: 'payment-completed', finalised: 'finalised', - completed: 'completed' + completed: 'completed', + recurringAgreement: 'recurring-agreement' } export const GOVPAYFAIL = { @@ -69,3 +70,5 @@ export const LICENCE_SUMMARY_SEEN = 'licence-summary' // These cookies are used by the load balancer export const ALB_COOKIE_NAME = 'AWSALBTG' export const ALBCORS_COOKIE_NAME = 'AWSALBTGCORS' + +export const RECURRING_PAYMENT = 'recurring-payment' diff --git a/packages/gafl-webapp-service/src/handlers/__tests__/agreed-handler-happy-path.spec.js b/packages/gafl-webapp-service/src/handlers/__tests__/agreed-handler-happy-path.spec.js index d372d309f5..c69b4d3ad2 100644 --- a/packages/gafl-webapp-service/src/handlers/__tests__/agreed-handler-happy-path.spec.js +++ b/packages/gafl-webapp-service/src/handlers/__tests__/agreed-handler-happy-path.spec.js @@ -14,6 +14,7 @@ import { COMPLETION_STATUS } from '../../constants.js' import { AGREED, TEST_TRANSACTION, TEST_STATUS, ORDER_COMPLETE } from '../../uri.js' import { PAYMENT_JOURNAL_STATUS_CODES } from '@defra-fish/business-rules-lib' import agreedHandler from '../agreed-handler.js' +import { v4 as uuidv4 } from 'uuid' beforeAll(() => { process.env.ANALYTICS_PRIMARY_PROPERTY = 'GJDJKDKFJ' @@ -32,6 +33,9 @@ afterAll(() => { }) jest.mock('@defra-fish/connectors-lib') +jest.mock('uuid', () => ({ + v4: jest.fn(() => '') +})) mockSalesApi() const paymentStatusSuccess = cost => ({ @@ -58,6 +62,7 @@ describe('The agreed handler', () => { beforeEach(async () => { await journey.setup() + uuidv4.mockReturnValue(journey.transactionResponse.id) salesApi.createTransaction.mockResolvedValue(journey.transactionResponse) salesApi.finaliseTransaction.mockResolvedValue(journey.transactionResponse) govUkPayApi.createPayment.mockResolvedValue({ json: () => MOCK_PAYMENT_RESPONSE, ok: true, status: 201 }) @@ -179,6 +184,8 @@ describe('The agreed handler', () => { ])('no-payment journey %s', (desc, journey) => { beforeEach(async () => { await journey.setup() + + uuidv4.mockReturnValue(journey.transactionResponse.id) salesApi.createTransaction.mockResolvedValue(journey.transactionResponse) salesApi.finaliseTransaction.mockResolvedValue(journey.transactionResponse) }) @@ -244,6 +251,7 @@ describe('The agreed handler', () => { describe('finalised transactions', () => { beforeEach(async () => { await JUNIOR_LICENCE.setup() + uuidv4.mockReturnValue(JUNIOR_LICENCE.transactionResponse.id) salesApi.createTransaction.mockResolvedValue(JUNIOR_LICENCE.transactionResponse) salesApi.finaliseTransaction.mockResolvedValue(JUNIOR_LICENCE.transactionResponse) await injectWithCookies('GET', AGREED.uri) diff --git a/packages/gafl-webapp-service/src/handlers/__tests__/agreed-handler-payment-failure.spec.js b/packages/gafl-webapp-service/src/handlers/__tests__/agreed-handler-payment-failure.spec.js index ba811944dc..9bdf1798c7 100644 --- a/packages/gafl-webapp-service/src/handlers/__tests__/agreed-handler-payment-failure.spec.js +++ b/packages/gafl-webapp-service/src/handlers/__tests__/agreed-handler-payment-failure.spec.js @@ -9,8 +9,12 @@ import { AGREED, TEST_TRANSACTION, TEST_STATUS, PAYMENT_FAILED, PAYMENT_CANCELLE import agreedHandler from '../agreed-handler.js' import { getPaymentStatus, sendPayment } from '../../services/payment/govuk-pay-service.js' import { preparePayment } from '../../processors/payment.js' +import { v4 as uuidv4 } from 'uuid' jest.mock('../../services/payment/govuk-pay-service.js') jest.mock('../../processors/payment.js') +jest.mock('uuid', () => ({ + v4: jest.fn(() => '') +})) beforeAll(() => { process.env.ANALYTICS_PRIMARY_PROPERTY = 'GJDJKDKFJ' @@ -142,6 +146,8 @@ const getSendPaymentMockImplementation = () => ({ describe('The agreed handler', () => { beforeEach(() => { jest.clearAllMocks() + uuidv4.mockReset() + uuidv4.mockReturnValue('') const { sendPayment: realSendPayment, getPaymentStatus: realGetPaymentStatus } = jest.requireActual( '../../services/payment/govuk-pay-service.js' ) @@ -156,6 +162,7 @@ describe('The agreed handler', () => { ['expired', paymentStatusExpired], ['general-error', paymentGeneralError] ])('redirects to the payment-failed page if the GOV.UK Pay returns %s on payment status fetch', async (desc, pstat) => { + uuidv4.mockReturnValue(ADULT_FULL_1_DAY_LICENCE.transactionResponse.id) await ADULT_FULL_1_DAY_LICENCE.setup() salesApi.createTransaction.mockResolvedValue(ADULT_FULL_1_DAY_LICENCE.transactionResponse) @@ -257,6 +264,7 @@ describe('The agreed handler', () => { }) it('redirects to the payment-cancelled page if the GOV.UK Pay returns cancelled', async () => { + uuidv4.mockReturnValue(ADULT_FULL_1_DAY_LICENCE.transactionResponse.id) await ADULT_FULL_1_DAY_LICENCE.setup() salesApi.createTransaction = jest.fn(async () => new Promise(resolve => resolve(ADULT_FULL_1_DAY_LICENCE.transactionResponse))) @@ -347,6 +355,7 @@ describe('The agreed handler', () => { }) it('posts a 500 (server) error with the retry flag set if the GOV.UK Pay API throws a (recoverable) exception on payment creation', async () => { + uuidv4.mockReturnValueOnce(ADULT_FULL_1_DAY_LICENCE.transactionResponse.id) await ADULT_FULL_1_DAY_LICENCE.setup() salesApi.createTransaction = jest.fn(async () => new Promise(resolve => resolve(ADULT_FULL_1_DAY_LICENCE.transactionResponse))) @@ -375,9 +384,9 @@ describe('The agreed handler', () => { }) it('posts a 500 (server) error with the retry flag set if the GOV.UK Pay API rate limit is exceeded on create payment', async () => { + uuidv4.mockReturnValueOnce(ADULT_FULL_1_DAY_LICENCE.transactionResponse.id) await ADULT_FULL_1_DAY_LICENCE.setup() salesApi.createTransaction = jest.fn(async () => new Promise(resolve => resolve(ADULT_FULL_1_DAY_LICENCE.transactionResponse))) - govUkPayApi.createPayment = jest.fn( async () => new Promise(resolve => resolve({ json: () => paymentTooManyRequests, ok: false, status: 429 })) ) @@ -403,6 +412,7 @@ describe('The agreed handler', () => { it('posts a 500 error without the retry flag set if the GOV.UK Pay API returns any arbitrary 400 error on payment creation', async () => { await ADULT_FULL_1_DAY_LICENCE.setup() + uuidv4.mockReturnValueOnce(ADULT_FULL_1_DAY_LICENCE.transactionResponse.id) salesApi.createTransaction = jest.fn(async () => new Promise(resolve => resolve(ADULT_FULL_1_DAY_LICENCE.transactionResponse))) @@ -429,6 +439,7 @@ describe('The agreed handler', () => { it('posts a 500 error without the retry flag set if the GOV.UK Pay API returns any arbitrary 500 error on payment creation', async () => { await ADULT_FULL_1_DAY_LICENCE.setup() + uuidv4.mockReturnValueOnce(ADULT_FULL_1_DAY_LICENCE.transactionResponse.id) salesApi.createTransaction = jest.fn(async () => new Promise(resolve => resolve(ADULT_FULL_1_DAY_LICENCE.transactionResponse))) govUkPayApi.createPayment = jest.fn( diff --git a/packages/gafl-webapp-service/src/handlers/__tests__/agreed-handler-recurring-payments.spec.js b/packages/gafl-webapp-service/src/handlers/__tests__/agreed-handler-recurring-payments.spec.js new file mode 100644 index 0000000000..582f28a17c --- /dev/null +++ b/packages/gafl-webapp-service/src/handlers/__tests__/agreed-handler-recurring-payments.spec.js @@ -0,0 +1,216 @@ +import { salesApi } from '@defra-fish/connectors-lib' +import { COMPLETION_STATUS, RECURRING_PAYMENT } from '../../constants.js' +import agreedHandler from '../agreed-handler.js' +import { preparePayment, prepareRecurringPaymentAgreement } from '../../processors/payment.js' +import { sendPayment, sendRecurringPayment, getPaymentStatus } from '../../services/payment/govuk-pay-service.js' +import { prepareApiTransactionPayload } from '../../processors/api-transaction.js' +import { v4 as uuidv4 } from 'uuid' +import db from 'debug' + +jest.mock('@defra-fish/connectors-lib') +jest.mock('../../processors/payment.js') +jest.mock('../../services/payment/govuk-pay-service.js', () => ({ + sendPayment: jest.fn(() => ({ + payment_id: 'payment-id-1', + _links: { + next_url: { href: 'next-url' }, + self: { href: 'self-url' } + } + })), + getPaymentStatus: jest.fn(), + sendRecurringPayment: jest.fn(() => ({ agreementId: 'agr-eem-ent-id1' })) +})) +jest.mock('../../processors/api-transaction.js') +jest.mock('@defra-fish/connectors-lib') +jest.mock('uuid', () => ({ + v4: jest.fn(() => 'abc-123-def-456') +})) +jest.mock('debug', () => jest.fn(() => jest.fn())) + +const debugMock = db.mock.results[0].value + +describe('The agreed handler', () => { + beforeAll(() => { + salesApi.createTransaction.mockResolvedValue({ + id: 'transaction-id-1', + cost: 0 + }) + }) + beforeEach(jest.clearAllMocks) + + const getMockRequest = ({ overrides = {}, transactionSet = () => {} } = {}) => ({ + cache: () => ({ + helpers: { + transaction: { + get: async () => ({ cost: 0 }), + set: transactionSet + }, + status: { + get: async () => ({ + [COMPLETION_STATUS.agreed]: true, + [COMPLETION_STATUS.posted]: false, + [COMPLETION_STATUS.finalised]: true, + [RECURRING_PAYMENT]: true + }), + set: jest.fn() + }, + ...overrides + } + }) + }) + + const getRequestToolkit = () => ({ + redirect: jest.fn(), + redirectWithLanguageCode: jest.fn() + }) + + describe('recurring card payments', () => { + it('sends the request and transaction to prepare the recurring payment', async () => { + const transaction = { cost: 0 } + const mockRequest = getMockRequest({ + overrides: { + transaction: { + get: async () => transaction, + set: () => {} + } + } + }) + await agreedHandler(mockRequest, getRequestToolkit()) + expect(prepareRecurringPaymentAgreement).toHaveBeenCalledWith(mockRequest, transaction) + }) + + it('adds a v4 guid to the transaction as an id', async () => { + let transactionPayload = null + prepareRecurringPaymentAgreement.mockImplementationOnce((_p1, tp) => { + transactionPayload = { ...tp } + }) + const v4guid = Symbol('v4guid') + uuidv4.mockReturnValue(v4guid) + const transaction = { cost: 0 } + const mockRequest = getMockRequest({ + transaction: { + get: async () => transaction, + set: () => {} + } + }) + + await agreedHandler(mockRequest, getRequestToolkit()) + + expect(transactionPayload.id).toBe(v4guid) + }) + + it('sends a recurring payment agreement creation request to Gov.UK Pay', async () => { + const preparedPayment = Symbol('preparedPayment') + prepareRecurringPaymentAgreement.mockResolvedValueOnce(preparedPayment) + await agreedHandler(getMockRequest(), getRequestToolkit()) + expect(sendRecurringPayment).toHaveBeenCalledWith(preparedPayment) + }) + + describe('when there is a cost and recurringAgreement status is set to true', () => { + beforeEach(() => { + salesApi.createTransaction.mockResolvedValueOnce({ + id: 'transaction-id-1', + cost: 100 + }) + }) + + it('calls preparePayment', async () => { + const transaction = { id: Symbol('transaction') } + const request = getMockRequest({ + overrides: { + transaction: { + get: async () => transaction, + set: () => {} + } + } + }) + const toolkit = getRequestToolkit() + + await agreedHandler(request, toolkit) + expect(preparePayment).toHaveBeenCalledWith(request, transaction) + }) + + it('calls sendPayment with recurring as true', async () => { + const preparedPayment = Symbol('preparedPayment') + preparePayment.mockReturnValueOnce(preparedPayment) + + await agreedHandler(getMockRequest(), getRequestToolkit()) + expect(sendPayment).toHaveBeenCalledWith(preparedPayment, true) + }) + + it('calls getPaymentStatus with recurring as true', async () => { + const id = Symbol('paymentId') + const transaction = { id: '123', payment: { payment_id: id } } + const request = getMockRequest({ + overrides: { + transaction: { + get: async () => transaction, + set: () => {} + }, + status: { + get: async () => ({ + [COMPLETION_STATUS.agreed]: true, + [COMPLETION_STATUS.posted]: false, + [COMPLETION_STATUS.finalised]: true, + [RECURRING_PAYMENT]: true, + [COMPLETION_STATUS.paymentCreated]: true + }), + set: () => {} + } + } + }) + const toolkit = getRequestToolkit() + + getPaymentStatus.mockReturnValueOnce({ state: { finished: true, status: 'success' } }) + + await agreedHandler(request, toolkit) + expect(getPaymentStatus).toHaveBeenCalledWith(id, true) + }) + }) + + // this doesn't really belong here, but until the other agreed handler tests are refactored to + // not use injectWithCookies, it'll have to live here + it('sends the generated guid to the sales api, rather than requesting it from the sales api', async () => { + const v4guid = Symbol('v4guid') + uuidv4.mockReturnValue(v4guid) + + await agreedHandler(getMockRequest(), getRequestToolkit()) + + expect(prepareApiTransactionPayload).toHaveBeenCalledWith(expect.any(Object), v4guid, undefined) + }) + + it.each(['zxy-098-wvu-765', '467482f1-099d-403d-b6b3-8db7e70d19e3'])( + "logs out agreement id '%s' when recurring payment agreement created", + // eslint-disable-next-line camelcase + async agreement_id => { + sendRecurringPayment.mockResolvedValueOnce({ + // eslint-disable-next-line camelcase + agreement_id + }) + + await agreedHandler(getMockRequest(), getRequestToolkit()) + + // eslint-disable-next-line camelcase + expect(debugMock).toHaveBeenCalledWith(`Created agreement with id ${agreement_id}`) + } + ) + + it.each(['zxy-098-wvu-765', '467482f1-099d-403d-b6b3-8db7e70d19e3'])( + "assigns agreement id '%s' to the transaction when recurring payment agreement created", + async agreementId => { + const mockTransactionCacheSet = jest.fn() + sendRecurringPayment.mockResolvedValueOnce({ + agreement_id: agreementId + }) + + await agreedHandler(getMockRequest({ transactionSet: mockTransactionCacheSet }), getRequestToolkit()) + + expect(mockTransactionCacheSet).toHaveBeenCalledWith( + expect.objectContaining({ + agreementId + }) + ) + } + ) + }) +}) diff --git a/packages/gafl-webapp-service/src/handlers/__tests__/agreed-handler-sales-api-failure.spec.js b/packages/gafl-webapp-service/src/handlers/__tests__/agreed-handler-sales-api-failure.spec.js index 6e2f0f48c8..c5ba374829 100644 --- a/packages/gafl-webapp-service/src/handlers/__tests__/agreed-handler-sales-api-failure.spec.js +++ b/packages/gafl-webapp-service/src/handlers/__tests__/agreed-handler-sales-api-failure.spec.js @@ -6,6 +6,7 @@ import { COMPLETION_STATUS } from '../../constants.js' import { AGREED, TEST_TRANSACTION, TEST_STATUS, ORDER_COMPLETE } from '../../uri.js' import mockPermits from '../../__mocks__/data/permits.js' +import { v4 as uuidv4 } from 'uuid' beforeAll(() => { process.env.ANALYTICS_PRIMARY_PROPERTY = 'GJDJKDKFJ' @@ -20,6 +21,9 @@ afterAll(() => { }) jest.mock('@defra-fish/connectors-lib') +jest.mock('uuid', () => ({ + v4: jest.fn(() => '') +})) mockSalesApi() const paymentStatusSuccess = cost => ({ @@ -41,6 +45,7 @@ describe('The agreed handler', () => { const { payload } = await injectWithCookies('GET', TEST_TRANSACTION.uri) expect(JSON.parse(payload).id).not.toBeTruthy() const { payload: status } = await injectWithCookies('GET', TEST_STATUS.uri) + const parsedStatus = JSON.parse(status) expect(parsedStatus[COMPLETION_STATUS.agreed]).toBeTruthy() expect(parsedStatus[COMPLETION_STATUS.posted]).not.toBeTruthy() @@ -66,6 +71,7 @@ describe('The agreed handler', () => { it('throws a status 500 (server) exception and if there is an exception thrown finalizing the transaction', async () => { await ADULT_FULL_1_DAY_LICENCE.setup() + uuidv4.mockReturnValue(ADULT_FULL_1_DAY_LICENCE.transactionResponse.id) salesApi.createTransaction.mockResolvedValue(ADULT_FULL_1_DAY_LICENCE.transactionResponse) salesApi.finaliseTransaction.mockRejectedValue(new Error()) govUkPayApi.createPayment.mockResolvedValue({ json: () => MOCK_PAYMENT_RESPONSE, ok: true, status: 201 }) diff --git a/packages/gafl-webapp-service/src/handlers/agreed-handler.js b/packages/gafl-webapp-service/src/handlers/agreed-handler.js index 2bf3d10abf..01b3c748f9 100644 --- a/packages/gafl-webapp-service/src/handlers/agreed-handler.js +++ b/packages/gafl-webapp-service/src/handlers/agreed-handler.js @@ -13,11 +13,12 @@ import Boom from '@hapi/boom' import db from 'debug' import { salesApi } from '@defra-fish/connectors-lib' import { prepareApiTransactionPayload, prepareApiFinalisationPayload } from '../processors/api-transaction.js' -import { sendPayment, getPaymentStatus } from '../services/payment/govuk-pay-service.js' -import { preparePayment } from '../processors/payment.js' -import { COMPLETION_STATUS } from '../constants.js' +import { sendPayment, getPaymentStatus, sendRecurringPayment } from '../services/payment/govuk-pay-service.js' +import { preparePayment, prepareRecurringPaymentAgreement } from '../processors/payment.js' +import { COMPLETION_STATUS, RECURRING_PAYMENT } from '../constants.js' import { ORDER_COMPLETE, PAYMENT_CANCELLED, PAYMENT_FAILED } from '../uri.js' import { PAYMENT_JOURNAL_STATUS_CODES, GOVUK_PAY_ERROR_STATUS_CODES } from '@defra-fish/business-rules-lib' +import { v4 as uuidv4 } from 'uuid' const debug = db('webapp:agreed-handler') /** @@ -28,7 +29,7 @@ const debug = db('webapp:agreed-handler') * @returns {Promise<*>} */ const sendToSalesApi = async (request, transaction, status) => { - const apiTransactionPayload = await prepareApiTransactionPayload(request) + const apiTransactionPayload = await prepareApiTransactionPayload(request, transaction.id, transaction.agreementId) let response try { response = await salesApi.createTransaction(apiTransactionPayload) @@ -36,10 +37,9 @@ const sendToSalesApi = async (request, transaction, status) => { debug('Error creating transaction', JSON.stringify(apiTransactionPayload)) throw e } - transaction.id = response.id transaction.cost = response.cost status[COMPLETION_STATUS.posted] = true - debug('Got transaction identifier: %s', transaction.id) + debug('Transaction identifier: %s', transaction.id) await request.cache().helpers.transaction.set(transaction) await request.cache().helpers.status.set(status) @@ -47,6 +47,38 @@ const sendToSalesApi = async (request, transaction, status) => { return transaction } +/** + * Grab the agreement id in GOV.UK pay using the API + * (1) Prepare payment (payload) for the API (processor) + * (2) Send to GOV.UK pay API (connector) + * (3) Handle exceptions and error (agreed handler) + * (4) Write into the journal tables (agreed handler) + * (5) Process the results - write into the cache + * @param request + * @param transaction + * @param status + * @returns {Promise} + */ +const createRecurringPayment = async (request, transaction, status) => { + /* + * Prepare the payment payload + */ + const preparedPayment = await prepareRecurringPaymentAgreement(request, transaction) + + /* + * Send the prepared payment to the GOV.UK pay API using the connector + */ + const paymentResponse = await sendRecurringPayment(preparedPayment) + + debug(`Created agreement with id ${paymentResponse.agreement_id}`) + status[COMPLETION_STATUS.recurringAgreement] = true + + transaction.agreementId = paymentResponse.agreement_id + + await request.cache().helpers.status.set(status) + await request.cache().helpers.transaction.set(transaction) +} + /** * Create a new payment in GOV.UK pay using the API * (1) Prepare payment (payload) for the API (processor) @@ -60,6 +92,8 @@ const sendToSalesApi = async (request, transaction, status) => { * @returns {Promise} */ const createPayment = async (request, transaction, status) => { + const recurring = status && status[COMPLETION_STATUS.recurringAgreement] === true + /* * Prepare the payment payload */ @@ -68,7 +102,7 @@ const createPayment = async (request, transaction, status) => { /* * Send the prepared payment to the GOV.UK pay API using the connector */ - const paymentResponse = await sendPayment(preparedPayment) + const paymentResponse = await sendPayment(preparedPayment, recurring) /* * Used by the payment mop up job, create the payment journal entry which is removed when the user completes the journey @@ -115,10 +149,12 @@ const createPayment = async (request, transaction, status) => { * @returns {Promise} */ const processPayment = async (request, transaction, status) => { + const recurring = status && status[COMPLETION_STATUS.recurringAgreement] === true + /* * Get the payment status */ - const { state } = await getPaymentStatus(transaction.payment.payment_id) + const { state } = await getPaymentStatus(transaction.payment.payment_id, recurring) if (!state.finished) { throw Boom.forbidden('Attempt to access the agreed handler during payment journey') @@ -213,6 +249,9 @@ const finaliseTransaction = async (request, transaction, status) => { export default async (request, h) => { const status = await request.cache().helpers.status.get() const transaction = await request.cache().helpers.transaction.get() + if (!transaction.id) { + transaction.id = uuidv4() + } // If the agreed flag is not set to true then throw an exception if (!status[COMPLETION_STATUS.agreed]) { @@ -221,6 +260,10 @@ export default async (request, h) => { // Send the transaction to the sales API and process the response if (!status[COMPLETION_STATUS.posted]) { + // Create the agreement if a recurring payment + if (status[RECURRING_PAYMENT] === true) { + await createRecurringPayment(request, transaction, status) + } try { await sendToSalesApi(request, transaction, status) } catch (e) { diff --git a/packages/gafl-webapp-service/src/locales/cy.json b/packages/gafl-webapp-service/src/locales/cy.json index de810421bf..2a87d1b4ca 100644 --- a/packages/gafl-webapp-service/src/locales/cy.json +++ b/packages/gafl-webapp-service/src/locales/cy.json @@ -114,13 +114,14 @@ "address_lookup_title_you": "Dewch o hyd i’ch cyfeiriad", "address_select_error_choose": "Dewisiwch gyfeiriad", "address_select_addresses": " chyfeiriad", + "address_select_edit_search": "Addasu’r chwiliad", "address_select_found_for": " i’w weld ar gyfer ", "address_select_link": " cyfeiriad", "address_select_title_other": "Dewisiwch eu cyfeiriad", "address_select_title_you": "Dewiswch eich cyfeiriad", "age_concession": "Consesiwn oed", "age_junior_concession": "Iau (13 i 16)", - "age_junior": "Iau, ", + "age_junior": "iau ", "age_senior_concession": "66 mlwydd oed a throsodd", "age_senior": "Uwch", "analytics_banner_acceptance_1": "Rydych wedi derbyn cwcis ychwanegol. Gallwch ", @@ -141,6 +142,7 @@ "buy_different_licence": "Prynu trwydded wahanol", "change_licence_details_you": "Adolygu neu newid manylion eich trwydded", "change_licence_details_other": "Adolygu neu newid manylion y drwydded", + "change_licence_number": "Newid rhif y drwydded", "check_conf_contact_correct": "Mae’n gywir", "check_conf_contact_title_1": "A yw’r ", "check_conf_contact_title_2": " yn gywir?", @@ -270,10 +272,17 @@ "disability_concession_title_you": "Ydych chi’n derbyn unrhyw un o’r canlynol?", "dob_day": "Diwrnod", "dob_entry_hint": "Er enghraifft, 23 11 1979", - "dob_error_format_max": "Mae'n rhaid i’r dyddiad geni fod yn y gorffennol", - "dob_error_format_min": "Nodwch eich dyddiad geni a chynnwys y diwrnod, y mis a’r flwyddyn", - "dob_error_format": "Nodwch ddyddiad geni deiliad y drwydded a chynnwys diwrnod, mis a blwyddyn", - "dob_error": "Rhowch y dyddiad geni", + "dob_error_date_real": "Mae’n rhaid i’r dyddiad geni fod yn ddyddiad dilys", + "dob_error_missing_day_and_month": "Mae’n rhaid i’r dyddiad geni gynnwys diwrnod a mis", + "dob_error_missing_day_and_year": "Mae’n rhaid i’r dyddiad geni gynnwys diwrnod a blwyddyn", + "dob_error_missing_month_and_year": "Mae’n rhaid i’r dyddiad geni gynnwys mis a blwyddyn", + "dob_error_missing_day": "Mae’n rhaid i’r dyddiad geni gynnwys diwrnod", + "dob_error_missing_month": "Mae’n rhaid i’r dyddiad geni gynnwys mis", + "dob_error_missing_year": "Mae’n rhaid i’r dyddiad geni gynnwys blwyddyn", + "dob_error_non_numeric": "Rhowch rifau yn unig", + "dob_error_year_min": "Mae’r dyddiad geni yn rhy bell yn ôl", + "dob_error_year_max": "Mae'n rhaid i’r dyddiad geni fod yn y gorffennol", + "dob_error": "Rhowch dyddiad geni", "dob_month": "Mis", "dob_privacy_link_prefix": "Os nad ydych yn darparu dyddiad geni cywir, gallai hynny achosi oedi wrth adnewyddu trwydded, neu olygu nad yw’r drwydded yn ddilys. Darllenwch am ", "dob_privacy_link": "sut yr ydym ni'n defnyddio data personol (yn agor ar dudalen newydd)", @@ -316,15 +325,19 @@ "header_service_name_title": " - Cael trwydded bysgota â gwialen", "identification": "Rhif adnabod", "identify_body_protect_info": "Er mwyn dod o hyd i fanylion eich trwydded, bydd angen i ni wybod pwy ydych chi. Mae hyn yn ein helpu i ddiogelu eich gwybodaeth bersonol.", + "identify_error_date_real": "Mae’n rhaid i’r dyddiad geni fod yn ddyddiad dilys", "identify_error_empty_postcode": "Nid ydych wedi nodi cod post", "identify_error_empty": "Rhowch chwe nodyn olaf eich trwydded", - "identify_error_enter_bday_max": "Mae’n rhaid i’ch dyddiad geni fod yn y gorffennol", - "identify_error_enter_bday_min": "Mae eich dyddiad geni yn rhy bell yn ôl", - "identify_error_enter_bday": "Nodwch eich dyddiad geni a chynnwys y diwrnod, y mis a’r flwyddyn", "identify_error_invalid_1": "Nid oes gennym gofnod o rif trwydded sy'n gorffen gyda ", "identify_error_invalid_2": " sy’n cyd-fynd â'r manylion hyn.", + "identify_error_missing_day": "Mae’n rhaid i’r dyddiad geni gynnwys diwrnod", + "identify_error_missing_month": "Mae’n rhaid i’r dyddiad geni gynnwys mis", + "identify_error_missing_year": "Mae’n rhaid i’r dyddiad geni gynnwys blwyddyn", + "identify_error_non_numeric": "Rhowch rifau yn unig", "identify_error_pattern_postcode": "Your postcode doesn’t look right. Check and enter again", "identify_error_pattern": "Nid yw chwe nodyn olaf eich trwydded yn edrych yn gywir. Gwiriwch a rhowch gynnig arall arni", + "identify_error_year_min": "Mae’r dyddiad geni yn rhy bell yn ôl", + "identify_error_year_max": "Mae'n rhaid i’r dyddiad geni fod yn y gorffennol", "identify_label_last_six_hint": "Er enghraifft F4A315", "identify_label_last_six": "Chwe nodyn olaf eich trwydded.", "identify_label_licence_ending": "Rhif trwydded yn gorffen gyda’r canlynol", @@ -359,6 +372,9 @@ "important_info_contact_post": "Post", "important_info_contact_title_other": "Sut hoffech chi i ni gysylltu â deiliad y drwydded ynglÅ·n â diweddariadau sy’n effeithio ar eu trwydded?", "important_info_contact_title_you": "Sut hoffech chi i ni gysylltu â chi ynglÅ·n â diweddariadau sy’n effeithio ar eich trwydded?", + "licence_1_day": "Trwydded 1 diwrnod", + "licence_8_day": "Trwydded 8 diwrnod", + "licence_12_month": "Trwydded 12 mis", "licence_confirm_method_error_choose": "Dewiswch sut dylem anfon y drwydded", "licence_confirm_method_how_body_item": "Gwneud nodyn o rif y drwydded", "licence_confirm_method_how_body_text": "Dyma sut y byddwn yn anfon cadarnhad o’r drwydded bysgota", @@ -386,8 +402,8 @@ "licence_fulfilment_content_info": "Mae derbyn trwydded dros e-bost neu neges destun yn helpu'r amgylchedd drwy leihau faint o bapur yr ydym ni'n ei ddefnyddio, ac mae'n golygu ein bod yn gallu buddsoddi mwy mewn pysgota.", "licence_fulfilment_error": "Dewiswch sut yr hoffech dderbyn y drwydded bysgota", "licence_fulfilment_error_msg": "Dewiswch opsiwn", - "licence_fulfilment_radio_card_hint_other": "Bydd trwydded yn cael ei hanfon drwy'r post o fewn 15 diwrnod gwaith. Gall deiliad y drwydded bysgota o'r dyddiad a'r amser a ddewiswyd, ac nid oes angen aros i'r drwydded gyrraedd.", - "licence_fulfilment_radio_card_hint_you": "Bydd trwydded yn cael ei hanfon drwy'r post o fewn 15 diwrnod gwaith. Gallwch bysgota o'r dyddiad a'r amser a ddewiswyd, ac nid oes angen i chi aros i'r drwydded gyrraedd.", + "licence_fulfilment_radio_card_hint_other": "Bydd trwydded yn cael ei hanfon drwy'r post o fewn 10 diwrnod gwaith. Gall deiliad y drwydded bysgota o'r dyddiad a'r amser a ddewiswyd, ac nid oes angen aros i'r drwydded gyrraedd.", + "licence_fulfilment_radio_card_hint_you": "Bydd trwydded yn cael ei hanfon drwy'r post o fewn 10 diwrnod gwaith. Gallwch bysgota o'r dyddiad a'r amser a ddewiswyd, ac nid oes angen i chi aros i'r drwydded gyrraedd.", "licence_fulfilment_radio_card": "Drwy'r post", "licence_fulfilment_radio_paperless_hint_other": "Mae'r e-bost neu'r neges destun yn brawf o'i drwydded bysgota.", "licence_fulfilment_radio_paperless_hint_you": "Mae'r e-bost neu'r neges destun yn brawf o'ch trwydded bysgota.", @@ -402,8 +418,16 @@ "licence_num": "Rhif trwydded", "licence_start_days": " diwrnod nesaf", "licence_start_enter_todays_date": "Rhowch ddyddiad heddiw os ydych chi am i’r drwydded 1 diwrnod neu 8 diwrnod ddechrau yn hwyrach heddiw.", + "licence_start_error_date_real": "Mae’n rhaid i ddyddiad dechrau’r drwydded fod yn ddyddiad dilys", + "licence_start_error_missing_day_and_month": "Mae’n rhaid i ddyddiad dechrau’r drwydded gynnwys diwrnod a mis", + "licence_start_error_missing_day_and_year": "Mae’n rhaid i ddyddiad dechrau’r drwydded gynnwys diwrnod a blwyddyn", + "licence_start_error_missing_month_and_year": "Mae’n rhaid i ddyddiad dechrau’r drwydded gynnwys mis a blwyddyn", + "licence_start_error": "Rhowch ddyddiad dechrau’r drwydded", + "licence_start_error_missing_day": "Mae’n rhaid i ddyddiad dechrau’r drwydded gynnwys diwrnod", + "licence_start_error_missing_month": "Mae’n rhaid i ddyddiad dechrau’r drwydded gynnwys mis", + "licence_start_error_missing_year": "Mae’n rhaid i ddyddiad dechrau’r drwydded gynnwys blwyddyn", + "licence_start_error_non_numeric": "Rhowch rifau yn unig", "licence_start_error_choose_when": "Dewiswch pryd y dylai'r drwydded ddechrau", - "licence_start_error_format": "Nodwch y dyddiad y mae angen i'r drwydded ddechrau a chynnwys diwrnod, mis a blwyddyn", "licence_start_error_within": "Nodwch ddyddiad o fewn y ", "licence_start_hint": "Rhowch ddyddiad hyd at a chan gynnwys ", "licence_start_later": "Yn hwyrach", @@ -461,8 +485,11 @@ "licence_type_payment_edge_case": "Mae’n rhaid i chi gwblhau eich taliad cyn 11:30pm ar 31 Mawrth 2024 i dalu’r pris a ddangosir", "licence_type_radio_salmon_hint": "Mae'n cynnwys brithyllod a physgod bras (hyd at 3 gwialen)", "licence_type_radio_salmon": "Eogiaid a brithyllod y môr", + "licence_type_radio_salmon_payment_summary": "Eogiaid a brithyllod y môr", "licence_type_radio_trout_three_rod": "Brithyllod a physgod bras (hyd at 3 gwialen)", + "licence_type_radio_trout_three_rod_payment_summary": "Brithyllod a physgod bras (hyd at 3 gwialen)", "licence_type_radio_trout_two_rod": "Brithyllod a physgod bras (hyd at 2 wialen)", + "licence_type_radio_trout_two_rod_payment_summary": "Brithyllod a physgod bras (hyd at 2 wialen)", "licence_type_rules": "rheolau pysgota â gwialen (yn agor ar dudalen newydd)", "licence_type_salmon_acr_note_1": "Yn ôl y gyfraith, mae'n rhaid i chi roi gwybod am ", "licence_type_salmon_acr_note_2": " ffurflen daliadau (yn agor ar dudalen newydd)", @@ -531,11 +558,11 @@ "order_complete_future_payments_title": "Taliadau am drwyddedau yn y dyfodol", "order_complete_licence_details_bobo_digital_paragraph": "Bydd y drwydded hefyd yn cael ei hanfon ar ffurf e-bost neu neges destun.", "order_complete_licence_details_bobo_digital_confirmation_paragraph": "Bydd manylion y drwydded yn cael eu hanfon ar ffurf e-bost neu neges destun.", - "order_complete_licence_details_bobo_paragraph": "Bydd y drwydded yn cyrraedd drwy’r post o fewn 15 diwrnod. Gall deiliad y drwydded bysgota o’r dyddiad a’r amser a ddewiswyd, ac nid oes angen aros i’r drwydded gyrraedd.", + "order_complete_licence_details_bobo_paragraph": "Bydd y drwydded yn cyrraedd drwy’r post o fewn 10 diwrnod. Gall deiliad y drwydded bysgota o’r dyddiad a’r amser a ddewiswyd, ac nid oes angen aros i’r drwydded gyrraedd.", "order_complete_licence_details_bobo_title": "Gweld manylion y drwydded", "order_complete_licence_details_self_digital_paragraph": "Bydd eich trwydded hefyd yn cael ei hanfon ar ffurf e-bost neu neges destun.", "order_complete_licence_details_self_digital_confirmation_paragraph": "Bydd manylion eich trwydded yn cael eu hanfon dros e-bost neu neges destun.", - "order_complete_licence_details_self_paragraph": "Bydd eich trwydded yn cyrraedd drwy’r post o fewn 15 diwrnod gwaith. Gallwch bysgota o’r dyddiad a’r amser a ddewiswyd, ac nid oes angen i chi aros i’r drwydded gyrraedd.", + "order_complete_licence_details_self_paragraph": "Bydd eich trwydded yn cyrraedd drwy’r post o fewn 10 diwrnod gwaith. Gallwch bysgota o’r dyddiad a’r amser a ddewiswyd, ac nid oes angen i chi aros i’r drwydded gyrraedd.", "order_complete_licence_details_self_title": "Gweld manylion eich trwydded", "order_complete_when_fishing_bobo_non_postal_digital": "Gallai swyddog gorfodi ofyn i ddeiliad y drwydded a oes ganddo drwydded bysgota ddilys. Gall deiliad y drwydded ddangos y drwydded ar neges e-bost neu’r neges destun ar ei ffôn. Os bydd deiliad y drwydded yn dangos sgrinlun o fanylion y drwydded, bydd y swyddog gorfodi hefyd yn gofyn am brawf o’i hunaniaeth.", "order_complete_when_fishing_bobo_postal_digital": "Gallai swyddog gorfodi ofyn i ddeiliad y drwydded a oes ganddo drwydded bysgota ddilys. Gall deiliad y drwydded ddangos y drwydded a dderbyniwyd drwy’r post, neu ddangos y drwydded ar e-bost neu neges destun ar ei ffôn.", @@ -577,7 +604,7 @@ "payment_failed_title_3": "Your payment has failed", "phone_number": "rhif ffôn", "pound": "£", - "notification_heading": "Bydd llythyr wedi’i argraffu yn cymryd lle trwyddedau cerdyn wedi’u gorchuddio â phlastig o 31 Mai 2024.", + "notification_heading": "Mae llythyr wedi’i argraffu wedi cymryd lle trwyddedau cerdyn wedi’u gorchuddio â phlastig ers 31 Mai 2024", "notification_label": "Pwysig", "notification_text": "Mae dewis derbyn trwydded drwy e-bost neu neges destun yn helpu i leihau faint o bapur yr ydym yn ei ddefnyddio, ac yn buddsoddi mwy o incwm trwyddedau i’r sector pysgota.", "privacy_dp_officer_body_1": "Mae ein Swyddog Diogelu Data yn gyfrifol am gyngor annibynnol a monitro defnydd Asiantaeth yr Amgylchedd o ddata personol. Gallwch ddarllen ein ", @@ -638,6 +665,7 @@ "privacy_where_stored_body_3": "Nid ydym yn storio manylion talu.", "privacy_where_stored_body_4": "Mae ein partneriaid gwasanaeth talu Worldpay a GOV.UK Pay yn cydymffurfio gyda Safon Diogelwch Data Diwydiant y Cardiau Talu.", "purpose": "Diben", + "recurring_payment_description": "Y taliad cerdyn sy’n ailadrodd ar gyfer eich trwydded bysgota â gwialen", "recurring_payment_set_up": "Rwy’n cytuno i greu taliad cerdyn sy’n ailadrodd", "recurring_payment_set_up_back": "Neu ewch yn ôl i dalu am drwydded unigol", "recurring_payment_set_up_body": "Mae creu taliad cerdyn sy’n ailadrodd yn golygu eich bod yn deall ac yn cytuno:", diff --git a/packages/gafl-webapp-service/src/locales/en.json b/packages/gafl-webapp-service/src/locales/en.json index ddfdbcbab0..ca26e7c886 100644 --- a/packages/gafl-webapp-service/src/locales/en.json +++ b/packages/gafl-webapp-service/src/locales/en.json @@ -114,13 +114,14 @@ "address_lookup_title_you": "Find your address", "address_select_error_choose": "Choose an address", "address_select_addresses": " addresses", + "address_select_edit_search": "Edit search", "address_select_found_for": " found for ", "address_select_link": " address", "address_select_title_other": "Choose their address", "address_select_title_you": "Choose your address", "age_concession": "Age concession", "age_junior_concession": "Junior (13 to 16)", - "age_junior": "Junior, ", + "age_junior": "junior ", "age_senior_concession": "Age 66 or over", "age_senior": "Senior", "analytics_banner_acceptance_1": "You have accepted additional cookies. You can ", @@ -141,6 +142,7 @@ "buy_different_licence": "Buy a different licence", "change_licence_details_you": "Review or change the licence details", "change_licence_details_other": "Review or change the licence details", + "change_licence_number": "Change licence number", "choose_payment_heading": "Would you like to set up a recurring card payment to renew this licence each year?", "choose_payment_error": "Select yes or no", "choose_payment_hint": "A recurring card payment is an automatic payment taken from your debit or credit card", @@ -270,10 +272,19 @@ "disability_concession_title_you": "Do you receive any of the following?", "dob_day": "day", "dob_entry_hint": "For example, 23 11 1979", - "dob_error_format_max": "The date of birth must be in the past", - "dob_error_format_min": "Enter the date of birth and include a day, month and year", - "dob_error_format": "Enter the licence holder’s date of birth and include a day, month and year", - "dob_error": "Enter the date of birth", + + "dob_error_date_real": "Date of birth must be a real date", + "dob_error_missing_day_and_month": "Date of birth must include a day and month", + "dob_error_missing_day_and_year": "Date of birth must include a day and year", + "dob_error_missing_month_and_year": "Date of birth must include a month and year", + "dob_error_missing_day": "Date of birth must include a day", + "dob_error_missing_month": "Date of birth must include a month", + "dob_error_missing_year": "Date of birth must include a year", + "dob_error_non_numeric": "Enter only numbers", + "dob_error_year_min": "Date of birth is too long ago", + "dob_error_year_max": "The date of birth must be in the past", + "dob_error": "Enter a date of birth", + "dob_month": "month", "dob_privacy_link_prefix": "If you do not provide a correct date of birth, this may cause delays when a licence is renewed or mean that a licence is not valid. Read about ", "dob_privacy_link": "how we use personal information (opens in new tab)", @@ -316,15 +327,19 @@ "header_service_name_title": " - Get a rod fishing licence", "identification": "Identification", "identify_body_protect_info": "To find your licence details we first need to identify you. This helps us protect your personal information.", + "identify_error_date_real": "Date of birth must be a real date", "identify_error_empty_postcode": "You did not enter a postcode", "identify_error_empty": "Enter the last six characters of your licence number", - "identify_error_enter_bday_max": "Your date of birth must be in the past", - "identify_error_enter_bday_min": "Your date of birth is too long ago", - "identify_error_enter_bday": "Enter your date of birth and include a day, month and year", "identify_error_invalid_1": "We do not have any record of a licence number ending ", "identify_error_invalid_2": " matching these details.", + "identify_error_missing_day": "Date of birth must include a day", + "identify_error_missing_month": "Date of birth must include a month", + "identify_error_missing_year": "Date of birth must include a year", + "identify_error_non_numeric": "Enter only numbers", "identify_error_pattern_postcode": "Your postcode doesn’t look right. Check and enter again", "identify_error_pattern": "The last six characters of your licence number don’t look right. Check and enter again", + "identify_error_year_min": "Date of birth is too long ago", + "identify_error_year_max": "The date of birth must be in the past", "identify_label_last_six_hint": "For example F4A315", "identify_label_last_six": "The last six characters of your licence number", "identify_label_licence_ending": "The licence number ending", @@ -359,6 +374,9 @@ "important_info_contact_post": "Post", "important_info_contact_title_other": "How should we contact the licence holder about updates affecting their licence?", "important_info_contact_title_you": "How should we contact you about updates affecting your licence?", + "licence_1_day": "1-day", + "licence_8_day": "8-day", + "licence_12_month": "12-month", "licence_confirm_method_error_choose": "Choose how we should send the licence", "licence_confirm_method_how_body_item": "I will make a note of the licence number", "licence_confirm_method_how_body_text": "This is where we will send confirmation of the fishing licence", @@ -386,8 +404,8 @@ "licence_fulfilment_content_info": "Getting a licence by email or text message helps the environment by reducing the paper we use and means we can invest more money in fishing.", "licence_fulfilment_error": "Choose how you want to get the fishing licence", "licence_fulfilment_error_msg": "Select an option", - "licence_fulfilment_radio_card_hint_other": "A licence will be posted within 15 working days. Licence holders can fish from the start date and time selected and do not have to wait for the licence to arrive.", - "licence_fulfilment_radio_card_hint_you": "A licence will be posted within 15 working days. You can fish from the start date and time selected and do not need to wait for the licence to arrive.", + "licence_fulfilment_radio_card_hint_other": "A licence will be posted within 10 working days. Licence holders can fish from the start date and time selected and do not have to wait for the licence to arrive.", + "licence_fulfilment_radio_card_hint_you": "A licence will be posted within 10 working days. You can fish from the start date and time selected and do not need to wait for the licence to arrive.", "licence_fulfilment_radio_card": "By post", "licence_fulfilment_radio_paperless_hint_other": "The email or text message is proof of their fishing licence.", "licence_fulfilment_radio_paperless_hint_you": "The email or text message is proof of your fishing licence.", @@ -402,8 +420,16 @@ "licence_num": "Licence number", "licence_start_days": " days", "licence_start_enter_todays_date": "Enter today’s date if you want the 1-day or 8-day licence to start later today.", + "licence_start_error_date_real": "Licence start date must be a real date", + "licence_start_error_missing_day_and_month": "Licence start date must include a day and month", + "licence_start_error_missing_day_and_year": "Licence start date must include a day and year", + "licence_start_error_missing_month_and_year": "Licence start date must include a month and year", + "licence_start_error_missing_day": "Licence start date must include a day", + "licence_start_error_missing_month": "Licence start date must include a month", + "licence_start_error_missing_year": "Licence start date must include a year", + "licence_start_error_non_numeric": "Enter only numbers", + "licence_start_error": "Enter a licence start date", "licence_start_error_choose_when": "Choose when the licence should start", - "licence_start_error_format": "Enter the date the licence needs to start, include a day, month and year", "licence_start_error_within": "Enter a date within the next ", "licence_start_hint": "Enter a date up to and including ", "licence_start_later": "Later", @@ -461,8 +487,11 @@ "licence_type_payment_edge_case": "You must complete payment before 11:30pm on 31 March 2024 to get the price shown", "licence_type_radio_salmon_hint": "Includes trout and coarse (up to 3 rods)", "licence_type_radio_salmon": "Salmon and sea trout", + "licence_type_radio_salmon_payment_summary": "Salmon and sea trout", "licence_type_radio_trout_three_rod": "Trout and coarse (up to 3 rods)", + "licence_type_radio_trout_three_rod_payment_summary": "Trout and coarse (up to 3 rods)", "licence_type_radio_trout_two_rod": "Trout and coarse (up to 2 rods)", + "licence_type_radio_trout_two_rod_payment_summary": "Trout and coarse (up to 2 rods)", "licence_type_rules": "rod fishing rules (opens in new tab)", "licence_type_salmon_acr_note_1": "Licence holders must by law ", "licence_type_salmon_acr_note_2": " report a catch return (opens in new tab)", @@ -508,7 +537,7 @@ "newsletter_error_set_email": "Enter an email address in the correct format, like name@example.com", "newsletter_subscribe": "Stay up to date with the latest news on angling, fisheries and how we spend your licence money. You can unsubscribe at any time.", "newsletter_title": "Would you like to receive our email newsletter?", - "notification_heading": "Plastic-coated card licences will be replaced by a printed letter from 31 May 2024.", + "notification_heading": "Plastic-coated card licences were replaced by printed letters on 31 May 2024.", "notification_label": "Important", "notification_text": "Choosing to get a licence by email or text message helps reduce the paper we use and invests more licence income back into fishing.", "no_licence_req_body": "How much will this cost?", @@ -534,11 +563,11 @@ "order_complete_future_payments_title": "Future licence payments", "order_complete_licence_details_bobo_digital_paragraph": "The licence will also be sent by email or text message.", "order_complete_licence_details_bobo_digital_confirmation_paragraph": "Licence details will also be sent by email or text message.", - "order_complete_licence_details_bobo_paragraph": "The licence will arrive in the post within 15 working days. The licence holder can fish from the date and time selected and does not need to wait for a licence to arrive.", + "order_complete_licence_details_bobo_paragraph": "The licence will arrive in the post within 10 working days. The licence holder can fish from the date and time selected and does not need to wait for a licence to arrive.", "order_complete_licence_details_bobo_title": "View the licence details", "order_complete_licence_details_self_digital_paragraph": "Your licence will also be sent by email or text message.", "order_complete_licence_details_self_digital_confirmation_paragraph": "Your licence details will also be sent by email or text message.", - "order_complete_licence_details_self_paragraph": "Your licence will arrive in the post within 15 working days. You can fish from the date and time selected and do not need to wait for a licence to arrive.", + "order_complete_licence_details_self_paragraph": "Your licence will arrive in the post within 10 working days. You can fish from the date and time selected and do not need to wait for a licence to arrive.", "order_complete_licence_details_self_title": "View your licence details", "order_complete_when_fishing_bobo_non_postal_digital": "An enforcement officer may ask the licence holder if they are covered by a valid fishing licence. They can show them their licence email or text message on their phone. If they show a screenshot of their licence details, the enforcement officer will also ask for proof of their identity.", "order_complete_when_fishing_bobo_postal_digital": "An enforcement officer may ask the licence holder if they are covered by a valid fishing licence. They can show them the licence they got in the post, or show their licence email or text message on their phone.", @@ -638,6 +667,7 @@ "privacy_where_stored_body_3": "We do not store payment details.", "privacy_where_stored_body_4": "Our payment service partners Worldpay and GOV.UK Pay are compliant with the Payment Card Industry Data Security Standard (PCI DSS).", "purpose": "Purpose", + "recurring_payment_description": "The recurring card payment for your rod fishing licence", "recurring_payment_set_up": "I agree to set up a recurring card payment", "recurring_payment_set_up_back": "Or go back and choose to pay for a single licence", "recurring_payment_set_up_body": "Setting up a recurring payment means you understand and agree that:", diff --git a/packages/gafl-webapp-service/src/pages/concessions/date-of-birth/__tests__/route.spec.js b/packages/gafl-webapp-service/src/pages/concessions/date-of-birth/__tests__/route.spec.js index 43ffc49a6c..7dc8cd1fcd 100644 --- a/packages/gafl-webapp-service/src/pages/concessions/date-of-birth/__tests__/route.spec.js +++ b/packages/gafl-webapp-service/src/pages/concessions/date-of-birth/__tests__/route.spec.js @@ -1,15 +1,30 @@ -import { getData, validator } from '../route' +import { getData } from '../route' import pageRoute from '../../../../routes/page-route.js' import { nextPage } from '../../../../routes/next-page.js' -import { LICENCE_FOR } from '../../../../uri.js' +import { DATE_OF_BIRTH, LICENCE_FOR } from '../../../../uri.js' +import { dateOfBirthValidator, getDateErrorFlags } from '../../../../schema/validators/validators.js' -jest.mock('../../../../routes/next-page.js', () => ({ - nextPage: jest.fn() -})) +jest.mock('../../../../routes/next-page.js') jest.mock('../../../../routes/page-route.js') +jest.mock('../../../../schema/validators/validators.js') +jest.mock('../../../../uri.js', () => ({ + ...jest.requireActual('../../../../uri.js'), + DATE_OF_BIRTH: { + page: Symbol('date-of-birth-page'), + uri: Symbol('/date-of-birth') + }, + LICENCE_TO_START: { + page: Symbol('licence-to-start-page'), + uri: Symbol('/licence-to-start') + } +})) describe('name > route', () => { - const mockRequest = (statusGet = () => {}, transactionGet = () => {}) => ({ + const mockRequest = ({ + pageGet = async () => {}, + statusGet = async () => ({ [LICENCE_FOR.page]: true }), + transactionGet = async () => ({ isLicenceForYou: null }) + } = {}) => ({ cache: () => ({ helpers: { transaction: { @@ -17,6 +32,9 @@ describe('name > route', () => { }, status: { getCurrentPermission: statusGet + }, + page: { + getCurrentPermission: pageGet } } }) @@ -24,52 +42,89 @@ describe('name > route', () => { describe('getData', () => { it('should return isLicenceForYou as true, if isLicenceForYou is true on the transaction cache', async () => { - const transaction = () => ({ + const transactionGet = async () => ({ isLicenceForYou: true }) - const status = () => ({ + const statusGet = async () => ({ [LICENCE_FOR.page]: true }) - const result = await getData(mockRequest(status, transaction)) + + const result = await getData(mockRequest({ statusGet, transactionGet })) expect(result.isLicenceForYou).toBeTruthy() }) it('should return isLicenceForYou as false, if isLicenceForYou is false on the transaction cache', async () => { - const transaction = () => ({ + const transactionGet = async () => ({ isLicenceForYou: false }) - const status = () => ({ + const statusGet = async () => ({ [LICENCE_FOR.page]: true }) - const result = await getData(mockRequest(status, transaction)) + const result = await getData(mockRequest({ statusGet, transactionGet })) expect(result.isLicenceForYou).toBeFalsy() }) + + it.each([ + ['full-date', 'object.missing'], + ['day', 'any.required'] + ])('should add error details ({%s: %s}) to the page data', async (errorKey, errorValue) => { + const pageGet = async () => ({ + error: { [errorKey]: errorValue } + }) + + const result = await getData(mockRequest({ pageGet })) + expect(result.error).toEqual({ errorKey, errorValue }) + }) + + it('omits error if there is no error', async () => { + const result = await getData(mockRequest()) + expect(result.error).toBeUndefined() + }) + + it('adds return value of getErrorFlags to the page data', async () => { + const errorFlags = { unique: Symbol('error-flags') } + getDateErrorFlags.mockReturnValueOnce(errorFlags) + const result = await getData(mockRequest()) + expect(result).toEqual(expect.objectContaining(errorFlags)) + }) + + it('passes error to getErrorFlags', async () => { + const error = Symbol('error') + await getData(mockRequest({ pageGet: async () => ({ error }) })) + expect(getDateErrorFlags).toHaveBeenCalledWith(error) + }) + + it('passes correct page name when getting page cache', async () => { + const pageGet = jest.fn(() => ({})) + await getData(mockRequest({ pageGet })) + expect(pageGet).toHaveBeenCalledWith(DATE_OF_BIRTH.page) + }) }) describe('redirectToStartOfJourney', () => { it('should throw a redirect if not been to LICENCE_FOR page', async () => { - const transaction = () => ({ + const transactionGet = async () => ({ isLicenceForYou: true }) - const status = () => ({ + const statusGet = async () => ({ [LICENCE_FOR.page]: false }) - const func = () => getData(mockRequest(status, transaction)) + const func = () => getData(mockRequest({ statusGet, transactionGet })) await expect(func).rejects.toThrowRedirectTo(LICENCE_FOR.uri) }) it('should not throw a redirect if not been to LICENCE_FOR page', async () => { - const transaction = () => ({ + const transactionGet = async () => ({ isLicenceForYou: true }) - const status = () => ({ + const statusGet = async () => ({ [LICENCE_FOR.page]: true }) let error try { - await getData(mockRequest(status, transaction)) + await getData(mockRequest({ statusGet, transactionGet })) } catch (e) { error = e } @@ -79,8 +134,8 @@ describe('name > route', () => { }) describe('default', () => { - it('should call the pageRoute with date-of-birth, /buy/date-of-birth, validator and nextPage', async () => { - expect(pageRoute).toBeCalledWith('date-of-birth', '/buy/date-of-birth', validator, nextPage, getData) + it('should call the pageRoute with date-of-birth, /buy/date-of-birth, dateOfBirthValidator and nextPage', async () => { + expect(pageRoute).toBeCalledWith(DATE_OF_BIRTH.page, DATE_OF_BIRTH.uri, dateOfBirthValidator, nextPage, getData) }) }) }) diff --git a/packages/gafl-webapp-service/src/pages/concessions/date-of-birth/date-of-birth.njk b/packages/gafl-webapp-service/src/pages/concessions/date-of-birth/date-of-birth.njk index 0a29acedb0..65512b9fd4 100644 --- a/packages/gafl-webapp-service/src/pages/concessions/date-of-birth/date-of-birth.njk +++ b/packages/gafl-webapp-service/src/pages/concessions/date-of-birth/date-of-birth.njk @@ -9,11 +9,37 @@ {% set errorMap = { - 'date-of-birth': { - 'date.format': { ref: '#date-of-birth-day', text: mssgs.dob_error_format }, - 'date.max': { ref: '#date-of-birth-day', text: mssgs.dob_error_format_max }, - 'date.min': { ref: '#date-of-birth-day', text: mssgs.dob_error_format_min } - } + 'full-date': { + 'object.missing': { ref: '#date-of-birth-day', text: mssgs.dob_error } + }, + 'day-and-month': { + 'object.missing': { ref: '#date-of-birth-day', text: mssgs.dob_error_missing_day_and_month } + }, + 'day-and-year': { + 'object.missing': { ref: '#date-of-birth-day', text: mssgs.dob_error_missing_day_and_year } + }, + 'month-and-year': { + 'object.missing': { ref: '#date-of-birth-month', text: mssgs.dob_error_missing_month_and_year } + }, + 'day': { + 'any.required': { ref: '#date-of-birth-day', text: mssgs.dob_error_missing_day } + }, + 'month': { + 'any.required': { ref: '#date-of-birth-month', text: mssgs.dob_error_missing_month } + }, + 'year': { + 'any.required': { ref: '#date-of-birth-year', text: mssgs.dob_error_missing_year } + }, + 'non-numeric': { + 'number.base': { ref: '#date-of-birth-day', text: mssgs.dob_error_non_numeric } + }, + 'invalid-date': { + 'any.custom': { ref: '#date-of-birth-day', text: mssgs.dob_error_date_real } + }, + 'date-range': { + 'date.min': { ref: '#date-of-birth-day', text: mssgs.dob_error_year_min }, + 'date.max': { ref: '#date-of-birth-day', text: mssgs.dob_error_year_max } + } } %} @@ -21,21 +47,21 @@ { label: mssgs.dob_day, name: 'day', - classes: "govuk-input--width-2", + classes: "govuk-input--width-2 govuk-input--error" if data.isDayError else "govuk-input--width-2", value: payload['date-of-birth-day'], attributes: { maxlength : 2 } }, { label: mssgs.dob_month, name: 'month', - classes: "govuk-input--width-2", + classes: "govuk-input--width-2 govuk-input--error" if data.isMonthError else "govuk-input--width-2", value: payload['date-of-birth-month'], attributes: { maxlength : 2 } }, { label: mssgs.dob_year, name: 'year', - classes: "govuk-input--width-4", + classes: "govuk-input--width-4 govuk-input--error" if data.isYearError else "govuk-input--width-4", value: payload['date-of-birth-year'], attributes: { maxlength : 4 } } @@ -56,6 +82,6 @@ id: "date-of-birth", namePrefix: "date-of-birth", items: dateInputItems, - errorMessage: { text: mssgs.dob_error } if error + errorMessage: { text: errorMap[data.error.errorKey][data.error.errorValue].text } if data.error }) }} {% endblock %} diff --git a/packages/gafl-webapp-service/src/pages/concessions/date-of-birth/route.js b/packages/gafl-webapp-service/src/pages/concessions/date-of-birth/route.js index c20b48634d..1e0b00bc1b 100644 --- a/packages/gafl-webapp-service/src/pages/concessions/date-of-birth/route.js +++ b/packages/gafl-webapp-service/src/pages/concessions/date-of-birth/route.js @@ -1,19 +1,8 @@ import { DATE_OF_BIRTH, LICENCE_FOR } from '../../../uri.js' -import Joi from 'joi' import pageRoute from '../../../routes/page-route.js' -import { validation } from '@defra-fish/business-rules-lib' import { nextPage } from '../../../routes/next-page.js' import GetDataRedirect from '../../../handlers/get-data-redirect.js' - -export const validator = payload => { - const dateOfBirth = `${payload['date-of-birth-year']}-${payload['date-of-birth-month']}-${payload['date-of-birth-day']}` - Joi.assert( - { 'date-of-birth': dateOfBirth }, - Joi.object({ - 'date-of-birth': validation.contact.createBirthDateValidator(Joi) - }) - ) -} +import { dateOfBirthValidator, getDateErrorFlags } from '../../../schema/validators/validators.js' const redirectToStartOfJourney = status => { if (!status[LICENCE_FOR.page]) { @@ -24,10 +13,17 @@ const redirectToStartOfJourney = status => { export const getData = async request => { const { isLicenceForYou } = await request.cache().helpers.transaction.getCurrentPermission() const status = await request.cache().helpers.status.getCurrentPermission() + const page = await request.cache().helpers.page.getCurrentPermission(DATE_OF_BIRTH.page) + const pageData = { isLicenceForYou, ...getDateErrorFlags(page?.error) } redirectToStartOfJourney(status) - return { isLicenceForYou } + if (page?.error) { + const [errorKey] = Object.keys(page.error) + const errorValue = page.error[errorKey] + pageData.error = { errorKey, errorValue } + } + return pageData } -export default pageRoute(DATE_OF_BIRTH.page, DATE_OF_BIRTH.uri, validator, nextPage, getData) +export default pageRoute(DATE_OF_BIRTH.page, DATE_OF_BIRTH.uri, dateOfBirthValidator, nextPage, getData) diff --git a/packages/gafl-webapp-service/src/pages/contact/address/select/address-select.njk b/packages/gafl-webapp-service/src/pages/contact/address/select/address-select.njk index 80201190b3..5622dfd1ae 100644 --- a/packages/gafl-webapp-service/src/pages/contact/address/select/address-select.njk +++ b/packages/gafl-webapp-service/src/pages/contact/address/select/address-select.njk @@ -26,7 +26,7 @@ {{ data.addresses.length }} {{ mssgs.address_select_link if data.addresses.length === 1 else mssgs.address_select_addresses }} {{ mssgs.address_select_found_for }} {{ data.searchTerms.premises }}{{ mssgs.and }}{{ data.searchTerms.postcode }}   - {{ mssgs.licence_summary_change }}{{ mssgs.address_select_link }} + {{ mssgs.address_select_edit_search }}

diff --git a/packages/gafl-webapp-service/src/pages/contact/contact/__tests__/contact.spec.js b/packages/gafl-webapp-service/src/pages/contact/contact/__tests__/contact.spec.js index 98c30c8da2..b9950de9fb 100644 --- a/packages/gafl-webapp-service/src/pages/contact/contact/__tests__/contact.spec.js +++ b/packages/gafl-webapp-service/src/pages/contact/contact/__tests__/contact.spec.js @@ -122,15 +122,15 @@ describe('The contact preferences page', () => { } ) - it('post response none sets how-contacted - letter, in the cache', async () => { - await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'none' }) + it('post response as post sets how-contacted - letter, in the cache', async () => { + await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'post' }) const { payload } = await injectWithCookies('GET', TEST_TRANSACTION.uri) expect(JSON.parse(payload).permissions[0].licensee.preferredMethodOfConfirmation).toBeUndefined() expect(JSON.parse(payload).permissions[0].licensee.preferredMethodOfReminder).toEqual(HOW_CONTACTED.letter) }) it('if letter is specified then the licence is subsequently changed to junior, contact type none is set in the cache, in the cache', async () => { - await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'none' }) + await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'post' }) await injectWithCookies('POST', DATE_OF_BIRTH.uri, dobHelper(JUNIOR_TODAY)) const { payload } = await injectWithCookies('GET', TEST_TRANSACTION.uri) expect(JSON.parse(payload).permissions[0].licensee.preferredMethodOfConfirmation).toEqual(HOW_CONTACTED.none) @@ -197,8 +197,8 @@ describe('The contact preferences page', () => { isPhysical.mockReturnValueOnce(false) }) - it('post response none sets how-contacted - none in the cache', async () => { - await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'none' }) + it('post response none sets how-contacted - post in the cache', async () => { + await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'post' }) const { payload } = await injectWithCookies('GET', TEST_TRANSACTION.uri) expect(JSON.parse(payload).permissions[0].licensee.preferredMethodOfConfirmation).toEqual(HOW_CONTACTED.none) expect(JSON.parse(payload).permissions[0].licensee.preferredMethodOfReminder).toEqual(HOW_CONTACTED.none) @@ -216,7 +216,7 @@ describe('The contact preferences page', () => { }) it('post response none sets how-contacted - none in the cache', async () => { - await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'none' }) + await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'post' }) const { payload } = await injectWithCookies('GET', TEST_TRANSACTION.uri) expect(JSON.parse(payload).permissions[0].licensee.preferredMethodOfConfirmation).toEqual(HOW_CONTACTED.none) expect(JSON.parse(payload).permissions[0].licensee.preferredMethodOfReminder).toEqual(HOW_CONTACTED.none) @@ -244,7 +244,7 @@ describe('The contact preferences page', () => { }) it('controller redirects to the summary page', async () => { - const response = await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'none' }) + const response = await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'post' }) expect(response.statusCode).toBe(302) expect(response.headers.location).toHaveValidPathFor(CONTACT_SUMMARY.uri) }) diff --git a/packages/gafl-webapp-service/src/pages/contact/contact/contact.njk b/packages/gafl-webapp-service/src/pages/contact/contact/contact.njk index fd3c0ee15f..ea17d2ea84 100644 --- a/packages/gafl-webapp-service/src/pages/contact/contact/contact.njk +++ b/packages/gafl-webapp-service/src/pages/contact/contact/contact.njk @@ -137,7 +137,7 @@ {% if data.licensee.postalFulfilment %} {% set itemsArray = (itemsArray.push( { - value: "none", + value: "post", text: mssgs.important_info_contact_post, checked: payload['how-contacted'] === 'post', hint: { diff --git a/packages/gafl-webapp-service/src/pages/contact/contact/route.js b/packages/gafl-webapp-service/src/pages/contact/contact/route.js index 0528afb474..e939767503 100644 --- a/packages/gafl-webapp-service/src/pages/contact/contact/route.js +++ b/packages/gafl-webapp-service/src/pages/contact/contact/route.js @@ -84,7 +84,7 @@ const getErrorText = (messages, twelveMonthNonJuniorLicence) => twelveMonthNonJuniorLicence ? messages.important_info_contact_error_choose : messages.important_info_contact_error_choose_short export const validator = Joi.object({ - 'how-contacted': Joi.string().valid('email', 'text', 'none').required(), + 'how-contacted': Joi.string().valid('email', 'text', 'post').required(), email: Joi.alternatives().conditional('how-contacted', { is: 'email', then: validation.contact.createEmailValidator(Joi), diff --git a/packages/gafl-webapp-service/src/pages/guidance/recurring-payment-terms-conditions.njk b/packages/gafl-webapp-service/src/pages/guidance/recurring-payment-terms-conditions.njk index d650a0335f..479e41cb03 100644 --- a/packages/gafl-webapp-service/src/pages/guidance/recurring-payment-terms-conditions.njk +++ b/packages/gafl-webapp-service/src/pages/guidance/recurring-payment-terms-conditions.njk @@ -6,35 +6,35 @@
-

{{ mssgs.recurring_terms_conditions_title }}

+

{{ mssgs.recurring_terms_conditions_title }}

{{ mssgs.recurring_terms_conditions_body }}

-

{{ mssgs.recurring_terms_conditions_payment_subheading }}

+

{{ mssgs.recurring_terms_conditions_payment_subheading }}

{{ mssgs.recurring_terms_conditions_payment_body_1 }}

{{ mssgs.recurring_terms_conditions_payment_body_2 }}

{{ mssgs.recurring_terms_conditions_payment_body_3 }}

{{ mssgs.recurring_terms_conditions_payment_body_4 }}

{{ mssgs.recurring_terms_conditions_payment_body_5 }}

-

{{ mssgs.recurring_terms_conditions_payment_reminders_subheading }}

+

{{ mssgs.recurring_terms_conditions_payment_reminders_subheading }}

{{ mssgs.recurring_terms_conditions_payment_reminders_body_1 }}

{{ mssgs.recurring_terms_conditions_payment_reminders_body_2_1 }}{{ mssgs.recurring_terms_conditions_enquiries_link }}{{ mssgs.recurring_terms_conditions_payment_reminders_body_2_2 }}{{ mssgs.recurring_terms_conditions_call_charges_link }}{{ mssgs.full_stop }}

-

{{ mssgs.recurring_terms_conditions_cancel_heading }}

+

{{ mssgs.recurring_terms_conditions_cancel_heading }}

{{ mssgs.recurring_terms_conditions_cancel_body_1 }}

{{ mssgs.recurring_terms_conditions_cancel_body_2_1 }}{{ mssgs.recurring_terms_conditions_refund_link }}{{ mssgs.recurring_terms_conditions_cancel_body_2_2 }}

{{ mssgs.recurring_terms_conditions_cancel_body_3 }}

{{ mssgs.recurring_terms_conditions_cancel_body_4 }}

-

{{ mssgs.recurring_terms_conditions_cancel_phone_email_subheading }}

+

{{ mssgs.recurring_terms_conditions_cancel_phone_email_subheading }}

{{ mssgs.recurring_terms_conditions_cancel_phone_email_subheading_body_1 }}{{ mssgs.recurring_terms_conditions_call_charges_link }}{{ mssgs.full_stop }}

{{ mssgs.recurring_terms_conditions_cancel_phone_email_subheading_body_2_1 }}{{ mssgs.recurring_terms_conditions_enquiries_link }}{{ mssgs.recurring_terms_conditions_cancel_phone_email_subheading_body_2_2 }}

-

{{ mssgs.recurring_terms_conditions_cancel_bank_subheading }}

+

{{ mssgs.recurring_terms_conditions_cancel_bank_subheading }}

{{ mssgs.recurring_terms_conditions_cancel_bank_body }}

-

{{ mssgs.recurring_terms_conditions_security_heading }}

+

{{ mssgs.recurring_terms_conditions_security_heading }}

{{ mssgs.recurring_terms_conditions_security_body }}{{ mssgs.recurring_terms_conditions_privacy_link }}{{ mssgs.full_stop }}

-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/packages/gafl-webapp-service/src/pages/licence-details/licence-length/__tests__/licence-length.spec.js b/packages/gafl-webapp-service/src/pages/licence-details/licence-length/__tests__/licence-length.spec.js index 9585ab71a2..b46a1ad67d 100644 --- a/packages/gafl-webapp-service/src/pages/licence-details/licence-length/__tests__/licence-length.spec.js +++ b/packages/gafl-webapp-service/src/pages/licence-details/licence-length/__tests__/licence-length.spec.js @@ -51,17 +51,17 @@ describe('The licence length page', () => { expect(response.headers.location).toHaveValidPathFor(LICENCE_START_TIME.uri) }) - it("where contact is 'none' setting a 12 month licence changes it to 'post'", async () => { + it("where contact is 'post' setting a 12 month licence changes it to 'post'", async () => { await injectWithCookies('POST', LICENCE_TYPE.uri, { 'licence-type': 'trout-and-coarse-2-rod' }) - await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'none' }) + await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'post' }) await injectWithCookies('POST', LICENCE_LENGTH.uri, { 'licence-length': '12M' }) const { payload } = await injectWithCookies('GET', TEST_TRANSACTION.uri) expect(JSON.parse(payload).permissions[0].licensee.preferredMethodOfConfirmation).toEqual(HOW_CONTACTED.letter) }) - it("where contact is 'none' setting a 12 month licence, then changing it to 1 day sets preferredMethodOfConfirmation to none and sets postalFulfilment to false", async () => { + it("where contact is 'post' setting a 12 month licence, then changing it to 1 day sets preferredMethodOfConfirmation to none and sets postalFulfilment to false", async () => { await injectWithCookies('POST', LICENCE_TYPE.uri, { 'licence-type': 'trout-and-coarse-2-rod' }) - await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'none' }) + await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'post' }) await injectWithCookies('POST', LICENCE_LENGTH.uri, { 'licence-length': '12M' }) await injectWithCookies('POST', LICENCE_LENGTH.uri, { 'licence-length': '1D' }) const { payload: payload2 } = await injectWithCookies('GET', TEST_TRANSACTION.uri) @@ -69,17 +69,17 @@ describe('The licence length page', () => { expect(JSON.parse(payload2).permissions[0].licensee.postalFulfilment).toBeFalsy() }) - it("where contact is 'none', setting a 1 day licence keeps it at 'none'", async () => { + it("where contact is 'post', setting a 1 day licence keeps it at 'none'", async () => { await injectWithCookies('POST', LICENCE_TYPE.uri, { 'licence-type': 'trout-and-coarse-2-rod' }) - await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'none' }) + await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'post' }) await injectWithCookies('POST', LICENCE_LENGTH.uri, { 'licence-length': '1D' }) const { payload } = await injectWithCookies('GET', TEST_TRANSACTION.uri) expect(JSON.parse(payload).permissions[0].licensee.preferredMethodOfConfirmation).toEqual(HOW_CONTACTED.none) }) - it("where contact is 'none' setting a 1 day licence, then changing to 12 months sets preferredMethodOfConfirmation to letter and set postalFulfilment to true", async () => { + it("where contact is 'post' setting a 1 day licence, then changing to 12 months sets preferredMethodOfConfirmation to letter and set postalFulfilment to true", async () => { await injectWithCookies('POST', LICENCE_TYPE.uri, { 'licence-type': 'trout-and-coarse-2-rod' }) - await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'none' }) + await injectWithCookies('POST', CONTACT.uri, { 'how-contacted': 'post' }) await injectWithCookies('POST', LICENCE_LENGTH.uri, { 'licence-length': '1D' }) await injectWithCookies('POST', LICENCE_LENGTH.uri, { 'licence-length': '12M' }) const { payload: payload2 } = await injectWithCookies('GET', TEST_TRANSACTION.uri) diff --git a/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/__tests__/route.spec.js b/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/__tests__/route.spec.js index 3d76e2b007..11519e4f07 100644 --- a/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/__tests__/route.spec.js +++ b/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/__tests__/route.spec.js @@ -1,21 +1,42 @@ +import pageRoute from '../../../../routes/page-route.js' +import { nextPage } from '../../../../routes/next-page.js' import { getData } from '../route' +import { LICENCE_TO_START } from '../../../../uri.js' +import { startDateValidator, getDateErrorFlags } from '../../../../schema/validators/validators.js' -jest.mock('../../../../processors/uri-helper.js') +jest.mock('../../../../routes/next-page.js') +jest.mock('../../../../routes/page-route.js') +jest.mock('../../../../schema/validators/validators.js') +jest.mock('../../../../uri.js', () => ({ + ...jest.requireActual('../../../../uri.js'), + LICENCE_TO_START: { + page: Symbol('licence-to-start-page'), + uri: Symbol('/licence-to-start') + } +})) +jest.mock('../../../../schema/validators/validators.js') describe('licence-to-start > route', () => { - const getMockRequest = (isLicenceForYou = true) => ({ + const getMockRequest = (isLicenceForYou = true, pageGet = () => {}) => ({ cache: () => ({ helpers: { transaction: { getCurrentPermission: () => ({ isLicenceForYou }) + }, + page: { + getCurrentPermission: pageGet } } }) }) describe('getData', () => { + beforeEach(() => { + getDateErrorFlags.mockClear() + }) + it('should return isLicenceForYou as true, if isLicenceForYou is true on the transaction cache', async () => { const request = getMockRequest() const result = await getData(request) @@ -27,5 +48,47 @@ describe('licence-to-start > route', () => { const result = await getData(request) expect(result.isLicenceForYou).toBeFalsy() }) + + it.each([ + ['full-date', 'object.missing'], + ['day', 'any.required'] + ])('should add error details ({%s: %s}) to the page data', async (errorKey, errorValue) => { + const pageGet = async () => ({ + error: { [errorKey]: errorValue } + }) + + const result = await getData(getMockRequest(undefined, pageGet)) + expect(result.error).toEqual({ errorKey, errorValue }) + }) + + it('omits error if there is no error', async () => { + const result = await getData(getMockRequest()) + expect(result.error).toBeUndefined() + }) + + it('passes correct page name when getting page cache', async () => { + const pageGet = jest.fn(() => {}) + await getData(getMockRequest(undefined, pageGet)) + expect(pageGet).toHaveBeenCalledWith(LICENCE_TO_START.page) + }) + + it('adds return value of getErrorFlags to the page data', async () => { + const errorFlags = { unique: Symbol('error-flags') } + getDateErrorFlags.mockReturnValueOnce(errorFlags) + const result = await getData(getMockRequest()) + expect(result).toEqual(expect.objectContaining(errorFlags)) + }) + + it('passes error to getErrorFlags', async () => { + const error = Symbol('error') + await getData(getMockRequest(undefined, async () => ({ error }))) + expect(getDateErrorFlags).toHaveBeenCalledWith(error) + }) + }) + + describe('default', () => { + it('should call the pageRoute with date-of-birth, /buy/date-of-birth, dateOfBirthValidator and nextPage', async () => { + expect(pageRoute).toBeCalledWith(LICENCE_TO_START.page, LICENCE_TO_START.uri, startDateValidator, nextPage, getData) + }) }) }) diff --git a/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/licence-to-start.njk b/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/licence-to-start.njk index b64afbf24d..3d7a2d423c 100644 --- a/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/licence-to-start.njk +++ b/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/licence-to-start.njk @@ -8,14 +8,40 @@ {% set errorMap = { - 'licence-to-start': { - 'any.required': { ref: '#licence-to-start', text: mssgs.licence_start_error_choose_when } - }, - 'licence-start-date': { - 'date.format': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_format }, - 'date.max': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_within + data.advancedPurchaseMaxDays + mssgs.licence_start_days }, - 'date.min': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_within + data.advancedPurchaseMaxDays + mssgs.licence_start_days } - } + 'full-date': { + 'object.missing': { ref: '#licence-start-date-day', text: mssgs.licence_start_error } + }, + 'day-and-month': { + 'object.missing': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_missing_day_and_month } + }, + 'day-and-year': { + 'object.missing': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_missing_day_and_year } + }, + 'month-and-year': { + 'object.missing': { ref: '#licence-start-date-month', text: mssgs.licence_start_error_missing_month_and_year } + }, + 'day': { + 'any.required': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_missing_day } + }, + 'month': { + 'any.required': { ref: '#licence-start-date-month', text: mssgs.licence_start_error_missing_month } + }, + 'year': { + 'any.required': { ref: '#licence-start-date-year', text: mssgs.licence_start_error_missing_year } + }, + 'non-numeric': { + 'number.base': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_non_numeric } + }, + 'invalid-date': { + 'any.custom': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_date_real } + }, + 'date-range': { + 'date.min': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_within + data.advancedPurchaseMaxDays + mssgs.licence_start_days }, + 'date.max': { ref: '#licence-start-date-day', text: mssgs.licence_start_error_within + data.advancedPurchaseMaxDays + mssgs.licence_start_days } + }, + 'licence-to-start': { + 'any.required': { ref: '#licence-to-start', text: mssgs.licence_start_error_choose_when } + } } %} @@ -23,21 +49,21 @@ { name: "day", label: mssgs.dob_day, - classes: "govuk-input--width-2", + classes: "govuk-input--width-2 govuk-input--error" if data.isDayError else "govuk-input--width-2", value: payload['licence-start-date-day'], attributes: { maxlength : 2 } }, { name: "month", label: mssgs.dob_month, - classes: "govuk-input--width-2", + classes: "govuk-input--width-2 govuk-input--error" if data.isMonthError else "govuk-input--width-2", value: payload['licence-start-date-month'], attributes: { maxlength : 2 } }, { name: "year", label: mssgs.dob_year, - classes: "govuk-input--width-4", + classes: "govuk-input--width-4 govuk-input--error" if data.isYearError else "govuk-input--width-4", value: payload['licence-start-date-year'], attributes: { maxlength : 4 } } @@ -53,7 +79,7 @@ id: "licence-start-date", namePrefix: "licence-start-date", items: dateInputItems, - errorMessage: { text: mssgs.licence_start_error_within + data.advancedPurchaseMaxDays + mssgs.licence_start_days } if error['licence-start-date'], + errorMessage: { text: errorMap[data.error.errorKey][data.error.errorValue].text } if data.error, hint: { text: mssgs.licence_start_hint + data.maxStartDate } diff --git a/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/route.js b/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/route.js index 31ae6d2ffa..6e063b4881 100644 --- a/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/route.js +++ b/packages/gafl-webapp-service/src/pages/licence-details/licence-to-start/route.js @@ -1,49 +1,31 @@ -import Joi from 'joi' import moment from 'moment-timezone' - -import JoiDate from '@hapi/joi-date' import { START_AFTER_PAYMENT_MINUTES, ADVANCED_PURCHASE_MAX_DAYS, SERVICE_LOCAL_TIME } from '@defra-fish/business-rules-lib' import { LICENCE_TO_START } from '../../../uri.js' import pageRoute from '../../../routes/page-route.js' -import { dateFormats } from '../../../constants.js' import { nextPage } from '../../../routes/next-page.js' - -const JoiX = Joi.extend(JoiDate) - -const validator = payload => { - const licenceStartDate = `${payload['licence-start-date-year']}-${payload['licence-start-date-month']}-${payload['licence-start-date-day']}` - Joi.assert( - { - 'licence-start-date': licenceStartDate, - 'licence-to-start': payload['licence-to-start'] - }, - Joi.object({ - 'licence-to-start': Joi.string().valid('after-payment', 'another-date').required(), - 'licence-start-date': Joi.alternatives().conditional('licence-to-start', { - is: 'another-date', - then: JoiX.date() - .format(dateFormats) - .min(moment().tz(SERVICE_LOCAL_TIME).startOf('day')) - .max(moment().tz(SERVICE_LOCAL_TIME).add(ADVANCED_PURCHASE_MAX_DAYS, 'days')) - .required(), - otherwise: Joi.string().empty('') - }) - }).options({ abortEarly: false, allowUnknown: true }) - ) -} +import { getDateErrorFlags, startDateValidator } from '../../../schema/validators/validators.js' export const getData = async request => { const fmt = 'DD MM YYYY' const { isLicenceForYou } = await request.cache().helpers.transaction.getCurrentPermission() - - return { + const page = await request.cache().helpers.page.getCurrentPermission(LICENCE_TO_START.page) + const pageData = { isLicenceForYou, exampleStartDate: moment().tz(SERVICE_LOCAL_TIME).add(1, 'days').format(fmt), minStartDate: moment().tz(SERVICE_LOCAL_TIME).format(fmt), maxStartDate: moment().tz(SERVICE_LOCAL_TIME).add(ADVANCED_PURCHASE_MAX_DAYS, 'days').format(fmt), advancedPurchaseMaxDays: ADVANCED_PURCHASE_MAX_DAYS, - startAfterPaymentMinutes: START_AFTER_PAYMENT_MINUTES + startAfterPaymentMinutes: START_AFTER_PAYMENT_MINUTES, + ...getDateErrorFlags(page?.error) + } + + if (page?.error) { + const [errorKey] = Object.keys(page.error) + const errorValue = page.error[errorKey] + pageData.error = { errorKey, errorValue } } + + return pageData } -export default pageRoute(LICENCE_TO_START.page, LICENCE_TO_START.uri, validator, nextPage, getData) +export default pageRoute(LICENCE_TO_START.page, LICENCE_TO_START.uri, startDateValidator, nextPage, getData) diff --git a/packages/gafl-webapp-service/src/pages/recurring-payments/set-up-payment/__tests__/update-transaction.spec.js b/packages/gafl-webapp-service/src/pages/recurring-payments/set-up-payment/__tests__/update-transaction.spec.js index 73433a4fbe..167b66d776 100644 --- a/packages/gafl-webapp-service/src/pages/recurring-payments/set-up-payment/__tests__/update-transaction.spec.js +++ b/packages/gafl-webapp-service/src/pages/recurring-payments/set-up-payment/__tests__/update-transaction.spec.js @@ -1,5 +1,5 @@ import updateTransaction from '../update-transaction.js' -import { COMPLETION_STATUS } from '../../../../constants.js' +import { COMPLETION_STATUS, RECURRING_PAYMENT } from '../../../../constants.js' import db from 'debug' jest.mock('debug', () => jest.fn(() => jest.fn())) @@ -33,4 +33,15 @@ describe('update transaction', () => { expect(debug).toHaveBeenCalledWith('Setting status to agreed') }) + + it('should set status to a recurring payment', async () => { + const statusSet = jest.fn() + await updateTransaction(getSampleRequest(statusSet)) + + expect(statusSet).toHaveBeenCalledWith( + expect.objectContaining({ + [RECURRING_PAYMENT]: true + }) + ) + }) }) diff --git a/packages/gafl-webapp-service/src/pages/recurring-payments/set-up-payment/update-transaction.js b/packages/gafl-webapp-service/src/pages/recurring-payments/set-up-payment/update-transaction.js index a683279855..d5847d8174 100644 --- a/packages/gafl-webapp-service/src/pages/recurring-payments/set-up-payment/update-transaction.js +++ b/packages/gafl-webapp-service/src/pages/recurring-payments/set-up-payment/update-transaction.js @@ -1,8 +1,8 @@ import db from 'debug' -import { COMPLETION_STATUS } from '../../../constants.js' +import { COMPLETION_STATUS, RECURRING_PAYMENT } from '../../../constants.js' const debug = db('webapp:set-agreed') export default async request => { debug('Setting status to agreed') - await request.cache().helpers.status.set({ [COMPLETION_STATUS.agreed]: true }) + await request.cache().helpers.status.set({ [COMPLETION_STATUS.agreed]: true, [RECURRING_PAYMENT]: true }) } diff --git a/packages/gafl-webapp-service/src/pages/renewals/identify/__tests__/identity.next-page.spec.js b/packages/gafl-webapp-service/src/pages/renewals/identify/__tests__/identity.next-page.spec.js deleted file mode 100644 index d6a639cbb7..0000000000 --- a/packages/gafl-webapp-service/src/pages/renewals/identify/__tests__/identity.next-page.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import pageRoute from '../../../../routes/page-route.js' -import { addLanguageCodeToUri } from '../../../../processors/uri-helper.js' -require('../route.js') // require rather than import to avoid lint error with unused variable - -jest.mock('../../../../routes/page-route.js', () => jest.fn()) -jest.mock('../../../../uri.js', () => ({ - IDENTIFY: { page: 'identify page', uri: 'identify uri' }, - AUTHENTICATE: { uri: Symbol('authenticate uri') } -})) -jest.mock('../../../../processors/uri-helper.js') - -describe('page route next', () => { - const nextPage = pageRoute.mock.calls[0][3] - beforeEach(jest.clearAllMocks) - - it('passes a function as the nextPage argument', () => { - expect(typeof nextPage).toBe('function') - }) - - it('calls addLanguageCodeToUri', () => { - nextPage() - expect(addLanguageCodeToUri).toHaveBeenCalled() - }) - - it('passes request to addLanguageCodeToUri', () => { - const request = Symbol('request') - nextPage(request) - expect(addLanguageCodeToUri).toHaveBeenCalledWith(request, expect.anything()) - }) - - it('next page returns result of addLanguageCodeToUri', () => { - const expectedResult = Symbol('add language code to uri') - addLanguageCodeToUri.mockReturnValueOnce(expectedResult) - expect(nextPage()).toBe(expectedResult) - }) -}) diff --git a/packages/gafl-webapp-service/src/pages/renewals/identify/__tests__/identity.spec.js b/packages/gafl-webapp-service/src/pages/renewals/identify/__tests__/identity.spec.js index d4d1f1f18a..ae72140f2f 100644 --- a/packages/gafl-webapp-service/src/pages/renewals/identify/__tests__/identity.spec.js +++ b/packages/gafl-webapp-service/src/pages/renewals/identify/__tests__/identity.spec.js @@ -116,6 +116,9 @@ describe('The easy renewal identification page', () => { referenceNumber: 'ABC123' }), setCurrentPermission: () => {} + }, + page: { + getCurrentPermission: async () => ({}) } } }) diff --git a/packages/gafl-webapp-service/src/pages/renewals/identify/__tests__/route-spec.js b/packages/gafl-webapp-service/src/pages/renewals/identify/__tests__/route-spec.js deleted file mode 100644 index 8e66fe6174..0000000000 --- a/packages/gafl-webapp-service/src/pages/renewals/identify/__tests__/route-spec.js +++ /dev/null @@ -1,38 +0,0 @@ -import { addLanguageCodeToUri } from '../../../../processors/uri-helper.js' -import { getData } from '../route.js' -import { NEW_TRANSACTION } from '../../../../uri.js' - -jest.mock('../../../../processors/uri-helper.js') - -const getMockRequest = referenceNumber => ({ - cache: () => ({ - helpers: { - status: { - getCurrentPermission: () => ({ - referenceNumber: referenceNumber - }) - } - } - }) -}) - -describe('getData', () => { - it('addLanguageCodeToUri is called with the expected arguments', async () => { - const request = getMockRequest('013AH6') - await getData(request) - expect(addLanguageCodeToUri).toHaveBeenCalledWith(request, NEW_TRANSACTION.uri) - }) - - it('getData returns correct URI', async () => { - const expectedUri = Symbol('decorated uri') - addLanguageCodeToUri.mockReturnValueOnce(expectedUri) - - const result = await getData(getMockRequest('013AH6')) - expect(result.uri.new).toEqual(expectedUri) - }) - - it.each([['09F6VF'], ['013AH6'], ['LK563F']])('getData returns referenceNumber', async referenceNumber => { - const result = await getData(getMockRequest(referenceNumber)) - expect(result.referenceNumber).toEqual(referenceNumber) - }) -}) diff --git a/packages/gafl-webapp-service/src/pages/renewals/identify/__tests__/route.spec.js b/packages/gafl-webapp-service/src/pages/renewals/identify/__tests__/route.spec.js new file mode 100644 index 0000000000..51aff21010 --- /dev/null +++ b/packages/gafl-webapp-service/src/pages/renewals/identify/__tests__/route.spec.js @@ -0,0 +1,143 @@ +import pageRoute from '../../../../routes/page-route.js' +import { addLanguageCodeToUri } from '../../../../processors/uri-helper.js' +import { getData, validator } from '../route.js' +import { IDENTIFY, NEW_TRANSACTION } from '../../../../uri.js' +import { dateOfBirthValidator, getDateErrorFlags } from '../../../../schema/validators/validators.js' + +jest.mock('../../../../routes/page-route.js', () => jest.fn()) +jest.mock('../../../../uri.js', () => ({ + IDENTIFY: { page: 'identify page', uri: 'identify uri' }, + AUTHENTICATE: { uri: Symbol('authenticate uri') }, + NEW_TRANSACTION: { uri: Symbol('new transaction uri') } +})) +jest.mock('../../../../processors/uri-helper.js') +jest.mock('../../../../schema/validators/validators.js') + +describe('getData', () => { + const getMockRequest = (referenceNumber, pageGet = async () => ({})) => ({ + cache: () => ({ + helpers: { + status: { + getCurrentPermission: () => ({ + referenceNumber: referenceNumber + }) + }, + page: { + getCurrentPermission: pageGet + } + } + }) + }) + + it('addLanguageCodeToUri is called with the expected arguments', async () => { + const request = getMockRequest('013AH6') + await getData(request) + expect(addLanguageCodeToUri).toHaveBeenCalledWith(request, NEW_TRANSACTION.uri) + }) + + it('getData returns correct URI', async () => { + const expectedUri = Symbol('decorated uri') + addLanguageCodeToUri.mockReturnValueOnce(expectedUri) + + const result = await getData(getMockRequest('013AH6')) + expect(result.uri.new).toEqual(expectedUri) + }) + + it.each([['09F6VF'], ['013AH6'], ['LK563F']])('getData returns referenceNumber', async referenceNumber => { + const result = await getData(getMockRequest(referenceNumber)) + expect(result.referenceNumber).toEqual(referenceNumber) + }) + + it('adds return value of getErrorFlags to the page data', async () => { + const errorFlags = { unique: Symbol('error-flags') } + getDateErrorFlags.mockReturnValueOnce(errorFlags) + const result = await getData(getMockRequest()) + expect(result).toEqual(expect.objectContaining(errorFlags)) + }) + + it('passes error to getErrorFlags', async () => { + const error = Symbol('error') + await getData(getMockRequest(undefined, async () => ({ error }))) + expect(getDateErrorFlags).toHaveBeenCalledWith(error) + }) + + it('passes correct page name when getting page cache', async () => { + const pageGet = jest.fn(() => ({})) + await getData(getMockRequest(undefined, pageGet)) + expect(pageGet).toHaveBeenCalledWith(IDENTIFY.page) + }) + + it.each([ + ['full-date', 'object.missing'], + ['day', 'any.required'] + ])('should add error details ({%s: %s}) to the page data', async (errorKey, errorValue) => { + const pageGet = async () => ({ + error: { [errorKey]: errorValue } + }) + + const result = await getData(getMockRequest(undefined, pageGet)) + expect(result.error).toEqual({ errorKey, errorValue }) + }) + + it('omits error if there is no error', async () => { + const result = await getData(getMockRequest()) + expect(result.error).toBeUndefined() + }) +}) + +describe('default', () => { + it('should call the pageRoute with date-of-birth, /buy/date-of-birth, dateOfBirthValidator and nextPage', async () => { + expect(pageRoute).toBeCalledWith(IDENTIFY.page, IDENTIFY.uri, validator, expect.any(Function), getData) + }) +}) + +describe('page route next', () => { + const nextPage = pageRoute.mock.calls[0][3] + beforeEach(jest.clearAllMocks) + + it('passes a function as the nextPage argument', () => { + expect(typeof nextPage).toBe('function') + }) + + it('calls addLanguageCodeToUri', () => { + nextPage() + expect(addLanguageCodeToUri).toHaveBeenCalled() + }) + + it('passes request to addLanguageCodeToUri', () => { + const request = Symbol('request') + nextPage(request) + expect(addLanguageCodeToUri).toHaveBeenCalledWith(request, expect.anything()) + }) + + it('next page returns result of addLanguageCodeToUri', () => { + const expectedResult = Symbol('add language code to uri') + addLanguageCodeToUri.mockReturnValueOnce(expectedResult) + expect(nextPage()).toBe(expectedResult) + }) +}) + +describe('validator', () => { + const getMockRequest = (postcode = 'AA1 1AA', referenceNumber = 'A1B2C3') => ({ + postcode, + referenceNumber + }) + + it('fails if dateOfBirth validator fails', () => { + const expectedError = new Error('expected error') + dateOfBirthValidator.mockImplementationOnce(() => { + throw expectedError + }) + expect(() => validator(getMockRequest)).toThrow(expectedError) + }) + + it('passes if dateOfBirth validator passes', () => { + expect(() => validator(getMockRequest())).not.toThrow() + }) + + it('passes payload to dateOfBirth validator', () => { + const payload = getMockRequest() + validator(payload) + expect(dateOfBirthValidator).toHaveBeenCalledWith(payload) + }) +}) diff --git a/packages/gafl-webapp-service/src/pages/renewals/identify/identify.njk b/packages/gafl-webapp-service/src/pages/renewals/identify/identify.njk index 529c821b32..d863e772ee 100644 --- a/packages/gafl-webapp-service/src/pages/renewals/identify/identify.njk +++ b/packages/gafl-webapp-service/src/pages/renewals/identify/identify.njk @@ -21,14 +21,40 @@ ref: "#ref" } }, - 'date-of-birth': { - 'date.format': { ref: '#date-of-birth-day', text: mssgs.identify_error_enter_bday }, - 'date.max': { ref: '#date-of-birth-day', text: mssgs.identify_error_enter_bday_max }, - 'date.min': { ref: '#date-of-birth-day', text: mssgs.identify_error_enter_bday_min } - }, 'postcode': { 'string.empty': { ref: '#postcode', text: mssgs.identify_error_empty_postcode }, 'string.pattern.base': { ref: '#postcode', text: mssgs.identify_error_pattern_postcode } + }, + 'full-date': { + 'object.missing': { ref: '#date-of-birth-day', text: mssgs.dob_error } + }, + 'day-and-month': { + 'object.missing': { ref: '#date-of-birth-day', text: mssgs.dob_error_missing_day_and_month } + }, + 'day-and-year': { + 'object.missing': { ref: '#date-of-birth-day', text: mssgs.dob_error_missing_day_and_year } + }, + 'month-and-year': { + 'object.missing': { ref: '#date-of-birth-month', text: mssgs.dob_error_missing_month_and_year } + }, + 'day': { + 'any.required': { ref: '#date-of-birth-day', text: mssgs.dob_error_missing_day } + }, + 'month': { + 'any.required': { ref: '#date-of-birth-month', text: mssgs.dob_error_missing_month } + }, + 'year': { + 'any.required': { ref: '#date-of-birth-year', text: mssgs.dob_error_missing_year } + }, + 'non-numeric': { + 'number.base': { ref: '#date-of-birth-day', text: mssgs.dob_error_non_numeric } + }, + 'invalid-date': { + 'any.custom': { ref: '#date-of-birth-day', text: mssgs.dob_error_date_real } + }, + 'date-range': { + 'date.min': { ref: '#date-of-birth-day', text: mssgs.dob_error_year_min }, + 'date.max': { ref: '#date-of-birth-day', text: mssgs.dob_error_year_max } } } %} @@ -37,21 +63,21 @@ { label: mssgs.dob_day, name: "day", - classes: "govuk-input--width-2", + classes: "govuk-input--width-2 govuk-input--error" if data.isDayError else "govuk-input--width-2", value: payload['date-of-birth-day'], attributes: { maxlength : 2 } }, { label: mssgs.dob_month, name: "month", - classes: "govuk-input--width-2", + classes: "govuk-input--width-2 govuk-input--error" if data.isMonthError else "govuk-input--width-2", value: payload['date-of-birth-month'], attributes: { maxlength : 2 } }, { label: mssgs.dob_year, name: "year", - classes: "govuk-input--width-4", + classes: "govuk-input--width-4 govuk-input--error" if data.isYearError else "govuk-input--width-4", value: payload['date-of-birth-year'], attributes: { maxlength : 4 } } @@ -100,7 +126,7 @@
{{ mssgs.identify_label_licence_ending }} {{ payload.referenceNumber if payload.referenceNumber else data.referenceNumber }} -   {{ mssgs.licence_summary_change }} +   {{ mssgs.change_licence_number }}
@@ -115,7 +141,7 @@ classes: "govuk-!-font-weight-bold govuk-label" } }, - errorMessage: { text: mssgs.enter_dob } if error['date-of-birth'], + errorMessage: { text: errorMap[data.error.errorKey][data.error.errorValue].text } if data.error, hint: { text: mssgs.enter_dob_example } diff --git a/packages/gafl-webapp-service/src/pages/renewals/identify/route.js b/packages/gafl-webapp-service/src/pages/renewals/identify/route.js index a873c01e0f..45fc367fb9 100644 --- a/packages/gafl-webapp-service/src/pages/renewals/identify/route.js +++ b/packages/gafl-webapp-service/src/pages/renewals/identify/route.js @@ -4,10 +4,12 @@ import Joi from 'joi' import { validation } from '@defra-fish/business-rules-lib' import { addLanguageCodeToUri } from '../../../processors/uri-helper.js' import GetDataRedirect from '../../../handlers/get-data-redirect.js' +import { dateOfBirthValidator, getDateErrorFlags } from '../../../schema/validators/validators.js' export const getData = async request => { // If we are supplied a permission number, validate it or throw 400 const permission = await request.cache().helpers.status.getCurrentPermission() + const page = await request.cache().helpers.page.getCurrentPermission(IDENTIFY.page) if (permission.referenceNumber) { const validatePermissionNumber = validation.permission @@ -19,29 +21,35 @@ export const getData = async request => { } } - return { + const pageData = { referenceNumber: permission.referenceNumber, uri: { new: addLanguageCodeToUri(request, NEW_TRANSACTION.uri) - } + }, + ...getDateErrorFlags(page?.error) } + + if (page?.error) { + const [errorKey] = Object.keys(page.error) + const errorValue = page.error[errorKey] + pageData.error = { errorKey, errorValue } + } + + return pageData } -const schema = Joi.object({ - referenceNumber: validation.permission.permissionNumberUniqueComponentValidator(Joi), - 'date-of-birth': validation.contact.createBirthDateValidator(Joi), - postcode: validation.contact.createOverseasPostcodeValidator(Joi) -}).options({ abortEarly: false, allowUnknown: true }) +export const validator = payload => { + dateOfBirthValidator(payload) -const validator = async payload => { - const dateOfBirth = `${payload['date-of-birth-year']}-${payload['date-of-birth-month']}-${payload['date-of-birth-day']}` Joi.assert( { - 'date-of-birth': dateOfBirth, postcode: payload.postcode, referenceNumber: payload.referenceNumber }, - schema + Joi.object({ + referenceNumber: validation.permission.permissionNumberUniqueComponentValidator(Joi), + postcode: validation.contact.createOverseasPostcodeValidator(Joi) + }).options({ abortEarly: false }) ) } diff --git a/packages/gafl-webapp-service/src/pages/summary/licence-summary/__tests__/__snapshots__/route.spec.js.snap b/packages/gafl-webapp-service/src/pages/summary/licence-summary/__tests__/__snapshots__/route.spec.js.snap index 233578b8c1..d689b8af12 100644 --- a/packages/gafl-webapp-service/src/pages/summary/licence-summary/__tests__/__snapshots__/route.spec.js.snap +++ b/packages/gafl-webapp-service/src/pages/summary/licence-summary/__tests__/__snapshots__/route.spec.js.snap @@ -246,6 +246,127 @@ Array [ ] `; +exports[`licence-summary > route licence summary rows creates licence summary name rows for 1 year new three rod licence 1`] = ` +Array [ + Object { + "actions": Object { + "items": Array [ + Object { + "attributes": Object { + "id": "change-name", + }, + "href": "/buy/name", + "text": "contact_summary_change", + "visuallyHiddenText": "licence_summary_name", + }, + ], + }, + "key": Object { + "text": "licence_summary_name", + }, + "value": Object { + "html": "Brenin Pysgotwr", + }, + }, + Object { + "actions": Object { + "items": Array [ + Object { + "attributes": Object { + "id": "change-birth-date", + }, + "href": "/buy/date-of-birth", + "text": "contact_summary_change", + "visuallyHiddenText": "licence_summary_dob", + }, + ], + }, + "key": Object { + "text": "licence_summary_dob", + }, + "value": Object { + "html": "1st January 1946", + }, + }, + Object { + "actions": Object { + "items": Array [ + Object { + "attributes": Object { + "id": "change-licence-type", + }, + "href": "/buy/licence-type", + "text": "contact_summary_change", + "visuallyHiddenText": "licence_summary_type", + }, + ], + }, + "key": Object { + "text": "licence_summary_type", + }, + "value": Object { + "html": "Special Canal Licence, Shopping Trollies and Old Wellies", + }, + }, + Object { + "key": Object { + "text": "licence_summary_length", + }, + "value": Object { + "html": "licence_type_12m", + }, + }, + Object { + "actions": Object { + "items": Array [ + Object { + "attributes": Object { + "id": "change-licence-to-start", + }, + "href": "/buy/start-kind", + "text": "contact_summary_change", + "visuallyHiddenText": "licence_summary_start_date", + }, + ], + }, + "key": Object { + "text": "licence_summary_start_date", + }, + "value": Object { + "html": "30licence_summary_minutes_after_payment", + }, + }, + Object { + "actions": Object { + "items": Array [ + Object { + "attributes": Object { + "id": "change-benefit-check", + }, + "href": "/buy/disability-concession", + "text": "contact_summary_change", + "visuallyHiddenText": "licence_summary_ni_num", + }, + ], + }, + "key": Object { + "text": "licence_summary_ni_num", + }, + "value": Object { + "html": "AB 12 34 56 A", + }, + }, + Object { + "key": Object { + "text": "damage", + }, + "value": Object { + "html": "#6", + }, + }, +] +`; + exports[`licence-summary > route licence summary rows creates licence summary name rows for 1 year renewal 1`] = ` Array [ Object { @@ -1007,18 +1128,6 @@ Array [ }, }, Object { - "actions": Object { - "items": Array [ - Object { - "attributes": Object { - "id": "change-licence-length", - }, - "href": "/buy/licence-length", - "text": "contact_summary_change", - "visuallyHiddenText": "licence_summary_length", - }, - ], - }, "key": Object { "text": "licence_summary_length", }, diff --git a/packages/gafl-webapp-service/src/pages/summary/licence-summary/__tests__/route.spec.js b/packages/gafl-webapp-service/src/pages/summary/licence-summary/__tests__/route.spec.js index 3015c8565b..777a8ee5f3 100644 --- a/packages/gafl-webapp-service/src/pages/summary/licence-summary/__tests__/route.spec.js +++ b/packages/gafl-webapp-service/src/pages/summary/licence-summary/__tests__/route.spec.js @@ -7,7 +7,11 @@ import { licenceTypeDisplay } from '../../../../processors/licence-type-display. import { addLanguageCodeToUri } from '../../../../processors/uri-helper.js' import mappingConstants from '../../../../processors/mapping-constants.js' import { displayPermissionPrice } from '../../../../processors/price-display.js' +import { hasJunior } from '../../../../processors/concession-helper.js' +jest.mock('../../../../processors/concession-helper.js', () => ({ + hasJunior: jest.fn(() => false) +})) jest.mock('../../../../processors/licence-type-display.js', () => ({ licenceTypeDisplay: jest.fn(() => 'Special Canal Licence, Shopping Trollies and Old Wellies') })) @@ -116,7 +120,7 @@ const getMockPermission = (licenseeOverrides = {}) => ({ licenceToStart: 'after-payment', licenceStartDate: '2022-11-10', licenceType: 'Trout and coarse', - numberOfRods: '3', + numberOfRods: '2', permit: { cost: 6 } }) @@ -370,19 +374,30 @@ describe('licence-summary > route', () => { ) }) + it('calls hasJunior with permission', async () => { + const currentPermission = getMockNewPermission() + const mockRequest = getMockRequest({ currentPermission }) + + await getData(mockRequest) + + expect(hasJunior).toHaveBeenCalledWith(currentPermission) + }) + describe('licence summary rows', () => { it.each` - desc | currentPermission - ${'1 year renewal'} | ${getMockPermission()} - ${'1 year new licence'} | ${getMockNewPermission()} - ${'1 year senior renewal'} | ${getMockSeniorPermission()} - ${'8 day licence'} | ${{ ...getMockNewPermission(), licenceLength: '8D' }} - ${'1 day licence'} | ${{ ...getMockNewPermission(), licenceLength: '1D' }} - ${'Junior licence'} | ${getMockJuniorPermission()} - ${'Blue badge concession'} | ${getMockBlueBadgePermission()} - ${'Continuing permission'} | ${getMockContinuingPermission()} - ${'Another date permission'} | ${{ ...getMockPermission(), licenceToStart: 'another-date' }} - `('creates licence summary name rows for $desc', async ({ currentPermission }) => { + desc | currentPermission | junior + ${'1 year renewal'} | ${getMockPermission()} | ${false} + ${'1 year new licence'} | ${getMockNewPermission()} | ${false} + ${'1 year senior renewal'} | ${getMockSeniorPermission()} | ${false} + ${'8 day licence'} | ${{ ...getMockNewPermission(), licenceLength: '8D' }} | ${false} + ${'1 day licence'} | ${{ ...getMockNewPermission(), licenceLength: '1D' }} | ${false} + ${'Junior licence'} | ${getMockJuniorPermission()} | ${true} + ${'Blue badge concession'} | ${getMockBlueBadgePermission()} | ${false} + ${'Continuing permission'} | ${getMockContinuingPermission()} | ${false} + ${'Another date permission'} | ${{ ...getMockPermission(), licenceToStart: 'another-date' }} | ${false} + ${'1 year new three rod licence '} | ${{ ...getMockNewPermission(), numberOfRods: '3' }} | ${false} + `('creates licence summary name rows for $desc', async ({ currentPermission, junior }) => { + hasJunior.mockReturnValueOnce(junior) const mockRequest = getMockRequest({ currentPermission }) const data = await getData(mockRequest) expect(data.licenceSummaryRows).toMatchSnapshot() diff --git a/packages/gafl-webapp-service/src/pages/summary/licence-summary/route.js b/packages/gafl-webapp-service/src/pages/summary/licence-summary/route.js index b91b3b4ec2..643c547941 100644 --- a/packages/gafl-webapp-service/src/pages/summary/licence-summary/route.js +++ b/packages/gafl-webapp-service/src/pages/summary/licence-summary/route.js @@ -22,6 +22,7 @@ import { CONCESSION, CONCESSION_PROOF } from '../../../processors/mapping-consta import { nextPage } from '../../../routes/next-page.js' import { addLanguageCodeToUri } from '../../../processors/uri-helper.js' import { displayPermissionPrice } from '../../../processors/price-display.js' +import { hasJunior } from '../../../processors/concession-helper.js' import db from 'debug' const debug = db('webapp:licence-summary') @@ -113,12 +114,13 @@ class RowGenerator { } generateLicenceLengthRow () { - return this.generateStandardRow( - 'licence_summary_length', - this.labels[`licence_type_${this.permission.licenceLength.toLowerCase()}`], - LICENCE_LENGTH.uri, - 'change-licence-length' - ) + const args = ['licence_summary_length', this.labels[`licence_type_${this.permission.licenceLength.toLowerCase()}`]] + + if (this.permission.numberOfRods !== '3' && !hasJunior(this.permission)) { + args.push(LICENCE_LENGTH.uri, 'change-licence-length') + } + + return this.generateStandardRow(...args) } } diff --git a/packages/gafl-webapp-service/src/processors/__tests__/api-transaction.spec.js b/packages/gafl-webapp-service/src/processors/__tests__/api-transaction.spec.js index 20be1dd5ce..8382922427 100644 --- a/packages/gafl-webapp-service/src/processors/__tests__/api-transaction.spec.js +++ b/packages/gafl-webapp-service/src/processors/__tests__/api-transaction.spec.js @@ -63,6 +63,22 @@ describe('prepareApiTransactionPayload', () => { }) }) + it('adds transactionId to payload', async () => { + const transactionId = Symbol('transactionId') + + const payload = await prepareApiTransactionPayload(getMockRequest(), transactionId) + + expect(payload.transactionId).toBe(transactionId) + }) + + it('adds agreementId to payload', async () => { + const agreementId = Symbol('agreementId') + + const payload = await prepareApiTransactionPayload(getMockRequest(), 'transaction_id', agreementId) + + expect(payload.agreementId).toBe(agreementId) + }) + const getMockRequest = (overrides = {}, state = {}) => ({ cache: () => ({ helpers: { diff --git a/packages/gafl-webapp-service/src/processors/__tests__/licence-type-display.spec.js b/packages/gafl-webapp-service/src/processors/__tests__/licence-type-display.spec.js index d6bec4207f..83b180860d 100644 --- a/packages/gafl-webapp-service/src/processors/__tests__/licence-type-display.spec.js +++ b/packages/gafl-webapp-service/src/processors/__tests__/licence-type-display.spec.js @@ -3,16 +3,16 @@ import { licenceTypeDisplay, licenceTypeAndLengthDisplay, isPhysical, recurringL const getCatalog = () => ({ over_66: ' (over_66)', - age_junior: 'Junior, ', - licence_type_radio_salmon: 'Salmon and sea trout', - licence_type_radio_trout_two_rod: 'Trout and coarse, up to 2 rods', - licence_type_radio_trout_three_rod: 'Trout and coarse, up to 3 rods', + age_junior: 'junior ', + licence_type_radio_salmon_payment_summary: 'salmon and sea trout', + licence_type_radio_trout_two_rod_payment_summary: 'trout and coarse (up to 2 rods)', + licence_type_radio_trout_three_rod_payment_summary: 'trout and coarse (up to 3 rods)', recurring_payment_set_up_bulletpoint_1_trout_2_rod: ' trout and coarse (2 rod)', recurring_payment_set_up_bulletpoint_1_trout_3_rod: ' trout and coarse (3 rod)', recurring_payment_set_up_bulletpoint_1_salmon: ' salmon and sea trout', - licence_type_12m: '12 months', - licence_type_8d: '8 days', - licence_type_1d: '1 day' + licence_1_day: '1-day', + licence_8_day: '8-day', + licence_12_month: '12-month' }) jest.mock('../concession-helper', () => ({ @@ -39,20 +39,20 @@ describe('licenceTypeDisplay', () => { const permission = getPermission() hasJunior.mockImplementationOnce(() => true) const result = licenceTypeDisplay(permission, getCatalog()) - expect(result).toEqual('Junior, Salmon and sea trout') + expect(result).toEqual('junior salmon and sea trout') }) it('returns senior if person is senior', () => { const permission = getPermission() hasSenior.mockImplementationOnce(() => true) const result = licenceTypeDisplay(permission, getCatalog()) - expect(result).toEqual('Salmon and sea trout (over_66)') + expect(result).toEqual('salmon and sea trout (over_66)') }) it.each([ - ['Salmon and sea trout', null, 'Salmon and sea trout'], - ['Trout and coarse', '2', 'Trout and coarse, up to 2 rods'], - ['Trout and coarse', '3', 'Trout and coarse, up to 3 rods'] + ['Salmon and sea trout', null, 'salmon and sea trout'], + ['Trout and coarse', '2', 'trout and coarse (up to 2 rods)'], + ['Trout and coarse', '3', 'trout and coarse (up to 3 rods)'] ])('returns correct licence type', (licenceType, numberOfRods, expected) => { const permission = getPermission({ licenceType, numberOfRods }) const result = licenceTypeDisplay(permission, getCatalog()) @@ -62,15 +62,15 @@ describe('licenceTypeDisplay', () => { describe('licenceTypeAndLengthDisplay', () => { it.each([ - ['12M', 'Salmon and sea trout', null, 'Salmon and sea trout, 12 months'], - ['12M', 'Trout and coarse', '2', 'Trout and coarse, up to 2 rods, 12 months'], - ['12M', 'Trout and coarse', '3', 'Trout and coarse, up to 3 rods, 12 months'], - ['8D', 'Salmon and sea trout', null, 'Salmon and sea trout, 8 days'], - ['8D', 'Trout and coarse', '2', 'Trout and coarse, up to 2 rods, 8 days'], - ['8D', 'Trout and coarse', '3', 'Trout and coarse, up to 3 rods, 8 days'], - ['1D', 'Salmon and sea trout', null, 'Salmon and sea trout, 1 day'], - ['1D', 'Trout and coarse', '2', 'Trout and coarse, up to 2 rods, 1 day'], - ['1D', 'Trout and coarse', '3', 'Trout and coarse, up to 3 rods, 1 day'] + ['12M', 'Salmon and sea trout', null, '12-month salmon and sea trout'], + ['12M', 'Trout and coarse', '2', '12-month trout and coarse (up to 2 rods)'], + ['12M', 'Trout and coarse', '3', '12-month trout and coarse (up to 3 rods)'], + ['8D', 'Salmon and sea trout', null, '8-day salmon and sea trout'], + ['8D', 'Trout and coarse', '2', '8-day trout and coarse (up to 2 rods)'], + ['8D', 'Trout and coarse', '3', '8-day trout and coarse (up to 3 rods)'], + ['1D', 'Salmon and sea trout', null, '1-day salmon and sea trout'], + ['1D', 'Trout and coarse', '2', '1-day trout and coarse (up to 2 rods)'], + ['1D', 'Trout and coarse', '3', '1-day trout and coarse (up to 3 rods)'] ])('returns correct licence length', (licenceLength, licenceType, numberOfRods, expected) => { const permission = getPermission({ licenceLength, licenceType, numberOfRods }) const result = licenceTypeAndLengthDisplay(permission, getCatalog()) @@ -81,32 +81,32 @@ describe('licenceTypeAndLengthDisplay', () => { const permission = getPermission() hasJunior.mockImplementationOnce(() => true) const result = licenceTypeAndLengthDisplay(permission, getCatalog()) - expect(result).toEqual('Junior, Salmon and sea trout, 12 months') + expect(result).toEqual('12-month junior salmon and sea trout') }) it('returns senior if licence length is senior', () => { const permission = getPermission() hasSenior.mockImplementationOnce(() => true) const result = licenceTypeAndLengthDisplay(permission, getCatalog()) - expect(result).toEqual('Salmon and sea trout (over_66), 12 months') + expect(result).toEqual('12-month salmon and sea trout (over_66)') }) it('returns correct licence length, 12 months', () => { const permission = getPermission({ licenceLength: Symbol('12M') }) const result = licenceTypeAndLengthDisplay(permission, getCatalog()) - expect(result).toEqual('Salmon and sea trout, 12 months') + expect(result).toEqual('12-month salmon and sea trout') }) it('returns correct licence length, 8 days', () => { const permission = getPermission({ licenceLength: Symbol('8D') }) const result = licenceTypeAndLengthDisplay(permission, getCatalog()) - expect(result).toEqual('Salmon and sea trout, 8 days') + expect(result).toEqual('8-day salmon and sea trout') }) it('returns correct licence length, 1 day', () => { const permission = getPermission({ licenceLength: Symbol('1D') }) const result = licenceTypeAndLengthDisplay(permission, getCatalog()) - expect(result).toEqual('Salmon and sea trout, 1 day') + expect(result).toEqual('1-day salmon and sea trout') }) }) diff --git a/packages/gafl-webapp-service/src/processors/__tests__/payment.spec.js b/packages/gafl-webapp-service/src/processors/__tests__/payment.spec.js index a3ba138b50..f96d6f4af7 100644 --- a/packages/gafl-webapp-service/src/processors/__tests__/payment.spec.js +++ b/packages/gafl-webapp-service/src/processors/__tests__/payment.spec.js @@ -1,12 +1,15 @@ -import { preparePayment } from '../payment.js' +import { preparePayment, prepareRecurringPaymentAgreement } from '../payment.js' import { licenceTypeAndLengthDisplay } from '../licence-type-display.js' import { addLanguageCodeToUri } from '../uri-helper.js' import { AGREED } from '../../uri.js' +import db from 'debug' +const { value: debug } = db.mock.results[db.mock.calls.findIndex(c => c[0] === 'webapp:payment-processors')] jest.mock('../uri-helper.js') jest.mock('../licence-type-display.js') licenceTypeAndLengthDisplay.mockReturnValue('Trout and coarse, up to 2 rods, 8 day') +jest.mock('debug', () => jest.fn(() => jest.fn())) const createRequest = (opts = {}, catalog = {}) => ({ i18n: { @@ -17,11 +20,19 @@ const createRequest = (opts = {}, catalog = {}) => ({ server: { info: { protocol: opts.protocol || '' } } }) -const createTransaction = ({ isLicenceForYou = true, additionalPermissions = [], cost = 12, licenseeOverrides = {} } = {}) => ({ +const createTransaction = ({ + isLicenceForYou = true, + additionalPermissions = [], + cost = 12, + licenseeOverrides = {}, + agreementId +} = {}) => ({ id: 'transaction-id', cost, + agreementId, permissions: [ { + id: 'permission-id', licensee: { firstName: 'Lando', lastName: 'Norris', @@ -45,7 +56,7 @@ describe('preparePayment', () => { it.each(['http', 'https'])('uses SSL when "x-forwarded-proto" header is present, proto "%s"', proto => { addLanguageCodeToUri.mockReturnValue(proto + '://localhost:1234/buy/agreed') const request = createRequest({ headers: { 'x-forwarded-proto': proto } }) - const result = preparePayment(request, createTransaction()) + const result = preparePayment(request, createTransaction(), false) expect(result.return_url).toBe(`${proto}://localhost:1234/buy/agreed`) }) @@ -207,4 +218,49 @@ describe('preparePayment', () => { expect(result.email).toBe(undefined) }) }) + + describe('if agreementId is not present', () => { + it('does not include set_up_agreement', () => { + const result = preparePayment(createRequest(), createTransaction()) + expect(result.set_up_agreement).toBe(undefined) + }) + }) + + describe('if agreementId is present', () => { + it('set_up_agreement is set to agreementId', () => { + const agreementId = 'foo' + const recurringPaymentTransaction = createTransaction({ agreementId }) + + const result = preparePayment(createRequest(), recurringPaymentTransaction) + + expect(result.set_up_agreement).toBe(agreementId) + }) + }) +}) + +describe('prepareRecurringPaymentAgreement', () => { + it('reference equals transaction.id', async () => { + const transaction = createTransaction() + const result = await prepareRecurringPaymentAgreement(createRequest(), transaction) + expect(result.reference).toBe(transaction.id) + }) + + it('description equals the recurring payment description from catalog', async () => { + const mockCatalog = { + recurring_payment_description: 'The recurring card payment for your rod fishing licence' + } + const request = createRequest({}, mockCatalog) + const transaction = createTransaction() + + const result = await prepareRecurringPaymentAgreement(request, transaction) + expect(result.description).toBe(mockCatalog.recurring_payment_description) + }) + + it('logs to debug for recurring payment', async () => { + const transaction = createTransaction() + const request = createRequest() + + const result = await prepareRecurringPaymentAgreement(request, transaction) + expect(debug).toHaveBeenCalledWith('Creating prepared recurring payment agreement %o', result) + }) }) diff --git a/packages/gafl-webapp-service/src/processors/api-transaction.js b/packages/gafl-webapp-service/src/processors/api-transaction.js index 65d272736a..41d5fb23a9 100644 --- a/packages/gafl-webapp-service/src/processors/api-transaction.js +++ b/packages/gafl-webapp-service/src/processors/api-transaction.js @@ -7,7 +7,7 @@ import { countries } from './refdata-helper.js' import { salesApi } from '@defra-fish/connectors-lib' import { licenceToStart } from '../pages/licence-details/licence-to-start/update-transaction.js' -export const prepareApiTransactionPayload = async request => { +export const prepareApiTransactionPayload = async (request, transactionId, agreementId) => { const transactionCache = await request.cache().helpers.transaction.get() const concessions = await salesApi.concessions.getAll() const countryList = await countries.getAll() @@ -62,7 +62,9 @@ export const prepareApiTransactionPayload = async request => { createdBy: request.state && request.state[process.env.OIDC_SESSION_COOKIE_NAME] ? request.state[process.env.OIDC_SESSION_COOKIE_NAME].oid - : undefined + : undefined, + transactionId, + agreementId } } diff --git a/packages/gafl-webapp-service/src/processors/licence-type-display.js b/packages/gafl-webapp-service/src/processors/licence-type-display.js index a0e03888b0..ae288b96ee 100644 --- a/packages/gafl-webapp-service/src/processors/licence-type-display.js +++ b/packages/gafl-webapp-service/src/processors/licence-type-display.js @@ -9,12 +9,12 @@ export const licenceTypeDisplay = (permission, mssgs) => { typesStrArr.push(mssgs.age_junior) } if (permission.licenceType === mappings.LICENCE_TYPE['salmon-and-sea-trout']) { - typesStrArr.push(mssgs.licence_type_radio_salmon) + typesStrArr.push(mssgs.licence_type_radio_salmon_payment_summary) } else if (permission.licenceType === mappings.LICENCE_TYPE['trout-and-coarse']) { if (permission.numberOfRods === '2') { - typesStrArr.push(mssgs.licence_type_radio_trout_two_rod) + typesStrArr.push(mssgs.licence_type_radio_trout_two_rod_payment_summary) } else { - typesStrArr.push(mssgs.licence_type_radio_trout_three_rod) + typesStrArr.push(mssgs.licence_type_radio_trout_three_rod_payment_summary) } } @@ -27,17 +27,17 @@ export const licenceTypeDisplay = (permission, mssgs) => { export const licenceTypeAndLengthDisplay = (permission, mssgs) => { const licenceTypeMessage = getLicenceTypeMessage(permission.licenceLength, mssgs) - return `${licenceTypeDisplay(permission, mssgs)}, ${licenceTypeMessage}` + return `${licenceTypeMessage} ${licenceTypeDisplay(permission, mssgs)}` } const getLicenceTypeMessage = (licenceLength, mssgs) => { const length = typeof licenceLength === 'symbol' ? licenceLength.description : licenceLength if (length === '12M') { - return mssgs.licence_type_12m + return mssgs.licence_12_month } else if (length === '8D') { - return mssgs.licence_type_8d + return mssgs.licence_8_day } else { - return mssgs.licence_type_1d + return mssgs.licence_1_day } } diff --git a/packages/gafl-webapp-service/src/processors/payment.js b/packages/gafl-webapp-service/src/processors/payment.js index 770af422bd..320152a3a4 100644 --- a/packages/gafl-webapp-service/src/processors/payment.js +++ b/packages/gafl-webapp-service/src/processors/payment.js @@ -50,6 +50,21 @@ export const preparePayment = (request, transaction) => { } } + if (transaction.agreementId) { + result.set_up_agreement = transaction.agreementId + } + debug('Creating prepared payment %o', result) return result } + +export const prepareRecurringPaymentAgreement = async (request, transaction) => { + debug('Preparing recurring payment %s', JSON.stringify(transaction, undefined, '\t')) + // The recurring card payment agreement for your rod fishing licence + const result = { + reference: transaction.id, + description: request.i18n.getCatalog().recurring_payment_description + } + debug('Creating prepared recurring payment agreement %o', result) + return result +} diff --git a/packages/gafl-webapp-service/src/routes/__tests__/misc-routes-handlers.spec.js b/packages/gafl-webapp-service/src/routes/__tests__/misc-routes-handlers.spec.js index f115e9d271..4d8714454d 100644 --- a/packages/gafl-webapp-service/src/routes/__tests__/misc-routes-handlers.spec.js +++ b/packages/gafl-webapp-service/src/routes/__tests__/misc-routes-handlers.spec.js @@ -337,6 +337,60 @@ describe('guidance page handlers', () => { }) ) }) + + it.each([['csrf_token_cookie_name'], ['any_name'], ['token_cookie']])( + 'sets CSRF_TOKEN_NAME to match process.env.CSRF_TOKEN_COOKIE_NAME', + async csrf => { + process.env.CSRF_TOKEN_COOKIE_NAME = csrf + const toolkit = getMockToolkit() + await cookiesPagePostHandler(getMockRequest(), toolkit) + expect(toolkit.view).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + CSRF_TOKEN_NAME: csrf + }) + ) + } + ) + + it.each([['default cookie name'], ['csrf-token-cookie-name-default']])( + "sets CSRF_TOKEN_NAME to default if process.env.CSRF_TOKEN_COOKIE_NAME isn't provided", + async defaultName => { + delete process.env.CSRF_TOKEN_COOKIE_NAME + constants.CSRF_TOKEN_COOKIE_NAME_DEFAULT = defaultName + const toolkit = getMockToolkit() + await cookiesPagePostHandler(getMockRequest(), toolkit) + expect(toolkit.view).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + CSRF_TOKEN_NAME: defaultName + }) + ) + } + ) + + it.each([['csrf_token_cookie_value'], ['any_value'], ['token_value']])( + 'grabs CSRF_TOKEN_VALUE correctly based on generate from crumb', + async csrf => { + const request = getMockRequest( + { locale: 'this-locale', locales: ['this-locale', 'that-locale'], catalog: 'catalog' }, + CONTROLLER.uri, + '', + {}, + csrf + ) + const toolkit = getMockToolkit() + + await cookiesPagePostHandler(request, toolkit) + + expect(toolkit.view).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + CSRF_TOKEN_VALUE: csrf + }) + ) + } + ) }) }) @@ -497,7 +551,7 @@ describe('guidance page handlers', () => { }) }) - const getMockRequest = (i18nValues, referer, analytics, payload) => { + const getMockRequest = (i18nValues, referer, analytics, payload, csrfToken = 'token') => { const { catalog, locales, locale } = { catalog: {}, locales: [], @@ -523,7 +577,14 @@ describe('guidance page handlers', () => { get: jest.fn().mockResolvedValue(analytics) } } - })) + })), + server: { + plugins: { + crumb: { + generate: jest.fn().mockResolvedValue(csrfToken) + } + } + } } } diff --git a/packages/gafl-webapp-service/src/routes/misc-routes.js b/packages/gafl-webapp-service/src/routes/misc-routes.js index 77f38b0924..21e9fdb6e4 100644 --- a/packages/gafl-webapp-service/src/routes/misc-routes.js +++ b/packages/gafl-webapp-service/src/routes/misc-routes.js @@ -145,13 +145,15 @@ export default [ handler: async (request, h) => { await checkAnalyticsCookiesPage(request) const analyticsCache = await request.cache().helpers.analytics.get() - const showNotification = request.payload?.analyticsResponse !== undefined ? true : undefined + const csrfToken = await request.server.plugins.crumb.generate(request) return h.view(COOKIES.page, { ...cookiesView(request, analyticsCache), showNotification, - SHOW_WELSH_CONTENT: process.env.SHOW_WELSH_CONTENT?.toLowerCase() === 'true' + SHOW_WELSH_CONTENT: process.env.SHOW_WELSH_CONTENT?.toLowerCase() === 'true', + CSRF_TOKEN_NAME: process.env.CSRF_TOKEN_COOKIE_NAME || CSRF_TOKEN_COOKIE_NAME_DEFAULT, + CSRF_TOKEN_VALUE: csrfToken }) } }, diff --git a/packages/gafl-webapp-service/src/schema/__tests__/__snapshots__/date.schema.test.js.snap b/packages/gafl-webapp-service/src/schema/__tests__/__snapshots__/date.schema.test.js.snap new file mode 100644 index 0000000000..3619fadce7 --- /dev/null +++ b/packages/gafl-webapp-service/src/schema/__tests__/__snapshots__/date.schema.test.js.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dateSchemaInput matches expected format 1`] = ` +Object { + "day": "1", + "day-and-month": Object { + "day": "1", + "month": "2", + }, + "day-and-year": Object { + "day": "1", + "year": "2023", + }, + "full-date": Object { + "day": "1", + "month": "2", + "year": "2023", + }, + "invalid-date": "2023-02-01", + "month": "2", + "month-and-year": Object { + "month": "2", + "year": "2023", + }, + "non-numeric": Object { + "day": "1", + "month": "2", + "year": "2023", + }, + "year": "2023", +} +`; diff --git a/packages/gafl-webapp-service/src/schema/__tests__/date.schema.test.js b/packages/gafl-webapp-service/src/schema/__tests__/date.schema.test.js new file mode 100644 index 0000000000..6312a65f0a --- /dev/null +++ b/packages/gafl-webapp-service/src/schema/__tests__/date.schema.test.js @@ -0,0 +1,64 @@ +import Joi from 'joi' +import { dateSchemaInput, dateSchema } from '../date.schema.js' + +describe('dateSchemaInput', () => { + it('matches expected format', () => { + expect(dateSchemaInput('1', '2', '2023')).toMatchSnapshot() + }) + + it.each` + desc | day | month | year | result + ${'all empty'} | ${''} | ${''} | ${''} | ${{ 'full-date': { day: undefined, month: undefined, year: undefined } }} + ${'day and month empty'} | ${''} | ${''} | ${'2020'} | ${{ 'day-and-month': { day: undefined, month: undefined } }} + ${'day and year empty'} | ${''} | ${'11'} | ${''} | ${{ 'day-and-year': { day: undefined, year: undefined } }} + ${'month and year empty'} | ${'12'} | ${''} | ${''} | ${{ 'month-and-year': { month: undefined, year: undefined } }} + ${'day empty'} | ${''} | ${'3'} | ${'2021'} | ${{ day: undefined }} + ${'month empty'} | ${'4'} | ${''} | ${'2003'} | ${{ month: undefined }} + ${'year empty'} | ${'15'} | ${'11'} | ${''} | ${{ year: undefined }} + `('maps empty strings to undefined values when $desc', ({ day, month, year, result }) => { + expect(dateSchemaInput(day, month, year)).toEqual(expect.objectContaining(result)) + }) +}) + +describe('dateSchema', () => { + it.each` + payload | expectedError | payloadDesc + ${{}} | ${'full-date'} | ${'empty day, month and year'} + ${{ year: '1' }} | ${'day-and-month'} | ${'empty day and month'} + ${{ month: '2' }} | ${'day-and-year'} | ${'empty day and year'} + ${{ day: '3' }} | ${'month-and-year'} | ${'empty month and year'} + ${{ month: '5', year: '2023' }} | ${'day'} | ${'empty day'} + ${{ day: '12', year: '2024' }} | ${'month'} | ${'empty month'} + ${{ day: '15', month: '3' }} | ${'year'} | ${'empty year'} + ${{ day: 'Ides', month: 'March', year: '44 B.C.' }} | ${'non-numeric.day'} | ${'non-numerics entered'} + ${{ day: 'Thirteenth', month: '11', year: '1978' }} | ${'non-numeric.day'} | ${'non-numeric day'} + ${{ day: '29', month: 'MAR', year: '2002' }} | ${'non-numeric.month'} | ${'non-numeric month '} + ${{ day: '13', month: '1', year: 'Two thousand and five' }} | ${'non-numeric.year'} | ${'non-numeric year'} + ${{ day: '30', month: '2', year: '1994' }} | ${'invalid-date'} | ${'an invalid date - 1994-02-40'} + ${{ day: '1', month: '13', year: '2022' }} | ${'invalid-date'} | ${'an invalid date - 2022-13-01'} + ${{ day: '29', month: '2', year: '2023' }} | ${'invalid-date'} | ${'an invalid date - 1994-02-40'} + ${{ day: '-1.15', month: '18', year: '22.2222' }} | ${'invalid-date'} | ${'an invalid date - 22.2222-18-1.15'} + `('Error has $expectedError in details when payload has $payloadDesc', ({ payload: { day, month, year }, expectedError }) => { + expect(() => { + Joi.assert(dateSchemaInput(day, month, year), dateSchema) + }).toThrow( + expect.objectContaining({ + details: expect.arrayContaining([ + expect.objectContaining({ + path: expectedError.split('.'), + context: expect.objectContaining({ + label: expectedError, + key: expectedError.split('.').pop() + }) + }) + ]) + }) + ) + }) + + it('valid date passes validation', () => { + expect(() => { + Joi.assert(dateSchemaInput('12', '10', '1987'), dateSchema) + }).not.toThrow() + }) +}) diff --git a/packages/gafl-webapp-service/src/schema/date.schema.js b/packages/gafl-webapp-service/src/schema/date.schema.js new file mode 100644 index 0000000000..fa0cade8e5 --- /dev/null +++ b/packages/gafl-webapp-service/src/schema/date.schema.js @@ -0,0 +1,63 @@ +'use strict' +import Joi from 'joi' + +export const dateSchemaInput = (unparsedDay, unparsedMonth, unparsedYear) => { + const day = unparsedDay === '' ? undefined : unparsedDay + const month = unparsedMonth === '' ? undefined : unparsedMonth + const year = unparsedYear === '' ? undefined : unparsedYear + + return { + 'full-date': { day, month, year }, + 'day-and-month': { day, month }, + 'day-and-year': { day, year }, + 'month-and-year': { month, year }, + day, + month, + year, + 'non-numeric': { day, month, year }, + 'invalid-date': `${year}-${(month || '').padStart(2, '0')}-${(day || '').padStart(2, '0')}` + } +} + +export const dateSchema = Joi.object({ + 'full-date': Joi.object() + .keys({ + day: Joi.any(), + month: Joi.any(), + year: Joi.any() + }) + .or('day', 'month', 'year'), + 'day-and-month': Joi.object() + .keys({ + day: Joi.any(), + month: Joi.any() + }) + .or('day', 'month'), + 'day-and-year': Joi.object() + .keys({ + day: Joi.any(), + year: Joi.any() + }) + .or('day', 'year'), + 'month-and-year': Joi.object() + .keys({ + month: Joi.any(), + year: Joi.any() + }) + .or('month', 'year'), + day: Joi.any().required(), + month: Joi.any().required(), + year: Joi.any().required(), + 'non-numeric': Joi.object().keys({ + day: Joi.number(), + month: Joi.number(), + year: Joi.number() + }), + 'invalid-date': Joi.custom((dateToValidate, helpers) => { + if (new Date(dateToValidate).toISOString() !== `${dateToValidate}T00:00:00.000Z`) { + throw helpers.error('invalid-date') + } + + return dateToValidate + }) +}).options({ abortEarly: true }) diff --git a/packages/gafl-webapp-service/src/schema/validators/__tests__/validators.spec.js b/packages/gafl-webapp-service/src/schema/validators/__tests__/validators.spec.js new file mode 100644 index 0000000000..95ef43e7c1 --- /dev/null +++ b/packages/gafl-webapp-service/src/schema/validators/__tests__/validators.spec.js @@ -0,0 +1,208 @@ +import Joi from 'joi' +import { dateOfBirthValidator, startDateValidator, getDateErrorFlags } from '../validators.js' +import moment from 'moment-timezone' +const dateSchema = require('../../date.schema.js') + +const setupMocks = () => { + Joi.originalAssert = Joi.assert + dateSchema.originalDateSchema = dateSchema.dateSchema + dateSchema.originalDateSchemaInput = dateSchema.dateSchemaInput + + Joi.assert = jest.fn() + dateSchema.dateSchema = Symbol('dateSchema') + dateSchema.dateSchemaInput = jest.fn() +} + +const tearDownMocks = () => { + Joi.assert = Joi.originalAssert + dateSchema.dateSchema = dateSchema.originalDateSchema + dateSchema.dateSchemaInput = dateSchema.originalDateSchemaInput +} + +describe('dateOfBirth validator', () => { + beforeEach(jest.clearAllMocks) + + const getSamplePayload = ({ day = '', month = '', year = '' } = {}) => ({ + 'date-of-birth-day': day, + 'date-of-birth-month': month, + 'date-of-birth-year': year + }) + + it('throws an error for anyone over 120 years old', () => { + const invalidDoB = moment().subtract(120, 'years').subtract(1, 'day') + const samplePayload = getSamplePayload({ + day: invalidDoB.format('DD'), + month: invalidDoB.format('MM'), + year: invalidDoB.format('YYYY') + }) + expect(() => dateOfBirthValidator(samplePayload)).toThrow() + }) + + it('validates for anyone 120 years old', () => { + const validDoB = moment().subtract(120, 'years') + const samplePayload = getSamplePayload({ + day: validDoB.format('DD'), + month: validDoB.format('MM'), + year: validDoB.format('YYYY') + }) + expect(() => dateOfBirthValidator(samplePayload)).not.toThrow() + }) + + it.each([ + ['today', moment()], + ['tomorrow', moment().add(1, 'day')], + ['in the future', moment().add(1, 'month')] + ])('throws an error for a date of birth %s', (_desc, invalidDoB) => { + const samplePayload = getSamplePayload({ + day: invalidDoB.format('DD'), + month: invalidDoB.format('MM'), + year: invalidDoB.format('YYYY') + }) + expect(() => dateOfBirthValidator(samplePayload)).toThrow() + }) + + it.each([ + ['1-3-2004', '1', '3', '2004'], + ['12-1-1999', '12', '1', '1999'], + ['1-12-2006', '1', '12', '2006'] + ])('handles single digit date %s', (_desc, day, month, year) => { + const samplePayload = getSamplePayload({ + day, + month, + year + }) + expect(() => dateOfBirthValidator(samplePayload)).not.toThrow() + }) + + it.each([ + ['01', '03', '1994'], + ['10', '12', '2004'] + ])('passes date of birth day (%s), month (%s) and year (%s) to dateSchemaInput', (day, month, year) => { + setupMocks() + dateOfBirthValidator(getSamplePayload({ day, month, year })) + expect(dateSchema.dateSchemaInput).toHaveBeenCalledWith(day, month, year) + tearDownMocks() + }) + + it('passes dateSchemaInput output and dateSchema to Joi.assert', () => { + setupMocks() + const dsi = Symbol('dsi') + dateSchema.dateSchemaInput.mockReturnValueOnce(dsi) + dateOfBirthValidator(getSamplePayload()) + expect(Joi.assert).toHaveBeenCalledWith(dsi, dateSchema.dateSchema) + tearDownMocks() + }) +}) + +describe('startDate validator', () => { + beforeEach(jest.clearAllMocks) + + const getSamplePayload = ({ day = '', month = '', year = '' } = {}) => ({ + 'licence-start-date-day': day, + 'licence-start-date-month': month, + 'licence-start-date-year': year, + 'licence-to-start': 'another-date' + }) + + it('throws an error for a licence starting before today', () => { + const invalidStartDate = moment().subtract(1, 'day') + const samplePayload = getSamplePayload({ + day: invalidStartDate.format('DD'), + month: invalidStartDate.format('MM'), + year: invalidStartDate.format('YYYY') + }) + expect(() => startDateValidator(samplePayload)).toThrow() + }) + + it('throws an error for a licence starting more than 30 days hence', () => { + const invalidStartDate = moment().add(31, 'days') + const samplePayload = getSamplePayload({ + day: invalidStartDate.format('DD'), + month: invalidStartDate.format('MM'), + year: invalidStartDate.format('YYYY') + }) + expect(() => startDateValidator(samplePayload)).toThrow() + }) + + it('validates for a date within the next 30 days', () => { + const validStartDate = moment().add(4, 'days') + const samplePayload = getSamplePayload({ + day: validStartDate.format('DD'), + month: validStartDate.format('MM'), + year: validStartDate.format('YYYY') + }) + expect(() => startDateValidator(samplePayload)).not.toThrow() + }) + + it.each([ + ['1-3-2024', moment('2024-02-28')], + ['9-7-2024', moment('2024-07-08')] + ])('handles single digit date %s', (date, now) => { + jest.useFakeTimers() + jest.setSystemTime(now.toDate()) + + const [day, month, year] = date.split('-') + const samplePayload = getSamplePayload({ + day, + month, + year + }) + expect(() => startDateValidator(samplePayload)).not.toThrow() + jest.useRealTimers() + }) + + it.each([ + ['01', '03', '1994'], + ['10', '12', '2004'] + ])('passes start date day (%s), month (%s) and year (%s) to dateSchemaInput', (day, month, year) => { + setupMocks() + startDateValidator(getSamplePayload({ day, month, year })) + expect(dateSchema.dateSchemaInput).toHaveBeenCalledWith(day, month, year) + tearDownMocks() + }) + + it('passes dateSchemaInput output and dateSchema to Joi.assert', () => { + setupMocks() + const dsi = Symbol('dsi') + dateSchema.dateSchemaInput.mockReturnValueOnce(dsi) + startDateValidator(getSamplePayload()) + expect(Joi.assert).toHaveBeenCalledWith(dsi, dateSchema.dateSchema) + tearDownMocks() + }) + + it('passes validation if licence is set to start after payment', () => { + const samplePayload = { 'licence-to-start': 'after-payment' } + expect(() => startDateValidator(samplePayload)).not.toThrow() + }) + + it('throws an error if licence-to-start is set to an invalid value', () => { + const samplePayload = { 'licence-to-start': '12th-of-never' } + expect(() => startDateValidator(samplePayload)).toThrow() + }) +}) + +describe('getErrorFlags', () => { + it('sets all error flags to be false when there are no errors', () => { + const result = getDateErrorFlags() + expect(result).toEqual({ isDayError: false, isMonthError: false, isYearError: false }) + }) + + it.each([ + ['full-date', { isDayError: true, isMonthError: true, isYearError: true }], + ['day-and-month', { isDayError: true, isMonthError: true, isYearError: false }], + ['month-and-year', { isDayError: false, isMonthError: true, isYearError: true }], + ['day-and-year', { isDayError: true, isMonthError: false, isYearError: true }], + ['day', { isDayError: true, isMonthError: false, isYearError: false }], + ['month', { isDayError: false, isMonthError: true, isYearError: false }], + ['year', { isDayError: false, isMonthError: false, isYearError: true }], + ['invalid-date', { isDayError: true, isMonthError: true, isYearError: true }], + ['date-range', { isDayError: true, isMonthError: true, isYearError: true }], + ['non-numeric', { isDayError: true, isMonthError: true, isYearError: true }] + ])('when error is %s, should set %o in flags', (errorKey, expected) => { + const error = { [errorKey]: 'anything.at.all' } + + const result = getDateErrorFlags(error) + + expect(result).toEqual(expect.objectContaining(expected)) + }) +}) diff --git a/packages/gafl-webapp-service/src/schema/validators/validators.js b/packages/gafl-webapp-service/src/schema/validators/validators.js new file mode 100644 index 0000000000..70fefe6d25 --- /dev/null +++ b/packages/gafl-webapp-service/src/schema/validators/validators.js @@ -0,0 +1,65 @@ +import Joi from 'joi' +import moment from 'moment' +import { ADVANCED_PURCHASE_MAX_DAYS, SERVICE_LOCAL_TIME } from '@defra-fish/business-rules-lib' +import { dateSchema, dateSchemaInput } from '../date.schema.js' + +const MAX_AGE = 120 +const LICENCE_TO_START_FIELD = 'licence-to-start' +const AFTER_PAYMENT = 'after-payment' +const ANOTHER_DATE = 'another-date' + +const validateDate = (day, month, year, minDate, maxDate) => { + Joi.assert(dateSchemaInput(day, month, year), dateSchema) + const dateRange = moment(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`, 'YYYY-MM-DD') + .tz(SERVICE_LOCAL_TIME) + .startOf('day') + .toDate() + Joi.assert({ 'date-range': dateRange }, Joi.object({ 'date-range': Joi.date().min(minDate).max(maxDate) })) +} + +export const dateOfBirthValidator = payload => { + const day = payload['date-of-birth-day'] + const month = payload['date-of-birth-month'] + const year = payload['date-of-birth-year'] + + const minDate = moment().tz(SERVICE_LOCAL_TIME).subtract(MAX_AGE, 'years').startOf('day').toDate() + const maxDate = moment().tz(SERVICE_LOCAL_TIME).subtract(1, 'day').startOf('day').toDate() + validateDate(day, month, year, minDate, maxDate) +} + +export const startDateValidator = payload => { + Joi.assert( + { 'licence-to-start': payload[LICENCE_TO_START_FIELD] }, + Joi.object({ 'licence-to-start': Joi.string().valid(AFTER_PAYMENT, ANOTHER_DATE).required() }) + ) + if (payload[LICENCE_TO_START_FIELD] === ANOTHER_DATE) { + const day = payload['licence-start-date-day'] + const month = payload['licence-start-date-month'] + const year = payload['licence-start-date-year'] + + const minDate = moment().tz(SERVICE_LOCAL_TIME).startOf('day').toDate() + const maxDate = moment().tz(SERVICE_LOCAL_TIME).add(ADVANCED_PURCHASE_MAX_DAYS, 'days').toDate() + validateDate(day, month, year, minDate, maxDate) + } +} + +export const getDateErrorFlags = error => { + const errorFlags = { isDayError: false, isMonthError: false, isYearError: false } + const commonErrors = ['full-date', 'invalid-date', 'date-range', 'non-numeric'] + + if (error) { + const [errorKey] = Object.keys(error) + + if (['day-and-month', 'day-and-year', 'day', ...commonErrors].includes(errorKey)) { + errorFlags.isDayError = true + } + if (['day-and-month', 'month-and-year', 'month', ...commonErrors].includes(errorKey)) { + errorFlags.isMonthError = true + } + if (['day-and-year', 'month-and-year', 'year', ...commonErrors].includes(errorKey)) { + errorFlags.isYearError = true + } + } + + return errorFlags +} diff --git a/packages/gafl-webapp-service/src/server.js b/packages/gafl-webapp-service/src/server.js index 1b026d823b..57c7cae061 100644 --- a/packages/gafl-webapp-service/src/server.js +++ b/packages/gafl-webapp-service/src/server.js @@ -17,7 +17,15 @@ import { SESSION_COOKIE_NAME_DEFAULT, SESSION_TTL_MS_DEFAULT } from './constants.js' -import { ACCESSIBILITY_STATEMENT, COOKIES, PRIVACY_POLICY, REFUND_POLICY, NEW_TRANSACTION, NEW_PRICES } from './uri.js' +import { + ACCESSIBILITY_STATEMENT, + COOKIES, + PRIVACY_POLICY, + REFUND_POLICY, + NEW_TRANSACTION, + NEW_PRICES, + RECURRING_TERMS_CONDITIONS +} from './uri.js' import sessionManager, { isStaticResource } from './session-cache/session-manager.js' import { cacheDecorator } from './session-cache/cache-decorator.js' @@ -190,10 +198,15 @@ const init = async () => { */ server.decorate('request', 'cache', cacheDecorator(sessionCookieName)) + const redirectExceptionUris = [NEW_PRICES.uri, RECURRING_TERMS_CONDITIONS.uri] + server.decorate('toolkit', 'redirectWithLanguageCode', function (redirect) { const pathname = this.request.url.pathname - const uriWithLanguage = - pathname === NEW_PRICES.uri ? addLanguageCodeToUri(this.request, pathname) : addLanguageCodeToUri(this.request, redirect) + + const uriWithLanguage = redirectExceptionUris.includes(pathname) + ? addLanguageCodeToUri(this.request, pathname) + : addLanguageCodeToUri(this.request, redirect) + const uriWithLanguageAndEmptyFragment = addEmptyFragmentToUri(uriWithLanguage) return this.redirect(uriWithLanguageAndEmptyFragment) diff --git a/packages/gafl-webapp-service/src/services/payment/__test__/govuk-pay-service.spec.js b/packages/gafl-webapp-service/src/services/payment/__test__/govuk-pay-service.spec.js index e061689bc1..c027790bcf 100644 --- a/packages/gafl-webapp-service/src/services/payment/__test__/govuk-pay-service.spec.js +++ b/packages/gafl-webapp-service/src/services/payment/__test__/govuk-pay-service.spec.js @@ -2,7 +2,13 @@ import mockTransaction from './data/mock-transaction.js' import { preparePayment } from '../../../processors/payment.js' import { AGREED } from '../../../uri.js' import { addLanguageCodeToUri } from '../../../processors/uri-helper.js' +import { sendPayment, sendRecurringPayment, getPaymentStatus } from '../govuk-pay-service.js' +import { govUkPayApi } from '@defra-fish/connectors-lib' +import db from 'debug' +const { value: debug } = db.mock.results[db.mock.calls.findIndex(c => c[0] === 'webapp:govuk-pay-service')] +jest.mock('debug', () => jest.fn(() => jest.fn())) +jest.mock('@defra-fish/connectors-lib') jest.mock('../../../processors/uri-helper.js') describe('The govuk-pay-service', () => { @@ -15,7 +21,7 @@ describe('The govuk-pay-service', () => { i18n: { getCatalog: () => ({ over_66: 'Over 66', - licence_type_radio_trout_three_rod: 'Trout and coarse, up to 3 rods' + licence_type_radio_trout_three_rod_payment_summary: 'trout and coarse (up to 3 rods)' }) }, info: { host: '0.0.0.0:3000' }, @@ -35,7 +41,7 @@ describe('The govuk-pay-service', () => { i18n: { getCatalog: () => ({ over_66: 'Over 66', - licence_type_radio_trout_three_rod: 'Trout and coarse, up to 3 rods' + licence_type_radio_trout_three_rod_payment_summary: 'trout and coarse (up to 3 rods)' }) }, info: { host: '0.0.0.0:3000' }, @@ -55,8 +61,8 @@ describe('The govuk-pay-service', () => { i18n: { getCatalog: () => ({ over_66: ' (Over 66)', - licence_type_radio_salmon: 'Salmon and sea trout', - licence_type_12m: '12 months' + licence_type_radio_salmon_payment_summary: 'salmon and sea trout', + licence_12_month: '12-month' }) }, info: { host: '0.0.0.0:3000' }, @@ -68,7 +74,7 @@ describe('The govuk-pay-service', () => { ).toEqual({ amount: 5400, delayed_capture: false, - description: 'Salmon and sea trout (Over 66), 12 months', + description: '12-month salmon and sea trout (Over 66)', email: 'angling@email.com', reference: '44728b47-c809-4c31-8c92-bdf961be0c80', return_url: 'https://0.0.0.0:3000' + AGREED.uri, @@ -98,8 +104,8 @@ describe('The govuk-pay-service', () => { i18n: { getCatalog: () => ({ over_66: ' (Over 66)', - licence_type_radio_salmon: 'Salmon and sea trout', - licence_type_8d: '8 days' + licence_type_radio_salmon_payment_summary: 'salmon and sea trout', + licence_8_day: '8-day' }) }, info: { host: '0.0.0.0:3000' }, @@ -110,7 +116,7 @@ describe('The govuk-pay-service', () => { ).toEqual({ amount: 5400, delayed_capture: false, - description: 'Salmon and sea trout (Over 66), 8 days', + description: '8-day salmon and sea trout (Over 66)', email: 'angling@email.com', reference: '44728b47-c809-4c31-8c92-bdf961be0c80', return_url: 'https://0.0.0.0:3000' + AGREED.uri, @@ -151,4 +157,376 @@ describe('The govuk-pay-service', () => { const preparedPayment = preparePayment({ info: { host: '0.0.0.0:3000' }, headers: { 'x-forwarded-proto': 'https' } }, mockTransaction) console.log(preparedPayment) }) + + describe('sendPayment', () => { + const preparedPayment = { + id: '1234', + user_identifier: 'test-user' + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + it.each([ + [true, true], + [false, false], + [false, undefined] + ])('should call the govUkPayApi with recurring as %s if the argument is %s', async (expected, value) => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue({ success: true, paymentId: 'abc123' }) + } + govUkPayApi.createPayment.mockResolvedValue(mockResponse) + const unique = Symbol('payload') + const payload = { unique } + await sendPayment(payload, value) + expect(govUkPayApi.createPayment).toHaveBeenCalledWith(payload, expected) + }) + + it('should send provided payload data to Gov.UK Pay', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue({ success: true, paymentId: 'abc123' }) + } + govUkPayApi.createPayment.mockResolvedValue(mockResponse) + const unique = Symbol('payload') + const payload = { + reference: 'd81f1a2b-6508-468f-8342-b6770f60f7cd', + description: 'Fishing permission', + user_identifier: '1218c1c5-38e4-4bf3-81ea-9cbce3994d30', + unique + } + await sendPayment(payload) + expect(govUkPayApi.createPayment).toHaveBeenCalledWith(payload, false) + }) + + it('should return response body when payment creation is successful', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue({ success: true, paymentId: 'abc123' }) + } + govUkPayApi.createPayment.mockResolvedValue(mockResponse) + + const result = await sendPayment(preparedPayment) + + expect(result).toEqual({ success: true, paymentId: 'abc123' }) + }) + + it('should log debug message when response.ok is true', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue({ success: true, paymentId: 'abc123' }) + } + govUkPayApi.createPayment.mockResolvedValue(mockResponse) + + await sendPayment(preparedPayment) + + expect(debug).toHaveBeenCalledWith('Successful payment creation response: %o', { success: true, paymentId: 'abc123' }) + }) + + it('should log error message when response.ok is false', async () => { + const mockResponse = { + ok: false, + status: 500, + json: jest.fn().mockResolvedValue({ message: 'Server error' }) + } + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) + govUkPayApi.createPayment.mockResolvedValue(mockResponse) + + try { + await sendPayment(preparedPayment) + } catch (error) { + expect(consoleErrorSpy).toHaveBeenCalledWith('Failure creating payment in the GOV.UK API service', { + transactionId: preparedPayment.id, + method: 'POST', + payload: preparedPayment, + status: mockResponse.status, + response: { message: 'Server error' } + }) + } + }) + + it('should throw error when API call fails with network issue', async () => { + const mockError = new Error('Network error') + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn()) + govUkPayApi.createPayment.mockRejectedValue(mockError) + + try { + await sendPayment(preparedPayment) + } catch (error) { + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error creating payment in the GOV.UK API service - tid: ${preparedPayment.id}`, + mockError + ) + } + }) + + it('should throw error for when rate limit is breached', async () => { + const mockResponse = { + ok: false, + status: 429, + json: jest.fn().mockResolvedValue({ message: 'Rate limit exceeded' }) + } + const consoleErrorSpy = jest.spyOn(console, 'info').mockImplementation(jest.fn()) + govUkPayApi.createPayment.mockResolvedValue(mockResponse) + + try { + await sendPayment(preparedPayment) + } catch (error) { + expect(consoleErrorSpy).toHaveBeenCalledWith(`GOV.UK Pay API rate limit breach - tid: ${preparedPayment.id}`) + } + }) + + it('should throw error for unexpected response status', async () => { + const mockResponse = { + ok: false, + status: 500, + json: jest.fn().mockResolvedValue({ message: 'Server error' }) + } + govUkPayApi.createPayment.mockResolvedValue(mockResponse) + + try { + await sendPayment(preparedPayment) + } catch (error) { + expect(error.message).toBe('Unexpected response from GOV.UK pay API') + } + }) + }) + + describe('sendRecurringPayment', () => { + const preparedPayment = { + id: '1234', + user_identifier: 'test-user' + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should send provided payload data to Gov.UK Pay', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue({ success: true, paymentId: 'abc123' }) + } + govUkPayApi.createRecurringPaymentAgreement.mockResolvedValue(mockResponse) + const unique = Symbol('payload') + const payload = { + reference: 'd81f1a2b-6508-468f-8342-b6770f60f7cd', + description: 'Fishing permission', + user_identifier: '1218c1c5-38e4-4bf3-81ea-9cbce3994d30', + unique + } + await sendRecurringPayment(payload) + expect(govUkPayApi.createRecurringPaymentAgreement).toHaveBeenCalledWith(payload) + }) + + it('should return response body when payment creation is successful', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue({ success: true, paymentId: 'abc123' }) + } + govUkPayApi.createRecurringPaymentAgreement.mockResolvedValue(mockResponse) + + const result = await sendRecurringPayment(preparedPayment) + + expect(result).toEqual({ success: true, paymentId: 'abc123' }) + }) + + it('should log debug message when response.ok is true', async () => { + const mockResponse = { + ok: true, + json: jest.fn().mockResolvedValue({ success: true, paymentId: 'abc123' }) + } + govUkPayApi.createRecurringPaymentAgreement.mockResolvedValue(mockResponse) + + await sendRecurringPayment(preparedPayment) + + expect(debug).toHaveBeenCalledWith('Successful agreement creation response: %o', { success: true, paymentId: 'abc123' }) + }) + + it('should log error message when response.ok is false', async () => { + const mockResponse = { + ok: false, + status: 500, + json: jest.fn().mockResolvedValue({ message: 'Server error' }) + } + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) + govUkPayApi.createRecurringPaymentAgreement.mockResolvedValue(mockResponse) + + try { + await sendRecurringPayment(preparedPayment) + } catch (error) { + expect(consoleErrorSpy).toHaveBeenCalledWith('Failure creating agreement in the GOV.UK API service', { + transactionId: preparedPayment.reference, + method: 'POST', + payload: preparedPayment, + status: mockResponse.status, + response: { message: 'Server error' } + }) + } + }) + + it('should throw error when API call fails with network issue', async () => { + const mockError = new Error('Network error') + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn()) + govUkPayApi.createRecurringPaymentAgreement.mockRejectedValue(mockError) + + try { + await sendRecurringPayment(preparedPayment) + } catch (error) { + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error creating agreement in the GOV.UK API service - tid: ${preparedPayment.user_identifier}`, + mockError + ) + } + }) + + it('should throw error for when rate limit is breached', async () => { + const mockResponse = { + ok: false, + status: 429, + json: jest.fn().mockResolvedValue({ message: 'Rate limit exceeded' }) + } + const consoleErrorSpy = jest.spyOn(console, 'info').mockImplementation(jest.fn()) + govUkPayApi.createRecurringPaymentAgreement.mockResolvedValue(mockResponse) + + try { + await sendRecurringPayment(preparedPayment) + } catch (error) { + expect(consoleErrorSpy).toHaveBeenCalledWith(`GOV.UK Pay API rate limit breach - tid: ${preparedPayment.id}`) + } + }) + + it('should throw error for unexpected response status', async () => { + const mockResponse = { + ok: false, + status: 500, + json: jest.fn().mockResolvedValue({ message: 'Server error' }) + } + govUkPayApi.createRecurringPaymentAgreement.mockResolvedValue(mockResponse) + + try { + await sendRecurringPayment(preparedPayment) + } catch (error) { + expect(error.message).toBe('Unexpected response from GOV.UK pay API') + } + }) + }) + + describe('getPaymentStatus', () => { + const paymentId = '1234' + + beforeEach(() => { + jest.clearAllMocks() + }) + + it.each([ + [true, true], + [false, false], + [false, undefined] + ])('should call the govUkPayApi with recurring as %s if the argument is %s', async (expected, value) => { + const mockResponse = { ok: true, status: 200, json: () => {} } + govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockResponse) + await getPaymentStatus(paymentId, value) + expect(govUkPayApi.fetchPaymentStatus).toHaveBeenCalledWith(paymentId, expected) + }) + + it('should send provided paymentId to Gov.UK Pay', async () => { + const mockResponse = { ok: true, status: 200, json: () => {} } + govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockResponse) + await getPaymentStatus(paymentId) + expect(govUkPayApi.fetchPaymentStatus).toHaveBeenCalledWith(paymentId, false) + }) + + it('should return response body when payment status check is successful', async () => { + const resBody = Symbol('body') + const mockResponse = { ok: true, status: 200, json: jest.fn().mockResolvedValue(resBody) } + govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockResponse) + + const result = await getPaymentStatus(paymentId) + + expect(result).toEqual(resBody) + }) + + it('should log debug message when response.ok is true', async () => { + const resBody = Symbol('body') + const mockResponse = { ok: true, status: 200, json: jest.fn().mockResolvedValue(resBody) } + govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockResponse) + + await getPaymentStatus(paymentId) + + expect(debug).toHaveBeenCalledWith('Payment status response: %o', resBody) + }) + + it('should log error message when response.ok is false', async () => { + const mockResponse = { + ok: false, + status: 500, + json: jest.fn().mockResolvedValue({ message: 'Server error' }) + } + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) + govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockResponse) + + try { + await getPaymentStatus(paymentId) + } catch (error) { + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error retrieving the payment status from the GOV.UK API service - tid: ${paymentId}`, + { + method: 'GET', + paymentId: paymentId, + status: mockResponse.status, + response: { message: 'Server error' } + } + ) + } + }) + + it('should throw error when API call fails with network issue', async () => { + const mockError = new Error('Network error') + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn()) + govUkPayApi.fetchPaymentStatus.mockRejectedValue(mockError) + + try { + await getPaymentStatus(paymentId) + } catch (error) { + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error retrieving the payment status from the GOV.UK API service - paymentId: ${paymentId}`, + mockError + ) + } + }) + + it('should throw error for when rate limit is breached', async () => { + const mockResponse = { + ok: false, + status: 429, + json: jest.fn().mockResolvedValue({ message: 'Rate limit exceeded' }) + } + const consoleErrorSpy = jest.spyOn(console, 'info').mockImplementation(jest.fn()) + govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockResponse) + + try { + await getPaymentStatus(paymentId) + } catch (error) { + expect(consoleErrorSpy).toHaveBeenCalledWith(`GOV.UK Pay API rate limit breach - paymentId: ${paymentId}`) + } + }) + + it('should throw error for unexpected response status', async () => { + const mockResponse = { + ok: false, + status: 500, + json: jest.fn().mockResolvedValue({ message: 'Server error' }) + } + govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockResponse) + + try { + await getPaymentStatus(paymentId) + } catch (error) { + expect(error.message).toBe('Unexpected response from GOV.UK pay API') + } + }) + }) }) diff --git a/packages/gafl-webapp-service/src/services/payment/govuk-pay-service.js b/packages/gafl-webapp-service/src/services/payment/govuk-pay-service.js index 85979ef4fc..ee150457d3 100644 --- a/packages/gafl-webapp-service/src/services/payment/govuk-pay-service.js +++ b/packages/gafl-webapp-service/src/services/payment/govuk-pay-service.js @@ -10,24 +10,52 @@ import db from 'debug' import Boom from '@hapi/boom' const debug = db('webapp:govuk-pay-service') +const HTTP_TOO_MANY_REQUESTS = 429 +const getErrorType = (response, id, idTag = 'tid', payloadOrigin = GOVPAYFAIL.prePaymentRetry) => { + if (response.status === HTTP_TOO_MANY_REQUESTS) { + const msg = `GOV.UK Pay API rate limit breach - ${idTag}: ${id}` + console.info(msg) + const badImplementationError = Boom.badImplementation(msg) + badImplementationError.output.payload.origin = payloadOrigin + return badImplementationError + } + return Boom.badImplementation('Unexpected response from GOV.UK pay API') +} + +const getRetryError = (err, actionMessage, id, idTag = 'tid', payloadOrigin = GOVPAYFAIL.prePaymentRetry) => { + console.error(`Error ${actionMessage} GOV.UK API service - ${idTag}: ${id}`, err) + const badImplementationError = Boom.boomify(err, { statusCode: 500 }) + badImplementationError.output.payload.origin = payloadOrigin + return badImplementationError +} + +const getErrorMessage = async (method, response) => ({ + method, + status: response.status, + response: await response.json() +}) + +const getTransactionErrorMessage = async (transactionId, payload, response) => ({ + ...(await getErrorMessage('POST', response)), + transactionId, + payload +}) + /** * Post prepared payment to the GOV.UK PAY API and handle the exceptions and results * @param preparedPayment - the prepared payload for the payment. See in processors/payment.js * @returns {Promise<*>} */ -export const sendPayment = async preparedPayment => { +export const sendPayment = async (preparedPayment, recurring = false) => { let response try { - response = await govUkPayApi.createPayment(preparedPayment) + response = await govUkPayApi.createPayment(preparedPayment, recurring) } catch (err) { /* * Potentially errors caught here (unreachable, timeouts) may be retried - set origin on the error to indicate * a prepayment error in the POST request */ - console.error(`Error creating payment in the GOV.UK API service - tid: ${preparedPayment.id}`, err) - const badImplementationError = Boom.boomify(err, { statusCode: 500 }) - badImplementationError.output.payload.origin = GOVPAYFAIL.prePaymentRetry - throw badImplementationError + throw getRetryError(err, 'creating payment in the', preparedPayment.id) } if (response.ok) { @@ -35,27 +63,13 @@ export const sendPayment = async preparedPayment => { debug('Successful payment creation response: %o', resBody) return resBody } else { - const errMsg = { - transactionId: preparedPayment.id, - method: 'POST', - payload: preparedPayment, - status: response.status, - response: await response.json() - } + const errMsg = await getTransactionErrorMessage(preparedPayment.id, preparedPayment, response) console.error('Failure creating payment in the GOV.UK API service', errMsg) /* * Detect the rate limit error and present the retry content. Otherwise throw the general server error */ - if (response.status === 429) { - const msg = `GOV.UK Pay API rate limit breach - tid: ${preparedPayment.id}` - console.info(msg) - const badImplementationError = Boom.badImplementation(msg) - badImplementationError.output.payload.origin = GOVPAYFAIL.prePaymentRetry - throw badImplementationError - } else { - throw Boom.badImplementation('Unexpected response from GOV.UK pay API') - } + throw getErrorType(response, preparedPayment.id) } } @@ -64,20 +78,17 @@ export const sendPayment = async preparedPayment => { * @param paymentId - the paymentId * @returns {Promise} */ -export const getPaymentStatus = async paymentId => { +export const getPaymentStatus = async (paymentId, recurring = false) => { debug(`Get payment status for paymentId: ${paymentId}`) let response try { - response = await govUkPayApi.fetchPaymentStatus(paymentId) + response = await govUkPayApi.fetchPaymentStatus(paymentId, recurring) } catch (err) { /* * Errors caught here (unreachable, timeouts) may be retried - set origin on the error to indicate * a post-payment error in the request */ - console.error(`Error retrieving the payment status from the GOV.UK API service - paymentId: ${paymentId}`, err) - const badImplementationError = Boom.boomify(err, { statusCode: 500 }) - badImplementationError.output.payload.origin = GOVPAYFAIL.postPaymentRetry - throw badImplementationError + throw getRetryError(err, 'retrieving the payment status from the', paymentId, 'paymentId', GOVPAYFAIL.postPaymentRetry) } if (response.ok) { @@ -87,23 +98,43 @@ export const getPaymentStatus = async paymentId => { } else { const mes = { paymentId, - method: 'GET', - status: response.status, - response: await response.json() + ...(await getErrorMessage('GET', response)) } console.error(`Error retrieving the payment status from the GOV.UK API service - tid: ${paymentId}`, mes) /* * Detect the rate limit error and present the retry content. Otherwise throw the general server error */ - if (response.status === 429) { - const msg = `GOV.UK Pay API rate limit breach - paymentId: ${paymentId}` - console.info(msg) - const badImplementationError = Boom.badImplementation(msg) - badImplementationError.output.payload.origin = GOVPAYFAIL.postPaymentRetry - throw badImplementationError - } else { - throw Boom.badImplementation('Unexpected response from GOV.UK pay API') - } + throw getErrorType(response, paymentId, 'paymentId', GOVPAYFAIL.postPaymentRetry) + } +} + +const createRecurringPaymentAgreement = async preparedPayment => { + try { + return await govUkPayApi.createRecurringPaymentAgreement(preparedPayment) + } catch (err) { + /* + * Potentially errors caught here (unreachable, timeouts) may be retried - set origin on the error to indicate + * a prepayment error in the POST request + */ + throw getRetryError(err, 'creating agreement in the', preparedPayment.user_identifier) + } +} + +export const sendRecurringPayment = async preparedPayment => { + const response = await createRecurringPaymentAgreement(preparedPayment) + + if (response.ok) { + const resBody = await response.json() + debug('Successful agreement creation response: %o', resBody) + return resBody + } else { + const errMsg = await getTransactionErrorMessage(preparedPayment.reference, preparedPayment, response) + console.error('Failure creating agreement in the GOV.UK API service', errMsg) + + /* + * Detect the rate limit error and present the retry content. Otherwise throw the general server error + */ + throw getErrorType(response, preparedPayment.id) } } diff --git a/packages/payment-mop-up-job/package-lock.json b/packages/payment-mop-up-job/package-lock.json index 03f55cf393..6ec08d09cf 100644 --- a/packages/payment-mop-up-job/package-lock.json +++ b/packages/payment-mop-up-job/package-lock.json @@ -1,14 +1,16 @@ { "name": "@defra-fish/payment-mop-up-job", - "version": "1.49.0-rc.10", + "version": "1.57.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@defra-fish/payment-mop-up-job", - "version": "1.49.0-rc.10", + "version": "1.57.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { + "@defra-fish/business-rules-lib": "1.57.0", + "@defra-fish/connectors-lib": "1.57.0", "bottleneck": "^2.19.5", "debug": "^4.3.3", "moment": "^2.29.1" diff --git a/packages/payment-mop-up-job/package.json b/packages/payment-mop-up-job/package.json index 644b1445b5..9fb8c63e63 100644 --- a/packages/payment-mop-up-job/package.json +++ b/packages/payment-mop-up-job/package.json @@ -1,6 +1,6 @@ { "name": "@defra-fish/payment-mop-up-job", - "version": "1.49.0-rc.10", + "version": "1.57.0", "description": "Process incomplete web-sales", "type": "module", "engines": { @@ -36,8 +36,8 @@ "test": "echo \"Error: run tests from root\" && exit 1" }, "dependencies": { - "@defra-fish/business-rules-lib": "1.49.0-rc.10", - "@defra-fish/connectors-lib": "1.49.0-rc.10", + "@defra-fish/business-rules-lib": "1.57.0", + "@defra-fish/connectors-lib": "1.57.0", "bottleneck": "^2.19.5", "debug": "^4.3.3", "moment": "^2.29.1" diff --git a/packages/pocl-job/README.md b/packages/pocl-job/README.md index aca7e211e2..475e61e9ef 100644 --- a/packages/pocl-job/README.md +++ b/packages/pocl-job/README.md @@ -9,21 +9,16 @@ to maintain state during the import process. # Environment variables -| name | description | required | default | valid | notes | -| ------------------------- | ----------------------------------------------------------------------------------- | :------: | --------- | ----------------------------------------------- | ----- | -| NODE_ENV | Node environment | no | | development, test, production | | -| POCL_FILE_STAGING_TABLE | The DynamoDB table used for staging POCL files | yes | | | | -| POCL_RECORD_STAGING_TABLE | The DynamoDB table used for staging POCL records | yes | | | | -| POCL_STAGING_TTL | The time to live for records in either staging table | no | 168 hours | | | -| POCL_FTP_HOST | The hostname of the target FTP server | yes | | | | -| POCL_FTP_PORT | The port of the FTP service on the target server | yes | | | | -| POCL_FTP_PATH | The base path under which files should be written to the FTP server | yes | | | | -| POCL_FTP_USERNAME | The username used to authenticate with the FTP server | yes | | | | -| POCL_FTP_PRIVATE_KEY_PATH | The path to the folder containing the keys used to authenticate with the FTP server | yes | | | | -| POCL_S3_BUCKET | The name of the AWS S3 bucket in which to stage pocl data | yes | | | | -| DEBUG | Use to enable output of debug information to the console | yes | | pocl:\*, pocl:staging, pocl:transport, pocl:ftp | | -| AIRBRAKE_HOST | URL of airbrake host | no | | | | -| AIRBRAKE_PROJECT_KEY | Project key for airbrake logging | no | | | | +| name | description | required | default | valid | notes | +| ------------------------- | --------------------------------------------------------- | :------: | --------- | ----------------------------------------------- | ----- | +| NODE_ENV | Node environment | no | | development, test, production | | +| POCL_FILE_STAGING_TABLE | The DynamoDB table used for staging POCL files | yes | | | | +| POCL_RECORD_STAGING_TABLE | The DynamoDB table used for staging POCL records | yes | | | | +| POCL_STAGING_TTL | The time to live for records in either staging table | no | 168 hours | | | +| POCL_S3_BUCKET | The name of the AWS S3 bucket in which to stage pocl data | yes | | | | +| DEBUG | Use to enable output of debug information to the console | yes | | pocl:\*, pocl:staging, pocl:transport, pocl:ftp | | +| AIRBRAKE_HOST | URL of airbrake host | no | | | | +| AIRBRAKE_PROJECT_KEY | Project key for airbrake logging | no | | | | ### See also: diff --git a/packages/pocl-job/package-lock.json b/packages/pocl-job/package-lock.json index 6209e2056b..b15f9c8507 100644 --- a/packages/pocl-job/package-lock.json +++ b/packages/pocl-job/package-lock.json @@ -1,111 +1,28 @@ { "name": "@defra-fish/pocl-job", - "version": "1.49.0-rc.10", + "version": "1.57.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@defra-fish/pocl-job", - "version": "1.49.0-rc.10", + "version": "1.57.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { + "@defra-fish/business-rules-lib": "1.57.0", + "@defra-fish/connectors-lib": "1.57.0", "commander": "^7.2.0", "debug": "^4.3.3", "filesize": "^6.4.0", "md5-file": "^5.0.0", "moment": "^2.29.1", "moment-timezone": "^0.5.34", - "sax-stream": "^1.3.0", - "ssh2-sftp-client": "^6.0.1" + "sax-stream": "^1.3.0" }, "engines": { "node": ">=18.17" } }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", - "integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==", - "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "node_modules/asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/async": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", - "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "node_modules/color": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", - "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", - "dependencies": { - "color-convert": "^1.9.1", - "color-string": "^1.5.2" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "node_modules/color-string": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz", - "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/colorspace": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", - "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", - "dependencies": { - "color": "3.0.x", - "text-hex": "1.0.x" - } - }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -114,25 +31,6 @@ "node": ">= 10" } }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, "node_modules/debug": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", @@ -149,26 +47,6 @@ } } }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" - }, - "node_modules/fast-safe-stringify": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", - "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" - }, - "node_modules/fecha": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz", - "integrity": "sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg==" - }, "node_modules/filesize": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.4.0.tgz", @@ -177,51 +55,6 @@ "node": ">= 0.4.0" } }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - }, - "node_modules/is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" - }, - "node_modules/logform": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz", - "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==", - "dependencies": { - "colors": "^1.2.1", - "fast-safe-stringify": "^2.0.4", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "triple-beam": "^1.3.0" - } - }, "node_modules/md5-file": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-5.0.0.tgz", @@ -257,76 +90,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "dependencies": { - "fn.name": "1.x.x" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", - "engines": { - "node": ">= 4" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -353,156 +116,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/ssh2": { - "version": "0.8.9", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz", - "integrity": "sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==", - "dependencies": { - "ssh2-streams": "~0.4.10" - }, - "engines": { - "node": ">=5.2.0" - } - }, - "node_modules/ssh2-sftp-client": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssh2-sftp-client/-/ssh2-sftp-client-6.0.1.tgz", - "integrity": "sha512-Glut2SmK/XpNOBiEuzqlKZGKkIyha2XMbuWVXR2hFUJkNsbyl/wmlZSeUEPxKFp/dC9UEvUKzanKydgLmNdfkw==", - "dependencies": { - "concat-stream": "^2.0.0", - "promise-retry": "^2.0.1", - "ssh2": "^0.8.9", - "winston": "^3.3.3" - } - }, - "node_modules/ssh2-streams": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz", - "integrity": "sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==", - "dependencies": { - "asn1": "~0.2.0", - "bcrypt-pbkdf": "^1.0.2", - "streamsearch": "~0.1.2" - }, - "engines": { - "node": ">=5.2.0" - } - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", - "engines": { - "node": "*" - } - }, - "node_modules/streamsearch": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" - }, - "node_modules/triple-beam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", - "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "node_modules/winston": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", - "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==", - "dependencies": { - "@dabh/diagnostics": "^2.0.2", - "async": "^3.1.0", - "is-stream": "^2.0.0", - "logform": "^2.2.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.4.0" - }, - "engines": { - "node": ">= 6.4.0" - } - }, - "node_modules/winston-transport": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", - "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", - "dependencies": { - "readable-stream": "^2.3.7", - "triple-beam": "^1.2.0" - }, - "engines": { - "node": ">= 6.4.0" - } - }, - "node_modules/winston-transport/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/winston-transport/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/winston-transport/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } } } } \ No newline at end of file diff --git a/packages/pocl-job/package.json b/packages/pocl-job/package.json index d3124b2e77..96ae23d21d 100644 --- a/packages/pocl-job/package.json +++ b/packages/pocl-job/package.json @@ -1,6 +1,6 @@ { "name": "@defra-fish/pocl-job", - "version": "1.49.0-rc.10", + "version": "1.57.0", "description": "Post Office Counter Licence sales processor", "type": "module", "engines": { @@ -35,15 +35,14 @@ "test": "echo \"Error: run tests from root\" && exit 1" }, "dependencies": { - "@defra-fish/business-rules-lib": "1.49.0-rc.10", - "@defra-fish/connectors-lib": "1.49.0-rc.10", + "@defra-fish/business-rules-lib": "1.57.0", + "@defra-fish/connectors-lib": "1.57.0", "commander": "^7.2.0", "debug": "^4.3.3", "filesize": "^6.4.0", "md5-file": "^5.0.0", "moment": "^2.29.1", "moment-timezone": "^0.5.34", - "sax-stream": "^1.3.0", - "ssh2-sftp-client": "^6.0.1" + "sax-stream": "^1.3.0" } } diff --git a/packages/pocl-job/src/__mocks__/ssh2-sftp-client.js b/packages/pocl-job/src/__mocks__/ssh2-sftp-client.js deleted file mode 100644 index f24599f0b1..0000000000 --- a/packages/pocl-job/src/__mocks__/ssh2-sftp-client.js +++ /dev/null @@ -1,11 +0,0 @@ -const ssh2sftpClient = jest.genMockFromModule('ssh2-sftp-client') - -export const mockedFtpMethods = { - connect: jest.fn(), - list: jest.fn(), - fastGet: jest.fn(), - delete: jest.fn(), - end: jest.fn() -} -ssh2sftpClient.mockImplementation(() => mockedFtpMethods) -export default ssh2sftpClient diff --git a/packages/pocl-job/src/__tests__/config.spec.js b/packages/pocl-job/src/__tests__/config.spec.js index f0c1c6d8eb..5fd427d0f6 100644 --- a/packages/pocl-job/src/__tests__/config.spec.js +++ b/packages/pocl-job/src/__tests__/config.spec.js @@ -11,11 +11,6 @@ describe('config', () => { process.env.POCL_RECORD_STAGING_TABLE = 'test-record-staging-table' process.env.POCL_STAGING_TTL = 1234 - process.env.POCL_FTP_HOST = 'test-host' - process.env.POCL_FTP_PORT = 2222 - process.env.POCL_FTP_PATH = '/remote/share' - process.env.POCL_FTP_USERNAME = 'test-user' - process.env.POCL_FTP_KEY_SECRET_ID = 'test-secret-id' process.env.POCL_S3_BUCKET = 'test-bucket' await config.initialise() }) @@ -38,32 +33,6 @@ describe('config', () => { }) }) - describe('ftp', () => { - it('provides properties relating the use of SFTP', async () => { - expect(config.ftp).toEqual( - expect.objectContaining({ - host: 'test-host', - port: '2222', - path: '/remote/share', - username: 'test-user', - privateKey: 'test-ssh-key', - algorithms: { cipher: expect.any(Array), kex: expect.any(Array) }, - // Wait up to 60 seconds for the SSH handshake - readyTimeout: expect.any(Number), - // Retry 5 times over a minute - retries: expect.any(Number), - retry_minTimeout: expect.any(Number), - debug: expect.any(Function) - }) - ) - }) - it('defaults the sftp port to 22 if the environment variable is not configured', async () => { - delete process.env.POCL_FTP_PORT - await config.initialise() - expect(config.ftp.port).toEqual('22') - }) - }) - describe('s3', () => { it('provides properties relating the use of Amazon S3', async () => { expect(config.s3.bucket).toEqual('test-bucket') diff --git a/packages/pocl-job/src/__tests__/pocl-processor.spec.js b/packages/pocl-job/src/__tests__/pocl-processor.spec.js index f3a5e4696a..ade97101d7 100644 --- a/packages/pocl-job/src/__tests__/pocl-processor.spec.js +++ b/packages/pocl-job/src/__tests__/pocl-processor.spec.js @@ -41,7 +41,7 @@ jest.mock('../config.js', () => ({ bucket: 'testbucket' } })) -jest.mock('../transport/ftp-to-s3.js') +jest.mock('../transport/storeS3MetaData.js') jest.mock('../transport/s3-to-local.js') jest.mock('../io/db.js') jest.mock('../io/s3.js') diff --git a/packages/pocl-job/src/config.js b/packages/pocl-job/src/config.js index 0430b4a457..d08132335b 100644 --- a/packages/pocl-job/src/config.js +++ b/packages/pocl-job/src/config.js @@ -1,49 +1,5 @@ -import { AWS } from '@defra-fish/connectors-lib' -import db from 'debug' -const { secretsManager } = AWS() - -/** - * Key exchange algorithms for public key authentication - in descending order of priority - * @type {string[]} - */ -export const SFTP_KEY_EXCHANGE_ALGORITHMS = [ - 'curve25519-sha256@libssh.org', - 'curve25519-sha256', - 'ecdh-sha2-nistp521', - 'ecdh-sha2-nistp384', - 'ecdh-sha2-nistp256', - 'diffie-hellman-group-exchange-sha256', - 'diffie-hellman-group14-sha256', - 'diffie-hellman-group16-sha512', - 'diffie-hellman-group18-sha512', - 'diffie-hellman-group14-sha1', - 'diffie-hellman-group-exchange-sha1', - 'diffie-hellman-group1-sha1' -] -/** - * Ciphers for SFTP support - in descending order of priority - * @type {string[]} - */ -export const SFTP_CIPHERS = [ - // http://tools.ietf.org/html/rfc4344#section-4 - 'aes256-ctr', - 'aes192-ctr', - 'aes128-ctr', - 'aes256-gcm', - 'aes256-gcm@openssh.com', - 'aes128-gcm', - 'aes128-gcm@openssh.com', - 'aes256-cbc', - 'aes192-cbc', - 'aes128-cbc', - 'blowfish-cbc', - '3des-cbc', - 'cast128-cbc' -] - class Config { _db - _ftp _s3 async initialise () { @@ -53,21 +9,6 @@ class Config { stagingTtlDelta: Number.parseInt(process.env.POCL_STAGING_TTL || 60 * 60 * 168) } - this.ftp = { - host: process.env.POCL_FTP_HOST, - port: process.env.POCL_FTP_PORT || '22', - path: process.env.POCL_FTP_PATH, - username: process.env.POCL_FTP_USERNAME, - privateKey: (await secretsManager.getSecretValue({ SecretId: process.env.POCL_FTP_KEY_SECRET_ID }).promise()).SecretString, - algorithms: { cipher: SFTP_CIPHERS, kex: SFTP_KEY_EXCHANGE_ALGORITHMS }, - // Wait up to 60 seconds for the SSH handshake - readyTimeout: 60000, - // Retry 5 times over a minute - retries: 5, - retry_minTimeout: 12000, - debug: db('pocl:ftp') - } - this.s3 = { bucket: process.env.POCL_S3_BUCKET } @@ -85,18 +26,6 @@ class Config { this._db = cfg } - /** - * FTP configuration settings - * @type {object} - */ - get ftp () { - return this._ftp - } - - set ftp (cfg) { - this._ftp = cfg - } - /** * S3 configuration settings * @type {object} diff --git a/packages/pocl-job/src/io/__tests__/s3.spec.js b/packages/pocl-job/src/io/__tests__/s3.spec.js index 4a1a2da226..26c7fe9be1 100644 --- a/packages/pocl-job/src/io/__tests__/s3.spec.js +++ b/packages/pocl-job/src/io/__tests__/s3.spec.js @@ -5,7 +5,6 @@ import { DYNAMICS_IMPORT_STAGE, FILE_STAGE, POST_OFFICE_DATASOURCE } from '../.. import { salesApi } from '@defra-fish/connectors-lib' import fs from 'fs' import AwsMock from 'aws-sdk' -import { mockedFtpMethods } from 'ssh2-sftp-client' jest.mock('fs') jest.mock('md5-file') @@ -159,7 +158,6 @@ describe('s3 operations', () => { }) it('skips file processing if a file has already been marked as processed in Dynamics', async () => { - mockedFtpMethods.list.mockResolvedValue([{ name: 'test-already-processed.xml' }]) fs.createReadStream.mockReturnValueOnce('teststream') fs.statSync.mockReturnValueOnce({ size: 1024 }) salesApi.getTransactionFile.mockResolvedValueOnce({ status: { description: 'Processed' } }) diff --git a/packages/pocl-job/src/io/s3.js b/packages/pocl-job/src/io/s3.js index c24d3ba3d0..66a97cdbfb 100644 --- a/packages/pocl-job/src/io/s3.js +++ b/packages/pocl-job/src/io/s3.js @@ -2,7 +2,7 @@ import moment from 'moment' import filesize from 'filesize' import config from '../config.js' import { DYNAMICS_IMPORT_STAGE } from '../staging/constants.js' -import { storeS3Metadata } from '../transport/ftp-to-s3.js' +import { storeS3Metadata } from '../transport/storeS3MetaData.js' import { AWS, salesApi } from '@defra-fish/connectors-lib' const { s3 } = AWS() diff --git a/packages/pocl-job/src/transport/__tests__/ftp-to-s3.spec.js b/packages/pocl-job/src/transport/__tests__/ftp-to-s3.spec.js deleted file mode 100644 index 7b5259da27..0000000000 --- a/packages/pocl-job/src/transport/__tests__/ftp-to-s3.spec.js +++ /dev/null @@ -1,145 +0,0 @@ -import { ftpToS3 } from '../ftp-to-s3.js' -import moment from 'moment' -import { updateFileStagingTable } from '../../io/db.js' -import { getTempDir } from '../../io/file.js' -import { DYNAMICS_IMPORT_STAGE, FILE_STAGE, POST_OFFICE_DATASOURCE } from '../../staging/constants.js' -import { salesApi } from '@defra-fish/connectors-lib' -import fs from 'fs' -import md5File from 'md5-file' -import AwsMock from 'aws-sdk' -import { mockedFtpMethods } from 'ssh2-sftp-client' - -jest.mock('fs') -jest.mock('md5-file') -jest.mock('../../io/db.js') -jest.mock('../../io/file.js') - -jest.mock('@defra-fish/connectors-lib', () => { - const actual = jest.requireActual('@defra-fish/connectors-lib') - return { - AWS: actual.AWS, - salesApi: { - ...Object.keys(actual.salesApi).reduce((acc, k) => ({ ...acc, [k]: jest.fn(async () => {}) }), {}) - } - } -}) - -jest.mock('../../config.js', () => ({ - ftp: { - path: '/ftpservershare/' - }, - s3: { - bucket: 'testbucket' - } -})) - -describe('ftp-to-s3', () => { - beforeAll(() => { - getTempDir.mockReturnValue('/local/tmp') - md5File.mockResolvedValue('example-md5') - }) - beforeEach(() => { - jest.clearAllMocks() - AwsMock.__resetAll() - }) - - it('retrieves files from SFTP and stores in S3', async () => { - mockedFtpMethods.list.mockResolvedValue([{ name: 'test1.xml' }, { name: 'test2.xml' }]) - fs.createReadStream.mockReturnValueOnce('test1stream') - fs.createReadStream.mockReturnValueOnce('test2stream') - fs.statSync.mockReturnValueOnce({ size: 1024 }) - fs.statSync.mockReturnValueOnce({ size: 2048 }) - await ftpToS3() - - const localPath1 = '/local/tmp/test1.xml' - const localPath2 = '/local/tmp/test2.xml' - - const s3Key1 = `${moment().format('YYYY-MM-DD')}/test1.xml` - const s3Key2 = `${moment().format('YYYY-MM-DD')}/test2.xml` - - expect(mockedFtpMethods.fastGet).toHaveBeenNthCalledWith(1, '/ftpservershare/test1.xml', localPath1, {}) - expect(mockedFtpMethods.fastGet).toHaveBeenNthCalledWith(2, '/ftpservershare/test2.xml', localPath2, {}) - expect(AwsMock.S3.mockedMethods.putObject).toHaveBeenNthCalledWith(1, { - Bucket: 'testbucket', - Key: s3Key1, - Body: 'test1stream' - }) - expect(AwsMock.S3.mockedMethods.putObject).toHaveBeenNthCalledWith(2, { - Bucket: 'testbucket', - Key: s3Key2, - Body: 'test2stream' - }) - expect(updateFileStagingTable).toHaveBeenNthCalledWith(1, { - filename: 'test1.xml', - md5: 'example-md5', - fileSize: '1 KB', - stage: FILE_STAGE.Pending, - s3Key: s3Key1 - }) - expect(updateFileStagingTable).toHaveBeenNthCalledWith(2, { - filename: 'test2.xml', - md5: 'example-md5', - fileSize: '2 KB', - stage: FILE_STAGE.Pending, - s3Key: s3Key2 - }) - expect(salesApi.upsertTransactionFile).toHaveBeenNthCalledWith(1, 'test1.xml', { - status: DYNAMICS_IMPORT_STAGE.Pending, - dataSource: POST_OFFICE_DATASOURCE, - fileSize: '1 KB', - receiptTimestamp: expect.any(String), - salesDate: expect.any(String), - notes: 'Retrieved from the remote server and awaiting processing' - }) - expect(salesApi.upsertTransactionFile).toHaveBeenNthCalledWith(2, 'test2.xml', { - status: DYNAMICS_IMPORT_STAGE.Pending, - dataSource: POST_OFFICE_DATASOURCE, - fileSize: '2 KB', - receiptTimestamp: expect.any(String), - salesDate: expect.any(String), - notes: 'Retrieved from the remote server and awaiting processing' - }) - expect(fs.unlinkSync).toHaveBeenNthCalledWith(1, localPath1) - expect(fs.unlinkSync).toHaveBeenNthCalledWith(2, localPath2) - expect(mockedFtpMethods.end).toHaveBeenCalledTimes(1) - }) - - it('moves the file to s3 but skips file processing if a file has already been marked as processed in Dynamics', async () => { - mockedFtpMethods.list.mockResolvedValue([{ name: 'test-already-processed.xml' }]) - fs.createReadStream.mockReturnValueOnce('teststream') - fs.statSync.mockReturnValueOnce({ size: 1024 }) - salesApi.getTransactionFile.mockResolvedValueOnce({ status: { description: 'Processed' } }) - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn()) - await ftpToS3() - const localPath = '/local/tmp/test-already-processed.xml' - const s3Key = `${moment().format('YYYY-MM-DD')}/test-already-processed.xml` - expect(mockedFtpMethods.fastGet).toHaveBeenCalledWith('/ftpservershare/test-already-processed.xml', localPath, {}) - expect(AwsMock.S3.mockedMethods.putObject).toHaveBeenCalledWith({ - Bucket: 'testbucket', - Key: s3Key, - Body: 'teststream' - }) - expect(updateFileStagingTable).not.toHaveBeenCalled() - expect(salesApi.upsertTransactionFile).not.toHaveBeenCalled() - expect(consoleErrorSpy).toHaveBeenCalled() - expect(fs.unlinkSync).toHaveBeenCalledWith(localPath) - expect(mockedFtpMethods.end).toHaveBeenCalledTimes(1) - }) - - it('logs and propogates errors back up the stack', async () => { - const testError = new Error('Test error') - mockedFtpMethods.list.mockRejectedValue(testError) - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}) - await expect(ftpToS3).rejects.toThrow(testError) - expect(consoleErrorSpy).toHaveBeenCalled() - }) - - it('ignores non-xml files', async () => { - mockedFtpMethods.list.mockResolvedValue([{ name: 'test1.pdf' }, { name: 'test2.md' }]) - await ftpToS3() - expect(mockedFtpMethods.fastGet).not.toHaveBeenCalled() - expect(AwsMock.S3.mockedMethods.putObject).not.toHaveBeenCalled() - expect(fs.unlinkSync).not.toHaveBeenCalled() - expect(mockedFtpMethods.end).toHaveBeenCalledTimes(1) - }) -}) diff --git a/packages/pocl-job/src/transport/__tests__/storeS3MetaData.spec.js b/packages/pocl-job/src/transport/__tests__/storeS3MetaData.spec.js new file mode 100644 index 0000000000..57d4434d65 --- /dev/null +++ b/packages/pocl-job/src/transport/__tests__/storeS3MetaData.spec.js @@ -0,0 +1,64 @@ +import moment from 'moment' +import { salesApi } from '@defra-fish/connectors-lib' +import { DYNAMICS_IMPORT_STAGE, FILE_STAGE, POST_OFFICE_DATASOURCE } from '../../staging/constants.js' +import { updateFileStagingTable } from '../../io/db.js' +import { storeS3Metadata } from '../storeS3MetaData.js' + +jest.mock('../../io/db.js', () => ({ + updateFileStagingTable: jest.fn() +})) +jest.mock('@defra-fish/connectors-lib', () => ({ + salesApi: { + upsertTransactionFile: jest.fn() + } +})) + +describe('storeS3Metadata', () => { + const md5 = 'mockMd5Hash' + const fileSize = 12345 + const filename = 'testfile' + const s3Key = 'mock/s3/key/testfile' + const receiptMoment = new Date('2024-10-17T00:00:00Z') + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('console log should output "Storing metadata for s3Key"', async () => { + const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(jest.fn()) + await storeS3Metadata(md5, fileSize, filename, s3Key, receiptMoment) + expect(consoleLogSpy).toHaveBeenCalledWith(`Storing metadata for ${s3Key}`) + }) + + it('should call updateFileStagingTable with correct arguments', async () => { + await storeS3Metadata(md5, fileSize, filename, s3Key, receiptMoment) + expect(updateFileStagingTable).toHaveBeenCalledWith({ + filename, + md5, + fileSize, + s3Key, + stage: FILE_STAGE.Pending + }) + }) + + it('should call salesApi.upsertTransactionFile with correct arguments', async () => { + const expectedSalesDate = moment(receiptMoment).subtract(1, 'days').toISOString() + const expectedReceiptTimestamp = receiptMoment.toISOString() + + await storeS3Metadata(md5, fileSize, filename, s3Key, receiptMoment) + expect(salesApi.upsertTransactionFile).toHaveBeenCalledWith(filename, { + status: DYNAMICS_IMPORT_STAGE.Pending, + dataSource: POST_OFFICE_DATASOURCE, + fileSize: fileSize, + salesDate: expectedSalesDate, + receiptTimestamp: expectedReceiptTimestamp, + notes: 'Retrieved from the remote server and awaiting processing' + }) + }) + + test('should log "Stored metadata for s3Key"', async () => { + const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(jest.fn()) + await storeS3Metadata(md5, fileSize, filename, s3Key, receiptMoment) + expect(consoleLogSpy).toHaveBeenCalledWith(`Stored metadata for ${s3Key}`) + }) +}) diff --git a/packages/pocl-job/src/transport/ftp-to-s3.js b/packages/pocl-job/src/transport/ftp-to-s3.js deleted file mode 100644 index b3c9376448..0000000000 --- a/packages/pocl-job/src/transport/ftp-to-s3.js +++ /dev/null @@ -1,91 +0,0 @@ -import FtpClient from 'ssh2-sftp-client' -import moment from 'moment' -import Path from 'path' -import fs from 'fs' -import db from 'debug' -import md5File from 'md5-file' -import filesize from 'filesize' -import config from '../config.js' -import { getTempDir } from '../io/file.js' -import { DYNAMICS_IMPORT_STAGE, FILE_STAGE, POST_OFFICE_DATASOURCE } from '../staging/constants.js' -import { AWS, salesApi } from '@defra-fish/connectors-lib' -import { updateFileStagingTable } from '../io/db.js' -const { s3 } = AWS() - -const debug = db('pocl:transport') -const sftp = new FtpClient() - -export async function ftpToS3 () { - try { - debug('Connecting to SFTP endpoint at sftp://%s:%s%s', config.ftp.host, config.ftp.port, config.ftp.path) - await sftp.connect(config.ftp) - const fileList = await sftp.list(config.ftp.path) - debug('Discovered the following files on the SFTP server: %o', fileList) - const xmlFiles = fileList.filter(f => Path.extname(f.name).toLowerCase() === '.xml') - - if (!xmlFiles.length) { - debug('No XML files were waiting to be processed on the SFTP server.') - } else { - await retrieveAllFiles(xmlFiles) - } - } catch (e) { - console.error('Error migrating files from the SFTP endpoint', e) - throw e - } finally { - debug('Closing SFTP connection.') - await sftp.end() - } -} - -export async function storeS3Metadata (md5, fileSize, filename, s3Key, receiptMoment) { - console.log(`Storing metadata for ${s3Key}`) - await updateFileStagingTable({ filename, md5, fileSize, s3Key, stage: FILE_STAGE.Pending }) - - await salesApi.upsertTransactionFile(filename, { - status: DYNAMICS_IMPORT_STAGE.Pending, - dataSource: POST_OFFICE_DATASOURCE, - fileSize: fileSize, - salesDate: moment(receiptMoment).subtract(1, 'days').toISOString(), - receiptTimestamp: receiptMoment.toISOString(), - notes: 'Retrieved from the remote server and awaiting processing' - }) - - console.log(`Stored metadata for ${s3Key}`) -} - -const retrieveAllFiles = async xmlFiles => { - const tempDir = getTempDir('ftp') - - for (const fileEntry of xmlFiles) { - const filename = fileEntry.name - const remoteFilePath = Path.join(config.ftp.path, filename) - const localFilePath = Path.resolve(tempDir, filename) - - // Retrieve from FTP server to local temporary directory - debug('Transferring %s to %s', remoteFilePath, localFilePath) - await sftp.fastGet(remoteFilePath, localFilePath, {}) - - // Transfer to S3 - const receiptMoment = moment() - const s3Key = Path.join(receiptMoment.format('YYYY-MM-DD'), filename) - debug('Transferring file to S3 bucket %s with key %s', config.s3.bucket, s3Key) - await s3.putObject({ Bucket: config.s3.bucket, Key: s3Key, Body: fs.createReadStream(localFilePath) }).promise() - - const dynamicsRecord = await salesApi.getTransactionFile(filename) - if (dynamicsRecord && DYNAMICS_IMPORT_STAGE.isAlreadyProcessed(dynamicsRecord.status.description)) { - console.error( - 'Retrieved file %s from SFTP and stored in S3, however an entry already exists in Dynamics with this filename. Skipping import.', - filename - ) - } else { - const md5 = await md5File(localFilePath) - const fileSize = filesize(fs.statSync(localFilePath).size) - await storeS3Metadata(md5, fileSize, filename, s3Key, receiptMoment) - } - - // Remove from FTP server and local tmp - debug('Removing remote file %s', remoteFilePath) - await sftp.delete(remoteFilePath) - fs.unlinkSync(localFilePath) - } -} diff --git a/packages/pocl-job/src/transport/storeS3MetaData.js b/packages/pocl-job/src/transport/storeS3MetaData.js new file mode 100644 index 0000000000..31eb6a4810 --- /dev/null +++ b/packages/pocl-job/src/transport/storeS3MetaData.js @@ -0,0 +1,20 @@ +import moment from 'moment' +import { salesApi } from '@defra-fish/connectors-lib' +import { DYNAMICS_IMPORT_STAGE, FILE_STAGE, POST_OFFICE_DATASOURCE } from '../staging/constants.js' +import { updateFileStagingTable } from '../io/db.js' + +export async function storeS3Metadata (md5, fileSize, filename, s3Key, receiptMoment) { + console.log(`Storing metadata for ${s3Key}`) + await updateFileStagingTable({ filename, md5, fileSize, s3Key, stage: FILE_STAGE.Pending }) + + await salesApi.upsertTransactionFile(filename, { + status: DYNAMICS_IMPORT_STAGE.Pending, + dataSource: POST_OFFICE_DATASOURCE, + fileSize: fileSize, + salesDate: moment(receiptMoment).subtract(1, 'days').toISOString(), + receiptTimestamp: receiptMoment.toISOString(), + notes: 'Retrieved from the remote server and awaiting processing' + }) + + console.log(`Stored metadata for ${s3Key}`) +} diff --git a/packages/recurring-payments-job/Dockerfile b/packages/recurring-payments-job/Dockerfile index 1343d85743..82e32ea0eb 100644 --- a/packages/recurring-payments-job/Dockerfile +++ b/packages/recurring-payments-job/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /app # Install app dependencies COPY packages/recurring-payments-job/package*.json /app/ -RUN npm install && npm cache clean --force > /dev/null 2>&1 +RUN npm install --production && npm cache clean --force > /dev/null 2>&1 # Bundle app source COPY packages/recurring-payments-job/ /app diff --git a/packages/recurring-payments-job/package-lock.json b/packages/recurring-payments-job/package-lock.json index 6d8422743f..6b51463dcd 100644 --- a/packages/recurring-payments-job/package-lock.json +++ b/packages/recurring-payments-job/package-lock.json @@ -1,16 +1,16 @@ { "name": "@defra-fish/recurring-payments-job", - "version": "1.49.0-rc.10", + "version": "1.57.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@defra-fish/recurring-payments-job", - "version": "1.49.0-rc.10", + "version": "1.57.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@defra-fish/business-rules-lib": "1.49.0-rc.10", - "@defra-fish/connectors-lib": "1.49.0-rc.10", + "@defra-fish/business-rules-lib": "1.57.0", + "@defra-fish/connectors-lib": "1.57.0", "commander": "^7.2.0", "moment-timezone": "^0.5.34" }, @@ -18,251 +18,6 @@ "node": ">=18.17" } }, - "node_modules/@airbrake/browser": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@airbrake/browser/-/browser-2.1.8.tgz", - "integrity": "sha512-3xzpkQUq48R+hVbGlxUFLnv8dZg7M9OhBceX473ZrX4osxgfuKRqB+ecNawevKOftBrsjK2gNBayCXTbE+yFzQ==", - "dependencies": { - "@types/promise-polyfill": "^6.0.3", - "@types/request": "2.48.8", - "cross-fetch": "^3.1.5", - "error-stack-parser": "^2.0.4", - "promise-polyfill": "^8.1.3", - "tdigest": "^0.1.1" - } - }, - "node_modules/@airbrake/node": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@airbrake/node/-/node-2.1.8.tgz", - "integrity": "sha512-JuEFJk9hW+5YL4kSS+E6KuiBS9YleWnzo+Fu1j9E3VXOC8bGr+wxMGfhQGFuDBHmpco3g4wAY4t+IHZMtaN0rQ==", - "dependencies": { - "@airbrake/browser": "^2.1.8", - "cross-fetch": "^3.1.5", - "error-stack-parser": "^2.0.4", - "tdigest": "^0.1.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@defra-fish/business-rules-lib": { - "version": "1.49.0-rc.6", - "resolved": "https://registry.npmjs.org/@defra-fish/business-rules-lib/-/business-rules-lib-1.49.0-rc.6.tgz", - "integrity": "sha512-6uQ7vS1nYI9GHVgmGCC82Qgcn1b1MumQRWlEmytKXoj9d+BNcp0wVovaoaAgnev+71O5wvPYe9rYx4ucET5CEA==", - "dependencies": { - "joi": "^17.6.0", - "moment": "^2.29.1", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">=18.17" - } - }, - "node_modules/@defra-fish/connectors-lib": { - "version": "1.49.0-rc.6", - "resolved": "https://registry.npmjs.org/@defra-fish/connectors-lib/-/connectors-lib-1.49.0-rc.6.tgz", - "integrity": "sha512-VmIN4ijVXNnYdfA/J/zOQPdaLb7oaU7fetwcuzYqW4RBfeFdyDlAEaWTfpCTR6DQmOwsVmadzzoDksR+xFaa1Q==", - "dependencies": { - "@airbrake/node": "^2.1.7", - "aws-sdk": "^2.1074.0", - "debug": "^4.3.3", - "ioredis": "^4.28.5", - "node-fetch": "^2.6.7", - "redlock": "^4.2.0" - }, - "engines": { - "node": ">=18.17" - } - }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" - }, - "node_modules/@types/caseless": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", - "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==" - }, - "node_modules/@types/node": { - "version": "20.14.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", - "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/promise-polyfill": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/@types/promise-polyfill/-/promise-polyfill-6.0.6.tgz", - "integrity": "sha512-nKg0HIgdKRKfi5S3IlrpiNWqxiJOqYOV70jAtalqhvb5zJt5IoQMgy1QS3y5wsbUQPOCZHQxaPg+btBUVbA+hA==" - }, - "node_modules/@types/request": { - "version": "2.48.8", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", - "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", - "dependencies": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.0" - } - }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/aws-sdk": { - "version": "2.1641.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1641.0.tgz", - "integrity": "sha512-XkKbVu4VKFjY7wsTSWEYxBR2fVN8gUovAvRrHuJt9mMDdDh/wPkBZ04ayGT+Bd5bgmmIeE3sk3UMokKQEudJEQ==", - "hasInstallScript": true, - "dependencies": { - "buffer": "4.9.2", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.16.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "util": "^0.12.4", - "uuid": "8.0.0", - "xml2js": "0.6.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/aws-sdk/node_modules/uuid": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", - "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bintrees": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", - "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, - "node_modules/buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -271,361 +26,6 @@ "node": ">= 10" } }, - "node_modules/cross-fetch": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", - "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", - "dependencies": { - "node-fetch": "^2.6.12" - } - }, - "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/denque": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", - "integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/error-stack-parser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", - "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", - "dependencies": { - "stackframe": "^1.3.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ioredis": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz", - "integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==", - "dependencies": { - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.1", - "denque": "^1.1.0", - "lodash.defaults": "^4.2.0", - "lodash.flatten": "^4.4.0", - "lodash.isarguments": "^3.1.0", - "p-map": "^2.1.0", - "redis-commands": "1.7.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dependencies": { - "which-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/jmespath": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", - "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/joi": { - "version": "17.13.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.1.tgz", - "integrity": "sha512-vaBlIKCyo4FCUtCm7Eu4QZd/q02bWcxfUO6YSXAZOWF6gzcLBeba8kwotUdYJjDLW8Cz8RywsSOqiNJZW0mNvg==", - "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" - }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" - }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -644,230 +44,6 @@ "engines": { "node": "*" } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/promise-polyfill": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", - "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==" - }, - "node_modules/punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" - }, - "node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/redis-commands": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", - "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==" - }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/redlock": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/redlock/-/redlock-4.2.0.tgz", - "integrity": "sha512-j+oQlG+dOwcetUt2WJWttu4CZVeRzUrcVcISFmEmfyuwCVSJ93rDT7YSgg7H7rnxwoRyk/jU46kycVka5tW7jA==", - "dependencies": { - "bluebird": "^3.7.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/stackframe": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" - }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" - }, - "node_modules/tdigest": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", - "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", - "dependencies": { - "bintrees": "1.0.2" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - }, - "node_modules/url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", - "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "engines": { - "node": ">=4.0" - } } } } \ No newline at end of file diff --git a/packages/recurring-payments-job/package.json b/packages/recurring-payments-job/package.json index a21dc5a27d..8f097d6f3a 100644 --- a/packages/recurring-payments-job/package.json +++ b/packages/recurring-payments-job/package.json @@ -1,6 +1,6 @@ { "name": "@defra-fish/recurring-payments-job", - "version": "1.49.0-rc.10", + "version": "1.57.0", "description": "Rod Licensing Recurring Payments Job", "type": "module", "engines": { @@ -36,8 +36,8 @@ "test": "echo \"Error: run tests from root\" && exit 1" }, "dependencies": { - "@defra-fish/business-rules-lib": "1.49.0-rc.10", - "@defra-fish/connectors-lib": "1.49.0-rc.10", + "@defra-fish/business-rules-lib": "1.57.0", + "@defra-fish/connectors-lib": "1.57.0", "commander": "^7.2.0", "moment-timezone": "^0.5.34" } diff --git a/packages/sales-api-service/Dockerfile b/packages/sales-api-service/Dockerfile index 08062c1c4b..bcba21b0dc 100644 --- a/packages/sales-api-service/Dockerfile +++ b/packages/sales-api-service/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /app # Install app dependencies COPY packages/sales-api-service/package*.json /app/ -RUN npm install && npm cache clean --force > /dev/null 2>&1 +RUN npm install --production && npm cache clean --force > /dev/null 2>&1 # Bundle app source COPY packages/sales-api-service/ /app diff --git a/packages/sales-api-service/package-lock.json b/packages/sales-api-service/package-lock.json index 4310c6f193..ed699fa023 100644 --- a/packages/sales-api-service/package-lock.json +++ b/packages/sales-api-service/package-lock.json @@ -1,14 +1,17 @@ { "name": "@defra-fish/sales-api-service", - "version": "1.49.0-rc.10", + "version": "1.57.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@defra-fish/sales-api-service", - "version": "1.49.0-rc.10", + "version": "1.57.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { + "@defra-fish/business-rules-lib": "1.57.0", + "@defra-fish/connectors-lib": "1.57.0", + "@defra-fish/dynamics-lib": "1.57.0", "@hapi/boom": "^9.1.2", "@hapi/hapi": "^20.1.3", "@hapi/inert": "^6.0.3", @@ -619,9 +622,9 @@ } }, "node_modules/mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "engines": { "node": ">= 0.6" } diff --git a/packages/sales-api-service/package.json b/packages/sales-api-service/package.json index 53f3821b94..9171e408e5 100644 --- a/packages/sales-api-service/package.json +++ b/packages/sales-api-service/package.json @@ -1,6 +1,6 @@ { "name": "@defra-fish/sales-api-service", - "version": "1.49.0-rc.10", + "version": "1.57.0", "description": "Rod Licensing Sales API", "type": "module", "engines": { @@ -35,9 +35,9 @@ "test": "echo \"Error: run tests from root\" && exit 1" }, "dependencies": { - "@defra-fish/business-rules-lib": "1.49.0-rc.10", - "@defra-fish/connectors-lib": "1.49.0-rc.10", - "@defra-fish/dynamics-lib": "1.49.0-rc.10", + "@defra-fish/business-rules-lib": "1.57.0", + "@defra-fish/connectors-lib": "1.57.0", + "@defra-fish/dynamics-lib": "1.57.0", "@hapi/boom": "^9.1.2", "@hapi/hapi": "^20.1.3", "@hapi/inert": "^6.0.3", diff --git a/packages/sales-api-service/src/schema/__tests__/transaction.schema.spec.js b/packages/sales-api-service/src/schema/__tests__/transaction.schema.spec.js index 91ff7014a5..ace8302b15 100644 --- a/packages/sales-api-service/src/schema/__tests__/transaction.schema.spec.js +++ b/packages/sales-api-service/src/schema/__tests__/transaction.schema.spec.js @@ -18,7 +18,7 @@ describe('createTransactionSchema', () => { it('validates successfully when issueDate and startDate are null', async () => { const mockPayload = mockTransactionPayload() - mockPayload.permissions.map(p => ({ ...p, issueDate: null, startDate: null })) + mockPayload.permissions = mockPayload.permissions.map(p => ({ ...p, issueDate: null, startDate: null })) const result = await createTransactionSchema.validateAsync(mockPayload) expect(result).toBeInstanceOf(Object) }) @@ -70,6 +70,54 @@ describe('createTransactionSchema', () => { mockPayload.permissions[0].isLicenceForYou = 'test' await expect(createTransactionSchema.validateAsync(mockPayload)).rejects.toThrow('"permissions[0].isLicenceForYou" must be a boolean') }) + + it('validates successfully when a uuid v4 transactionId is supplied', async () => { + const mockPayload = mockTransactionPayload() + mockPayload.transactionId = '25fa0126-55da-4309-9bce-9957990d141e' + await expect(createTransactionSchema.validateAsync(mockPayload)).resolves.not.toThrow() + }) + + it('validates successfully when transactionId is omitted', async () => { + const mockPayload = mockTransactionPayload() + await expect(createTransactionSchema.validateAsync(mockPayload)).resolves.not.toThrow() + }) + + it.each([ + ['uuid1 string', '5a429f62-871b-11ef-b864-0242ac120002'], + ['uuid2 string', '000003e8-871b-21ef-8000-325096b39f47'], + ['uuid3 string', 'a3bb189e-8bf9-3888-9912-ace4e6543002'], + ['uuid5 string', 'a6edc906-2f9f-5fb2-a373-efac406f0ef2'], + ['uuid6 string', 'a3bb189e-8bf9-3888-9912-ace4e6543002'], + ['uuid7 string', '01927705-ffac-77b5-89af-c97451b1bbe2'], + ['numeric', 4567] + ])('fails validation when provided with a %s for transactionId', async (_d, transactionId) => { + const mockPayload = mockTransactionPayload() + mockPayload.transactionId = transactionId + await expect(createTransactionSchema.validateAsync(mockPayload)).rejects.toThrow() + }) + + it('validates successfully when an agreementId is supplied', async () => { + const mockPayload = mockTransactionPayload() + mockPayload.agreementId = 't3jl08v2nqqmujrnhs09pmhtjx' + await expect(createTransactionSchema.validateAsync(mockPayload)).resolves.not.toThrow() + }) + + it('validates successfully when agreementId is omitted', async () => { + const mockPayload = mockTransactionPayload() + await expect(createTransactionSchema.validateAsync(mockPayload)).resolves.not.toThrow() + }) + + it.each([ + ['too short string', 'foo'], + ['too long string', 'foobarbazfoobarbazfoobarbaz'], + ['string containing invalid characters', '!3j@08v2nqqmujrnhs09_mhtjx'], + ['null', null], + ['numeric', 4567] + ])('fails validation when provided with a %s for agreementId', async (_d, agreementId) => { + const mockPayload = mockTransactionPayload() + mockPayload.agreementId = agreementId + await expect(createTransactionSchema.validateAsync(mockPayload)).rejects.toThrow() + }) }) describe('createTransactionResponseSchema', () => { diff --git a/packages/sales-api-service/src/schema/transaction.schema.js b/packages/sales-api-service/src/schema/transaction.schema.js index 8cf9b97b55..44f9c61fc3 100644 --- a/packages/sales-api-service/src/schema/transaction.schema.js +++ b/packages/sales-api-service/src/schema/transaction.schema.js @@ -7,6 +7,8 @@ import { MAX_PERMISSIONS_PER_TRANSACTION, POCL_TRANSACTION_SOURCES } from '@defr import { v4 as uuidv4 } from 'uuid' +const AGREEMENT_ID_LENGTH = 26 + /** * Maximum number of items that can be created in a batch - limited by DynamoDB max batch size * @type {number} @@ -32,7 +34,9 @@ const createTransactionRequestSchemaContent = { then: Joi.string().trim().min(1).required() }), createdBy: Joi.string().optional(), - journalId: Joi.string().optional() + journalId: Joi.string().optional(), + transactionId: Joi.string().guid({ version: 'uuidv4' }).optional(), + agreementId: Joi.string().alphanum().length(AGREEMENT_ID_LENGTH).optional() } /** diff --git a/packages/sales-api-service/src/services/__tests__/__snapshots__/recurring-payments.service.spec.js.snap b/packages/sales-api-service/src/services/__tests__/__snapshots__/recurring-payments.service.spec.js.snap index e67aacad92..7c91321273 100644 --- a/packages/sales-api-service/src/services/__tests__/__snapshots__/recurring-payments.service.spec.js.snap +++ b/packages/sales-api-service/src/services/__tests__/__snapshots__/recurring-payments.service.spec.js.snap @@ -8,7 +8,7 @@ Object { "endDate": 2023-11-12T00:00:00.000Z, "name": "Test Name", "nextDueDate": 2023-11-02T00:00:00.000Z, - "publicId": "1234456", + "publicId": "abcdef99987", "status": 0, } `; diff --git a/packages/sales-api-service/src/services/__tests__/recurring-payments.service.spec.js b/packages/sales-api-service/src/services/__tests__/recurring-payments.service.spec.js index 6631d0c5e9..12f17f891c 100644 --- a/packages/sales-api-service/src/services/__tests__/recurring-payments.service.spec.js +++ b/packages/sales-api-service/src/services/__tests__/recurring-payments.service.spec.js @@ -1,5 +1,6 @@ -import { findDueRecurringPayments } from '@defra-fish/dynamics-lib' -import { getRecurringPayments, processRecurringPayment } from '../recurring-payments.service.js' +import { findDueRecurringPayments, Permission } from '@defra-fish/dynamics-lib' +import { getRecurringPayments, processRecurringPayment, generateRecurringPaymentRecord } from '../recurring-payments.service.js' +import { createHash } from 'node:crypto' jest.mock('@defra-fish/dynamics-lib', () => ({ ...jest.requireActual('@defra-fish/dynamics-lib'), @@ -8,6 +9,13 @@ jest.mock('@defra-fish/dynamics-lib', () => ({ findDueRecurringPayments: jest.fn() })) +jest.mock('node:crypto', () => ({ + createHash: jest.fn(() => ({ + update: () => {}, + digest: () => 'abcdef99987' + })) +})) + const dynamicsLib = jest.requireMock('@defra-fish/dynamics-lib') const getMockRecurringPayment = () => ({ @@ -81,6 +89,22 @@ const getMockPermission = () => ({ }) describe('recurring payments service', () => { + const createSimpleSampleTransactionRecord = () => ({ payment: { recurring: true }, permissions: [{}] }) + const createSamplePermission = overrides => { + const p = new Permission() + p.referenceNumber = 'ABC123' + p.issueDate = '2024-12-04T11:15:12Z' + p.startDate = '2024-12-04T11:45:12Z' + p.endDate = '2025-12-03T23:59:59.999Z' + p.stagingId = 'aaa-111-bbb-222' + p.isRenewal = false + p.isLicenseForYou = 1 + for (const key in overrides) { + p[key] = overrides[key] + } + return p + } + beforeEach(jest.clearAllMocks) describe('getRecurringPayments', () => { it('should equal result of findDueRecurringPayments query', async () => { @@ -123,7 +147,6 @@ describe('recurring payments service', () => { cancelledReason: null, endDate: new Date('2023-11-12'), agreementId: '435678', - publicId: '1234456', status: 0 } }, @@ -133,5 +156,187 @@ describe('recurring payments service', () => { const result = await processRecurringPayment(transactionRecord, contact) expect(result.recurringPayment).toMatchSnapshot() }) + + it.each(['abc-123', 'def-987'])('generates a publicId %s for the recurring payment', async samplePublicId => { + createHash.mockReturnValue({ + update: () => {}, + digest: () => samplePublicId + }) + const result = await processRecurringPayment(createSimpleSampleTransactionRecord(), getMockContact()) + expect(result.recurringPayment.publicId).toBe(samplePublicId) + }) + + it('passes the unique id of the entity to the hash.update function', async () => { + const update = jest.fn() + createHash.mockReturnValueOnce({ + update, + digest: () => {} + }) + const { recurringPayment } = await processRecurringPayment(createSimpleSampleTransactionRecord(), getMockContact()) + expect(update).toHaveBeenCalledWith(recurringPayment.uniqueContentId) + }) + + it('hashes using sha256', async () => { + await processRecurringPayment(createSimpleSampleTransactionRecord(), getMockContact()) + expect(createHash).toHaveBeenCalledWith('sha256') + }) + + it('uses base64 hash string', async () => { + const digest = jest.fn() + createHash.mockReturnValueOnce({ + update: () => {}, + digest + }) + await processRecurringPayment(createSimpleSampleTransactionRecord(), getMockContact()) + expect(digest).toHaveBeenCalledWith('base64') + }) + }) + + describe('generateRecurringPaymentRecord', () => { + const createFinalisedSampleTransaction = (agreementId, permission) => ({ + expires: 1732892402, + cost: 35.8, + isRecurringPaymentSupported: true, + permissions: [ + { + permitId: 'permit-id-1', + licensee: {}, + referenceNumber: '23211125-2WC3FBP-ABNDT8', + isLicenceForYou: true, + ...permission + } + ], + agreementId, + payment: { + amount: 35.8, + source: 'Gov Pay', + method: 'Debit card', + timestamp: '2024-11-22T15:00:45.922Z' + }, + id: 'd26d646f-ed0f-4cf1-b6c1-ccfbbd611757', + dataSource: 'Web Sales', + transactionId: 'd26d646f-ed0f-4cf1-b6c1-ccfbbd611757', + status: { id: 'FINALISED' } + }) + + it.each([ + [ + 'same day start - next due on issue date plus one year minus ten days', + 'iujhy7u8ijhy7u8iuuiuu8ie89', + { + startDate: '2024-11-22T15:30:45.922Z', + issueDate: '2024-11-22T15:00:45.922Z', + endDate: '2025-11-21T23:59:59.999Z' + }, + '2025-11-12T00:00:00.000Z' + ], + [ + 'next day start - next due on end date minus ten days', + '89iujhy7u8i87yu9iokjuij901', + { + startDate: '2024-11-23T00:00:00.000Z', + issueDate: '2024-11-22T15:00:45.922Z', + endDate: '2025-11-22T23:59:59.999Z' + }, + '2025-11-12T00:00:00.000Z' + ], + [ + 'starts ten days after issue - next due on issue date plus one year', + '9o8u7yhui89u8i9oiu8i8u7yhu', + { + startDate: '2024-11-22T00:00:00.000Z', + issueDate: '2024-11-12T15:00:45.922Z', + endDate: '2025-11-21T23:59:59.999Z' + }, + '2025-11-12T00:00:00.000Z' + ], + [ + 'starts twenty days after issue - next due on issue date plus one year', + '9o8u7yhui89u8i9oiu8i8u7yhu', + { + startDate: '2024-12-01T00:00:00.000Z', + issueDate: '2024-11-12T15:00:45.922Z', + endDate: '2025-01-30T23:59:59.999Z' + }, + '2025-11-12T00:00:00.000Z' + ], + [ + "issued on 29th Feb '24, starts on 30th March '24 - next due on 28th Feb '25", + 'hy7u8ijhyu78jhyu8iu8hjiujn', + { + startDate: '2024-03-30T00:00:00.000Z', + issueDate: '2024-02-29T12:38:24.123Z', + endDate: '2025-03-29T23:59:59.999Z' + }, + '2025-02-28T00:00:00.000Z' + ], + [ + "issued on 30th March '25 at 1am, starts at 1:30am - next due on 20th March '26", + 'jhy67uijhy67u87yhtgjui8u7j', + { + startDate: '2025-03-30T01:30:00.000Z', + issueDate: '2025-03-30T01:00:00.000Z', + endDate: '2026-03-29T23:59:59.999Z' + }, + '2026-03-20T00:00:00.000Z' + ] + ])('creates record from transaction with %s', (_d, agreementId, permissionData, expectedNextDueDate) => { + const sampleTransaction = createFinalisedSampleTransaction(agreementId, permissionData) + const permission = createSamplePermission(permissionData) + + const rpRecord = generateRecurringPaymentRecord(sampleTransaction, permission) + + expect(rpRecord).toEqual( + expect.objectContaining({ + payment: expect.objectContaining({ + recurring: expect.objectContaining({ + name: '', + nextDueDate: expectedNextDueDate, + cancelledDate: null, + cancelledReason: null, + endDate: permissionData.endDate, + agreementId, + status: 1 + }) + }), + permissions: expect.arrayContaining([permission]) + }) + ) + }) + + it.each([ + [ + 'start date is thirty one days after issue date', + { + startDate: '2024-12-14T00:00:00.000Z', + issueDate: '2024-11-12T15:00:45.922Z', + endDate: '2025-12-13T23:59:59.999Z' + } + ], + [ + 'start date precedes issue date', + { + startDate: '2024-11-11T00:00:00.000Z', + issueDate: '2024-11-12T15:00:45.922Z', + endDate: '2025-11-10T23:59:59.999Z' + } + ] + ])('throws an error for invalid dates when %s', (_d, permission) => { + const sampleTransaction = createFinalisedSampleTransaction('hyu78ijhyu78ijuhyu78ij9iu6', permission) + + expect(() => generateRecurringPaymentRecord(sampleTransaction)).toThrow('Invalid dates provided for permission') + }) + + it('returns a false flag when agreementId is not present', () => { + const sampleTransaction = createFinalisedSampleTransaction(null, { + startDate: '2024-11-22T15:30:45.922Z', + issueDate: '2024-11-22T15:00:45.922Z', + endDate: '2025-11-21T23:59:59.999Z' + }) + + const rpRecord = generateRecurringPaymentRecord(sampleTransaction) + + expect(rpRecord.payment.recurring).toBeFalsy() + }) }) }) diff --git a/packages/sales-api-service/src/services/recurring-payments.service.js b/packages/sales-api-service/src/services/recurring-payments.service.js index aee997d2a7..36e8e0bf3a 100644 --- a/packages/sales-api-service/src/services/recurring-payments.service.js +++ b/packages/sales-api-service/src/services/recurring-payments.service.js @@ -1,22 +1,62 @@ import { executeQuery, findDueRecurringPayments, RecurringPayment } from '@defra-fish/dynamics-lib' +import { createHash } from 'node:crypto' +import { ADVANCED_PURCHASE_MAX_DAYS } from '@defra-fish/business-rules-lib' +import moment from 'moment' export const getRecurringPayments = date => executeQuery(findDueRecurringPayments(date)) +const getNextDueDate = (startDate, issueDate, endDate) => { + const mStart = moment(startDate) + if (mStart.isAfter(moment(issueDate)) && mStart.isSameOrBefore(moment(issueDate).add(ADVANCED_PURCHASE_MAX_DAYS, 'days'), 'day')) { + if (mStart.isSame(moment(issueDate), 'day')) { + return moment(startDate).add(1, 'year').subtract(10, 'days').startOf('day').toISOString() + } + if (mStart.isBefore(moment(issueDate).add(10, 'days'), 'day')) { + return moment(endDate).subtract(10, 'days').startOf('day').toISOString() + } + return moment(issueDate).add(1, 'year').startOf('day').toISOString() + } + throw new Error('Invalid dates provided for permission') +} + +export const generateRecurringPaymentRecord = (transactionRecord, permission) => { + if (transactionRecord.agreementId) { + const [{ startDate, issueDate, endDate }] = transactionRecord.permissions + return { + payment: { + recurring: { + name: '', + nextDueDate: getNextDueDate(startDate, issueDate, endDate), + cancelledDate: null, + cancelledReason: null, + endDate, + agreementId: transactionRecord.agreementId, + status: 1 + } + }, + permissions: [permission] + } + } + return { payment: { recurring: false } } +} + /** * Process a recurring payment instruction * @param transactionRecord * @returns {Promise<{recurringPayment: RecurringPayment | null}>} */ export const processRecurringPayment = async (transactionRecord, contact) => { + const hash = createHash('sha256') if (transactionRecord.payment?.recurring) { const recurringPayment = new RecurringPayment() + hash.update(recurringPayment.uniqueContentId) recurringPayment.name = transactionRecord.payment.recurring.name recurringPayment.nextDueDate = transactionRecord.payment.recurring.nextDueDate recurringPayment.cancelledDate = transactionRecord.payment.recurring.cancelledDate recurringPayment.cancelledReason = transactionRecord.payment.recurring.cancelledReason recurringPayment.endDate = transactionRecord.payment.recurring.endDate recurringPayment.agreementId = transactionRecord.payment.recurring.agreementId - recurringPayment.publicId = transactionRecord.payment.recurring.publicId + recurringPayment.publicId = hash.digest('base64') recurringPayment.status = transactionRecord.payment.recurring.status const [permission] = transactionRecord.permissions recurringPayment.bindToEntity(RecurringPayment.definition.relationships.activePermission, permission) diff --git a/packages/sales-api-service/src/services/transactions/__tests__/create-transaction.spec.js b/packages/sales-api-service/src/services/transactions/__tests__/create-transaction.spec.js index 767528221d..c5da3b2f64 100644 --- a/packages/sales-api-service/src/services/transactions/__tests__/create-transaction.spec.js +++ b/packages/sales-api-service/src/services/transactions/__tests__/create-transaction.spec.js @@ -77,6 +77,13 @@ describe('transaction service', () => { AwsMock.DynamoDB.DocumentClient.__throwWithErrorOn('put') await expect(createTransaction(mockTransactionPayload())).rejects.toThrow('Test error') }) + + it('uses transaction id if supplied in payload', async () => { + const mockPayload = mockTransactionPayload() + mockPayload.transactionId = 'abc-123-def-456' + const result = await createTransaction(mockPayload) + expect(result.id).toBe(mockPayload.transactionId) + }) }) describe('createTransactions', () => { diff --git a/packages/sales-api-service/src/services/transactions/__tests__/process-transaction-queue.spec.js b/packages/sales-api-service/src/services/transactions/__tests__/process-transaction-queue.spec.js index f86a4c786d..4196a345b8 100644 --- a/packages/sales-api-service/src/services/transactions/__tests__/process-transaction-queue.spec.js +++ b/packages/sales-api-service/src/services/transactions/__tests__/process-transaction-queue.spec.js @@ -26,6 +26,7 @@ import { TRANSACTION_STAGING_TABLE, TRANSACTION_STAGING_HISTORY_TABLE } from '.. import AwsMock from 'aws-sdk' import { POCL_DATA_SOURCE, DDE_DATA_SOURCE } from '@defra-fish/business-rules-lib' import moment from 'moment' +import { processRecurringPayment, generateRecurringPaymentRecord } from '../../recurring-payments.service.js' jest.mock('../../reference-data.service.js', () => ({ ...jest.requireActual('../../reference-data.service.js'), @@ -64,9 +65,12 @@ jest.mock('@defra-fish/business-rules-lib', () => ({ START_AFTER_PAYMENT_MINUTES: 30 })) +jest.mock('../../recurring-payments.service.js') + describe('transaction service', () => { beforeAll(() => { TRANSACTION_STAGING_TABLE.TableName = 'TestTable' + processRecurringPayment.mockResolvedValue({}) }) beforeEach(jest.clearAllMocks) @@ -125,6 +129,7 @@ describe('transaction service', () => { [ 'licences with a recurring payment', () => { + processRecurringPayment.mockResolvedValueOnce({ recurringPayment: new RecurringPayment() }) const mockRecord = mockFinalisedTransactionRecord() mockRecord.payment.recurring = { name: 'Test name', @@ -143,9 +148,9 @@ describe('transaction service', () => { expect.any(Transaction), expect.any(TransactionJournal), expect.any(TransactionJournal), - expect.any(RecurringPayment), expect.any(Contact), expect.any(Permission), + expect.any(RecurringPayment), expect.any(RecurringPaymentInstruction), expect.any(ConcessionProof) ] @@ -369,6 +374,43 @@ describe('transaction service', () => { expect(paymentJournal.total).toBe(cost) }) }) + + describe('recurring payment processing', () => { + it('passes transaction record to generateRecurringPaymentRecord', async () => { + const callingArgs = {} + generateRecurringPaymentRecord.mockImplementationOnce(transaction => { + callingArgs.transaction = JSON.parse(JSON.stringify(transaction)) + }) + const mockRecord = mockFinalisedTransactionRecord() + AwsMock.DynamoDB.DocumentClient.__setResponse('get', { Item: mockRecord }) + await processQueue({ id: mockRecord.id }) + // jest.fn args aren't immutable and transaction is changed in processQueue, so we use our clone that hasn't changed + expect(callingArgs.transaction).toEqual(mockRecord) + }) + + it('passes permission to generateRecurringPaymentRecord', async () => { + const mockRecord = mockFinalisedTransactionRecord() + const expectedPermissionData = {} + const keysToCopy = ['referenceNumber', 'issueDate', 'startDate', 'endDate', 'isRenewal'] + for (const key of keysToCopy) { + expectedPermissionData[key] = mockRecord.permissions[0][key] + } + AwsMock.DynamoDB.DocumentClient.__setResponse('get', { Item: mockRecord }) + + await processQueue({ id: mockRecord.id }) + + expect(generateRecurringPaymentRecord).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining(expectedPermissionData)) + }) + + it('passes return value of generateRecurringPaymentRecord to processRecurringPayment', async () => { + const rprSymbol = Symbol('rpr') + const finalisedTransaction = mockFinalisedTransactionRecord() + generateRecurringPaymentRecord.mockReturnValueOnce(rprSymbol) + AwsMock.DynamoDB.DocumentClient.__setResponse('get', { Item: finalisedTransaction }) + await processQueue({ id: finalisedTransaction.id }) + expect(processRecurringPayment).toHaveBeenCalledWith(rprSymbol, expect.any(Contact)) + }) + }) }) describe('.getTransactionJournalRefNumber', () => { diff --git a/packages/sales-api-service/src/services/transactions/create-transaction.js b/packages/sales-api-service/src/services/transactions/create-transaction.js index 0d535d1d72..df2cc296bf 100644 --- a/packages/sales-api-service/src/services/transactions/create-transaction.js +++ b/packages/sales-api-service/src/services/transactions/create-transaction.js @@ -48,7 +48,7 @@ export async function createTransactions (payload) { * @returns {Promise<*>} */ async function createTransactionRecord (payload) { - const transactionId = uuidv4() + const transactionId = payload.transactionId || uuidv4() debug('Creating new transaction %s for %s', transactionId, payload.dataSource) const record = { id: transactionId, diff --git a/packages/sales-api-service/src/services/transactions/process-transaction-queue.js b/packages/sales-api-service/src/services/transactions/process-transaction-queue.js index 0af121ba12..e10221e4b7 100644 --- a/packages/sales-api-service/src/services/transactions/process-transaction-queue.js +++ b/packages/sales-api-service/src/services/transactions/process-transaction-queue.js @@ -12,7 +12,7 @@ import { } from '@defra-fish/dynamics-lib' import { DDE_DATA_SOURCE, FULFILMENT_SWITCHOVER_DATE, POCL_TRANSACTION_SOURCES } from '@defra-fish/business-rules-lib' import { getReferenceDataForEntityAndId, getGlobalOptionSetValue, getReferenceDataForEntity } from '../reference-data.service.js' -import { processRecurringPayment } from '../recurring-payments.service.js' +import { generateRecurringPaymentRecord, processRecurringPayment } from '../recurring-payments.service.js' import { resolveContactPayload } from '../contacts.service.js' import { retrieveStagedTransaction } from './retrieve-transaction.js' import { TRANSACTION_STAGING_TABLE, TRANSACTION_STAGING_HISTORY_TABLE } from '../../config.js' @@ -65,11 +65,6 @@ export async function processQueue ({ id }) { isRenewal ) - const { recurringPayment } = await processRecurringPayment(transactionRecord, contact) - if (recurringPayment) { - entities.push(recurringPayment) - } - permission.bindToEntity(Permission.definition.relationships.licensee, contact) permission.bindToEntity(Permission.definition.relationships.permit, permit) permission.bindToEntity(Permission.definition.relationships.transaction, transaction) @@ -78,7 +73,10 @@ export async function processQueue ({ id }) { entities.push(contact, permission) + const { recurringPayment } = await processRecurringPayment(generateRecurringPaymentRecord(transactionRecord, permission), contact) + if (recurringPayment && permit.isRecurringPaymentSupported) { + entities.push(recurringPayment) const paymentInstruction = new RecurringPaymentInstruction() paymentInstruction.bindToEntity(RecurringPaymentInstruction.definition.relationships.licensee, contact) paymentInstruction.bindToEntity(RecurringPaymentInstruction.definition.relationships.permit, permit) diff --git a/packages/sqs-receiver-service/package-lock.json b/packages/sqs-receiver-service/package-lock.json index 712bd5b31b..1efe962bf4 100644 --- a/packages/sqs-receiver-service/package-lock.json +++ b/packages/sqs-receiver-service/package-lock.json @@ -1,14 +1,15 @@ { "name": "@defra-fish/sqs-receiver-service", - "version": "1.49.0-rc.10", + "version": "1.57.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@defra-fish/sqs-receiver-service", - "version": "1.49.0-rc.10", + "version": "1.57.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { + "@defra-fish/connectors-lib": "1.57.0", "debug": "^4.3.1", "joi": "^17.3.0", "node-fetch": "^2.6.1", @@ -831,10 +832,13 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/get-uri": { "version": "3.0.2", @@ -1244,9 +1248,9 @@ } }, "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, diff --git a/packages/sqs-receiver-service/package.json b/packages/sqs-receiver-service/package.json index 9481fe36e3..1398b284b1 100644 --- a/packages/sqs-receiver-service/package.json +++ b/packages/sqs-receiver-service/package.json @@ -1,6 +1,6 @@ { "name": "@defra-fish/sqs-receiver-service", - "version": "1.49.0-rc.10", + "version": "1.57.0", "description": "SQS Receiver service", "type": "module", "engines": { @@ -35,7 +35,7 @@ "test": "echo \"Error: run tests from root\" && exit 1" }, "dependencies": { - "@defra-fish/connectors-lib": "1.49.0-rc.10", + "@defra-fish/connectors-lib": "1.57.0", "debug": "^4.3.1", "joi": "^17.3.0", "node-fetch": "^2.6.1",