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
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",