From ea81819396a7acc4c784c7c6beac318f1d74363b Mon Sep 17 00:00:00 2001 From: laila aleissa <138867360+lailien3@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:56:06 +0100 Subject: [PATCH 1/5] Cancel agreement via Sales API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://eaflood.atlassian.net/browse/IWTF-5037 We want to notify GovPay when an agreement is cancelled So that we’re not relying on the CRM for this logic It’s done when we don’t have special handling in packages/sales-api-service/src/services/recurring-payments.service.js for omitting the cancellation date when it’s user cancelled cancellation is initiated by a POST from the Sales API to GovPay API unless by refund using the 3-step CRM process all possible responses from Gov Pay are handled appropriately --- .../src/__tests__/govuk-pay-api.spec.js | 21 +++++++ packages/connectors-lib/src/govuk-pay-api.js | 18 ++++++ .../recurring-payments.service.spec.js | 60 ++++++++++++++++++- .../services/recurring-payments.service.js | 23 +++++-- 4 files changed, 115 insertions(+), 7 deletions(-) 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 22a37f6e94..7461452696 100644 --- a/packages/connectors-lib/src/__tests__/govuk-pay-api.spec.js +++ b/packages/connectors-lib/src/__tests__/govuk-pay-api.spec.js @@ -203,4 +203,25 @@ describe('govuk-pay-api-connector', () => { expect(consoleErrorSpy).toHaveBeenCalled() }) }) + + describe('cancelRecurringPaymentAgreement', () => { + it('cancels a recurring payment agreement', async () => { + fetch.mockReturnValueOnce({ ok: true, status: 204 }) + await expect(govUkPayApi.cancelRecurringPaymentAgreement(123)).resolves.toEqual(expect.objectContaining({ ok: true, status: 204 })) + expect(fetch).toHaveBeenCalledWith('http://0.0.0.0/agreement/123/cancel', { + 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('cancel error') + }) + await expect(govUkPayApi.cancelRecurringPaymentAgreement(123)).rejects.toEqual(Error('cancel error')) + expect(consoleErrorSpy).toHaveBeenCalled() + }) + }) }) diff --git a/packages/connectors-lib/src/govuk-pay-api.js b/packages/connectors-lib/src/govuk-pay-api.js index 1274ba2d5e..0c8f3ed585 100644 --- a/packages/connectors-lib/src/govuk-pay-api.js +++ b/packages/connectors-lib/src/govuk-pay-api.js @@ -113,3 +113,21 @@ export const getRecurringPaymentAgreementInformation = async agreementId => { throw err } } + +/** + * Cancel a recurring payment agreement in GOV.UK Pay + * @param agreementId - the agreement to cancel + * @returns {Promise<*>} + */ +export const cancelRecurringPaymentAgreement = async agreementId => { + try { + return fetch(`${process.env.GOV_PAY_RCP_API_URL}/${agreementId}/cancel`, { + headers: headers(true), + method: 'post', + timeout: process.env.GOV_PAY_REQUEST_TIMEOUT_MS || GOV_PAY_REQUEST_TIMEOUT_MS_DEFAULT + }) + } catch (err) { + console.error(`Error cancelling recurring payment agreement in the GOV.UK API service - agreementId: ${agreementId}`, err) + throw err + } +} 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 f6e6f8e797..4ff06eee8b 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 @@ -70,7 +70,8 @@ jest.mock('@defra-fish/connectors-lib', () => ({ } })), govUkPayApi: { - getRecurringPaymentAgreementInformation: jest.fn() + getRecurringPaymentAgreementInformation: jest.fn(), + cancelRecurringPaymentAgreement: jest.fn() } })) @@ -895,6 +896,10 @@ describe('recurring payments service', () => { }) describe('cancelRecurringPayment', () => { + beforeEach(() => { + govUkPayApi.cancelRecurringPaymentAgreement.mockResolvedValue({ ok: true, status: 204 }) + }) + it('should call findById with RecurringPayment and the provided id', async () => { retrieveGlobalOptionSets.mockReturnValueOnce({ cached: jest.fn().mockResolvedValue({ definition: 'mock-def' }) }) findById.mockReturnValueOnce(getMockRecurringPayment()) @@ -911,7 +916,7 @@ describe('recurring payments service', () => { expect(getGlobalOptionSetValue).toHaveBeenCalledWith(RecurringPayment.definition.mappings.cancelledReason.ref, reason) }) - it('should set cancelledDate when reason is not User Cancelled and call persist with the updated RecurringPayment', async () => { + it('should set cancelledDate when reason is Payment Failure and call persist with the updated RecurringPayment', async () => { retrieveGlobalOptionSets.mockReturnValueOnce({ cached: jest.fn().mockResolvedValue({ defra_cancelledreasons: { @@ -970,11 +975,60 @@ describe('recurring payments service', () => { expect.objectContaining({ ...recurringPayment, cancelledReason, - cancelledDate: null + cancelledDate: expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/) }) ]) }) + it('should call cancelRecurringPaymentAgreement on GovPay when agreementId exists', async () => { + const recurringPayment = getMockRecurringPayment() + findById.mockReturnValueOnce(recurringPayment) + + await cancelRecurringPayment('id', 'User Cancelled') + + expect(govUkPayApi.cancelRecurringPaymentAgreement).toHaveBeenCalledWith(recurringPayment.agreementId) + }) + + it('should not call cancelRecurringPaymentAgreement on GovPay when agreementId does not exist', async () => { + const recurringPayment = getMockRecurringPayment({ agreementId: undefined }) + findById.mockReturnValueOnce(recurringPayment) + + await cancelRecurringPayment('id', 'Payment Failure') + + expect(govUkPayApi.cancelRecurringPaymentAgreement).not.toHaveBeenCalled() + }) + + it('should succeed when GovPay returns 404 (agreement already cancelled)', async () => { + const recurringPayment = getMockRecurringPayment() + findById.mockReturnValueOnce(recurringPayment) + govUkPayApi.cancelRecurringPaymentAgreement.mockResolvedValueOnce({ ok: false, status: 404, statusText: 'Not Found' }) + + await expect(cancelRecurringPayment('id', 'User Cancelled')).resolves.toBeDefined() + expect(persist).toHaveBeenCalled() + }) + + it('should succeed when GovPay returns 400 (agreement in invalid state)', async () => { + const recurringPayment = getMockRecurringPayment() + findById.mockReturnValueOnce(recurringPayment) + govUkPayApi.cancelRecurringPaymentAgreement.mockResolvedValueOnce({ ok: false, status: 400, statusText: 'Bad Request' }) + + await expect(cancelRecurringPayment('id', 'User Cancelled')).resolves.toBeDefined() + expect(persist).toHaveBeenCalled() + }) + + it('should throw when GovPay returns an unexpected error', async () => { + const recurringPayment = getMockRecurringPayment() + findById.mockReturnValueOnce(recurringPayment) + govUkPayApi.cancelRecurringPaymentAgreement.mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + text: jest.fn().mockResolvedValue('Server error') + }) + + await expect(cancelRecurringPayment('id', 'User Cancelled')).rejects.toThrow('Failed to cancel GovPay agreement') + }) + it('should raise an error when there are no matches', async () => { findById.mockReturnValueOnce(undefined) 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 83003db82a..61a9b0124e 100644 --- a/packages/sales-api-service/src/services/recurring-payments.service.js +++ b/packages/sales-api-service/src/services/recurring-payments.service.js @@ -170,13 +170,14 @@ export const cancelRecurringPayment = async (id, reason) => { const recurringPayment = await findById(RecurringPayment, id) if (recurringPayment) { const data = recurringPayment - const isUserCancelled = reason === 'User Cancelled' - if (!isUserCancelled) { - data.cancelledDate = new Date().toISOString().split('T')[0] + data.cancelledDate = new Date().toISOString().split('T')[0] + data.cancelledReason = await getGlobalOptionSetValue(RecurringPayment.definition.mappings.cancelledReason.ref, reason) + + if (data.agreementId) { + await cancelGovPayAgreement(data.agreementId) } - data.cancelledReason = await getGlobalOptionSetValue(RecurringPayment.definition.mappings.cancelledReason.ref, reason) const updatedRecurringPayment = Object.assign(new RecurringPayment(), data) await persist([updatedRecurringPayment]) return updatedRecurringPayment @@ -185,6 +186,20 @@ export const cancelRecurringPayment = async (id, reason) => { } } +const cancelGovPayAgreement = async agreementId => { + const response = await govUkPayApi.cancelRecurringPaymentAgreement(agreementId) + if (response.ok) { + debug('Successfully cancelled GovPay agreement: %s', agreementId) + } else if (response.status === 404) { + debug('GovPay agreement not found (already cancelled or does not exist): %s', agreementId) + } else if (response.status === 400) { + debug('GovPay agreement cannot be cancelled (invalid state): %s', agreementId) + } else { + const body = await response.text().catch(() => 'Unable to read response body') + throw new Error(`Failed to cancel GovPay agreement ${agreementId}: ${response.status} ${response.statusText} - ${body}`) + } +} + const determineRecurringPaymentName = (transactionRecord, contact) => { const [dueYear] = transactionRecord.payment.recurring.nextDueDate.split('-') return [contact.firstName, contact.lastName, dueYear].join(' ') From 204f3ef75825ff6fb59b8544b78ef74f3abc7616 Mon Sep 17 00:00:00 2001 From: laila aleissa <138867360+lailien3@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:00:43 +0100 Subject: [PATCH 2/5] split tests --- .../src/__tests__/govuk-pay-api.spec.js | 5 +++++ .../recurring-payments.service.spec.js | 20 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) 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 7461452696..a043bbe4be 100644 --- a/packages/connectors-lib/src/__tests__/govuk-pay-api.spec.js +++ b/packages/connectors-lib/src/__tests__/govuk-pay-api.spec.js @@ -208,6 +208,11 @@ describe('govuk-pay-api-connector', () => { it('cancels a recurring payment agreement', async () => { fetch.mockReturnValueOnce({ ok: true, status: 204 }) await expect(govUkPayApi.cancelRecurringPaymentAgreement(123)).resolves.toEqual(expect.objectContaining({ ok: true, status: 204 })) + }) + + it('calls the correct endpoint with recurring headers', async () => { + fetch.mockReturnValueOnce({ ok: true, status: 204 }) + await govUkPayApi.cancelRecurringPaymentAgreement(123) expect(fetch).toHaveBeenCalledWith('http://0.0.0.0/agreement/123/cancel', { headers: recurringHeaders, method: 'post', 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 4ff06eee8b..6f9eb96074 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 @@ -998,21 +998,37 @@ describe('recurring payments service', () => { expect(govUkPayApi.cancelRecurringPaymentAgreement).not.toHaveBeenCalled() }) - it('should succeed when GovPay returns 404 (agreement already cancelled)', async () => { + it('should not throw when GovPay returns 404 (agreement already cancelled)', async () => { const recurringPayment = getMockRecurringPayment() findById.mockReturnValueOnce(recurringPayment) govUkPayApi.cancelRecurringPaymentAgreement.mockResolvedValueOnce({ ok: false, status: 404, statusText: 'Not Found' }) await expect(cancelRecurringPayment('id', 'User Cancelled')).resolves.toBeDefined() + }) + + it('should still persist to CRM when GovPay returns 404', async () => { + const recurringPayment = getMockRecurringPayment() + findById.mockReturnValueOnce(recurringPayment) + govUkPayApi.cancelRecurringPaymentAgreement.mockResolvedValueOnce({ ok: false, status: 404, statusText: 'Not Found' }) + + await cancelRecurringPayment('id', 'User Cancelled') expect(persist).toHaveBeenCalled() }) - it('should succeed when GovPay returns 400 (agreement in invalid state)', async () => { + it('should not throw when GovPay returns 400 (agreement in invalid state)', async () => { const recurringPayment = getMockRecurringPayment() findById.mockReturnValueOnce(recurringPayment) govUkPayApi.cancelRecurringPaymentAgreement.mockResolvedValueOnce({ ok: false, status: 400, statusText: 'Bad Request' }) await expect(cancelRecurringPayment('id', 'User Cancelled')).resolves.toBeDefined() + }) + + it('should still persist to CRM when GovPay returns 400', async () => { + const recurringPayment = getMockRecurringPayment() + findById.mockReturnValueOnce(recurringPayment) + govUkPayApi.cancelRecurringPaymentAgreement.mockResolvedValueOnce({ ok: false, status: 400, statusText: 'Bad Request' }) + + await cancelRecurringPayment('id', 'User Cancelled') expect(persist).toHaveBeenCalled() }) From 33017b6f2f7966de6b1d7cae31561e21358a92e5 Mon Sep 17 00:00:00 2001 From: laila aleissa <138867360+lailien3@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:10:42 +0100 Subject: [PATCH 3/5] change status codes --- .../src/services/recurring-payments.service.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 61a9b0124e..f04041602b 100644 --- a/packages/sales-api-service/src/services/recurring-payments.service.js +++ b/packages/sales-api-service/src/services/recurring-payments.service.js @@ -21,6 +21,7 @@ import { getGlobalOptionSetValue } from './reference-data.service.js' import moment from 'moment' import { AWS, govUkPayApi } from '@defra-fish/connectors-lib' import db from 'debug' +import { StatusCodes } from 'http-status-codes' const debug = db('sales:recurring') const { sqs, docClient } = AWS() @@ -190,9 +191,9 @@ const cancelGovPayAgreement = async agreementId => { const response = await govUkPayApi.cancelRecurringPaymentAgreement(agreementId) if (response.ok) { debug('Successfully cancelled GovPay agreement: %s', agreementId) - } else if (response.status === 404) { + } else if (response.status === StatusCodes.BAD_REQUEST) { debug('GovPay agreement not found (already cancelled or does not exist): %s', agreementId) - } else if (response.status === 400) { + } else if (response.status === StatusCodes.BAD_REQUEST) { debug('GovPay agreement cannot be cancelled (invalid state): %s', agreementId) } else { const body = await response.text().catch(() => 'Unable to read response body') From 63bdec41d03efcc7b83dea24aa46e9a61defc242 Mon Sep 17 00:00:00 2001 From: laila aleissa <138867360+lailien3@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:13:17 +0100 Subject: [PATCH 4/5] oopsie --- .../src/services/recurring-payments.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f04041602b..4ab515fb30 100644 --- a/packages/sales-api-service/src/services/recurring-payments.service.js +++ b/packages/sales-api-service/src/services/recurring-payments.service.js @@ -191,7 +191,7 @@ const cancelGovPayAgreement = async agreementId => { const response = await govUkPayApi.cancelRecurringPaymentAgreement(agreementId) if (response.ok) { debug('Successfully cancelled GovPay agreement: %s', agreementId) - } else if (response.status === StatusCodes.BAD_REQUEST) { + } else if (response.status === StatusCodes.NOT_FOUND) { debug('GovPay agreement not found (already cancelled or does not exist): %s', agreementId) } else if (response.status === StatusCodes.BAD_REQUEST) { debug('GovPay agreement cannot be cancelled (invalid state): %s', agreementId) From 9251f9f60e3dbdbc65ba422f147883fcf62b829e Mon Sep 17 00:00:00 2001 From: laila aleissa <138867360+lailien3@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:29:35 +0100 Subject: [PATCH 5/5] add new schema for if business cancel --- .../src/schema/recurring-payments.schema.js | 2 +- .../recurring-payments.service.spec.js | 70 +++++++++++++++++-- .../services/recurring-payments.service.js | 24 ++++++- 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/packages/sales-api-service/src/schema/recurring-payments.schema.js b/packages/sales-api-service/src/schema/recurring-payments.schema.js index 1e5e9ab03b..5bac35b64e 100644 --- a/packages/sales-api-service/src/schema/recurring-payments.schema.js +++ b/packages/sales-api-service/src/schema/recurring-payments.schema.js @@ -35,5 +35,5 @@ export const cancelRecurringPaymentRequestParamsSchema = Joi.object({ }) export const cancelRecurringPaymentRequestQuerySchema = Joi.object({ - reason: Joi.string().required().valid('Payment Failure', 'User Cancelled') + reason: Joi.string().required().valid('Payment Failure', 'User Cancelled', 'Business Cancelled') }) 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 6f9eb96074..4a09898206 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 @@ -52,7 +52,8 @@ jest.mock('@defra-fish/dynamics-lib', () => ({ findDueRecurringPayments: jest.fn(), findRecurringPaymentsByAgreementId: jest.fn(() => ({ toRetrieveRequest: () => {} })), dynamicsClient: { - retrieveMultipleRequest: jest.fn(() => ({ value: [] })) + retrieveMultipleRequest: jest.fn(() => ({ value: [] })), + retrieveRequest: jest.fn(() => ({ _defra_activepermission_value: 'mock-permission-id' })) }, persist: jest.fn(), findRecurringPaymentByPermissionId: jest.fn(() => ({ toRetrieveRequest: () => {} })), @@ -896,8 +897,16 @@ describe('recurring payments service', () => { }) describe('cancelRecurringPayment', () => { + const mockPermission = new Permission() + mockPermission.isRecurringPayment = true + beforeEach(() => { govUkPayApi.cancelRecurringPaymentAgreement.mockResolvedValue({ ok: true, status: 204 }) + dynamicsClient.retrieveRequest.mockResolvedValue({ _defra_activepermission_value: 'mock-permission-id' }) + findById.mockImplementation(entityType => { + if (entityType === Permission) return mockPermission + return null + }) }) it('should call findById with RecurringPayment and the provided id', async () => { @@ -943,11 +952,12 @@ describe('recurring payments service', () => { ...recurringPayment, cancelledReason, cancelledDate: expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/) - }) + }), + mockPermission ]) }) - it('should not set cancelledDate when reason is User Cancelled', async () => { + it('should set cancelledDate when reason is User Cancelled', async () => { retrieveGlobalOptionSets.mockReturnValueOnce({ cached: jest.fn().mockResolvedValue({ defra_cancelledreasons: { @@ -976,7 +986,8 @@ describe('recurring payments service', () => { ...recurringPayment, cancelledReason, cancelledDate: expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/) - }) + }), + mockPermission ]) }) @@ -1045,6 +1056,57 @@ describe('recurring payments service', () => { await expect(cancelRecurringPayment('id', 'User Cancelled')).rejects.toThrow('Failed to cancel GovPay agreement') }) + it('should set isRecurringPayment to false on the linked permission', async () => { + const recurringPayment = getMockRecurringPayment() + findById.mockReturnValueOnce(recurringPayment) + + await cancelRecurringPayment('id', 'User Cancelled') + + expect(mockPermission.isRecurringPayment).toBe(false) + }) + + it('should persist the linked permission alongside the recurring payment', async () => { + const recurringPayment = getMockRecurringPayment() + findById.mockReturnValueOnce(recurringPayment) + + await cancelRecurringPayment('id', 'User Cancelled') + + expect(persist).toHaveBeenCalledWith([expect.any(RecurringPayment), mockPermission]) + }) + + it('should query dynamics for the active permission lookup value', async () => { + const recurringPayment = getMockRecurringPayment() + findById.mockReturnValueOnce(recurringPayment) + + await cancelRecurringPayment('id', 'User Cancelled') + + expect(dynamicsClient.retrieveRequest).toHaveBeenCalledWith({ + key: 'id', + collection: RecurringPayment.definition.dynamicsCollection, + select: ['_defra_activepermission_value'] + }) + }) + + it('should look up the permission by the id returned from dynamics', async () => { + const recurringPayment = getMockRecurringPayment() + findById.mockReturnValueOnce(recurringPayment) + dynamicsClient.retrieveRequest.mockResolvedValueOnce({ _defra_activepermission_value: 'specific-permission-id' }) + + await cancelRecurringPayment('id', 'User Cancelled') + + expect(findById).toHaveBeenCalledWith(Permission, 'specific-permission-id') + }) + + it('should still persist when no linked permission is found', async () => { + const recurringPayment = getMockRecurringPayment() + findById.mockReturnValueOnce(recurringPayment) + dynamicsClient.retrieveRequest.mockResolvedValueOnce({ _defra_activepermission_value: null }) + + await cancelRecurringPayment('id', 'User Cancelled') + + expect(persist).toHaveBeenCalledWith([expect.any(RecurringPayment)]) + }) + it('should raise an error when there are no matches', async () => { findById.mockReturnValueOnce(undefined) 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 4ab515fb30..177ec8ade8 100644 --- a/packages/sales-api-service/src/services/recurring-payments.service.js +++ b/packages/sales-api-service/src/services/recurring-payments.service.js @@ -5,6 +5,7 @@ import { findDueRecurringPayments, findRecurringPaymentsByAgreementId, persist, + Permission, RecurringPayment, findRecurringPaymentByPermissionId, retrieveGlobalOptionSets @@ -180,13 +181,34 @@ export const cancelRecurringPayment = async (id, reason) => { } const updatedRecurringPayment = Object.assign(new RecurringPayment(), data) - await persist([updatedRecurringPayment]) + const entitiesToPersist = [updatedRecurringPayment] + + const linkedPermission = await getLinkedPermission(id) + if (linkedPermission) { + linkedPermission.isRecurringPayment = false + entitiesToPersist.push(linkedPermission) + } + + await persist(entitiesToPersist) return updatedRecurringPayment } else { throw new Error('Invalid id provided for recurring payment cancellation') } } +const getLinkedPermission = async recurringPaymentId => { + const record = await dynamicsClient.retrieveRequest({ + key: recurringPaymentId, + collection: RecurringPayment.definition.dynamicsCollection, + select: ['_defra_activepermission_value'] + }) + const permissionId = record._defra_activepermission_value + if (permissionId) { + return findById(Permission, permissionId) + } + return null +} + const cancelGovPayAgreement = async agreementId => { const response = await govUkPayApi.cancelRecurringPaymentAgreement(agreementId) if (response.ok) {