@defra-fish/sales-api-service 1.75.0-rc.2 → 1.75.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra-fish/sales-api-service",
3
- "version": "1.75.0-rc.2",
3
+ "version": "1.75.0-rc.4",
4
4
  "description": "Rod Licensing Sales API",
5
5
  "type": "module",
6
6
  "engines": {
@@ -35,9 +35,9 @@
35
35
  "test": "echo \"Error: run tests from root\" && exit 1"
36
36
  },
37
37
  "dependencies": {
38
- "@defra-fish/business-rules-lib": "1.75.0-rc.2",
39
- "@defra-fish/connectors-lib": "1.75.0-rc.2",
40
- "@defra-fish/dynamics-lib": "1.75.0-rc.2",
38
+ "@defra-fish/business-rules-lib": "1.75.0-rc.4",
39
+ "@defra-fish/connectors-lib": "1.75.0-rc.4",
40
+ "@defra-fish/dynamics-lib": "1.75.0-rc.4",
41
41
  "@hapi/boom": "9.1.2",
42
42
  "@hapi/hapi": "20.1.3",
43
43
  "@hapi/inert": "6.0.3",
@@ -53,5 +53,5 @@
53
53
  "moment-timezone": "0.5.34",
54
54
  "uuid": "8.3.2"
55
55
  },
56
- "gitHead": "4e154247f5b5a430aa7e6c26e71d75546d8d2411"
56
+ "gitHead": "663e9bf7baaedb86adc812f4a99f4accd70191be"
57
57
  }
@@ -168,50 +168,29 @@ describe('cancelRecurringPaymentRequestParamsSchema', () => {
168
168
  })
169
169
  })
170
170
 
171
- describe('cancelRecurringPaymentRequestParamsSchema', () => {
172
- it('validates expected object', async () => {
173
- const sampleData = { id: 'abc123' }
174
- expect(() => cancelRecurringPaymentRequestParamsSchema.validateAsync(sampleData)).not.toThrow()
175
- })
176
-
177
- it('throws an error if id missing', async () => {
178
- expect(() => cancelRecurringPaymentRequestParamsSchema.validateAsync({}).rejects.toThrow())
179
- })
180
-
181
- it('throws an error if id is not the correct type', async () => {
182
- const sampleData = { id: 99 }
183
- expect(() => cancelRecurringPaymentRequestParamsSchema.validateAsync(sampleData).rejects.toThrow())
184
- })
185
- })
186
-
187
171
  describe('cancelRecurringPaymentRequestQuerySchema', () => {
188
- it('validates expected object if the reason is Payment Failure', async () => {
189
- const sampleData = { reason: 'Payment Failure' }
190
- expect(() => cancelRecurringPaymentRequestQuerySchema.validateAsync(sampleData)).not.toThrow()
191
- })
192
-
193
- it('validates expected object if the reason is User Cancelled', async () => {
194
- const sampleData = { reason: 'User Cancelled' }
195
- expect(() => cancelRecurringPaymentRequestQuerySchema.validateAsync(sampleData)).not.toThrow()
172
+ it.each(['Payment Failure', 'User Cancelled', 'Business Cancelled'])('successfully validates when reason is %s', async reason => {
173
+ const sampleData = { reason }
174
+ await expect(cancelRecurringPaymentRequestQuerySchema.validateAsync(sampleData)).resolves.toEqual(sampleData)
196
175
  })
197
176
 
198
177
  it('throws an error if query is empty', async () => {
199
178
  const sampleData = {}
200
- expect(() => cancelRecurringPaymentRequestQuerySchema.validateAsync(sampleData)).rejects.toThrow()
179
+ await expect(cancelRecurringPaymentRequestQuerySchema.validateAsync(sampleData)).rejects.toThrow()
201
180
  })
202
181
 
203
182
  it('throws an error if reason is not the correct type', async () => {
204
183
  const sampleData = { reason: 99 }
205
- expect(() => cancelRecurringPaymentRequestQuerySchema.validateAsync(sampleData).rejects.toThrow())
184
+ await expect(cancelRecurringPaymentRequestQuerySchema.validateAsync(sampleData)).rejects.toThrow()
206
185
  })
207
186
 
208
- it('throws an error if reason is not the correct value', async () => {
209
- const sampleData = { reason: 'Because I said so' }
210
- expect(() => cancelRecurringPaymentRequestQuerySchema.validateAsync(sampleData).rejects.toThrow())
187
+ it('throws an error if reason is invalid', async () => {
188
+ const sampleData = { reason: 'Not A Valid Reason' }
189
+ await expect(cancelRecurringPaymentRequestQuerySchema.validateAsync(sampleData)).rejects.toThrow()
211
190
  })
212
191
 
213
192
  it('throws an error if an incorrect key is provided', async () => {
214
193
  const sampleData = { foo: 'Payment Failure' }
215
- expect(() => cancelRecurringPaymentRequestQuerySchema.validateAsync(sampleData).rejects.toThrow())
194
+ await expect(cancelRecurringPaymentRequestQuerySchema.validateAsync(sampleData)).rejects.toThrow()
216
195
  })
217
196
  })
@@ -35,5 +35,5 @@ export const cancelRecurringPaymentRequestParamsSchema = Joi.object({
35
35
  })
36
36
 
37
37
  export const cancelRecurringPaymentRequestQuerySchema = Joi.object({
38
- reason: Joi.string().required().valid('Payment Failure', 'User Cancelled')
38
+ reason: Joi.string().required().valid('Payment Failure', 'User Cancelled', 'Business Cancelled')
39
39
  })
@@ -3,7 +3,7 @@ import {
3
3
  executeQuery,
4
4
  findDueRecurringPayments,
5
5
  findRecurringPaymentsByAgreementId,
6
- findById,
6
+ findRecurringPaymentById,
7
7
  Permission,
8
8
  persist,
9
9
  RecurringPayment,
@@ -48,7 +48,6 @@ const { docClient, sqs } = AWS.mock.results[0].value
48
48
  jest.mock('@defra-fish/dynamics-lib', () => ({
49
49
  ...jest.requireActual('@defra-fish/dynamics-lib'),
50
50
  executeQuery: jest.fn(),
51
- findById: jest.fn(),
52
51
  findDueRecurringPayments: jest.fn(),
53
52
  findRecurringPaymentsByAgreementId: jest.fn(() => ({ toRetrieveRequest: () => {} })),
54
53
  dynamicsClient: {
@@ -56,6 +55,7 @@ jest.mock('@defra-fish/dynamics-lib', () => ({
56
55
  },
57
56
  persist: jest.fn(),
58
57
  findRecurringPaymentByPermissionId: jest.fn(() => ({ toRetrieveRequest: () => {} })),
58
+ findRecurringPaymentById: jest.fn(() => ({ toRetrieveRequest: () => {} })),
59
59
  retrieveGlobalOptionSets: jest.fn()
60
60
  }))
61
61
 
@@ -70,7 +70,8 @@ jest.mock('@defra-fish/connectors-lib', () => ({
70
70
  }
71
71
  })),
72
72
  govUkPayApi: {
73
- getRecurringPaymentAgreementInformation: jest.fn()
73
+ getRecurringPaymentAgreementInformation: jest.fn(),
74
+ cancelRecurringPaymentAgreement: jest.fn()
74
75
  }
75
76
  }))
76
77
 
@@ -895,93 +896,237 @@ describe('recurring payments service', () => {
895
896
  })
896
897
 
897
898
  describe('cancelRecurringPayment', () => {
898
- it('should call findById with RecurringPayment and the provided id', async () => {
899
- retrieveGlobalOptionSets.mockReturnValueOnce({ cached: jest.fn().mockResolvedValue({ definition: 'mock-def' }) })
900
- findById.mockReturnValueOnce(getMockRecurringPayment())
901
- const id = 'abc123'
902
- await cancelRecurringPayment(id, 'Payment Failure')
903
- expect(findById).toHaveBeenCalledWith(RecurringPayment, id)
899
+ const createLinkedPermission = (overrides = {}) => {
900
+ const permission = new Permission()
901
+ permission.isRecurringPayment = true
902
+ return Object.assign(permission, overrides)
903
+ }
904
+
905
+ const arrangeCancelRecurringPayment = ({
906
+ recurringPayment = getMockRecurringPayment(),
907
+ permissionOverrides = {},
908
+ govUkPayResponse = { ok: true, status: 204 },
909
+ linkedPermission = createLinkedPermission(permissionOverrides)
910
+ } = {}) => {
911
+ govUkPayApi.cancelRecurringPaymentAgreement.mockResolvedValueOnce(govUkPayResponse)
912
+ executeQuery.mockResolvedValueOnce([{ entity: recurringPayment, expanded: { activePermission: { entity: linkedPermission } } }])
913
+ return { recurringPayment, linkedPermission }
914
+ }
915
+
916
+ it.each(['92207e2c-0aa2-4600-a9c4-a7eb2276e404', 'f794dd0d-b80a-4d51-b738-479c87b4765b', 'b437d543-430f-4eb1-830d-539148c8b440'])(
917
+ 'passes recurring payment id to findRecurringPaymentById when id is %s',
918
+ async id => {
919
+ arrangeCancelRecurringPayment()
920
+
921
+ await cancelRecurringPayment(id, 'Payment Failure')
922
+
923
+ expect(findRecurringPaymentById).toHaveBeenCalledWith(id)
924
+ }
925
+ )
926
+
927
+ it('passes query created by findRecurringPaymentById to executeQuery', async () => {
928
+ const query = Symbol('query')
929
+ findRecurringPaymentById.mockReturnValueOnce(query)
930
+ arrangeCancelRecurringPayment()
931
+
932
+ await cancelRecurringPayment('id', 'User Cancelled')
933
+
934
+ expect(executeQuery).toHaveBeenCalledWith(query)
904
935
  })
905
936
 
906
- it('should set the reason based on the provided argument', async () => {
907
- retrieveGlobalOptionSets.mockReturnValueOnce({ cached: jest.fn().mockResolvedValue({ definition: 'mock-def' }) })
908
- findById.mockReturnValueOnce(getMockRecurringPayment())
937
+ it('passes reason to getGlobalOptionSetValue', async () => {
938
+ arrangeCancelRecurringPayment()
909
939
  const reason = Symbol('unique-reason')
940
+
910
941
  await cancelRecurringPayment('abc123', reason)
942
+
911
943
  expect(getGlobalOptionSetValue).toHaveBeenCalledWith(RecurringPayment.definition.mappings.cancelledReason.ref, reason)
912
944
  })
913
945
 
914
- it('should set cancelledDate when reason is not User Cancelled and call persist with the updated RecurringPayment', async () => {
915
- retrieveGlobalOptionSets.mockReturnValueOnce({
916
- cached: jest.fn().mockResolvedValue({
917
- defra_cancelledreasons: {
918
- options: {
919
- 910400002: {
920
- id: 910400002,
921
- label: 'Payment Failure',
922
- description: 'Payment Failure'
923
- }
924
- }
925
- }
946
+ it('sets cancelledReason on the persisted recurring payment', async () => {
947
+ const cancelledReason = { description: 'User Cancelled', id: 910400003, label: 'User Cancelled' }
948
+ const recurringPayment = { ...getMockRecurringPayment(), cancelledDate: null }
949
+ const { linkedPermission } = arrangeCancelRecurringPayment({ recurringPayment })
950
+
951
+ getGlobalOptionSetValue.mockReturnValueOnce(cancelledReason)
952
+
953
+ await cancelRecurringPayment('id', 'User Cancelled')
954
+
955
+ expect(persist).toHaveBeenCalledWith([
956
+ expect.objectContaining({
957
+ ...recurringPayment,
958
+ cancelledReason
959
+ }),
960
+ linkedPermission
961
+ ])
962
+ })
963
+
964
+ it('returns the updated recurring payment', async () => {
965
+ const recurringPayment = getMockRecurringPayment()
966
+ arrangeCancelRecurringPayment({ recurringPayment })
967
+
968
+ const result = await cancelRecurringPayment('id', 'User Cancelled')
969
+
970
+ expect(result).toEqual(
971
+ expect.objectContaining({
972
+ ...recurringPayment,
973
+ cancelledDate: expect.any(String),
974
+ cancelledReason: expect.any(Object)
926
975
  })
927
- })
976
+ )
977
+ })
978
+
979
+ it.each(['2026-05-20T09:10:11.123Z', '2027-01-01T00:00:00.000Z'])(
980
+ 'sets cancelledDate to current ISO date-time when now is %s',
981
+ async nowIsoString => {
982
+ const recurringPayment = getMockRecurringPayment()
983
+ const { linkedPermission } = arrangeCancelRecurringPayment({ recurringPayment })
984
+ jest.useFakeTimers().setSystemTime(new Date(nowIsoString))
985
+
986
+ try {
987
+ await cancelRecurringPayment('id', 'User Cancelled')
988
+
989
+ expect(persist).toHaveBeenCalledWith([
990
+ expect.objectContaining({
991
+ ...recurringPayment,
992
+ cancelledDate: nowIsoString
993
+ }),
994
+ linkedPermission
995
+ ])
996
+ } finally {
997
+ jest.useRealTimers()
998
+ }
999
+ }
1000
+ )
928
1001
 
1002
+ it('calls GovUKPay cancellation with agreement id', async () => {
929
1003
  const recurringPayment = getMockRecurringPayment()
930
- findById.mockReturnValueOnce(recurringPayment)
1004
+ arrangeCancelRecurringPayment({ recurringPayment })
931
1005
 
932
- const cancelledReason = { description: 'Payment Failure', id: 910400002, label: 'Payment Failure' }
1006
+ await cancelRecurringPayment('id', 'User Cancelled')
933
1007
 
934
- await cancelRecurringPayment('id', 'Payment Failure')
1008
+ expect(govUkPayApi.cancelRecurringPaymentAgreement).toHaveBeenCalledWith(recurringPayment.agreementId)
1009
+ })
1010
+
1011
+ it('debug outputs success message with agreement id when GovUKPay cancellation succeeds', async () => {
1012
+ const recurringPayment = getMockRecurringPayment()
1013
+ arrangeCancelRecurringPayment({ recurringPayment })
1014
+
1015
+ await cancelRecurringPayment('id', 'User Cancelled')
1016
+
1017
+ expect(debug).toHaveBeenCalledWith('Successfully cancelled GovUkPay agreement: %s', recurringPayment.agreementId)
1018
+ })
1019
+
1020
+ it.each([
1021
+ ['404 not found', 404, 'GovUkPay agreement not found (already cancelled or does not exist): %s'],
1022
+ ['400 bad request', 400, 'GovUkPay agreement cannot be cancelled (invalid state): %s']
1023
+ ])('debug outputs message with agreement id when GovUKPay returns %s', async (_description, status, expectedMessage) => {
1024
+ const recurringPayment = getMockRecurringPayment()
1025
+ arrangeCancelRecurringPayment({ recurringPayment, govUkPayResponse: { ok: false, status } })
1026
+
1027
+ await cancelRecurringPayment('id', 'User Cancelled')
1028
+
1029
+ expect(debug).toHaveBeenCalledWith(expectedMessage, recurringPayment.agreementId)
1030
+ })
1031
+
1032
+ it('throws when recurring payment has no agreement id', async () => {
1033
+ const recurringPayment = getMockRecurringPayment({ agreementId: undefined })
1034
+
1035
+ executeQuery.mockResolvedValueOnce([
1036
+ { entity: recurringPayment, expanded: { activePermission: { entity: createLinkedPermission() } } }
1037
+ ])
1038
+
1039
+ await expect(cancelRecurringPayment('recurring-payment-id', 'Payment Failure')).rejects.toThrow(
1040
+ 'Cannot cancel a recurring payment without an agreement ID: recurring-payment-id'
1041
+ )
1042
+ })
1043
+
1044
+ it.each([
1045
+ ['404 not found', 404, 'Not Found'],
1046
+ ['400 bad request', 400, 'Bad Request']
1047
+ ])('does not throw when GovUKPay returns %s', async (_description, status, statusText) => {
1048
+ arrangeCancelRecurringPayment({ govUkPayResponse: { ok: false, status, statusText } })
1049
+
1050
+ await expect(cancelRecurringPayment('id', 'User Cancelled')).resolves.toBeDefined()
1051
+ })
1052
+
1053
+ it.each([
1054
+ ['404 not found', 404, 'Not Found'],
1055
+ ['400 bad request', 400, 'Bad Request']
1056
+ ])('persists cancellation in CRM when GovUKPay returns %s', async (_description, status, statusText) => {
1057
+ const recurringPayment = getMockRecurringPayment()
1058
+ const { linkedPermission } = arrangeCancelRecurringPayment({ recurringPayment, govUkPayResponse: { ok: false, status, statusText } })
1059
+
1060
+ await cancelRecurringPayment('id', 'User Cancelled')
935
1061
 
936
1062
  expect(persist).toHaveBeenCalledWith([
937
1063
  expect.objectContaining({
938
1064
  ...recurringPayment,
939
- cancelledReason,
940
- cancelledDate: expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/)
941
- })
1065
+ cancelledDate: expect.any(String),
1066
+ cancelledReason: expect.any(Object)
1067
+ }),
1068
+ linkedPermission
942
1069
  ])
943
1070
  })
944
1071
 
945
- it('should not set cancelledDate when reason is User Cancelled', async () => {
946
- retrieveGlobalOptionSets.mockReturnValueOnce({
947
- cached: jest.fn().mockResolvedValue({
948
- defra_cancelledreasons: {
949
- options: {
950
- 910400003: {
951
- id: 910400003,
952
- label: 'User Cancelled',
953
- description: 'User Cancelled'
954
- }
955
- }
956
- }
957
- })
1072
+ it('throws when GovUKPay returns unexpected status', async () => {
1073
+ arrangeCancelRecurringPayment()
1074
+ govUkPayApi.cancelRecurringPaymentAgreement.mockReset().mockResolvedValueOnce({
1075
+ ok: false,
1076
+ status: 500,
1077
+ statusText: 'Internal Server Error',
1078
+ text: jest.fn().mockResolvedValue('Server error')
958
1079
  })
959
1080
 
960
- const cancelledReason = { description: 'User Cancelled', id: 910400003, label: 'User Cancelled' }
1081
+ await expect(cancelRecurringPayment('id', 'User Cancelled')).rejects.toThrow('Failed to cancel GovUkPay agreement')
1082
+ })
961
1083
 
962
- const recurringPayment = { ...getMockRecurringPayment(), cancelledDate: null }
963
- findById.mockReturnValueOnce(recurringPayment)
1084
+ it('sets linked permission isRecurringPayment to false', async () => {
1085
+ const { linkedPermission } = arrangeCancelRecurringPayment()
964
1086
 
965
- getGlobalOptionSetValue.mockReturnValueOnce(cancelledReason)
1087
+ await cancelRecurringPayment('id', 'User Cancelled')
1088
+
1089
+ expect(linkedPermission.isRecurringPayment).toBe(false)
1090
+ })
1091
+
1092
+ it('persists linked permission alongside recurring payment', async () => {
1093
+ const recurringPayment = getMockRecurringPayment()
1094
+ const { linkedPermission } = arrangeCancelRecurringPayment({ recurringPayment })
966
1095
 
967
1096
  await cancelRecurringPayment('id', 'User Cancelled')
968
1097
 
969
1098
  expect(persist).toHaveBeenCalledWith([
970
1099
  expect.objectContaining({
971
1100
  ...recurringPayment,
972
- cancelledReason,
973
- cancelledDate: null
974
- })
1101
+ cancelledDate: expect.any(String),
1102
+ cancelledReason: expect.any(Object)
1103
+ }),
1104
+ linkedPermission
975
1105
  ])
976
1106
  })
977
1107
 
978
- it('should raise an error when there are no matches', async () => {
979
- findById.mockReturnValueOnce(undefined)
1108
+ it('throws when recurring payment id is invalid', async () => {
1109
+ executeQuery.mockResolvedValueOnce([])
980
1110
 
981
- await expect(cancelRecurringPayment('id', 'Payment Failure')).rejects.toThrow(
982
- 'Invalid id provided for recurring payment cancellation'
1111
+ await expect(cancelRecurringPayment('recurring-payment-id', 'Payment Failure')).rejects.toThrow(
1112
+ 'Invalid id provided for recurring payment cancellation: recurring-payment-id'
983
1113
  )
984
1114
  })
1115
+
1116
+ it('throws when no active permission is linked to the recurring payment', async () => {
1117
+ executeQuery.mockResolvedValueOnce([{ entity: getMockRecurringPayment(), expanded: {} }])
1118
+
1119
+ await expect(cancelRecurringPayment('recurring-payment-id', 'Payment Failure')).rejects.toThrow(
1120
+ 'No active permission linked to recurring payment: recurring-payment-id'
1121
+ )
1122
+ })
1123
+
1124
+ it('propagates persist error after successful GovUKPay cancellation', async () => {
1125
+ arrangeCancelRecurringPayment()
1126
+ persist.mockRejectedValueOnce(new Error('CRM unavailable'))
1127
+
1128
+ await expect(cancelRecurringPayment('id', 'User Cancelled')).rejects.toThrow('CRM unavailable')
1129
+ })
985
1130
  })
986
1131
 
987
1132
  describe('findLinkedRecurringPayment', () => {
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  dynamicsClient,
3
3
  executeQuery,
4
- findById,
5
4
  findDueRecurringPayments,
6
5
  findRecurringPaymentsByAgreementId,
6
+ findRecurringPaymentById,
7
7
  persist,
8
8
  RecurringPayment,
9
9
  findRecurringPaymentByPermissionId,
@@ -21,6 +21,7 @@ import { getGlobalOptionSetValue } from './reference-data.service.js'
21
21
  import moment from 'moment'
22
22
  import { AWS, govUkPayApi } from '@defra-fish/connectors-lib'
23
23
  import db from 'debug'
24
+ import { StatusCodes } from 'http-status-codes'
24
25
  const debug = db('sales:recurring')
25
26
  const { sqs, docClient } = AWS()
26
27
 
@@ -167,21 +168,43 @@ export const findNewestExistingRecurringPaymentInCrm = async agreementId => {
167
168
  }
168
169
 
169
170
  export const cancelRecurringPayment = async (id, reason) => {
170
- const recurringPayment = await findById(RecurringPayment, id)
171
- if (recurringPayment) {
172
- const data = recurringPayment
173
- const isUserCancelled = reason === 'User Cancelled'
171
+ const [result] = await executeQuery(findRecurringPaymentById(id))
172
+ if (!result) {
173
+ throw new Error(`Invalid id provided for recurring payment cancellation: ${id}`)
174
+ }
174
175
 
175
- if (!isUserCancelled) {
176
- data.cancelledDate = new Date().toISOString().split('T')[0]
177
- }
176
+ const recurringPayment = result.entity
177
+ const linkedPermission = result.expanded?.activePermission?.entity
178
+ if (!linkedPermission) {
179
+ throw new Error(`No active permission linked to recurring payment: ${id}`)
180
+ }
181
+ if (!recurringPayment.agreementId) {
182
+ throw new Error(`Cannot cancel a recurring payment without an agreement ID: ${id}`)
183
+ }
184
+
185
+ recurringPayment.cancelledDate = new Date().toISOString()
186
+ recurringPayment.cancelledReason = await getGlobalOptionSetValue(RecurringPayment.definition.mappings.cancelledReason.ref, reason)
178
187
 
179
- data.cancelledReason = await getGlobalOptionSetValue(RecurringPayment.definition.mappings.cancelledReason.ref, reason)
180
- const updatedRecurringPayment = Object.assign(new RecurringPayment(), data)
181
- await persist([updatedRecurringPayment])
182
- return updatedRecurringPayment
188
+ await cancelGovUkPayAgreement(recurringPayment.agreementId)
189
+
190
+ const updatedRecurringPayment = Object.assign(new RecurringPayment(), recurringPayment)
191
+ linkedPermission.isRecurringPayment = false
192
+
193
+ await persist([updatedRecurringPayment, linkedPermission])
194
+ return updatedRecurringPayment
195
+ }
196
+
197
+ const cancelGovUkPayAgreement = async agreementId => {
198
+ const response = await govUkPayApi.cancelRecurringPaymentAgreement(agreementId)
199
+ if (response.ok) {
200
+ debug('Successfully cancelled GovUkPay agreement: %s', agreementId)
201
+ } else if (response.status === StatusCodes.NOT_FOUND) {
202
+ debug('GovUkPay agreement not found (already cancelled or does not exist): %s', agreementId)
203
+ } else if (response.status === StatusCodes.BAD_REQUEST) {
204
+ debug('GovUkPay agreement cannot be cancelled (invalid state): %s', agreementId)
183
205
  } else {
184
- throw new Error('Invalid id provided for recurring payment cancellation')
206
+ const body = await response.text().catch(() => 'Unable to read response body')
207
+ throw new Error(`Failed to cancel GovUkPay agreement ${agreementId}: ${response.status} ${response.statusText} - ${body}`)
185
208
  }
186
209
  }
187
210