From b16693697cd1276b77f6d4d5b4bbb53c280415b7 Mon Sep 17 00:00:00 2001
From: Tedd <teddmason@gmail.com>
Date: Tue, 21 Jan 2025 15:32:07 +0000
Subject: [PATCH 1/5] FCRM-5514 - return england-only route to service (#453)

---
 server/constants.js                          |  2 +
 server/routes/__tests__/england-only.spec.js | 52 ++++++++++++++++++++
 server/routes/__tests__/location.spec.js     | 26 +++++++++-
 server/routes/england-only.js                |  4 +-
 server/routes/location.js                    | 40 +++++++++------
 server/services/__tests__/is-england.spec.js | 16 +++---
 server/services/is-england.js                | 19 +++----
 server/views/england-only.html               |  9 ++--
 8 files changed, 127 insertions(+), 41 deletions(-)
 create mode 100644 server/routes/__tests__/england-only.spec.js

diff --git a/server/constants.js b/server/constants.js
index 77d4866b..1fc61b46 100644
--- a/server/constants.js
+++ b/server/constants.js
@@ -3,6 +3,7 @@ const HOME = 'home'
 const TRIAGE = 'triage'
 const ABOUT_MAP = 'about-map'
 const LOCATION = 'location'
+const ENGLAND_ONLY = 'england-only'
 const MAP = 'map'
 const RESULTS = 'results'
 const CONTACT = 'contact'
@@ -18,6 +19,7 @@ const views = {
   TRIAGE,
   ABOUT_MAP,
   LOCATION,
+  ENGLAND_ONLY,
   MAP,
   RESULTS,
   CONTACT,
diff --git a/server/routes/__tests__/england-only.spec.js b/server/routes/__tests__/england-only.spec.js
new file mode 100644
index 00000000..2ad752b5
--- /dev/null
+++ b/server/routes/__tests__/england-only.spec.js
@@ -0,0 +1,52 @@
+const { submitGetRequest } = require('../../__test-helpers__/server')
+const { assertCopy } = require('../../__test-helpers__/copy')
+const constants = require('../../constants')
+
+const url = constants.routes.ENGLAND_ONLY
+
+describe('Triage Page', () => {
+  const getQueries = [{
+    testName: 'Happy get: with postcode',
+    queryParams: {
+      easting: '123456',
+      northing: '123456',
+      locationDetails: 'Some address in Wales',
+      isPostCode: true,
+      placeOrPostcode: 'SY16 1AA'
+    },
+    text: 'Your search for \'SY16 1AA\' has been placed in Some address in Wales'
+  }, {
+    testName: 'Happy get: with place',
+    queryParams: {
+      easting: '123456',
+      northing: '123456',
+      locationDetails: 'Some address in Wales',
+      isPostCode: false,
+      placeOrPostcode: 'Swansea'
+    },
+    text: 'Your search for \'Swansea\' has been placed in Some address in Wales'
+  }, {
+    testName: 'Happy get: with NGR',
+    queryParams: {
+      easting: '123456',
+      northing: '123456',
+      nationalGridReference: 'SN 85981 88534'
+    },
+    text: 'The location you submitted is not in England, or is only partly in England.'
+  }, {
+    testName: 'Happy get: with easting and northing',
+    queryParams: {
+      easting: '123456',
+      northing: '123456'
+    },
+    text: 'The location you submitted is not in England, or is only partly in England.'
+  }]
+  getQueries.forEach(({ testName, queryParams, text }) => {
+    it(testName, async () => {
+      const response = await submitGetRequest({ url: `${url}?${new URLSearchParams(queryParams).toString()}` }, 'This service provides data for locations in England only')
+      document.body.innerHTML = response.payload
+      assertCopy('title', 'This service is for England only - Flood map for planning - GOV.UK')
+      assertCopy('#not-england-page > div > p:nth-child(2)', text)
+    })
+  })
+})
diff --git a/server/routes/__tests__/location.spec.js b/server/routes/__tests__/location.spec.js
index f4b61b42..f46a4a13 100644
--- a/server/routes/__tests__/location.spec.js
+++ b/server/routes/__tests__/location.spec.js
@@ -7,9 +7,17 @@ const {
 const ngrToBngService = require('../../services/ngr-to-bng')
 const addressService = require('../../services/address')
 const isValidNgrService = require('../../services/is-valid-ngr')
-const url = '/location'
+const constants = require('../../constants')
+const url = constants.routes.LOCATION
+
+jest.mock('../../services/is-england')
+const isEnglandService = require('../../services/is-england')
 
 describe('location route', () => {
+  beforeEach(() => {
+    isEnglandService.mockResolvedValue(true)
+  })
+
   it(`Should return success response and correct view for ${url}`, async () => {
     const response = await submitGetRequest({ url }, 'Find the location')
     document.body.innerHTML = response.payload
@@ -550,4 +558,20 @@ describe('location route', () => {
     expect(response.payload).not.toContain('<a href="#easting">Enter an easting in the correct format</a>')
     expect(response.payload).toContain('<a href="#northing">Enter a northing in the correct format</a>')
   })
+
+  it('Should redirect to england-only if not isEngland', async () => {
+    const options = {
+      url,
+      payload: {
+        findby: 'eastingNorthing',
+        easting: '123456',
+        northing: '123456'
+      }
+    }
+    // Switch isEngland mock to be false
+    isEnglandService.mockResolvedValue(false)
+
+    const response = await submitPostRequest(options)
+    expect(response.headers.location).toEqual('/england-only?easting=123456&northing=123456')
+  })
 })
diff --git a/server/routes/england-only.js b/server/routes/england-only.js
index e66c87b9..034179be 100644
--- a/server/routes/england-only.js
+++ b/server/routes/england-only.js
@@ -1,6 +1,8 @@
+const constants = require('../constants')
+
 module.exports = {
   method: 'GET',
-  path: '/england-only',
+  path: constants.routes.ENGLAND_ONLY,
   options: {
     description: 'That location is not in England',
     handler: async (request, h) => {
diff --git a/server/routes/location.js b/server/routes/location.js
index 7997603e..d3096069 100644
--- a/server/routes/location.js
+++ b/server/routes/location.js
@@ -1,30 +1,38 @@
+const constants = require('../constants')
 const addressService = require('../services/address')
 const isValidEastingNorthingService = require('../services/is-valid-easting-northing')
 const isValidNgrService = require('../services/is-valid-ngr')
 const ngrToBng = require('../services/ngr-to-bng')
+const isEnglandService = require('../services/is-england')
 
 const handlers = {
-  get: async (_request, h) => h.view('location'),
+  get: async (_request, h) => h.view(constants.views.LOCATION),
   post: async (request, h) => {
     // Validate and process the payload
-    const { BNG, errorSummary } = await validatePayload(request.payload)
+    const { location, errorSummary } = await validatePayload(request.payload)
 
     // If any validation failed then return the location view with the captured validation error
     if (errorSummary.length > 0) {
       // Get the analytics object
-      return h.view('location', {
+      return h.view(constants.views.LOCATION, {
         ...request.payload,
         errorSummary,
         analyticsPageEvent: analyticsPageEvent(request.payload)
       })
     }
 
-    return h.redirect(`/map?cz=${BNG.easting},${BNG.northing},15`)
+    // Check final location is in England
+    if (!await isEnglandService(location.easting, location.northing)) {
+      const queryString = new URLSearchParams(location).toString()
+      return h.redirect(`${constants.routes.ENGLAND_ONLY}?${queryString}`)
+    }
+
+    return h.redirect(`${constants.routes.MAP}?cz=${location.easting},${location.northing},15`)
   }
 }
 
 const validatePayload = async payload => {
-  const BNG = {}
+  const location = {}
   const {
     findby,
     placeOrPostcode,
@@ -35,11 +43,11 @@ const validatePayload = async payload => {
   const errorSummary = []
 
   if (findby === 'placeOrPostcode') {
-    await validatePlace(BNG, placeOrPostcode, errorSummary)
+    await validatePlace(location, placeOrPostcode, errorSummary)
   } else if (findby === 'nationalGridReference') {
-    validateGridReference(BNG, nationalGridReference, errorSummary)
+    validateGridReference(location, nationalGridReference, errorSummary)
   } else if (findby === 'eastingNorthing') {
-    validateBNG(BNG, easting, northing, errorSummary)
+    validateBNG(location, easting, northing, errorSummary)
   } else {
     errorSummary.push({
       text: 'Select a place or postcode, National Grid Reference (NGR) or an Easting and northing',
@@ -48,12 +56,12 @@ const validatePayload = async payload => {
   }
 
   return {
-    BNG,
+    location,
     errorSummary
   }
 }
 
-const validatePlace = async (BNG, placeOrPostcode, errorSummary) => {
+const validatePlace = async (location, placeOrPostcode, errorSummary) => {
   placeOrPostcode = placeOrPostcode?.trim() || ''
   if (!validatePlaceOrPostcode(placeOrPostcode)) {
     errorSummary.push({
@@ -68,8 +76,11 @@ const validatePlace = async (BNG, placeOrPostcode, errorSummary) => {
         href: '#placeOrPostcode'
       })
     } else {
-      BNG.easting = address[0].geometry_x
-      BNG.northing = address[0].geometry_y
+      location.easting = address[0].geometry_x
+      location.northing = address[0].geometry_y
+      location.locationDetails = address[0].locationDetails
+      location.isPostCode = address[0].isPostCode
+      location.placeOrPostcode = placeOrPostcode
     }
   }
 }
@@ -80,6 +91,7 @@ const validateGridReference = (BNG, nationalGridReference, errorSummary) => {
     const convertedBNG = ngrToBng.convert(nationalGridReference)
     BNG.easting = convertedBNG.easting
     BNG.northing = convertedBNG.northing
+    BNG.nationalGridReference = nationalGridReference
   } else {
     errorSummary.push({
       text: 'Enter a real National Grid Reference (NGR)',
@@ -151,7 +163,7 @@ const analyticsPageEvent = payload => {
 module.exports = [
   {
     method: 'GET',
-    path: '/location',
+    path: constants.routes.LOCATION,
     options: {
       description: 'Get location for a postcode, national grid reference, easting or northing'
     },
@@ -159,7 +171,7 @@ module.exports = [
   },
   {
     method: 'POST',
-    path: '/location',
+    path: constants.routes.LOCATION,
     options: {
       description: 'Get location for a postcode, national grid reference, easting or northing',
       handler: handlers.post
diff --git a/server/services/__tests__/is-england.spec.js b/server/services/__tests__/is-england.spec.js
index 50832559..dfef26e4 100644
--- a/server/services/__tests__/is-england.spec.js
+++ b/server/services/__tests__/is-england.spec.js
@@ -21,33 +21,33 @@ describe('is-england', () => {
   it('is-england without easting or northing should throw "No point provided"', async () => {
     const point = {}
     try {
-      await isEnglandService.get(point.easting, point.northing)
+      await isEnglandService(point.easting, point.northing)
     } catch (err) {
-      expect(isEnglandService.get).toThrow(Error, 'No point provided')
+      expect(isEnglandService).toThrow(Error, 'No point provided')
     }
   })
 
   it('is-england without easting should throw "No point provided"', async () => {
     const point = { northing: 388244 }
     try {
-      await isEnglandService.get(point.easting, point.northing)
+      await isEnglandService(point.easting, point.northing)
     } catch (err) {
-      expect(isEnglandService.get).toThrow(Error, 'No point provided')
+      expect(isEnglandService).toThrow(Error, 'No point provided')
     }
   })
 
   it('is-england without northing should throw "No point provided"', async () => {
     const point = { easting: 388244 }
     try {
-      await isEnglandService.get(point.easting, point.northing)
+      await isEnglandService(point.easting, point.northing)
     } catch (err) {
-      expect(isEnglandService.get).toThrow(Error, 'No point provided')
+      expect(isEnglandService).toThrow(Error, 'No point provided')
     }
   })
 
   it('is-england with northing and easting should call esriRequest"', async () => {
     const point = { northing: 388244, easting: 388244 }
-    const response = await isEnglandService.get(point.easting, point.northing)
-    expect(response).toEqual({ is_england: true })
+    const response = await isEnglandService(point.easting, point.northing)
+    expect(response).toEqual(true)
   })
 })
diff --git a/server/services/is-england.js b/server/services/is-england.js
index 50724515..79aaf780 100644
--- a/server/services/is-england.js
+++ b/server/services/is-england.js
@@ -1,15 +1,12 @@
 const { config } = require('../../config')
 const { esriRequest, makePointGeometry } = require('./agol')
 
-module.exports = {
-  get: (easting, northing) => {
-    if (!easting || !northing) {
-      throw new Error('No point provided')
-    }
-    return esriRequest(config.agol.isEnglandEndPoint, makePointGeometry(easting, northing), 'esriGeometryPoint')
-      .then((esriResult) => {
-        const isEngland = esriResult && Array.isArray(esriResult) && esriResult.length > 0
-        return { is_england: isEngland }
-      })
-  } // returns a Promise that resolves as { is_england: true | false }
+module.exports = (easting, northing) => {
+  if (!easting || !northing) {
+    throw new Error('No point provided')
+  }
+  return esriRequest(config.agol.isEnglandEndPoint, makePointGeometry(easting, northing), 'esriGeometryPoint')
+    .then((esriResult) => {
+      return esriResult && Array.isArray(esriResult) && esriResult.length > 0
+    })
 }
diff --git a/server/views/england-only.html b/server/views/england-only.html
index ad1df691..6773c771 100644
--- a/server/views/england-only.html
+++ b/server/views/england-only.html
@@ -5,12 +5,9 @@
 {% block content %}
   {{ super() }}
 
- 
- 
-
-  <div id="not-england-page"  class="grid-row">
-    <div class="column-two-thirds">
-      <h1 class="heading-xlarge">This service provides data for locations in England only</h1>
+  <div id="not-england-page"  class="govuk-grid-row govuk-body">
+    <div class="govuk-grid-column-two-thirds">
+      <h1 class="govuk-heading-xl">This service provides data for locations in England only</h1>
       {% if locationDetails %} 
       <p>Your search for '{{placeOrPostcode}}' has been placed in {{locationDetails}}</p>
       {% else %}

From 182204fd7fe78096ffeaf0ac096c0f7757400ae8 Mon Sep 17 00:00:00 2001
From: Tedd <teddmason@gmail.com>
Date: Tue, 21 Jan 2025 15:39:13 +0000
Subject: [PATCH 2/5] Feature/fcrm 5429 - Block duplicate p4 requests (#451)

* WIP so far for a single p4 check

* Switching p4 cookie check to multiple request check

* sorting sonarcloud issues before unit test work

* Adding tests for check-your-details route

* review comments
---
 server/index.js                               |   1 +
 server/plugins/register-cookie.js             |   9 +-
 .../__tests__/check-your-details.spec.js      | 126 ++++++++++++++++++
 server/routes/check-your-details.js           |  57 ++++----
 server/routes/flood-zone-results.js           |   1 -
 server/services/__mocks__/address.js          |   5 +
 server/services/agol/__mocks__/getContacts.js |  11 ++
 7 files changed, 178 insertions(+), 32 deletions(-)
 create mode 100644 server/routes/__tests__/check-your-details.spec.js
 create mode 100644 server/services/__mocks__/address.js
 create mode 100644 server/services/agol/__mocks__/getContacts.js

diff --git a/server/index.js b/server/index.js
index 67fd16aa..902fe14b 100644
--- a/server/index.js
+++ b/server/index.js
@@ -43,6 +43,7 @@ async function createServer () {
   await server.register(require('./plugins/error-pages'))
   await server.register(require('blipp'))
   await server.register(require('./plugins/full-url'))
+  await server.register(require('./plugins/register-cookie'))
 
   server.ext('onPreResponse', async (request, h) => {
     request.response.header('cache-control', 'no-cache')
diff --git a/server/plugins/register-cookie.js b/server/plugins/register-cookie.js
index ea57caa1..5296591f 100644
--- a/server/plugins/register-cookie.js
+++ b/server/plugins/register-cookie.js
@@ -3,15 +3,14 @@ const { config } = require('../../config')
 exports.plugin = {
   name: 'register-cookie',
   register: (server, options) => {
-    // Cookie used to notify the client
-    // browser that the download is complete
-    server.state('pdf-download', {
-      ttl: 10000,
+    // Cookie used to store a successful p4 request to stop duplication
+    server.state('p4Request', {
+      ttl: null, // ttl is session
       isSecure: config.siteUrl.startsWith('https'),
       path: '/',
       isSameSite: false,
       isHttpOnly: true,
-      encoding: 'none',
+      encoding: 'base64json',
       clearInvalid: true,
       strictHeader: true
     })
diff --git a/server/routes/__tests__/check-your-details.spec.js b/server/routes/__tests__/check-your-details.spec.js
new file mode 100644
index 00000000..0782c454
--- /dev/null
+++ b/server/routes/__tests__/check-your-details.spec.js
@@ -0,0 +1,126 @@
+const { getServer } = require('../../../.jest/setup')
+const { assertCopy } = require('../../__test-helpers__/copy')
+const {
+  submitGetRequest,
+  submitPostRequest
+} = require('../../__test-helpers__/server')
+const { mockPolygons } = require('../../services/__tests__/__mocks__/floodZonesByPolygonMock')
+jest.mock('../../services/agol/getContacts')
+jest.mock('../../services/address')
+jest.mock('@hapi/wreck')
+const wreck = require('@hapi/wreck')
+const user = {
+  fullName: 'John Smith',
+  email: 'john.smith@email.com'
+}
+
+const url = '/check-your-details'
+
+describe('Check your details page', () => {
+  beforeEach(() => {
+    wreck.post.mockResolvedValue({
+      payload: {
+        applicationReferenceNumber: '12345',
+        nextTask: 'SEND_CONFIRMATION_EMAIL'
+      }
+    })
+  })
+  describe('GET', () => {
+    const floodZoneGets = [
+      { polygon: mockPolygons.fz1_only, floodZone: '1' },
+      { polygon: mockPolygons.fz2_only, floodZone: '2' },
+      { polygon: mockPolygons.fz3_only, floodZone: '3' }
+    ]
+    floodZoneGets.forEach(({ polygon, floodZone }) => {
+      it(`Happy get request for a flood zone ${floodZone} information`, async () => {
+        const response = await submitGetRequest({ url: `${url}?polygon=${polygon}&fullName=${user.fullName}&recipientemail=${user.email}` }, 'Check your details before requesting your data')
+        document.body.innerHTML = response.payload
+        assertCopy('title', 'Check your details - Flood map for planning - GOV.UK')
+        assertCopy('.govuk-summary-list__row > dd.govuk-summary-list__value', user.fullName)
+        assertCopy('.govuk-summary-list__row:nth-child(2) > dd.govuk-summary-list__value', user.email)
+        assertCopy('.govuk-summary-list__row:nth-child(4) > dd.govuk-summary-list__value', floodZone)
+      })
+    })
+  })
+  describe('POST', () => {
+    // Makes a first request with no cookie
+    const postTests = [
+      {
+        description: 'Happy post: request with p4request made with no p4Request cookie',
+        payload: {
+          recipientemail: user.email,
+          fullName: user.fullName,
+          polygon: mockPolygons.fz1_only
+        },
+        p4Cookie: {},
+        expectedAppRef: '12345',
+        expectedZoneNumber: '1',
+        expectedWreckCalls: 1
+      },
+      {
+        description: 'Happy post: Attempts a 2nd duplicate request and wreck should not be called to make p4',
+        payload: {
+          recipientemail: user.email,
+          fullName: user.fullName,
+          polygon: mockPolygons.fz1_only
+        },
+        p4Cookie: { [mockPolygons.fz1_only]: '12345' },
+        expectedAppRef: '12345',
+        expectedZoneNumber: '1',
+        expectedWreckCalls: 0
+      },
+      {
+        description: 'Happy post: Attempts a 2nd non duplicate request and wreck should be called to make p4',
+        payload: {
+          recipientemail: user.email,
+          fullName: user.fullName,
+          polygon: mockPolygons.fz2_only
+        },
+        p4Cookie: { [mockPolygons.fz1_only]: '12346' },
+        expectedAppRef: '12345',
+        expectedZoneNumber: '2',
+        expectedWreckCalls: 1
+      }
+    ]
+
+    postTests.forEach(({ description, payload, p4Cookie, expectedAppRef, expectedZoneNumber, expectedWreckCalls }) => {
+      it(description, async () => {
+        const options = { url, payload }
+
+        getServer().ext('onPreHandler', (request, h) => {
+          request.state = { p4Request: p4Cookie }
+          return h.continue
+        })
+
+        const queryParams = {
+          applicationReferenceNumber: expectedAppRef,
+          polygon: payload.polygon,
+          recipientemail: payload.recipientemail,
+          zoneNumber: expectedZoneNumber
+        }
+
+        const response = await submitPostRequest(options)
+        expect(response.headers.location).toEqual(`/confirmation?${new URLSearchParams(queryParams).toString()}`)
+        expect(response.request.state.p4Request).toEqual(p4Cookie)
+        expect(wreck.post).toHaveBeenCalledTimes(expectedWreckCalls)
+      })
+    })
+    // Sad paths
+    // Wreck returns an error
+    it('Sad: Wreck returns an error for p4Request', async () => {
+      const options = {
+        url,
+        payload: {
+          recipientemail: 'john.smith@email.com',
+          fullName: 'John Smith',
+          polygon: mockPolygons.fz2_only
+        }
+      }
+      wreck.post.mockImplementation(() => {
+        throw new Error()
+      })
+      const response = await submitPostRequest(options)
+      expect(response.headers.location).toEqual(`/order-not-submitted?polygon=${options.payload.polygon}`)
+    })
+  })
+})
diff --git a/server/routes/check-your-details.js b/server/routes/check-your-details.js
index 52bf5cd9..2b4a8748 100644
--- a/server/routes/check-your-details.js
+++ b/server/routes/check-your-details.js
@@ -12,7 +12,7 @@ const getFunctionAppResponse = async (data) => {
   )
   payload.postcode = postcode
 
-  return wreck.post(publishToQueueURL, { payload: JSON.stringify(payload) })
+  return wreck.post(publishToQueueURL, { json: true, payload: JSON.stringify(payload) })
 }
 
 const floodZoneResultsToFloodZone = (floodZoneResults) =>
@@ -40,55 +40,60 @@ module.exports = [
     options: {
       description: 'submits the page to Confirmation Screen',
       handler: async (request, h) => {
-        try {
-          const payload = request.payload || {}
-          const { recipientemail, fullName } = payload
-          const coordinates = getCentreOfPolygon(payload.polygon)
+        const payload = request.payload || {}
+        const { recipientemail, fullName, polygon } = payload
+        const coordinates = getCentreOfPolygon(polygon)
+        const floodZoneResults = await request.server.methods.getFloodZonesByPolygon(polygon)
+        const zoneNumber = floodZoneResultsToFloodZone(floodZoneResults)
+        let applicationReferenceNumber
 
+        // Check if p4Request is duplicate
+        if (!request.state?.p4Request?.[polygon]) {
           // Send details to function app
-          const plotSize = getAreaInHectares(payload.polygon)
-          const psoResults = await request.server.methods.getPsoContactsByPolygon(payload.polygon)
-          const floodZoneResults = await request.server.methods.getFloodZonesByPolygon(payload.polygon)
-          const zoneNumber = floodZoneResultsToFloodZone(floodZoneResults)
+          const plotSize = getAreaInHectares(polygon)
+          const psoResults = await request.server.methods.getPsoContactsByPolygon(polygon)
           const data = JSON.stringify({
             name: fullName,
             recipientemail,
             x: coordinates.x,
             y: coordinates.y,
-            polygon: '[' + payload.polygon + ']',
+            polygon: `[${polygon}]`,
             zoneNumber,
             plotSize,
             areaName: psoResults.AreaName,
             psoEmailAddress: psoResults.EmailAddress,
             llfa: psoResults.LocalAuthorities || ''
           })
-          let applicationReferenceNumber
           try {
             const result = await getFunctionAppResponse(data)
-            const response = result.payload.toString()
-            const { applicationReferenceNumber: appRef } = JSON.parse(response)
-            applicationReferenceNumber = appRef
+            applicationReferenceNumber = result.payload.applicationReferenceNumber
+            // Upsert p4Cookie to store app ref by polygon key
+            const p4Cookie = request.state.p4Request || {}
+            p4Cookie[polygon] = applicationReferenceNumber
+            h.state('p4Request', p4Cookie)
           } catch (error) {
             console.log(
               '\nFailed to POST these data to the functionsApp /order-product-four:\n',
               data
             )
             console.log('Error\n', JSON.stringify(error))
-            const redirectURL = `/order-not-submitted?polygon=${payload?.polygon}&center=[${payload?.easting},${payload?.northing}]`
+            const redirectURL = `/order-not-submitted?polygon=${payload?.polygon}`
             return h.redirect(redirectURL)
           }
+        } else {
+          applicationReferenceNumber = request.state.p4Request[polygon]
+        }
 
-          // Forward details to confirmation page
-          const queryParams = {
-            applicationReferenceNumber,
-            polygon: payload.polygon,
-            recipientemail,
-            zoneNumber
-          }
-          // During serializing, the UTF-8 encoding format is used to encode any character that requires percent-encoding.
-          const query = new URLSearchParams(queryParams).toString()
-          return h.redirect(`/confirmation?${query}`)
-        } catch (error) {}
+        // Forward details to confirmation page
+        const queryParams = {
+          applicationReferenceNumber,
+          polygon: polygon,
+          recipientemail,
+          zoneNumber
+        }
+        // During serializing, the UTF-8 encoding format is used to encode any character that requires percent-encoding.
+        const query = new URLSearchParams(queryParams).toString()
+        return h.redirect(`/confirmation?${query}`)
       }
     }
   }
diff --git a/server/routes/flood-zone-results.js b/server/routes/flood-zone-results.js
index 5b12ace8..f6c2c3a3 100644
--- a/server/routes/flood-zone-results.js
+++ b/server/routes/flood-zone-results.js
@@ -89,7 +89,6 @@ module.exports = [
           })
           return h
             .view('flood-zone-results', floodZoneResultsData)
-            .unstate('pdf-download')
         } catch (err) {
           console.log('Error in flood zone results', err.message)
           return Boom.badImplementation(err.message, err)
diff --git a/server/services/__mocks__/address.js b/server/services/__mocks__/address.js
new file mode 100644
index 00000000..d536074a
--- /dev/null
+++ b/server/services/__mocks__/address.js
@@ -0,0 +1,5 @@
+const getPostcodeFromEastingorNorthing = (easting, northing) => {
+  return 'M1 1AA'
+}
+
+module.exports = { getPostcodeFromEastingorNorthing }
diff --git a/server/services/agol/__mocks__/getContacts.js b/server/services/agol/__mocks__/getContacts.js
new file mode 100644
index 00000000..aa46df5c
--- /dev/null
+++ b/server/services/agol/__mocks__/getContacts.js
@@ -0,0 +1,11 @@
+const getContacts = () => {
+  return {
+    isEngland: true,
+    EmailAddress: 'inforequests.gmmc@environment-agency.gov.uk',
+    AreaName: 'Greater Manchester Merseyside and Cheshire',
+    LocalAuthorities: 'Cheshire West and Chester (B)',
+    useAutomatedService: true
+  }
+}
+
+module.exports = { getContacts }

From 56f7aed9779617d8eeee7864eba2a0cead9dc079 Mon Sep 17 00:00:00 2001
From: Mark Fee <mark.fee@defra.gov.uk>
Date: Tue, 21 Jan 2025 17:45:19 +0000
Subject: [PATCH 3/5] FCRM-5476 Upgrade @defra-map to 0.4.0 (#452)

* FCRM-5476 attempted an upgrade to 0.4.0 - getting invalid styles error

* FCRM-5476 fixed issue causing map to fail if basemap had an incorrect value

* FCRM-5476 refactor to avoid Sonar issue
---
 client/js/defra-map/index.js | 15 ++++++++++++++-
 package-lock.json            | 28 ++++++++++++++--------------
 package.json                 |  2 +-
 3 files changed, 29 insertions(+), 16 deletions(-)

diff --git a/client/js/defra-map/index.js b/client/js/defra-map/index.js
index bb06b3f0..b678868f 100644
--- a/client/js/defra-map/index.js
+++ b/client/js/defra-map/index.js
@@ -69,7 +69,20 @@ const surfaceWaterStyleLayers = [
 //   'Risk of Flooding from Surface Water Depth CCSW1 > 900mm/1',
 //   'Risk of Flooding from Surface Water Depth CCSW1 > 1200mm/1'
 // ]
+
+const fixLocalStorage = () => {
+  // Temp fix until 0.5.0
+  // Map will not load if localStorage basemap is not one of default OR dark
+  // but 0.3.0 sets the value to 'default,light', which screws up
+  // the map component after an upgrade to 0.4.0
+  const basemap = window.localStorage.getItem('basemap')
+  if (basemap !== 'default' && basemap !== 'dark') {
+    window.localStorage.removeItem('basemap')
+  }
+}
+
 getDefraMapConfig().then((defraMapConfig) => {
+  fixLocalStorage() // Temp fix until 0.5.0
   const getVectorTileUrl = (layerName) => `${defraMapConfig.agolVectorTileUrl}/${layerName + defraMapConfig.layerNameSuffix}/VectorTileServer`
   const getFeatureLayerUrl = (layerName) => `${defraMapConfig.agolServiceUrl}/${layerName}/FeatureServer`
   const getModelFeatureLayerUrl = (layerName) => `${defraMapConfig.agolServiceUrl}/${layerName + defraMapConfig.layerNameSuffix}/FeatureServer`
@@ -364,7 +377,7 @@ getDefraMapConfig().then((defraMapConfig) => {
     maxZoom: 20,
     centre: [340367, 322766],
     height: '100%',
-    hasGeoLocation: true,
+    hasGeoLocation: false,
     framework: 'esri',
     symbols: [symbols.waterStorageAreas, symbols.floodDefences, symbols.mainRivers],
     requestCallback: getRequest,
diff --git a/package-lock.json b/package-lock.json
index d1610f99..7e53fbda 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,7 +14,7 @@
         "@babel/core": "^7.26.0",
         "@babel/preset-env": "^7.26.0",
         "@babel/preset-react": "^7.25.9",
-        "@defra/flood-map": "^0.3.0",
+        "@defra/flood-map": "^0.4.0",
         "@esri/arcgis-rest-feature-service": "^4.0.6",
         "@esri/arcgis-rest-request": "^4.2.3",
         "@hapi/boom": "^9.1.4",
@@ -128,9 +128,9 @@
       }
     },
     "node_modules/@arcgis/core": {
-      "version": "4.31.3",
-      "resolved": "https://registry.npmjs.org/@arcgis/core/-/core-4.31.3.tgz",
-      "integrity": "sha512-exz54+7asVBITYnKRC0RpJl/r3YG12ZC+togFuVJlfCzk9DO60qCp6XxU/7Zw0QojV20bQFkKjBvzPY0udEtiA==",
+      "version": "4.31.6",
+      "resolved": "https://registry.npmjs.org/@arcgis/core/-/core-4.31.6.tgz",
+      "integrity": "sha512-+NSYtEJy/wRQcEX+f5OHBhg4MX26HeZFYghd7+VMTasalHUAD3y9PPiakLIfhahX/4EiSLuy25m1P93BBlybXg==",
       "dependencies": {
         "@esri/arcgis-html-sanitizer": "~4.1.0-next.4",
         "@esri/calcite-colors": "~6.1.0",
@@ -1867,14 +1867,14 @@
       "dev": true
     },
     "node_modules/@defra/flood-map": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/@defra/flood-map/-/flood-map-0.3.0.tgz",
-      "integrity": "sha512-qTMrz4LbXp7DcZsK46ETEdBZhZLuW7grZmOKh5Yfz2qaUqhLj8cZOhjBldyXaZjulSj5FEhWFkMeoSjMSeCY/Q==",
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/@defra/flood-map/-/flood-map-0.4.0.tgz",
+      "integrity": "sha512-4Fx0zhpdA6CTEn5D3+e83Mg7n9yDmNffxR8u/JztpmeMezsGlyng4taXedQH3jL6iL/+CJ/b91cDgLj/DbCnsA==",
       "dependencies": {
-        "@arcgis/core": "^4.31.0",
-        "maplibre-gl": "^4.7.0",
-        "maplibre-gl-legacy": "npm:maplibre-gl@^1.15.0",
-        "preact": "^10.23.1"
+        "@arcgis/core": "^4.31.6",
+        "maplibre-gl": "^4.7.1",
+        "maplibre-gl-legacy": "npm:maplibre-gl@^1.15.3",
+        "preact": "^10.25.2"
       }
     },
     "node_modules/@discoveryjs/json-ext": {
@@ -16423,9 +16423,9 @@
       "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw=="
     },
     "node_modules/preact": {
-      "version": "10.24.3",
-      "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
-      "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
+      "version": "10.25.4",
+      "resolved": "https://registry.npmjs.org/preact/-/preact-10.25.4.tgz",
+      "integrity": "sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==",
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/preact"
diff --git a/package.json b/package.json
index bf6aee56..7950c7ea 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,7 @@
     "@babel/core": "^7.26.0",
     "@babel/preset-env": "^7.26.0",
     "@babel/preset-react": "^7.25.9",
-    "@defra/flood-map": "^0.3.0",
+    "@defra/flood-map": "^0.4.0",
     "@esri/arcgis-rest-feature-service": "^4.0.6",
     "@esri/arcgis-rest-request": "^4.2.3",
     "@hapi/boom": "^9.1.4",

From dbf2b7df1e390b2589ccffdefcd2a4aa1bed7b3d Mon Sep 17 00:00:00 2001
From: Mark Fee <mark.fee@defra.gov.uk>
Date: Wed, 22 Jan 2025 08:37:00 +0000
Subject: [PATCH 4/5] FCRM-5495 added appType internal/public and refactored
 tests (#454)

* FCRM-5495 copy helper ammended to show failures better

* FCRM-5495 /results now includes contacts and optin/optout status

* FCRM-5495 moved getContacts.spec.js

* FCRM-5495 removed server dependency from getContacts.spec.js

* FCRM-5495 separated getContacts into getCustomerTeam and getLocalAuthority

* FCRM-5495 added tests for getEsriToken

* FCRM-5495 added tests for esriRequest

* FCRM-5495 added tests for getCustomerTeam

* FCRM-5495 added test coverage for server/services/agol/getContacts.js

* FCRM-5495 covered make...geometry functions

* FCRM-5495 added a config for fmpAppType

* FCRM-5495 Results page honours the appType==internal check when showing the P4 button

* FCRM-5495 added appType to the payload for requestProductFour
---
 .../@esri/arcgis-rest-feature-service.js      | 15 ++++
 __mocks__/@esri/arcgis-rest-request.js        | 17 ++++
 config/.env-example                           |  3 +-
 config/__tests__/config.spec.js               |  3 +-
 config/index.js                               |  1 +
 config/schema.js                              |  1 +
 server/__test-helpers__/copy.js               | 21 +++--
 .../__tests__/check-your-details.spec.js      | 26 +++++-
 server/routes/__tests__/results.spec.js       | 31 ++++++-
 server/routes/check-your-details.js           |  1 +
 server/routes/results.js                      |  6 +-
 server/services/__data__/mockPolygons.json    |  3 +
 server/services/__tests__/agol.spec.js        | 86 -------------------
 server/services/__tests__/getContacts.spec.js | 32 -------
 server/services/agol/__mocks__/getContacts.js | 42 +++++++--
 .../agol/__mocks__/getCustomerTeam.js         | 40 +++++++++
 .../services/agol/__mocks__/getFloodZones.js  |  1 +
 .../agol/__mocks__/getLocalAuthority.js       | 30 +++++++
 .../agol/__tests__/esriRequest.spec.js        | 28 ++++++
 .../services/agol/__tests__/geometry.spec.js  | 25 ++++++
 .../agol/__tests__/getContacts.spec.js        | 35 ++++++++
 .../agol/__tests__/getCustomerTeam.spec.js    | 46 ++++++++++
 .../agol/__tests__/getEsriToken.spec.js       | 19 ++++
 .../agol/__tests__/getLocalAuthority.spec.js  | 33 +++++++
 server/services/agol/esriRequest.js           | 20 +++++
 server/services/agol/getContacts.js           | 48 +++--------
 server/services/agol/getCustomerTeam.js       | 34 ++++++++
 server/services/agol/getLocalAuthority.js     | 15 ++++
 server/services/agol/index.js                 | 20 +----
 server/views/results.html                     |  8 +-
 30 files changed, 492 insertions(+), 198 deletions(-)
 create mode 100644 __mocks__/@esri/arcgis-rest-feature-service.js
 create mode 100644 __mocks__/@esri/arcgis-rest-request.js
 delete mode 100644 server/services/__tests__/agol.spec.js
 delete mode 100644 server/services/__tests__/getContacts.spec.js
 create mode 100644 server/services/agol/__mocks__/getCustomerTeam.js
 create mode 100644 server/services/agol/__mocks__/getLocalAuthority.js
 create mode 100644 server/services/agol/__tests__/esriRequest.spec.js
 create mode 100644 server/services/agol/__tests__/geometry.spec.js
 create mode 100644 server/services/agol/__tests__/getContacts.spec.js
 create mode 100644 server/services/agol/__tests__/getCustomerTeam.spec.js
 create mode 100644 server/services/agol/__tests__/getEsriToken.spec.js
 create mode 100644 server/services/agol/__tests__/getLocalAuthority.spec.js
 create mode 100644 server/services/agol/esriRequest.js
 create mode 100644 server/services/agol/getCustomerTeam.js
 create mode 100644 server/services/agol/getLocalAuthority.js

diff --git a/__mocks__/@esri/arcgis-rest-feature-service.js b/__mocks__/@esri/arcgis-rest-feature-service.js
new file mode 100644
index 00000000..6ec21882
--- /dev/null
+++ b/__mocks__/@esri/arcgis-rest-feature-service.js
@@ -0,0 +1,15 @@
+let expectedParameters
+const queryFeatureSpy = {
+  expectParameters: (params) => { expectedParameters = params }
+}
+
+const queryFeatures = async (requestObject) => {
+  if (expectedParameters) {
+    expect(requestObject).toEqual(expectedParameters)
+  }
+  return {
+    features: 'QUERY_FEATURES_RESPONSE'
+  }
+}
+
+module.exports = { queryFeatures, queryFeatureSpy }
diff --git a/__mocks__/@esri/arcgis-rest-request.js b/__mocks__/@esri/arcgis-rest-request.js
new file mode 100644
index 00000000..20fad093
--- /dev/null
+++ b/__mocks__/@esri/arcgis-rest-request.js
@@ -0,0 +1,17 @@
+
+const response = {
+  token: 'TEST_TOKEN',
+  refreshToken: () => {
+    response.token = 'REFRESHED_TOKEN'
+    return response.token
+  }
+}
+const _invalidateToken = () => { response.token = undefined }
+
+const _resetToken = () => { response.token = 'TEST_TOKEN' }
+
+const ApplicationCredentialsManager = {
+  fromCredentials: () => (response)
+}
+
+module.exports = { ApplicationCredentialsManager, _invalidateToken, _resetToken }
diff --git a/config/.env-example b/config/.env-example
index 36db0a8e..6c6de2e2 100644
--- a/config/.env-example
+++ b/config/.env-example
@@ -1,6 +1,7 @@
 ENV=dev
 HOST=0.0.0.0
 PORT=8050
+fmpAppType=internal
 CONNECTIONSTRING=http://dummyuri
 ERRBIT_POST_ERRORS=false
 ERRBIT_KEY=replace_this
@@ -19,7 +20,7 @@ ordnanceSurveyOsClientId=replace_this
 ordnanceSurveyOsClientSecret=replace_this
 ordnanceSurveyOsMapsKey=replace_this
 siteUrl=http://dummyuri
-functionAppUrl==http://dummyuri
+functionAppUrl=http://dummyuri
 ignoreUseAutomatedService=true
 placeApiUrl=http://dummyuri
 #AGOL
diff --git a/config/__tests__/config.spec.js b/config/__tests__/config.spec.js
index 90df3cb1..40add3ac 100644
--- a/config/__tests__/config.spec.js
+++ b/config/__tests__/config.spec.js
@@ -11,6 +11,7 @@ describe('Ensure config is correct', () => {
     const { config } = require('../index')
     const expectedConfig = {
       env: 'dev',
+      appType: 'internal',
       server: { port: '8050' },
       geoserver: 'http://dummyuri',
       views: { isCached: false },
@@ -28,7 +29,7 @@ describe('Ensure config is correct', () => {
         osClientSecret: 'replace_this'
       },
       siteUrl: 'http://dummyuri',
-      functionAppUrl: '=http://dummyuri',
+      functionAppUrl: 'http://dummyuri',
       ignoreUseAutomatedService: true,
       placeApi: { url: 'http://dummyuri' },
       agol: {
diff --git a/config/index.js b/config/index.js
index a92f591e..01718a0d 100644
--- a/config/index.js
+++ b/config/index.js
@@ -4,6 +4,7 @@ require('./environment')
 
 const config = {
   env: process.env.ENV,
+  appType: process.env.fmpAppType,
   server: {
     port: process.env.PORT
   },
diff --git a/config/schema.js b/config/schema.js
index 860686e5..4059fa0f 100644
--- a/config/schema.js
+++ b/config/schema.js
@@ -8,6 +8,7 @@ const serverSchema = Joi.object()
 
 const schema = Joi.object({
   env: Joi.string().required(),
+  appType: Joi.string().required().allow('internal', 'public'),
   server: serverSchema,
   geoserver: Joi.string().uri().required(),
   logging: Joi.object(),
diff --git a/server/__test-helpers__/copy.js b/server/__test-helpers__/copy.js
index acde2708..d45a2132 100644
--- a/server/__test-helpers__/copy.js
+++ b/server/__test-helpers__/copy.js
@@ -1,12 +1,19 @@
-
 const assertCopy = (selector, expectedCopy) => {
-  const element = document.querySelector(selector)
-  if (expectedCopy) {
+  try {
+    const element = document.querySelector(selector)
+    if (expectedCopy) {
     // the REGEX trims all whitespace and only compares the actual text that will be rendered
-    expect(element.textContent.replace(/\s\s+/g, ' ').trim())
-      .toContain(expectedCopy.replace(/\s\s+/g, ' ').trim())
-  } else {
-    expect(element).toBeNull()
+      expect(element.textContent.replace(/\s\s+/g, ' ').trim())
+        .toContain(expectedCopy.replace(/\s\s+/g, ' ').trim())
+    } else {
+      expect(element).toBeNull()
+    }
+  } catch (error) {
+    // This Assert only runs when the expected assert fails.
+    // It is added as the exact location of the failure is hard to infer, when a fail occurs
+    // and this helps give a more meaningful failure message.
+    const failMessage = `expected selector ${selector} to contain ${expectedCopy || 'NO CONTENT'}`
+    expect(failMessage).toBeNull()
   }
 }
 
diff --git a/server/routes/__tests__/check-your-details.spec.js b/server/routes/__tests__/check-your-details.spec.js
index 0782c454..d5c81c7c 100644
--- a/server/routes/__tests__/check-your-details.spec.js
+++ b/server/routes/__tests__/check-your-details.spec.js
@@ -5,6 +5,7 @@ const {
   submitPostRequest
 } = require('../../__test-helpers__/server')
 const { mockPolygons } = require('../../services/__tests__/__mocks__/floodZonesByPolygonMock')
+const { getCentreOfPolygon } = require('../../services/shape-utils')
 jest.mock('../../services/agol/getContacts')
 jest.mock('../../services/address')
 jest.mock('@hapi/wreck')
@@ -25,6 +26,7 @@ describe('Check your details page', () => {
       }
     })
   })
+
   describe('GET', () => {
     const floodZoneGets = [
       { polygon: mockPolygons.fz1_only, floodZone: '1' },
@@ -42,6 +44,7 @@ describe('Check your details page', () => {
       })
     })
   })
+
   describe('POST', () => {
     // Makes a first request with no cookie
     const postTests = [
@@ -91,10 +94,11 @@ describe('Check your details page', () => {
           request.state = { p4Request: p4Cookie }
           return h.continue
         })
-
+        const { polygon } = payload
+        const { x, y } = getCentreOfPolygon(polygon)
         const queryParams = {
           applicationReferenceNumber: expectedAppRef,
-          polygon: payload.polygon,
+          polygon,
           recipientemail: payload.recipientemail,
           zoneNumber: expectedZoneNumber
         }
@@ -103,6 +107,24 @@ describe('Check your details page', () => {
         expect(response.headers.location).toEqual(`/confirmation?${new URLSearchParams(queryParams).toString()}`)
         expect(response.request.state.p4Request).toEqual(p4Cookie)
         expect(wreck.post).toHaveBeenCalledTimes(expectedWreckCalls)
+        if (expectedWreckCalls) {
+          const expectedPayload = JSON.stringify({
+            appType: 'internal',
+            name: user.fullName,
+            recipientemail: user.email,
+            x,
+            y,
+            polygon: `[${polygon}]`,
+            zoneNumber: expectedZoneNumber,
+            plotSize: '0',
+            areaName: 'Yorkshire',
+            psoEmailAddress: 'neyorkshire@environment-agency.gov.uk',
+            llfa: 'North Yorkshire',
+            postcode: 'M1 1AA'
+          })
+
+          expect(wreck.post).toHaveBeenCalledWith('http://dummyuri/order-product-four', { json: true, payload: expectedPayload })
+        }
       })
     })
     // Sad paths
diff --git a/server/routes/__tests__/results.spec.js b/server/routes/__tests__/results.spec.js
index 4b664bde..66c9a8d4 100644
--- a/server/routes/__tests__/results.spec.js
+++ b/server/routes/__tests__/results.spec.js
@@ -1,6 +1,8 @@
 const { submitGetRequest } = require('../../__test-helpers__/server')
 const { assertCopy } = require('../../__test-helpers__/copy')
 const { mockPolygons } = require('../../services/__tests__/__mocks__/floodZonesByPolygonMock')
+const { config } = require('../../../config')
+jest.mock('../../services/agol/getContacts')
 
 const url = '/results'
 
@@ -16,12 +18,19 @@ const assertRiskAdminCopy = (expected) => {
   )
 }
 
-describe('Results Page', () => {
+const assertOrderFloodRiskDataButton = (expected = true) => {
+  assertCopy('[data-testid="order-product4"]', expected && 'Order flood risk data')
+}
+
+describe('Results Page On Public', () => {
+  beforeAll(() => { config.appType = 'public' })
+  afterAll(() => { config.appType = 'internal' })
   it('should have the correct copy for Zone 1"', async () => {
     const response = await submitGetRequest({ url: `${url}?polygon=${mockPolygons.fz1_only}` })
     document.body.innerHTML = response.payload
     assertFloodZoneCopy(1)
     assertRiskAdminCopy(false)
+    assertOrderFloodRiskDataButton()
   })
 
   it('should have the correct copy for Zone 1 with riskAdmin"', async () => {
@@ -29,5 +38,25 @@ describe('Results Page', () => {
     document.body.innerHTML = response.payload
     assertFloodZoneCopy(1)
     assertRiskAdminCopy(true)
+    assertOrderFloodRiskDataButton()
+  })
+
+  it('should not show the "Order flood risk data" for opted out areas', async () => {
+    const response = await submitGetRequest({ url: `${url}?polygon=${mockPolygons.optedOut.fz3_only}` })
+    document.body.innerHTML = response.payload
+    assertFloodZoneCopy(3)
+    assertRiskAdminCopy(false)
+    assertOrderFloodRiskDataButton(false)
+  })
+})
+
+describe('Results Page On Internal', () => {
+  beforeAll(() => { config.appType = 'internal' })
+  it('should show the "Order flood risk data" for opted out areas on internal', async () => {
+    const response = await submitGetRequest({ url: `${url}?polygon=${mockPolygons.optedOut.fz3_only}` })
+    document.body.innerHTML = response.payload
+    assertFloodZoneCopy(3)
+    assertRiskAdminCopy(false)
+    assertOrderFloodRiskDataButton(true)
   })
 })
diff --git a/server/routes/check-your-details.js b/server/routes/check-your-details.js
index 2b4a8748..d18384d4 100644
--- a/server/routes/check-your-details.js
+++ b/server/routes/check-your-details.js
@@ -53,6 +53,7 @@ module.exports = [
           const plotSize = getAreaInHectares(polygon)
           const psoResults = await request.server.methods.getPsoContactsByPolygon(polygon)
           const data = JSON.stringify({
+            appType: config.appType,
             name: fullName,
             recipientemail,
             x: coordinates.x,
diff --git a/server/routes/results.js b/server/routes/results.js
index bb132063..29d43f0c 100644
--- a/server/routes/results.js
+++ b/server/routes/results.js
@@ -1,3 +1,5 @@
+const { config } = require('../../config')
+
 module.exports = [
   {
     method: 'GET',
@@ -6,8 +8,10 @@ module.exports = [
       description: 'Results Page',
       handler: async (request, h) => {
         const { polygon } = request.query
+        const contactData = await request.server.methods.getPsoContactsByPolygon(polygon)
+        const showOrderProduct4Button = config.appType === 'internal' || contactData.useAutomatedService === true
         const floodData = await request.server.methods.getFloodZonesByPolygon(polygon)
-        return h.view('results', { polygon, floodData })
+        return h.view('results', { polygon, floodData, contactData, showOrderProduct4Button })
       }
     }
   }
diff --git a/server/services/__data__/mockPolygons.json b/server/services/__data__/mockPolygons.json
index 89acc8c0..4c0f153a 100644
--- a/server/services/__data__/mockPolygons.json
+++ b/server/services/__data__/mockPolygons.json
@@ -10,5 +10,8 @@
     "fz3_only": "[[1323,1323],[1323,1324],[1324,1324],[1324,1323],[1323,1323]]",
     "fz2_and_3": "[[1333,1333],[1333,1334],[1334,1334],[1334,1333],[1333,1333]]",
     "throws": "199999"
+  },
+  "optedOut": {
+    "fz3_only": "[[9323,1323],[9323,1324],[9324,1324],[9324,1323],[9323,1323]]"
   }
 }
diff --git a/server/services/__tests__/agol.spec.js b/server/services/__tests__/agol.spec.js
deleted file mode 100644
index ee1a4a53..00000000
--- a/server/services/__tests__/agol.spec.js
+++ /dev/null
@@ -1,86 +0,0 @@
-it('commented out as needs sorting at later date', () => {
-  expect(true)
-})
-
-// const sinon = require('sinon')
-// const proxyquire = require('proxyquire')
-
-// describe('agol.js', () => {
-//   let agol
-//   let stubQueryFeatures
-//   let stubFromCredentials
-//   let stubRefreshToken
-//   let stubTokenGetter
-
-//   beforeAll(async () => {
-//     stubQueryFeatures = sinon.stub().resolves({ features: {} })
-//     stubRefreshToken = sinon.stub().returns('dummy_token_refreshed')
-
-//     stubTokenGetter = sinon.stub()
-//     let callCount = 0
-//     stubTokenGetter.onCall(callCount++).returns('dummy_token_initial')
-//     stubTokenGetter.onCall(callCount++).returns('dummy_token_initial')
-//     stubTokenGetter.onCall(callCount++).returns(undefined)
-//     stubTokenGetter.returns('dummy_token_persisted')
-
-//     stubFromCredentials = sinon.stub()
-//     const appCredentialsManagerValue = {
-//       token: undefined,
-//       refreshToken: stubRefreshToken
-//     }
-
-//     sinon.stub(appCredentialsManagerValue, 'token').get(stubTokenGetter)
-
-//     stubFromCredentials.returns(appCredentialsManagerValue)
-
-//     stubFromCredentials.onCall(1).returns({
-//       token: 'dummy_token_2'
-//     })
-
-//     const { getEsriToken } = proxyquire('../agol/getEsriToken', {
-//       '@esri/arcgis-rest-request': { ApplicationCredentialsManager: { fromCredentials: stubFromCredentials } }
-//     })
-
-//     agol = proxyquire('../agol', {
-//       './getEsriToken': { getEsriToken },
-//       '@esri/arcgis-rest-feature-service': { queryFeatures: stubQueryFeatures }
-//     })
-//   })
-
-//   afterAll(async () => {
-//     sinon.restore()
-//   })
-
-//   it('esriRequest should call fromCredentials and refreshToken once when 2 esriRequest are made', async () => {
-//     await agol.esriRequest('/endPoint1')
-//     expect(stubFromCredentials.calledOnce).toBeTruthy() // Expect One Call and no further calls
-//     expect(stubQueryFeatures.callCount).toEqual(1)
-//     expect(stubRefreshToken.callCount).toEqual(0) // Should use the initial token getter
-//     expect(stubTokenGetter.callCount).toEqual(2) // Once to test the value and once to return the value
-
-//     // Test that the expected params get passed to queryFeatures
-//     const expectedParameters = {
-//       url: 'https://services1.arcgis.com/DUMMY_SERVICE_ID/arcgis/rest/services/endPoint1',
-//       geometry: undefined,
-//       geometryType: undefined,
-//       spatialRel: 'esriSpatialRelIntersects',
-//       returnGeometry: 'false',
-//       authentication: 'dummy_token_initial',
-//       outFields: '*'
-//     }
-//     expect(stubQueryFeatures.getCall(0).args[0]).toEqual(expectedParameters)
-
-//     await agol.esriRequest('/endPoint2')
-//     expect(stubFromCredentials.calledOnce).toBeTruthy() // Should not have been called again
-//     expect(stubQueryFeatures.callCount).toEqual(2) // Should have been be called again
-//     expect(stubRefreshToken.callCount).toEqual(1) // Should not be called again
-
-//     // Test that the modified expected params get passed to queryFeatures
-//     Object.assign(expectedParameters, {
-//       url: 'https://services1.arcgis.com/DUMMY_SERVICE_ID/arcgis/rest/services/endPoint2',
-//       authentication: 'dummy_token_refreshed'
-//     })
-
-//     expect(stubQueryFeatures.getCall(1).args[0]).toEqual(expectedParameters)
-//   })
-// })
diff --git a/server/services/__tests__/getContacts.spec.js b/server/services/__tests__/getContacts.spec.js
deleted file mode 100644
index 4e482043..00000000
--- a/server/services/__tests__/getContacts.spec.js
+++ /dev/null
@@ -1,32 +0,0 @@
-const { mockEsriRequest, stopMockingEsriRequests } = require('./__mocks__/agol')
-const createServer = require('../../../server')
-
-describe('getContacts', () => {
-  let server
-  beforeAll(async () => {
-    mockEsriRequest()
-    server = await createServer()
-    await server.initialize()
-  })
-
-  afterAll(async () => {
-    await server.stop()
-    stopMockingEsriRequests()
-  })
-
-  const errorsToTest = [
-    ['false', false],
-    ['a non array', 'not-array']
-  ]
-  errorsToTest.forEach(([titleText, returnValue]) => {
-    it(`if esriRequest returns ${titleText} expect an error`, async () => {
-      mockEsriRequest(returnValue)
-      const { getContacts } = require('../../services/agol/getContacts')
-      try {
-        await getContacts({ geometryType: 'esriGeometryPolygon', polygon: [[1, 2], [3, 4]] })
-      } catch (error) {
-        expect(error.message).toEqual('Invalid response from AGOL customerTeam request')
-      }
-    })
-  })
-})
diff --git a/server/services/agol/__mocks__/getContacts.js b/server/services/agol/__mocks__/getContacts.js
index aa46df5c..29b9c8f9 100644
--- a/server/services/agol/__mocks__/getContacts.js
+++ b/server/services/agol/__mocks__/getContacts.js
@@ -1,10 +1,38 @@
-const getContacts = () => {
-  return {
-    isEngland: true,
-    EmailAddress: 'inforequests.gmmc@environment-agency.gov.uk',
-    AreaName: 'Greater Manchester Merseyside and Cheshire',
-    LocalAuthorities: 'Cheshire West and Chester (B)',
-    useAutomatedService: true
+const mockPolygons = require('../../__data__/mockPolygons.json')
+
+const optedOutResponse = {
+  isEngland: true,
+  EmailAddress: 'wessexenquiries@environment-agency.gov.uk',
+  AreaName: 'Wessex',
+  LocalAuthorities: 'Bath and North East Somerset',
+  useAutomatedService: false
+}
+
+const optedInResponse = {
+  isEngland: true,
+  EmailAddress: 'neyorkshire@environment-agency.gov.uk',
+  AreaName: 'Yorkshire',
+  LocalAuthorities: 'North Yorkshire',
+  useAutomatedService: true
+}
+
+const getContacts = async (options = {}) => {
+  switch (options.polygon) {
+    case mockPolygons.optedOut.fz3_only:
+      return optedOutResponse
+    case mockPolygons.fz1_only:
+    case mockPolygons.inRiskAdmin.fz1_only:
+    case mockPolygons.inRiskAdmin.throws:
+    case mockPolygons.fz2_only:
+    case mockPolygons.inRiskAdmin.fz2_only:
+    case mockPolygons.fz3_only:
+    case mockPolygons.inRiskAdmin.fz3_only:
+    case mockPolygons.fz2_and_3:
+    case mockPolygons.inRiskAdmin.fz2_and_3:
+      return optedInResponse
+    default: {
+      throw new Error(`Error - No Polygon Mocked for getContacts- ${JSON.stringify(options.polygon)}`)
+    }
   }
 }
 
diff --git a/server/services/agol/__mocks__/getCustomerTeam.js b/server/services/agol/__mocks__/getCustomerTeam.js
new file mode 100644
index 00000000..7ad46964
--- /dev/null
+++ b/server/services/agol/__mocks__/getCustomerTeam.js
@@ -0,0 +1,40 @@
+const mockPolygons = require('../../__data__/mockPolygons.json')
+
+const optedOutResponse = {
+  isEngland: true,
+  EmailAddress: 'wessexenquiries@environment-agency.gov.uk',
+  AreaName: 'Wessex',
+  useAutomatedService: false
+}
+
+const optedInResponse = {
+  isEngland: true,
+  EmailAddress: 'neyorkshire@environment-agency.gov.uk',
+  AreaName: 'Yorkshire',
+  useAutomatedService: true
+}
+
+const getCustomerTeam = async (options = {}) => {
+  if (options.geometryType === 'esriGeometryPoint') {
+    return optedOutResponse
+  }
+  switch (options.polygon) {
+    case mockPolygons.optedOut.fz3_only:
+      return optedOutResponse
+    case mockPolygons.fz1_only:
+    case mockPolygons.inRiskAdmin.fz1_only:
+    case mockPolygons.inRiskAdmin.throws:
+    case mockPolygons.fz2_only:
+    case mockPolygons.inRiskAdmin.fz2_only:
+    case mockPolygons.fz3_only:
+    case mockPolygons.inRiskAdmin.fz3_only:
+    case mockPolygons.fz2_and_3:
+    case mockPolygons.inRiskAdmin.fz2_and_3:
+      return optedInResponse
+    default: {
+      throw new Error(`Error - No Polygon Mocked for getCustomerTeam- ${JSON.stringify(options.polygon)}`)
+    }
+  }
+}
+
+module.exports = { getCustomerTeam }
diff --git a/server/services/agol/__mocks__/getFloodZones.js b/server/services/agol/__mocks__/getFloodZones.js
index 91291ed5..dcecb8da 100644
--- a/server/services/agol/__mocks__/getFloodZones.js
+++ b/server/services/agol/__mocks__/getFloodZones.js
@@ -11,6 +11,7 @@ const getFloodZones = async (options) => {
       return { floodZone: '2', floodzone_2: true, floodzone_3: false }
     case mockPolygons.fz3_only:
     case mockPolygons.inRiskAdmin.fz3_only:
+    case mockPolygons.optedOut.fz3_only:
       return { floodZone: '3', floodzone_2: false, floodzone_3: true }
     case mockPolygons.fz2_and_3:
     case mockPolygons.inRiskAdmin.fz2_and_3:
diff --git a/server/services/agol/__mocks__/getLocalAuthority.js b/server/services/agol/__mocks__/getLocalAuthority.js
new file mode 100644
index 00000000..d9aa3c69
--- /dev/null
+++ b/server/services/agol/__mocks__/getLocalAuthority.js
@@ -0,0 +1,30 @@
+const mockPolygons = require('../../__data__/mockPolygons.json')
+
+const bathResponse = { LocalAuthorities: 'Bath and North East Somerset' }
+const yorkshireResponse = { LocalAuthorities: 'North Yorkshire' }
+const wessexResponse = { LocalAuthorities: 'Winchester' }
+
+const getLocalAuthority = async (options = {}) => {
+  if (options.geometryType === 'esriGeometryPoint') {
+    return wessexResponse
+  }
+  switch (options.polygon) {
+    case mockPolygons.optedOut.fz3_only:
+      return bathResponse
+    case mockPolygons.fz1_only:
+    case mockPolygons.inRiskAdmin.fz1_only:
+    case mockPolygons.inRiskAdmin.throws:
+    case mockPolygons.fz2_only:
+    case mockPolygons.inRiskAdmin.fz2_only:
+    case mockPolygons.fz3_only:
+    case mockPolygons.inRiskAdmin.fz3_only:
+    case mockPolygons.fz2_and_3:
+    case mockPolygons.inRiskAdmin.fz2_and_3:
+      return yorkshireResponse
+    default: {
+      throw new Error(`Error - No Polygon Mocked for getLocalAuthority- ${JSON.stringify(options.polygon)}`)
+    }
+  }
+}
+
+module.exports = { getLocalAuthority }
diff --git a/server/services/agol/__tests__/esriRequest.spec.js b/server/services/agol/__tests__/esriRequest.spec.js
new file mode 100644
index 00000000..80d14f90
--- /dev/null
+++ b/server/services/agol/__tests__/esriRequest.spec.js
@@ -0,0 +1,28 @@
+const { esriRequest } = require('../esriRequest')
+const { queryFeatureSpy } = require('@esri/arcgis-rest-feature-service')
+
+const geometry = {
+  rings: [[[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]],
+  spatialReference: { wkid: 27700 }
+}
+
+describe('esriRequest', () => {
+  it('should respond with the mocked object', async () => {
+    const response = await esriRequest('/endpoint', geometry, 'esriGeometryPolygon')
+    expect(response).toEqual('QUERY_FEATURES_RESPONSE')
+  })
+
+  it('should call queryFeatures with the expected object', async () => {
+    queryFeatureSpy.expectParameters({
+      url: 'https://services1.arcgis.com/DUMMY_SERVICE_ID/arcgis/rest/services/endpoint',
+      geometry,
+      geometryType: 'esriGeometryPolygon',
+      spatialRel: 'esriSpatialRelIntersects',
+      returnGeometry: 'false',
+      authentication: 'TEST_TOKEN',
+      outFields: '*'
+    })
+    const response = await esriRequest('/endpoint', geometry, 'esriGeometryPolygon')
+    expect(response).toEqual('QUERY_FEATURES_RESPONSE')
+  })
+})
diff --git a/server/services/agol/__tests__/geometry.spec.js b/server/services/agol/__tests__/geometry.spec.js
new file mode 100644
index 00000000..e80da8fd
--- /dev/null
+++ b/server/services/agol/__tests__/geometry.spec.js
@@ -0,0 +1,25 @@
+const { makePointGeometry, makePolygonGeometry } = require('../')
+
+describe('makePointGeometry', () => {
+  it('should form a valid esriPointGeometry object', async () => {
+    expect(makePointGeometry(17, 19)).toEqual({ x: 17, y: 19, spatialReference: { wkid: 27700 } })
+  })
+})
+
+describe('makePolygonGeometry', () => {
+  it('should form a valid esriPolygonGeometry object from an array', async () => {
+    expect(makePolygonGeometry([[111, 111], [111, 112], [112, 112], [112, 111], [111, 111]]))
+      .toEqual({
+        rings: [[[111, 111], [111, 112], [112, 112], [112, 111], [111, 111]]],
+        spatialReference: { wkid: 27700 }
+      })
+  })
+
+  it('should form a valid esriPolygonGeometry object from an array in string form', async () => {
+    expect(makePolygonGeometry('[[111, 111], [111, 112], [112, 112], [112, 111], [111, 111]]'))
+      .toEqual({
+        rings: [[[111, 111], [111, 112], [112, 112], [112, 111], [111, 111]]],
+        spatialReference: { wkid: 27700 }
+      })
+  })
+})
diff --git a/server/services/agol/__tests__/getContacts.spec.js b/server/services/agol/__tests__/getContacts.spec.js
new file mode 100644
index 00000000..7ac1158d
--- /dev/null
+++ b/server/services/agol/__tests__/getContacts.spec.js
@@ -0,0 +1,35 @@
+const { getContacts } = require('../getContacts')
+const mockPolygons = require('../../__data__/mockPolygons.json')
+jest.mock('../getCustomerTeam')
+jest.mock('../getLocalAuthority')
+
+describe('getContacts', () => {
+  it('should return the combined contents of getCustomerTeam and getLocalAuthority for a polygon', async () => {
+    const response = await getContacts({
+      geometryType: 'esriGeometryPolygon',
+      polygon: mockPolygons.fz1_only
+    })
+    expect(response).toEqual({
+      LocalAuthorities: 'North Yorkshire',
+      isEngland: true,
+      EmailAddress: 'neyorkshire@environment-agency.gov.uk',
+      AreaName: 'Yorkshire',
+      useAutomatedService: true
+    })
+  })
+
+  it('should return the combined contents of getCustomerTeam and getLocalAuthority for a point', async () => {
+    const response = await getContacts({
+      geometryType: 'esriGeometryPoint',
+      x: 123,
+      y: 456
+    })
+    expect(response).toEqual({
+      LocalAuthorities: 'Winchester',
+      isEngland: true,
+      EmailAddress: 'wessexenquiries@environment-agency.gov.uk',
+      AreaName: 'Wessex',
+      useAutomatedService: false
+    })
+  })
+})
diff --git a/server/services/agol/__tests__/getCustomerTeam.spec.js b/server/services/agol/__tests__/getCustomerTeam.spec.js
new file mode 100644
index 00000000..175e890d
--- /dev/null
+++ b/server/services/agol/__tests__/getCustomerTeam.spec.js
@@ -0,0 +1,46 @@
+const { mockEsriRequest, stopMockingEsriRequests, mockEsriRequestWithThrow } = require('../../__tests__/__mocks__/agol')
+const { getCustomerTeam } = require('../getCustomerTeam')
+
+describe('getCustomerTeam', () => {
+  beforeEach(async () => {
+    mockEsriRequest([{
+      attributes: {
+        contact_email: 'neyorkshire@environment-agency.gov.uk',
+        area_name_1: 'Environment Agency team in Yorkshire',
+        use_automated_service: true
+      }
+    }])
+  })
+
+  afterEach(async () => {
+    stopMockingEsriRequests()
+  })
+
+  it('should assign values as expected', async () => {
+    const response = await getCustomerTeam({})
+    expect(response).toEqual({
+      isEngland: true,
+      EmailAddress: 'neyorkshire@environment-agency.gov.uk',
+      AreaName: 'Environment Agency team in Yorkshire',
+      useAutomatedService: true
+    })
+  })
+
+  it('should throw if EsriRequest throws', async () => {
+    mockEsriRequestWithThrow()
+    try {
+      await getCustomerTeam({})
+    } catch (error) {
+      expect(error.message).toEqual('mocked error')
+    }
+  })
+
+  it('should throw if EsriRequest is invalid', async () => {
+    mockEsriRequest('NOT AN ARRAY')
+    try {
+      await getCustomerTeam({})
+    } catch (error) {
+      expect(error.message).toEqual('Invalid response from AGOL customerTeam request')
+    }
+  })
+})
diff --git a/server/services/agol/__tests__/getEsriToken.spec.js b/server/services/agol/__tests__/getEsriToken.spec.js
new file mode 100644
index 00000000..d6f19150
--- /dev/null
+++ b/server/services/agol/__tests__/getEsriToken.spec.js
@@ -0,0 +1,19 @@
+const { getEsriToken } = require('../getEsriToken')
+const { _invalidateToken, _resetToken } = require('@esri/arcgis-rest-request')
+
+describe('getEsriToken', () => {
+  afterEach(_resetToken)
+
+  it('should return TEST_TOKEN', async () => {
+    expect(await getEsriToken()).toEqual('TEST_TOKEN')
+  })
+
+  it('should return REFRESHED_TOKEN after token is invalidated', async () => {
+    _invalidateToken()
+    expect(await getEsriToken()).toEqual('REFRESHED_TOKEN')
+  })
+
+  it('should return TEST_TOKEN again', async () => {
+    expect(await getEsriToken()).toEqual('TEST_TOKEN')
+  })
+})
diff --git a/server/services/agol/__tests__/getLocalAuthority.spec.js b/server/services/agol/__tests__/getLocalAuthority.spec.js
new file mode 100644
index 00000000..d2237b32
--- /dev/null
+++ b/server/services/agol/__tests__/getLocalAuthority.spec.js
@@ -0,0 +1,33 @@
+const { mockEsriRequest, mockEsriRequestWithThrow, stopMockingEsriRequests } = require('../../__tests__/__mocks__/agol')
+const { getLocalAuthority } = require('../getLocalAuthority')
+
+describe('getLocalAuthority', () => {
+  beforeEach(async () => {
+    mockEsriRequest([{ attributes: { authority_name: 'Ryedale' } }])
+  })
+
+  afterEach(stopMockingEsriRequests)
+
+  it('should assign values as expected', async () => {
+    const response = await getLocalAuthority({})
+    expect(response).toEqual({ LocalAuthorities: 'Ryedale' })
+  })
+
+  it('should throw if EsriRequest throws', async () => {
+    mockEsriRequestWithThrow()
+    try {
+      await getLocalAuthority({})
+    } catch (error) {
+      expect(error.message).toEqual('mocked error')
+    }
+  })
+
+  it('should throw if EsriRequest is invalid', async () => {
+    mockEsriRequest('NOT AN ARRAY')
+    try {
+      await getLocalAuthority({})
+    } catch (error) {
+      expect(error.message).toEqual('Invalid response from AGOL localAuthority request')
+    }
+  })
+})
diff --git a/server/services/agol/esriRequest.js b/server/services/agol/esriRequest.js
new file mode 100644
index 00000000..8590ee11
--- /dev/null
+++ b/server/services/agol/esriRequest.js
@@ -0,0 +1,20 @@
+const { config } = require('../../../config')
+const { queryFeatures } = require('@esri/arcgis-rest-feature-service')
+const { getEsriToken } = require('./getEsriToken')
+
+const esriRequest = async (endPoint, geometry, geometryType) => {
+  const esriToken = await getEsriToken()
+  const requestObject = {
+    url: `${config.agol.serviceUrl}${endPoint}`,
+    geometry,
+    geometryType,
+    spatialRel: 'esriSpatialRelIntersects',
+    returnGeometry: 'false',
+    authentication: esriToken,
+    outFields: '*'
+  }
+  const result = await queryFeatures(requestObject)
+  return result.features
+}
+
+module.exports = { esriRequest }
diff --git a/server/services/agol/getContacts.js b/server/services/agol/getContacts.js
index 9c46e70b..28c7b657 100644
--- a/server/services/agol/getContacts.js
+++ b/server/services/agol/getContacts.js
@@ -1,45 +1,17 @@
-const { config } = require('../../../config')
-const { esriRequest, makePointGeometry, makePolygonGeometry } = require('./')
+const { makePointGeometry, makePolygonGeometry } = require('./')
+const { getCustomerTeam } = require('./getCustomerTeam')
+const { getLocalAuthority } = require('./getLocalAuthority')
 
-const getContacts = async (options = {}) => {
-  const geometry = options.geometryType === 'esriGeometryPolygon'
+const getContacts = async (options) => {
+  options.geometry = options.geometryType === 'esriGeometryPolygon'
     ? makePolygonGeometry(options.polygon)
     : makePointGeometry(options.x, options.y)
-
-  const response = {
-    isEngland: false,
-    EmailAddress: '',
-    AreaName: '',
-    LocalAuthorities: '',
-    useAutomatedService: false
-  }
   return await Promise.all([
-    esriRequest(config.agol.customerTeamEndPoint, geometry, options.geometryType)
-      .then((esriResult) => {
-        // Note This request WILL NOT return data for areas that are outside of England
-        if (!esriResult || !Array.isArray(esriResult)) {
-          throw new Error('Invalid response from AGOL customerTeam request')
-        }
-        response.isEngland = esriResult.length > 0
-        const { attributes } = esriResult[0]
-        // console.log('\ncustomerTeam Results ', esriResult)
-        Object.assign(response, {
-          EmailAddress: attributes.contact_email,
-          AreaName: attributes.area_name_1, // This will change back to area_name once Paul fixes the data.
-          useAutomatedService: Boolean(attributes.use_automated_service)
-        })
-      }),
-    esriRequest(config.agol.localAuthorityEndPoint, geometry, options.geometryType)
-      .then((esriResult) => {
-        // Note This request WILL return data for areas that are outside of England (Paul is going to fix that)
-        if (!esriResult || !Array.isArray(esriResult)) {
-          throw new Error('Invalid response from AGOL localAuthority request')
-        }
-        const { attributes } = esriResult[0]
-        // console.log('\nlocalAuthority Results ', esriResult)
-        Object.assign(response, { LocalAuthorities: attributes.authority_name })
-      })
-  ]).then(() => response)
+    getCustomerTeam(options),
+    getLocalAuthority(options)
+  ]).then((responseArray) => {
+    return Object.assign({}, ...responseArray)
+  })
 }
 
 module.exports = { getContacts }
diff --git a/server/services/agol/getCustomerTeam.js b/server/services/agol/getCustomerTeam.js
new file mode 100644
index 00000000..27c05836
--- /dev/null
+++ b/server/services/agol/getCustomerTeam.js
@@ -0,0 +1,34 @@
+const { config } = require('../../../config')
+const { esriRequest } = require('./')
+
+const errorMessage = 'Invalid response from AGOL customerTeam request'
+
+const getCustomerTeam = (options) => {
+  const response = {
+    isEngland: false,
+    EmailAddress: '',
+    AreaName: '',
+    useAutomatedService: false
+  }
+  return esriRequest(config.agol.customerTeamEndPoint, options.geometry, options.geometryType)
+    .then((esriResult) => {
+    // Note This request WILL NOT return data for areas that are outside of England
+      if (!esriResult || !Array.isArray(esriResult)) {
+        console.log(errorMessage, 'response was', esriResult)
+        throw new Error(errorMessage)
+      }
+      response.isEngland = esriResult.length > 0
+      const { attributes } = esriResult[0]
+      // console.log('\ncustomerTeam Results ', esriResult)
+      return Object.assign(response, {
+        EmailAddress: attributes.contact_email,
+        AreaName: attributes.area_name_1, // This will change back to area_name once Paul fixes the data.
+        useAutomatedService: Boolean(attributes.use_automated_service)
+      })
+    }).catch((error) => {
+      console.log('Error thrown in getCustomerTeam', error)
+      throw error
+    })
+}
+
+module.exports = { getCustomerTeam }
diff --git a/server/services/agol/getLocalAuthority.js b/server/services/agol/getLocalAuthority.js
new file mode 100644
index 00000000..e555723a
--- /dev/null
+++ b/server/services/agol/getLocalAuthority.js
@@ -0,0 +1,15 @@
+const { config } = require('../../../config')
+const { esriRequest } = require('./')
+
+const getLocalAuthority = (options) => {
+  return esriRequest(config.agol.localAuthorityEndPoint, options.geometry, options.geometryType)
+    .then((esriResult) => {
+      if (!esriResult || !Array.isArray(esriResult)) {
+        throw new Error('Invalid response from AGOL localAuthority request')
+      }
+      const { attributes } = esriResult[0]
+      return { LocalAuthorities: attributes.authority_name }
+    })
+}
+
+module.exports = { getLocalAuthority }
diff --git a/server/services/agol/index.js b/server/services/agol/index.js
index 6ada0d02..13bb51fd 100644
--- a/server/services/agol/index.js
+++ b/server/services/agol/index.js
@@ -1,6 +1,4 @@
-const { config } = require('../../../config')
-const { queryFeatures } = require('@esri/arcgis-rest-feature-service')
-const { getEsriToken } = require('./getEsriToken')
+const { esriRequest } = require('./esriRequest')
 
 const makePointGeometry = (x, y) => ({ x, y, spatialReference: { wkid: 27700 } })
 
@@ -12,20 +10,4 @@ const makePolygonGeometry = (polygon) => {
   }
 }
 
-const esriRequest = async (endPoint, geometry, geometryType) => {
-  const esriToken = await getEsriToken()
-  const requestObject = {
-    url: `${config.agol.serviceUrl}${endPoint}`,
-    geometry,
-    geometryType,
-    spatialRel: 'esriSpatialRelIntersects',
-    returnGeometry: 'false',
-    authentication: esriToken,
-    outFields: '*'
-  }
-  // console.dir(requestObject, { depth: null })
-  const result = await queryFeatures(requestObject)
-  return result.features
-}
-
 module.exports = { esriRequest, makePointGeometry, makePolygonGeometry }
diff --git a/server/views/results.html b/server/views/results.html
index 6ecc42dd..77bfc323 100644
--- a/server/views/results.html
+++ b/server/views/results.html
@@ -30,9 +30,11 @@ <h3 class="govuk-heading-s govuk-warning-text__text" data-testid="understanding-
     {% endif %}
 
     <div class="govuk-grid-column-two-thirds">
-      <a href="contact?polygon={{polygon}}" class="govuk-button" data-module="govuk-button">
-        Order flood risk data
-      </a>
+      {% if showOrderProduct4Button %}
+        <a href="contact?polygon={{polygon}}" class="govuk-button" data-module="govuk-button" data-testid="order-product4">
+          Order flood risk data
+        </a>
+      {% endif %}
     </div>
   </div>
 {% endblock %}

From c4eee7b531150a72236618bac1b9ea2e5348b296 Mon Sep 17 00:00:00 2001
From: Tedd <teddmason@gmail.com>
Date: Wed, 22 Jan 2025 11:45:50 +0000
Subject: [PATCH 5/5] FCRM-5487 - setting datasets and map feature toggles to
 expanded (#455)

---
 client/js/defra-map/index.js | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/client/js/defra-map/index.js b/client/js/defra-map/index.js
index b678868f..2b46db4d 100644
--- a/client/js/defra-map/index.js
+++ b/client/js/defra-map/index.js
@@ -400,7 +400,7 @@ getDefraMapConfig().then((defraMapConfig) => {
       keyDisplay: 'min',
       segments: [{
         heading: 'Datasets',
-        collapse: 'collapse',
+        collapse: 'expanded',
         items: [
           {
             id: 'fz',
@@ -583,7 +583,7 @@ getDefraMapConfig().then((defraMapConfig) => {
         {
           heading: 'Map features',
           parentIds: ['fz'],
-          collapse: 'collapse',
+          collapse: 'expanded',
           items: [
             keyItemDefinitions.floodZone1,
             keyItemDefinitions.floodZone2,
@@ -595,7 +595,7 @@ getDefraMapConfig().then((defraMapConfig) => {
         {
           heading: 'Map features',
           parentIds: ['rsd', 'rsu', 'sw'],
-          collapse: 'collapse',
+          collapse: 'expanded',
           items: [
             keyItemDefinitions.floodExtents,
             keyItemDefinitions.waterStorageAreas,
@@ -606,7 +606,7 @@ getDefraMapConfig().then((defraMapConfig) => {
         {
           heading: 'Map features',
           parentIds: ['mo'],
-          collapse: 'collapse',
+          collapse: 'expanded',
           items: [
             keyItemDefinitions.waterStorageAreas,
             keyItemDefinitions.floodDefences,