Repository: DEFRA/water-abstraction-system
Pull request #1917: Add code scaffolding script for services and presenters
Author: login: jonathangoulding
Labels: name: enhancement
description: New feature or request
Created: 2025-04-25T10:00:42Z | Merged: 2025-04-30T16:18:07Z

We regularly create new services and presenters with consistent structure, we have style guides to follow.

This script automates that repetitive setup, reduces human error, and saves time by enforcing conventions and reusing templates.

Includes support for:
- Nested module paths
- Auto-generation of service and presenter files in app/ and test/ directories
- Template placeholders (__MODULENAME__, __REQUIRE_PATH__, __DESCRIBE_LABEL__)
- Skipping existing files to prevent overwrites
- Separate test templates for services and presenters


Diff:
diff --git a/.labrc.js b/.labrc.js
index fa8371fb38..7f2f7c1e86 100644
--- a/.labrc.js
+++ b/.labrc.js
@@ -13,7 +13,8 @@ module.exports = {
     'app/plugins/hapi-pino.plugin.js',
     'app/plugins/stop.plugin.js',
     'app/plugins/views.plugin.js',
-    'config/notify.config.js'
+    'config/notify.config.js',
+    'templates'
   ],
   // lcov reporter required for SonarQube
   reporter: ['console', 'html', 'lcov'],
diff --git a/README.md b/README.md
index 16c8c8d5ef..8fe24a315c 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,10 @@ Check out [.env.example](/.env.example) for details of the required things you'l
 
 Refer to the [config files](config) for details of all the configuration used.
 
+## Scaffolding
+
+We use a set of tooling referred to as [Scaffolding](templates/README.md) to generate boilerplate templates. This system is designed for ongoing use to ensure consistency and alignment with our project standards whenever features / journeys are added.
+
 ## Contributing to this project
 
 If you have an idea you'd like to contribute please log an issue.
diff --git a/scaffold.sh b/scaffold.sh
new file mode 100644
index 0000000000..4d84f26f46
--- /dev/null
+++ b/scaffold.sh
@@ -0,0 +1,331 @@
+#!/bin/bash
+
+set -e
+
+# ------------------------------------------------------------------------------
+# Inputs and Initial Setup
+# ------------------------------------------------------------------------------
+
+INPUT_PATH="$1"
+
+if [ -z "$INPUT_PATH" ]; then
+  echo "Usage: $0 <name> or <path/name> (e.g., my-module or folder/my-module)"
+  exit 1
+fi
+
+RAW_NAME=$(basename "$INPUT_PATH")
+REL_DIR=$(dirname "$INPUT_PATH")
+if [ "$REL_DIR" = "." ]; then
+  REL_DIR=""
+fi
+
+to_pascal_case() {
+  echo "$1" | sed -E 's/[-_]+/ /g' | awk '{for(i=1;i<=NF;++i) $i=toupper(substr($i,1,1)) substr($i,2)}1' | tr -d ' '
+}
+
+PASCAL_NAME=$(to_pascal_case "$RAW_NAME")
+
+# ------------------------------------------------------------------------------
+# Templates
+# ------------------------------------------------------------------------------
+
+TEMPLATE_SERVICE="templates/service.js"
+TEMPLATE_SERVICE_FETCH="templates/service-fetch.service.js"
+TEMPLATE_SERVICE_PRESENTER_SESSION="templates/service-session-presenter.service.js"
+TEMPLATE_SUBMIT_SERVICE="templates/submit.service.js"
+TEMPLATE_VALIDATOR="templates/validator.js"
+TEMPLATE_VIEW="templates/view.njk"
+
+
+TEMPLATE_TEST_SERVICE="templates/service.test.js"
+TEMPLATE_TEST_SERVICE_FETCH="templates/service-fetch.service.test.js"
+TEMPLATE_TEST_SERVICE_PRESENTER_SESSION="templates/service-session-presenter.service.test.js"
+TEMPLATE_TEST_SUBMIT_SERVICE="templates/submit.service.test.js"
+TEMPLATE_TEST_VALIDATOR="templates/validator.test.js"
+
+
+TEMPLATE_FETCH="templates/fetch.js"
+TEMPLATE_TEST_FETCH="templates/fetch.test.js"
+TEMPLATE_PRESENTER="templates/presenter.js"
+TEMPLATE_TEST_PRESENTER="templates/presenter.test.js"
+
+
+# ------------------------------------------------------------------------------
+# Helper Functions
+# ------------------------------------------------------------------------------
+
+build_up_path() {
+  local slash_count=0
+  if [ -n "$REL_DIR" ]; then
+    slash_count=$(grep -o "/" <<< "$REL_DIR" | wc -l | tr -d ' ')
+  fi
+  printf '../%.0s' $(seq 1 $((slash_count + 2)))
+}
+
+render_file() {
+  local template="$1"
+  local output_path="$2"
+  local module_name="$3"
+  local presenter_path="$4"
+  local fetch_path="$5"
+  local session_model_path="$6"
+  local validator_path="$7"
+  local view_path="$8"
+
+  mkdir -p "$(dirname "$output_path")"
+
+  if [ -f "$output_path" ]; then
+    echo "⚠️  Skipped (already exists): $output_path"
+    return
+  fi
+
+  sed -e "s#__FETCH_PATH__#${fetch_path}#g" \
+      -e "s#__PRESENTER_PATH__#${presenter_path}#g" \
+      -e "s#__SESSION_MODEL_PATH__#${session_model_path}#g" \
+      -e "s#__VALIDATOR_PATH__#${validator_path}#g" \
+      -e "s#__VIEW_PATH__#${view_path}#g" \
+      -e "s/__CONTROLLER_NAME__/${PASCAL_NAME}Controller/g" \
+      -e "s/__FETCH_NAME__/Fetch${PASCAL_NAME}Service/g" \
+      -e "s/__NAME__/${PASCAL_NAME}/g" \
+      -e "s/__PRESENTER_NAME__/${PASCAL_NAME}Presenter/g" \
+      -e "s/__SERVICE_NAME__/${PASCAL_NAME}Service/g" \
+      -e "s/__SUBMIT_NAME__/Submit${PASCAL_NAME}Service/g" \
+      -e "s/__VALIDATOR_NAME__/${PASCAL_NAME}Validator/g" \
+      -e "s/__MODULE_NAME__/${module_name}/g" \
+      "$template" > "$output_path"
+
+  echo "✅ Created $output_path"
+}
+
+render_test_file() {
+  local template="$1"
+  local output_path="$2"
+  local module_name="$3"
+  local require_path="$4"
+  local describe_label="$5"
+  local presenter_path="$6"
+  local fetch_path="$7"
+  local session_helper_path="$8"
+
+  mkdir -p "$(dirname "$output_path")"
+
+  if [ -f "$output_path" ]; then
+    echo "⚠️  Skipped (already exists): $output_path"
+    return
+  fi
+
+  sed -e "s/__MODULE_NAME__/${module_name}/g" \
+      -e "s#__REQUIRE_PATH__#${require_path}#g" \
+      -e "s/__DESCRIBE_LABEL__/${describe_label}/g" \
+      -e "s#__PRESENTER_PATH__#${presenter_path}#g" \
+      -e "s/__PRESENTER_NAME__/${PASCAL_NAME}Presenter/g" \
+      -e "s/__FETCH_NAME__/Fetch${PASCAL_NAME}Service/g" \
+      -e "s#__FETCH_PATH__#${fetch_path}#g" \
+      -e "s#__SESSION_HELPER_PATH__#${session_helper_path}#g" \
+      "$template" > "$output_path"
+
+  echo "✅ Created $output_path"
+}
+
+generate_helper_snippet() {
+  local snippet_template="templates/helper.js"
+
+  if [ -f "$snippet_template" ]; then
+    echo ""
+    echo "Here is your ready-to-use controller + router snippet:"
+    echo ""
+
+    local service_path="../services/${REL_DIR}/${RAW_NAME}.service.js"
+    local submit_service_path="../services/${REL_DIR}/submit-${RAW_NAME}.service.js"
+
+    sed -e "s|__SERVICE_NAME__|${PASCAL_NAME}Service|g" \
+        -e "s|__SUBMIT_NAME__|Submit${PASCAL_NAME}Service|g" \
+        -e "s|__SERVICE_PATH__|${service_path}|g" \
+        -e "s|__NAME__|${PASCAL_NAME}|g" \
+        -e "s|__VIEW_PATH__|${VIEW_PATH}|g" \
+        -e "s|__CONTROLLER_NAME__|${PASCAL_NAME}Controller|g" \
+        -e "s|__SUBMIT_PATH__|${submit_service_path}|g" \
+        "$snippet_template"
+
+    echo ""
+  else
+    echo "❌ Snippet template not found: $snippet_template"
+  fi
+}
+
+generate_paths() {
+  local type="$1"    # Service, Presenter,
+  local service_variant="$2"  # service, presenter, fetch-presenter
+
+  TYPE="$type"
+  TYPE_LOWER=$(echo "$TYPE" | tr '[:upper:]' '[:lower:]')
+
+  MODULE_NAME="${PASCAL_NAME}${TYPE}"
+  SOURCE_FILE="${RAW_NAME}.${TYPE_LOWER}.js"
+  TEST_FILE="${RAW_NAME}.${TYPE_LOWER}.test.js"
+
+    if [ "$TYPE" = "Service" ]; then
+      SUBFOLDER="services"
+
+      # Additional input
+      FETCH_PATH="./fetch-${RAW_NAME}.service.js"
+
+      case "$service_variant" in
+        service)
+          SOURCE_TEMPLATE="$TEMPLATE_SERVICE"
+          TEST_TEMPLATE="$TEMPLATE_TEST_SERVICE"
+          ;;
+        service-with-fetch)
+          SOURCE_TEMPLATE="$TEMPLATE_SERVICE_FETCH"
+          TEST_TEMPLATE="$TEMPLATE_TEST_SERVICE_FETCH"
+          ;;
+        presenter-session)
+          SOURCE_TEMPLATE="$TEMPLATE_SERVICE_PRESENTER_SESSION"
+          TEST_TEMPLATE="$TEMPLATE_TEST_SERVICE_PRESENTER_SESSION"
+          ;;
+        fetch)
+            SOURCE_TEMPLATE="$TEMPLATE_FETCH"
+            TEST_TEMPLATE="$TEMPLATE_TEST_FETCH"
+            # We prefix our fetch services with 'fetch'
+            MODULE_NAME="Fetch${MODULE_NAME}"
+            SOURCE_FILE="fetch-${SOURCE_FILE}"
+            TEST_FILE="fetch-${TEST_FILE}"
+          ;;
+        submit)
+            SOURCE_TEMPLATE="$TEMPLATE_SUBMIT_SERVICE"
+            TEST_TEMPLATE="$TEMPLATE_TEST_SUBMIT_SERVICE"
+            # We prefix our submit services with 'submit'
+            MODULE_NAME="Submit${MODULE_NAME}"
+            SOURCE_FILE="submit-${SOURCE_FILE}"
+            TEST_FILE="submit-${TEST_FILE}"
+          ;;
+        *)
+          echo "❌ Unknown service variant: $service_variant"
+          exit 1
+          ;;
+      esac
+
+    elif [ "$TYPE" = "Presenter" ]; then
+      SOURCE_TEMPLATE="$TEMPLATE_PRESENTER"
+      TEST_TEMPLATE="$TEMPLATE_TEST_PRESENTER"
+
+      SUBFOLDER="presenters"
+
+    elif [ "$TYPE" = "Validator" ]; then
+        SOURCE_TEMPLATE="$TEMPLATE_VALIDATOR"
+        TEST_TEMPLATE="$TEMPLATE_TEST_VALIDATOR"
+
+        SUBFOLDER="validators"
+
+    elif [ "$TYPE" = "View" ]; then
+        SOURCE_FILE="${RAW_NAME}.${TYPE_LOWER}.njk"
+        SOURCE_TEMPLATE="$TEMPLATE_VIEW"
+
+        # Do nto generate a view test file
+        TEST_TEMPLATE=""
+        TEST_FILE=""
+
+        SUBFOLDER="views"
+    else
+      echo "❌ Unknown type: $TYPE"
+      exit 1
+    fi
+
+  # When a folder path is supplied
+  if [ -n "$REL_DIR" ]; then
+    SOURCE_OUTPUT="app/${SUBFOLDER}/${REL_DIR}/${SOURCE_FILE}"
+    TEST_OUTPUT="test/${SUBFOLDER}/${REL_DIR}/${TEST_FILE}"
+  else
+    SOURCE_OUTPUT="app/${SUBFOLDER}/${SOURCE_FILE}"
+    TEST_OUTPUT="test/${SUBFOLDER}/${TEST_FILE}"
+  fi
+
+  # Helper
+  RELATIVE_UP_PATH=$(build_up_path)
+
+  # Test
+  DESCRIBE_LABEL="$(echo "$RAW_NAME" | sed -E 's/[-_]+/ /g' | awk '{for(i=1;i<=NF;++i) $i=toupper(substr($i,1,1)) substr($i,2)}1') $TYPE" # Test describe block
+  REQUIRE_PATH="${RELATIVE_UP_PATH}../app/${SUBFOLDER}/${REL_DIR}/${SOURCE_FILE}"
+  TEST_FETCH_PATH="${RELATIVE_UP_PATH}../app/services/${REL_DIR}/fetch-${SOURCE_FILE}"
+  SESSION_HELPER_PATH="${RELATIVE_UP_PATH}support/helpers/session.helper.js"
+
+  # Additional paths
+  PRESENTER_PATH="${RELATIVE_UP_PATH}presenters/${REL_DIR}/${RAW_NAME}.presenter.js"
+  VALIDATOR_PATH="${RELATIVE_UP_PATH}validators/${REL_DIR}/${RAW_NAME}.validator.js"
+  SESSION_MODEL_PATH="${RELATIVE_UP_PATH}models/session.model.js"
+
+  VIEW_PATH="${REL_DIR}/${RAW_NAME}.njk" # The view path for the controllers `h.view()`
+}
+
+render_source_and_test() {
+  local type="$1"
+  local service_variant="$2"
+
+  generate_paths "$type" "$service_variant"
+
+  # Render source file
+  render_file "$SOURCE_TEMPLATE" "$SOURCE_OUTPUT" "$MODULE_NAME" "$PRESENTER_PATH" "$FETCH_PATH" "$SESSION_MODEL_PATH" "$VALIDATOR_PATH" "$VIEW_PATH"
+
+  # Render test file
+  if [ -n "$TEST_TEMPLATE" ]; then
+    render_test_file "$TEST_TEMPLATE" "$TEST_OUTPUT" "$MODULE_NAME" "$REQUIRE_PATH" "$DESCRIBE_LABEL" "$PRESENTER_PATH" "$TEST_FETCH_PATH" "$SESSION_HELPER_PATH"
+  fi
+}
+
+# ------------------------------------------------------------------------------
+# CLI Prompt
+# ------------------------------------------------------------------------------
+
+echo ""
+echo "What do you want to scaffold?"
+echo "1) Journey - Complete"
+echo "2) Journey - View"
+echo "3) Journey - Submit"
+echo "4) Presenter only"
+echo "5) Service only"
+echo "6) Service with Fetch"
+echo ""
+
+read -rp "> " choice
+
+case "$choice" in
+  1)
+    echo "📦 Generating complete journey..."
+    render_source_and_test "Presenter" ""
+    render_source_and_test "View" ""
+    render_source_and_test "Service" "presenter-session"
+    render_source_and_test "Service" "submit"
+    render_source_and_test "Validator" ""
+
+    # Generate helpers
+    generate_helper_snippet
+    ;;
+  2)
+    echo "📦 Generating view journey..."
+    render_source_and_test "Presenter" ""
+    render_source_and_test "View" ""
+    render_source_and_test "Service" "presenter-session"
+    ;;
+  3)
+    echo "📦 Generating submit journey..."
+    render_source_and_test "Service" "submit"
+    render_source_and_test "Validator" ""
+    ;;
+  4)
+    echo "📦 Generating Presenter only..."
+    render_source_and_test "Presenter" ""
+    ;;
+  5)
+    echo "📦 Generating Service only..."
+    render_source_and_test "Service" "service"
+    ;;
+  6)
+    echo "📦 Generating Service with Fetch..."
+    render_source_and_test "Service" "service-with-fetch"
+    render_source_and_test "Service" "fetch"
+    ;;
+  *)
+    echo "❌ Invalid selection. Exiting."
+    exit 1
+    ;;
+esac
diff --git a/templates/README.md b/templates/README.md
new file mode 100644
index 0000000000..f7bbcdbe37
--- /dev/null
+++ b/templates/README.md
@@ -0,0 +1,77 @@
+# 🛠 Scaffolding for adding new Journeys
+
+Quickly generates source files and matching test files based on templates.
+
+## ✅ Usage
+
+```bash
+./scaffold.sh <name> or <path/to/name>
+```
+
+## 📋 Examples
+
+```bash
+./scaffold.sh my-module
+# → app/services/my-module.service.js
+# → test/services/my-module.service.test.js
+
+./scaffold.sh notices/setup/fella/my-module
+# → app/services/notices/setup/fella/my-module.service.js
+# → test/services/notices/setup/fella/my-module.service.test.js
+```
+
+## 📦 Scaffold Options
+
+After running, you'll be prompted to choose:
+
+1. Journey - Complete
+2. Journey - View
+3. Journey - Submit
+4. Presenter only
+5. Service only
+6. Service with Fetch
+
+➡️ Templates are selected and filled automatically depending on your choice.
+
+## 🛠 Key Features
+
+- Always generates both source files and corresponding test files.
+- Correct `require()` paths are dynamically calculated based on folder depth.
+- Handles nested folder structures under `services/` cleanly.
+- Correct relative pathing for `SessionModel` imports inside services.
+- Skips file creation if the file already exists (safe by default).
+
+## 🧠 Special Handling
+
+- **`render_file()`**: Renders the actual source file (Service, Presenter, FetchService).
+- **`render_test_file()`**: Renders the corresponding test file.
+- **SessionModel imports** are dynamically resolved based on service file depth.
+
+## 🛠 Path Calculation Rules
+
+- **`build_up_path()`**: Calculates how many `../` are needed in tests to require the correct source file.
+- **`build_session_model_path()`**: Calculates how many `../` are needed to correctly import `models/session.model.js` inside a service.
+
+**Example**:
+
+```text
+Service at: app/services/a/b/c/my-service.service.js
+Will import: ../../../../models/session.model.js
+```
+
+## 📋 Placeholders Replaced in Templates
+
+These placeholders are automatically replaced inside templates:
+
+| Placeholder              | Description                            |
+| ------------------------ | -------------------------------------- |
+| `__MODULE_NAME__`        | PascalCase name of the module          |
+| `__REQUIRE_PATH__`       | Path to the source file from test file |
+| `__DESCRIBE_LABEL__`     | Test suite name for `describe()`       |
+| `__PRESENTER_NAME__`     | PascalCase Presenter module name       |
+| `__PRESENTER_PATH__`     | Path to Presenter file                 |
+| `__FETCH_NAME__`         | PascalCase FetchService module name    |
+| `__FETCH_PATH__`         | Path to FetchService file              |
+| `__SESSION_MODEL_PATH__` | Path to `models/session.model.js`      |
+| `__VALIDATOR_NAME__`     | PascalCase Validator module name       |
+| `__VALIDATOR_PATH__`     | Path to Validator file                 |
diff --git a/templates/fetch.js b/templates/fetch.js
new file mode 100644
index 0000000000..46ce9d1c35
--- /dev/null
+++ b/templates/fetch.js
@@ -0,0 +1,26 @@
+'use strict'
+
+/**
+ * Fetches
+ *
+ * @module __MODULE_NAME__
+ */
+
+/**
+ * Fetches
+ *
+ * @returns {Promise<object>} - The data
+ */
+async function go() {
+  const { rows } = await _fetch()
+
+  return rows
+}
+
+async function _fetch() {
+  return ''
+}
+
+module.exports = {
+  go
+}
diff --git a/templates/fetch.test.js b/templates/fetch.test.js
new file mode 100644
index 0000000000..e5587b7495
--- /dev/null
+++ b/templates/fetch.test.js
@@ -0,0 +1,30 @@
+'use strict'
+
+// Test framework dependencies
+const Lab = require('@hapi/lab')
+const Code = require('@hapi/code')
+const Sinon = require('sinon')
+
+const { describe, it, afterEach, beforeEach } = (exports.lab = Lab.script())
+const { expect } = Code
+
+// Thing under test
+const __MODULE_NAME__ = require('__REQUIRE_PATH__')
+
+describe('__DESCRIBE_LABEL__', () => {
+  let session
+
+  beforeEach(async () => {})
+
+  afterEach(() => {
+    Sinon.restore()
+  })
+
+  describe('when called', () => {
+    it('returns page data for the view', async () => {
+      const result = await __MODULE_NAME__.go(session.id)
+
+      expect(result).to.equal({})
+    })
+  })
+})
diff --git a/templates/helper.js b/templates/helper.js
new file mode 100644
index 0000000000..b81336c4f2
--- /dev/null
+++ b/templates/helper.js
@@ -0,0 +1,57 @@
+/* eslint-disable camelcase, no-unused-vars, strict, no-undef */
+
+// Controller code
+const __SERVICE_NAME__ = require('__SERVICE_PATH__')
+const __SUBMIT_NAME__ = require('__SUBMIT_PATH__')
+
+async function view__NAME__(request, h) {
+  const { sessionId } = request.params
+
+  const pageData = await __SERVICE_NAME__.go(sessionId)
+
+  return h.view(`__VIEW_PATH__`, pageData)
+}
+
+async function submit__NAME__(request, h) {
+  const {
+    payload,
+    params: { sessionId }
+  } = request
+
+  const pageData = await __SUBMIT_NAME__.go(sessionId, payload)
+
+  if (pageData.error) {
+    return h.view(`__VIEW_PATH__`, pageData)
+  }
+
+  return h.redirect('')
+}
+
+// router code
+
+const routes = [
+  {
+    method: 'GET',
+    path: '',
+    options: {
+      handler: __CONTROLLER_NAME__.view__NAME__,
+      auth: {
+        access: {
+          scope: ['']
+        }
+      }
+    }
+  },
+  {
+    method: 'POST',
+    path: '',
+    options: {
+      handler: __CONTROLLER_NAME__.submit__NAME__,
+      auth: {
+        access: {
+          scope: ['']
+        }
+      }
+    }
+  }
+]
diff --git a/templates/presenter.js b/templates/presenter.js
new file mode 100644
index 0000000000..10846d7f77
--- /dev/null
+++ b/templates/presenter.js
@@ -0,0 +1,19 @@
+'use strict'
+
+/**
+ * Formats data for the `` page
+ * @module __MODULE_NAME__
+ */
+
+/**
+ * Formats data for the `` page
+ *
+ * @returns {object} - The data formatted for the view template
+ */
+function go() {
+  return {}
+}
+
+module.exports = {
+  go
+}
diff --git a/templates/presenter.test.js b/templates/presenter.test.js
new file mode 100644
index 0000000000..42031b60e6
--- /dev/null
+++ b/templates/presenter.test.js
@@ -0,0 +1,25 @@
+'use strict'
+
+// Test framework dependencies
+const Lab = require('@hapi/lab')
+const Code = require('@hapi/code')
+
+const { describe, it, beforeEach } = (exports.lab = Lab.script())
+const { expect } = Code
+
+// Thing under test
+const __MODULE_NAME__ = require('__REQUIRE_PATH__')
+
+describe('__DESCRIBE_LABEL__', () => {
+  let session
+
+  beforeEach(async () => {})
+
+  describe('when called', () => {
+    it('returns page data for the view', () => {
+      const result = __MODULE_NAME__.go(session.id)
+
+      expect(result).to.equal({})
+    })
+  })
+})
diff --git a/templates/service-fetch.service.js b/templates/service-fetch.service.js
new file mode 100644
index 0000000000..302e3438e9
--- /dev/null
+++ b/templates/service-fetch.service.js
@@ -0,0 +1,24 @@
+'use strict'
+
+/**
+ * Orchestrates presenting the data for `` page
+ *
+ * @module __MODULE_NAME__
+ */
+
+const __FETCH_NAME__ = require('__FETCH_PATH__')
+
+/**
+ * Orchestrates presenting the data for `` page
+ *
+ * @returns {Promise<object>} - The data formatted for the view template
+ */
+async function go() {
+  const data = await __FETCH_NAME__.go()
+
+  return data
+}
+
+module.exports = {
+  go
+}
diff --git a/templates/service-fetch.service.test.js b/templates/service-fetch.service.test.js
new file mode 100644
index 0000000000..88a3dbce9e
--- /dev/null
+++ b/templates/service-fetch.service.test.js
@@ -0,0 +1,35 @@
+'use strict'
+
+// Test framework dependencies
+const Lab = require('@hapi/lab')
+const Code = require('@hapi/code')
+const Sinon = require('sinon')
+
+const { describe, it, afterEach, beforeEach } = (exports.lab = Lab.script())
+const { expect } = Code
+
+// Things we need to stub
+const __FETCH_NAME__ = require('__FETCH_PATH__')
+
+// Thing under test
+const __MODULE_NAME__ = require('__REQUIRE_PATH__')
+
+describe('__DESCRIBE_LABEL__', () => {
+  let session
+
+  beforeEach(async () => {
+    Sinon.stub(__FETCH_NAME__, 'go').resolves({})
+  })
+
+  afterEach(() => {
+    Sinon.restore()
+  })
+
+  describe('when called', () => {
+    it('returns page data for the view', async () => {
+      const result = await __MODULE_NAME__.go(session.id)
+
+      expect(result).to.equal({})
+    })
+  })
+})
diff --git a/templates/service-session-presenter.service.js b/templates/service-session-presenter.service.js
new file mode 100644
index 0000000000..31298dd51c
--- /dev/null
+++ b/templates/service-session-presenter.service.js
@@ -0,0 +1,31 @@
+'use strict'
+
+/**
+ * Orchestrates presenting the data for `` page
+ *
+ * @module __MODULE_NAME__
+ */
+
+const __PRESENTER_NAME__ = require('__PRESENTER_PATH__')
+const SessionModel = require('__SESSION_MODEL_PATH__')
+
+/**
+ * Orchestrates presenting the data for `` page
+ *
+ * @param {string} sessionId
+ *
+ * @returns {Promise<object>} - The data formatted for the view template
+ */
+async function go(sessionId) {
+  const session = await SessionModel.query().findById(sessionId)
+
+  const pageData = __PRESENTER_NAME__.go(session)
+
+  return {
+    ...pageData
+  }
+}
+
+module.exports = {
+  go
+}
diff --git a/templates/service-session-presenter.service.test.js b/templates/service-session-presenter.service.test.js
new file mode 100644
index 0000000000..abe72f97b1
--- /dev/null
+++ b/templates/service-session-presenter.service.test.js
@@ -0,0 +1,32 @@
+'use strict'
+
+// Test framework dependencies
+const Lab = require('@hapi/lab')
+const Code = require('@hapi/code')
+
+const { describe, it, beforeEach } = (exports.lab = Lab.script())
+const { expect } = Code
+
+// Test helpers
+const SessionHelper = require('__SESSION_HELPER_PATH__')
+
+// Thing under test
+const __MODULE_NAME__ = require('__REQUIRE_PATH__')
+
+describe('__DESCRIBE_LABEL__', () => {
+  let session
+  let sessionData
+
+  beforeEach(async () => {
+    sessionData = {}
+    session = await SessionHelper.add({ data: sessionData })
+  })
+
+  describe('when called', () => {
+    it('returns page data for the view', async () => {
+      const result = await __MODULE_NAME__.go(session.id)
+
+      expect(result).to.equal({})
+    })
+  })
+})
diff --git a/templates/service.js b/templates/service.js
new file mode 100644
index 0000000000..29c7604edb
--- /dev/null
+++ b/templates/service.js
@@ -0,0 +1,20 @@
+'use strict'
+
+/**
+ * Orchestrates presenting the data for `` page
+ *
+ * @module __MODULE_NAME__
+ */
+
+/**
+ * Orchestrates presenting the data for `` page
+ *
+ * @returns {Promise<object>} - The data formatted for the view template
+ */
+async function go() {
+  return ''
+}
+
+module.exports = {
+  go
+}
diff --git a/templates/service.test.js b/templates/service.test.js
new file mode 100644
index 0000000000..0a0d241031
--- /dev/null
+++ b/templates/service.test.js
@@ -0,0 +1,23 @@
+'use strict'
+
+// Test framework dependencies
+const Lab = require('@hapi/lab')
+const Code = require('@hapi/code')
+
+const { describe, it } = (exports.lab = Lab.script())
+const { expect } = Code
+
+// Thing under test
+const __MODULE_NAME__ = require('__REQUIRE_PATH__')
+
+describe('__DESCRIBE_LABEL__', () => {
+  let session
+
+  describe('when called', () => {
+    it('returns page data for the view', async () => {
+      const result = await __MODULE_NAME__.go(session.id)
+
+      expect(result).to.equal({})
+    })
+  })
+})
diff --git a/templates/submit.service.js b/templates/submit.service.js
new file mode 100644
index 0000000000..8c124f0177
--- /dev/null
+++ b/templates/submit.service.js
@@ -0,0 +1,60 @@
+'use strict'
+
+/**
+ * Orchestrates validating the data for `` page
+ *
+ * @module __MODULE_NAME__
+ */
+
+const __PRESENTER_NAME__ = require('__PRESENTER_PATH__')
+const __VALIDATOR_NAME__ = require('__VALIDATOR_PATH__')
+const SessionModel = require('__SESSION_MODEL_PATH__')
+
+/**
+ * Orchestrates validating the data for `` page
+ *
+ * @param {string} sessionId
+ * @param {object} payload - The submitted form data
+ *
+ * @returns {Promise<object>} - The data formatted for the view template
+ */
+async function go(sessionId, payload) {
+  const session = await SessionModel.query().findById(sessionId)
+
+  const validationResult = _validate(payload)
+
+  if (!validationResult) {
+    await _save(session, payload)
+
+    return {}
+  }
+
+  const pageData = __PRESENTER_NAME__.go(session)
+
+  return {
+    error: validationResult,
+    ...pageData
+  }
+}
+
+async function _save(session, payload) {
+  return session.$update()
+}
+
+function _validate(payload) {
+  const validation = __VALIDATOR_NAME__.go(payload)
+
+  if (!validation.error) {
+    return null
+  }
+
+  const { message } = validation.error.details[0]
+
+  return {
+    text: message
+  }
+}
+
+module.exports = {
+  go
+}
diff --git a/templates/submit.service.test.js b/templates/submit.service.test.js
new file mode 100644
index 0000000000..ea0963d30b
--- /dev/null
+++ b/templates/submit.service.test.js
@@ -0,0 +1,51 @@
+'use strict'
+
+// Test framework dependencies
+const Lab = require('@hapi/lab')
+const Code = require('@hapi/code')
+
+const { describe, it, beforeEach } = (exports.lab = Lab.script())
+const { expect } = Code
+
+// Test helpers
+const SessionHelper = require('__SESSION_HELPER_PATH__')
+
+// Thing under test
+const __MODULE_NAME__ = require('__REQUIRE_PATH__')
+
+describe('__DESCRIBE_LABEL__', () => {
+  let payload
+  let session
+  let sessionData
+
+  beforeEach(async () => {
+    payload = {}
+    sessionData = {}
+
+    session = await SessionHelper.add({ data: sessionData })
+  })
+
+  describe('when called', () => {
+    it('saves the submitted value', async () => {
+      await __MODULE_NAME__.go(session.id, payload)
+
+      const refreshedSession = await session.$query()
+
+      expect(refreshedSession).to.equal()
+    })
+
+    it('continues the journey', async () => {
+      const result = await __MODULE_NAME__.go(session.id, payload)
+
+      expect(result).to.equal({})
+    })
+  })
+
+  describe('when validation fails', () => {
+    it('returns page data for the view, with errors', async () => {
+      const result = await __MODULE_NAME__.go(session.id, payload)
+
+      expect(result).to.equal({})
+    })
+  })
+})
diff --git a/templates/validator.js b/templates/validator.js
new file mode 100644
index 0000000000..b743328445
--- /dev/null
+++ b/templates/validator.js
@@ -0,0 +1,27 @@
+'use strict'
+
+/**
+ * Validates data submitted for the `` page
+ *
+ * @module __MODULE_NAME__
+ */
+
+const Joi = require('joi')
+
+/**
+ * Validates data submitted for the `` page
+ *
+ * @param {object} payload - The payload from the request to be validated
+ *
+ * @returns {object} the result from calling Joi's schema.validate(). It will be an object with a `value:` property. If
+ * any errors are found the `error:` property will also exist detailing what the issues were
+ */
+function go(payload) {
+  const schema = Joi.object({})
+
+  return schema.validate(payload, { abortEarly: false })
+}
+
+module.exports = {
+  go
+}
diff --git a/templates/validator.test.js b/templates/validator.test.js
new file mode 100644
index 0000000000..ba9cd37e27
--- /dev/null
+++ b/templates/validator.test.js
@@ -0,0 +1,38 @@
+'use strict'
+
+// Test framework dependencies
+const Lab = require('@hapi/lab')
+const Code = require('@hapi/code')
+
+const { describe, it, beforeEach } = (exports.lab = Lab.script())
+const { expect } = Code
+
+// Thing under test
+const __MODULE_NAME__ = require('__REQUIRE_PATH__')
+
+describe('__DESCRIBE_LABEL__', () => {
+  let payload
+
+  beforeEach(() => {
+    payload = {}
+  })
+
+  describe('when called with valid data', () => {
+    it('returns with no errors', () => {
+      const result = __MODULE_NAME__.go(payload)
+
+      expect(result.value).to.exist()
+      expect(result.error).not.to.exist()
+    })
+  })
+
+  describe('when called with invalid data', () => {
+    it('returns with errors', () => {
+      const result = __MODULE_NAME__.go(payload)
+
+      expect(result.value).to.exist()
+      expect(result.error).to.exist()
+      expect(result.error.details[0].message).to.equal('')
+    })
+  })
+})
diff --git a/templates/view.njk b/templates/view.njk
new file mode 100644
index 0000000000..95ba291a44
--- /dev/null
+++ b/templates/view.njk
@@ -0,0 +1,31 @@
+{% extends 'layout.njk' %}
+
+{% from "govuk/components/back-link/macro.njk" import govukBackLink %}
+{% from "govuk/components/button/macro.njk" import govukButton %}
+{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}
+
+{% block breadcrumbs %}
+  {{
+  govukBackLink({
+    text: 'Back',
+    href: backLink
+  })
+  }}
+{% endblock %}
+
+{% block content %}
+  {% if error %}
+    {{ govukErrorSummary({
+      titleText: "There is a problem",
+      errorList: error.errorList
+    }) }}
+  {%endif%}
+
+  <div>
+    <form method="post">
+      <input type="hidden" name="wrlsCrumb" value="{{wrlsCrumb}}"/>
+
+      {{ govukButton({ text: "Continue", preventDoubleClick: true }) }}
+    </form>
+  </div>
+{% endblock %}


Reviews:
user: login: jonathangoulding
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T17:28:06Z
user: login: jonathangoulding
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T12:24:41Z
user: login: jonathangoulding
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-29T08:05:15Z
user: login: jonathangoulding
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-29T11:28:24Z
user: login: jonathangoulding
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-29T11:51:52Z
user: login: jonathangoulding
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-29T13:38:21Z
user: login: jonathangoulding
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-29T13:41:27Z
user: login: jonathangoulding
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-29T13:47:51Z
user: login: robertparkinson
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-04-30T08:17:56Z
user: login: Cruikshanks
body: I've requested a change regards the README.

I've not checked the rest, but I'm specifically keeping an 'arms-length' approach.

This idea has come from the team, so I'd like the team to own it and keep it maintained. A haven from the rest of the service where ...

<img src="https://media3.giphy.com/media/v1.Y2lkPWJkM2VhNTdlMmx5aWk3Y2huc2loc2phZ2RoN25jOTZuc205aWt4NDZpeTh3NzJ3ZiZlcD12MV9naWZzX3NlYXJjaCZjdD1n/useUF6IHpTqSc/giphy.gif"/> 
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-30T09:01:43Z
user: login: jonathangoulding
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T12:36:20Z
user: login: jonathangoulding
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T12:39:55Z
user: login: jonathangoulding
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T12:55:08Z
user: login: jonathangoulding
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T12:55:35Z
user: login: jonathangoulding
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T12:59:11Z
user: login: jonathangoulding
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T13:16:15Z
user: login: Cruikshanks
body: <img src="https://media3.giphy.com/media/v1.Y2lkPWJkM2VhNTdldmQ5MDJtY2RwczR5bWV2dzEyM2YxbXlma3NyaWFsODl1bmM4Mnh1eiZlcD12MV9naWZzX3NlYXJjaCZjdD1n/bKnEnd65zqxfq/giphy.gif"/>
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-30T16:07:00Z

Review comments:
diff_hunk: @@ -0,0 +1,16 @@
+'use strict'
+
+/**
+ * @module __MODULENAME__
+ */
+
+/**
+ * @returns {object} - The data formatted for the view template
+ */
+function go() {
+  return {}
path: templates/service.js
user: login: jonathangoulding
body: Add the code for the presenter to be required and called. 
created_at: 2025-04-25T17:28:02Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,16 @@
+'use strict'
+
+/**
+ * @module __MODULENAME__
path: templates/presenter.js
user: login: jonathangoulding
body: We can add the common description here 

```
Orchestrates presenting the data for `/notices/setup/{sessionId}/abstraction-alert/alert-type` page
```
created_at: 2025-04-28T12:24:22Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,16 @@
+'use strict'
+
+/**
+ * @module __MODULE_NAME__
+ */
+
+/**
+ * @returns {object} - The data formatted for the view template
path: templates/service.js
user: login: jonathangoulding
body: these should be 

```
{Promise<object>}
```
created_at: 2025-04-29T08:05:10Z
author_association: COLLABORATOR
diff_hunk: @@ -54,3 +54,95 @@ The following attribution statement MUST be cited in your products and applicati
 The Open Government Licence (OGL) was developed by the Controller of Her Majesty's Stationery Office (HMSO) to enable information providers in the public sector to license the use and re-use of their information under a common open licence.
 
 It is designed to encourage use and re-use of information freely and flexibly, with only a few conditions.
+
+## 🛠 Scaffold Script for adding new Journeys
+
+Quickly generates source files and matching test files based on templates.
+
+---
+
+### ✅ Usage
+
+```bash
+./scaffold.sh <name> or <path/to/name>
+```
+
+---
+
+### 📋 Examples
+
+```bash
+./scaffold.sh my-module
+# → app/services/my-module.service.js
+# → test/services/my-module.service.test.js
+
+./scaffold.sh notices/setup/fella/my-module
+# → app/services/notices/setup/fella/my-module.service.js
+# → test/services/notices/setup/fella/my-module.service.test.js
+```
+
+---
+
+### 📦 Scaffold Options
+
+After running, you'll be prompted to choose:
+
+1. Journey - Complete
path: README.md
user: login: jonathangoulding
body: Add a njk file with layout and continue button
created_at: 2025-04-29T10:31:45Z
author_association: COLLABORATOR
diff_hunk: @@ -54,3 +54,95 @@ The following attribution statement MUST be cited in your products and applicati
 The Open Government Licence (OGL) was developed by the Controller of Her Majesty's Stationery Office (HMSO) to enable information providers in the public sector to license the use and re-use of their information under a common open licence.
 
 It is designed to encourage use and re-use of information freely and flexibly, with only a few conditions.
+
+## 🛠 Scaffold Script for adding new Journeys
+
+Quickly generates source files and matching test files based on templates.
+
+---
+
+### ✅ Usage
+
+```bash
+./scaffold.sh <name> or <path/to/name>
+```
+
+---
+
+### 📋 Examples
+
+```bash
+./scaffold.sh my-module
+# → app/services/my-module.service.js
+# → test/services/my-module.service.test.js
+
+./scaffold.sh notices/setup/fella/my-module
+# → app/services/notices/setup/fella/my-module.service.js
+# → test/services/notices/setup/fella/my-module.service.test.js
+```
+
+---
+
+### 📦 Scaffold Options
+
+After running, you'll be prompted to choose:
+
+1. Journey - Complete
+2. Journey - View
path: README.md
user: login: jonathangoulding
body: Add controller and router output to the shell. 

demo scripts - maybe use a template and output in the shell
created_at: 2025-04-29T10:32:50Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,31 @@
+/* eslint-disable camelcase, no-unused-vars, strict */
+
+// Controller code
+const __SERVICE_NAME__ = require('__SERVICE_PATH__')
+
+async function view__NAME__(request, h) {
path: templates/helper.js
user: login: jonathangoulding
body: Add submit and POST
created_at: 2025-04-29T11:51:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,338 @@
+#!/bin/bash
+
+set -e
+
+# ------------------------------------------------------------------------------
+# Inputs and Initial Setup
+# ------------------------------------------------------------------------------
+
+INPUT_PATH="$1"
+
+if [ -z "$INPUT_PATH" ]; then
+  echo "Usage: $0 <name> or <path/name> (e.g., my-module or folder/my-module)"
+  exit 1
+fi
+
+RAW_NAME=$(basename "$INPUT_PATH")
+REL_DIR=$(dirname "$INPUT_PATH")
+if [ "$REL_DIR" = "." ]; then
+  REL_DIR=""
+fi
+
+to_pascal_case() {
+  echo "$1" | sed -E 's/[-_]+/ /g' | awk '{for(i=1;i<=NF;++i) $i=toupper(substr($i,1,1)) substr($i,2)}1' | tr -d ' '
+}
+
+PASCAL_NAME=$(to_pascal_case "$RAW_NAME")
+
+# ------------------------------------------------------------------------------
+# Templates
path: scaffold.sh
user: login: jonathangoulding
body: These could just be done inline, and are only used once.


created_at: 2025-04-29T12:53:36Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,338 @@
+#!/bin/bash
+
+set -e
+
+# ------------------------------------------------------------------------------
+# Inputs and Initial Setup
+# ------------------------------------------------------------------------------
+
+INPUT_PATH="$1"
+
+if [ -z "$INPUT_PATH" ]; then
+  echo "Usage: $0 <name> or <path/name> (e.g., my-module or folder/my-module)"
+  exit 1
+fi
+
+RAW_NAME=$(basename "$INPUT_PATH")
+REL_DIR=$(dirname "$INPUT_PATH")
+if [ "$REL_DIR" = "." ]; then
+  REL_DIR=""
+fi
+
+to_pascal_case() {
+  echo "$1" | sed -E 's/[-_]+/ /g' | awk '{for(i=1;i<=NF;++i) $i=toupper(substr($i,1,1)) substr($i,2)}1' | tr -d ' '
+}
+
+PASCAL_NAME=$(to_pascal_case "$RAW_NAME")
+
+# ------------------------------------------------------------------------------
+# Templates
+# ------------------------------------------------------------------------------
+
+TEMPLATE_SERVICE="templates/service.js"
+TEMPLATE_SERVICE_AND_PRESENTER="templates/service-and-presenter.service.js"
+TEMPLATE_SERVICE_FETCH="templates/service-fetch.service.js"
+TEMPLATE_SERVICE_PRESENTER_SESSION="templates/service-session-presenter.service.js"
+TEMPLATE_SUBMIT_SERVICE="templates/submit.service.js"
+TEMPLATE_VALIDATOR="templates/validator.js"
+TEMPLATE_VIEW="templates/view.njk"
+
+
+TEMPLATE_TEST_SERVICE="templates/service.test.js"
+TEMPLATE_TEST_SERVICE_AND_PRESENTER="templates/service-and-presenter.service.test.js"
+TEMPLATE_TEST_SERVICE_FETCH="templates/service-fetch.service.test.js"
+TEMPLATE_TEST_SERVICE_PRESENTER_SESSION="templates/service-session-presenter.service.test.js"
+TEMPLATE_TEST_SUBMIT_SERVICE="templates/submit.service.test.js"
+TEMPLATE_TEST_VALIDATOR="templates/validator.test.js"
+
+
+TEMPLATE_FETCH="templates/fetch.js"
+TEMPLATE_TEST_FETCH="templates/fetch.test.js"
+TEMPLATE_PRESENTER="templates/presenter.js"
+TEMPLATE_TEST_PRESENTER="templates/presenter.test.js"
+
+# ------------------------------------------------------------------------------
+# Helper Functions
+# ------------------------------------------------------------------------------
+
+build_up_path() {
+  local depth=3
+  if [ -n "$REL_DIR" ]; then
+    local slash_count
+    slash_count=$(grep -o "/" <<< "$REL_DIR" | wc -l | tr -d ' ')
+    depth=$((slash_count + 3))
+  fi
+
+  local result=""
+  for ((i=0; i<depth; i++)); do
+    result="../$result"
+  done
+  echo "$result"
+}
+
+render_file() {
+  local template="$1"
+  local output_path="$2"
+  local module_name="$3"
+  local presenter_path="$4"
+  local fetch_path="$5"
+  local session_model_path="$6"
+  local validator_path="$7"
+  local service_path="$8"
+  local view_path="$9"
+
+  mkdir -p "$(dirname "$output_path")"
+
+  if [ -f "$output_path" ]; then
+    echo "⚠️  Skipped (already exists): $output_path"
+    return
+  fi
+
+  sed -e "s#__FETCH_PATH__#${fetch_path}#g" \
path: scaffold.sh
user: login: jonathangoulding
body: Another iteration would be to just generate all paths and names, and pass them all in. 

With a clearer function / handler. This is quite long i think. 
created_at: 2025-04-29T12:54:53Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,338 @@
+#!/bin/bash
+
+set -e
+
+# ------------------------------------------------------------------------------
+# Inputs and Initial Setup
+# ------------------------------------------------------------------------------
+
+INPUT_PATH="$1"
+
+if [ -z "$INPUT_PATH" ]; then
+  echo "Usage: $0 <name> or <path/name> (e.g., my-module or folder/my-module)"
+  exit 1
+fi
+
+RAW_NAME=$(basename "$INPUT_PATH")
+REL_DIR=$(dirname "$INPUT_PATH")
+if [ "$REL_DIR" = "." ]; then
+  REL_DIR=""
+fi
+
+to_pascal_case() {
+  echo "$1" | sed -E 's/[-_]+/ /g' | awk '{for(i=1;i<=NF;++i) $i=toupper(substr($i,1,1)) substr($i,2)}1' | tr -d ' '
+}
+
+PASCAL_NAME=$(to_pascal_case "$RAW_NAME")
+
+# ------------------------------------------------------------------------------
+# Templates
+# ------------------------------------------------------------------------------
+
+TEMPLATE_SERVICE="templates/service.js"
+TEMPLATE_SERVICE_AND_PRESENTER="templates/service-and-presenter.service.js"
+TEMPLATE_SERVICE_FETCH="templates/service-fetch.service.js"
+TEMPLATE_SERVICE_PRESENTER_SESSION="templates/service-session-presenter.service.js"
+TEMPLATE_SUBMIT_SERVICE="templates/submit.service.js"
+TEMPLATE_VALIDATOR="templates/validator.js"
+TEMPLATE_VIEW="templates/view.njk"
+
+
+TEMPLATE_TEST_SERVICE="templates/service.test.js"
+TEMPLATE_TEST_SERVICE_AND_PRESENTER="templates/service-and-presenter.service.test.js"
+TEMPLATE_TEST_SERVICE_FETCH="templates/service-fetch.service.test.js"
+TEMPLATE_TEST_SERVICE_PRESENTER_SESSION="templates/service-session-presenter.service.test.js"
+TEMPLATE_TEST_SUBMIT_SERVICE="templates/submit.service.test.js"
+TEMPLATE_TEST_VALIDATOR="templates/validator.test.js"
+
+
+TEMPLATE_FETCH="templates/fetch.js"
+TEMPLATE_TEST_FETCH="templates/fetch.test.js"
+TEMPLATE_PRESENTER="templates/presenter.js"
+TEMPLATE_TEST_PRESENTER="templates/presenter.test.js"
+
+# ------------------------------------------------------------------------------
+# Helper Functions
+# ------------------------------------------------------------------------------
+
+build_up_path() {
+  local depth=3
+  if [ -n "$REL_DIR" ]; then
+    local slash_count
+    slash_count=$(grep -o "/" <<< "$REL_DIR" | wc -l | tr -d ' ')
+    depth=$((slash_count + 3))
+  fi
+
+  local result=""
+  for ((i=0; i<depth; i++)); do
+    result="../$result"
+  done
+  echo "$result"
+}
+
+render_file() {
+  local template="$1"
+  local output_path="$2"
+  local module_name="$3"
+  local presenter_path="$4"
+  local fetch_path="$5"
+  local session_model_path="$6"
+  local validator_path="$7"
+  local service_path="$8"
+  local view_path="$9"
+
+  mkdir -p "$(dirname "$output_path")"
+
+  if [ -f "$output_path" ]; then
+    echo "⚠️  Skipped (already exists): $output_path"
+    return
+  fi
+
+  sed -e "s#__FETCH_PATH__#${fetch_path}#g" \
+      -e "s#__PRESENTER_PATH__#${presenter_path}#g" \
+      -e "s#__SERVICE_PATH__#${service_path}#g" \
+      -e "s#__SESSION_MODEL_PATH__#${session_model_path}#g" \
+      -e "s#__VALIDATOR_PATH__#${validator_path}#g" \
+      -e "s#__VIEW_PATH__#${view_path}#g" \
+      -e "s/__CONTROLLER_NAME__/${PASCAL_NAME}Controller/g" \
+      -e "s/__FETCH_NAME__/Fetch${PASCAL_NAME}Service/g" \
+      -e "s/__NAME__/${PASCAL_NAME}/g" \
+      -e "s/__PRESENTER_NAME__/${PASCAL_NAME}Presenter/g" \
+      -e "s/__SERVICE_NAME__/${PASCAL_NAME}Service/g" \
+      -e "s/__SUBMIT_NAME__/Submit${PASCAL_NAME}Service/g" \
+      -e "s/__VALIDATOR_NAME__/${PASCAL_NAME}Validator/g" \
+      -e "s/__MODULE_NAME__/${module_name}/g" \
+      "$template" > "$output_path"
+
+  echo "✅ Created $output_path"
+}
+
+render_test_file() {
+  local template="$1"
+  local output_path="$2"
+  local module_name="$3"
+  local require_path="$4"
+  local describe_label="$5"
+  local presenter_path="$6"
+  local fetch_path="$7"
+  local session_model_path="$8"
+
+  mkdir -p "$(dirname "$output_path")"
+
+  if [ -f "$output_path" ]; then
+    echo "⚠️  Skipped (already exists): $output_path"
+    return
+  fi
+
+  sed -e "s/__MODULE_NAME__/${module_name}/g" \
+      -e "s#__REQUIRE_PATH__#${require_path}#g" \
+      -e "s/__DESCRIBE_LABEL__/${describe_label}/g" \
+      -e "s#__PRESENTER_PATH__#${presenter_path}#g" \
+      -e "s/__PRESENTER_NAME__/${PASCAL_NAME}Presenter/g" \
+      -e "s/__FETCH_NAME__/Fetch${PASCAL_NAME}Service/g" \
+      -e "s#__FETCH_PATH__#${fetch_path}#g" \
+      -e "s#__SESSION_MODEL_PATH__#${session_model_path}#g" \
+      "$template" > "$output_path"
+
+  echo "✅ Created $output_path"
+}
+
+generate_controller_snippet() {
+  local snippet_template="templates/helper.js"
+
+  if [ -f "$snippet_template" ]; then
+    echo ""
+    echo "Here is your ready-to-use controller + router snippet:"
+    echo ""
+
+    sed -e "s|__SERVICE_NAME__|${PASCAL_NAME}Service|g" \
+        -e "s|__SUBMIT_NAME__|Submit${PASCAL_NAME}Service|g" \
+        -e "s|__SERVICE_PATH__|${SERVICE_PATH}|g" \
+        -e "s|__NAME__|${PASCAL_NAME}|g" \
+        -e "s|__VIEW_PATH__|${VIEW_PATH}|g" \
+        -e "s|__CONTROLLER_NAME__|${PASCAL_NAME}Controller|g" \
+        -e "s|__CONTROLLER_PATH__|${CONTROLLER_PATH}|g" \
+        -e "s|__SUBMIT_PATH__|${SUBMIT_PATH}|g" \
+        "$snippet_template"
+
+    echo ""
+  else
+    echo "❌ Snippet template not found: $snippet_template"
+  fi
+}
+
+generate_paths() {
+  local type="$1"    # Service, Presenter, FetchService
+  local service_variant="$2"  # service, presenter, fetch-presenter
+
+  TYPE="$type"
+  TYPE_LOWER=$(echo "$TYPE" | tr '[:upper:]' '[:lower:]')
+
+  MODULE_NAME="${PASCAL_NAME}${TYPE}"
+  SOURCE_FILE="${RAW_NAME}.${TYPE_LOWER}.js"
+  TEST_FILE="${RAW_NAME}.${TYPE_LOWER}.test.js"
+
+    if [ "$TYPE" = "Service" ]; then
+      SUBFOLDER="services"
+
+      # Additional input
+      FETCH_PATH="./fetch-${RAW_NAME}.service.js"
+
+      case "$service_variant" in
path: scaffold.sh
user: login: jonathangoulding
body: I think this is better than multiple if else for the  "$TYPE"
created_at: 2025-04-29T12:59:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,19 @@
+'use strict'
+
+/**
+ * Formats data for the `` page
path: templates/presenter.js
user: login: jonathangoulding
body: We could maybe generate this from the folder paths. But it looks a but complicated. 

We could put a placeholder for the route and then it's a simple find and replace.
created_at: 2025-04-29T13:41:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,331 @@
+#!/bin/bash
+
+set -e
+
+# ------------------------------------------------------------------------------
+# Inputs and Initial Setup
+# ------------------------------------------------------------------------------
+
+INPUT_PATH="$1"
+
+if [ -z "$INPUT_PATH" ]; then
+  echo "Usage: $0 <name> or <path/name> (e.g., my-module or folder/my-module)"
+  exit 1
+fi
+
+RAW_NAME=$(basename "$INPUT_PATH")
+REL_DIR=$(dirname "$INPUT_PATH")
+if [ "$REL_DIR" = "." ]; then
+  REL_DIR=""
+fi
+
+to_pascal_case() {
+  echo "$1" | sed -E 's/[-_]+/ /g' | awk '{for(i=1;i<=NF;++i) $i=toupper(substr($i,1,1)) substr($i,2)}1' | tr -d ' '
+}
+
+PASCAL_NAME=$(to_pascal_case "$RAW_NAME")
+
+# ------------------------------------------------------------------------------
+# Templates
+# ------------------------------------------------------------------------------
+
+TEMPLATE_SERVICE="templates/service.js"
+TEMPLATE_SERVICE_FETCH="templates/service-fetch.service.js"
+TEMPLATE_SERVICE_PRESENTER_SESSION="templates/service-session-presenter.service.js"
+TEMPLATE_SUBMIT_SERVICE="templates/submit.service.js"
+TEMPLATE_VALIDATOR="templates/validator.js"
+TEMPLATE_VIEW="templates/view.njk"
+
+
+TEMPLATE_TEST_SERVICE="templates/service.test.js"
+TEMPLATE_TEST_SERVICE_FETCH="templates/service-fetch.service.test.js"
+TEMPLATE_TEST_SERVICE_PRESENTER_SESSION="templates/service-session-presenter.service.test.js"
+TEMPLATE_TEST_SUBMIT_SERVICE="templates/submit.service.test.js"
+TEMPLATE_TEST_VALIDATOR="templates/validator.test.js"
+
+
+TEMPLATE_FETCH="templates/fetch.js"
+TEMPLATE_TEST_FETCH="templates/fetch.test.js"
+TEMPLATE_PRESENTER="templates/presenter.js"
+TEMPLATE_TEST_PRESENTER="templates/presenter.test.js"
+
+
+# ------------------------------------------------------------------------------
+# Helper Functions
+# ------------------------------------------------------------------------------
+
+build_up_path() {
+  local slash_count=0
+  if [ -n "$REL_DIR" ]; then
+    slash_count=$(grep -o "/" <<< "$REL_DIR" | wc -l | tr -d ' ')
+  fi
+  printf '../%.0s' $(seq 1 $((slash_count + 3)))
+}
+
+render_file() {
+  local template="$1"
+  local output_path="$2"
+  local module_name="$3"
+  local presenter_path="$4"
+  local fetch_path="$5"
+  local session_model_path="$6"
+  local validator_path="$7"
+  local service_path="$8"
+  local view_path="$9"
+
+  mkdir -p "$(dirname "$output_path")"
+
+  if [ -f "$output_path" ]; then
+    echo "⚠️  Skipped (already exists): $output_path"
+    return
+  fi
+
+  sed -e "s#__FETCH_PATH__#${fetch_path}#g" \
+      -e "s#__PRESENTER_PATH__#${presenter_path}#g" \
+      -e "s#__SERVICE_PATH__#${service_path}#g" \
+      -e "s#__SESSION_MODEL_PATH__#${session_model_path}#g" \
+      -e "s#__VALIDATOR_PATH__#${validator_path}#g" \
+      -e "s#__VIEW_PATH__#${view_path}#g" \
+      -e "s/__CONTROLLER_NAME__/${PASCAL_NAME}Controller/g" \
+      -e "s/__FETCH_NAME__/Fetch${PASCAL_NAME}Service/g" \
+      -e "s/__NAME__/${PASCAL_NAME}/g" \
+      -e "s/__PRESENTER_NAME__/${PASCAL_NAME}Presenter/g" \
+      -e "s/__SERVICE_NAME__/${PASCAL_NAME}Service/g" \
+      -e "s/__SUBMIT_NAME__/Submit${PASCAL_NAME}Service/g" \
+      -e "s/__VALIDATOR_NAME__/${PASCAL_NAME}Validator/g" \
+      -e "s/__MODULE_NAME__/${module_name}/g" \
+      "$template" > "$output_path"
+
+  echo "✅ Created $output_path"
+}
+
+render_test_file() {
+  local template="$1"
+  local output_path="$2"
+  local module_name="$3"
+  local require_path="$4"
+  local describe_label="$5"
+  local presenter_path="$6"
+  local fetch_path="$7"
+  local session_model_path="$8"
+  local session_helper_path="$9"
+
+  mkdir -p "$(dirname "$output_path")"
+
+  if [ -f "$output_path" ]; then
+    echo "⚠️  Skipped (already exists): $output_path"
+    return
+  fi
+
+  sed -e "s/__MODULE_NAME__/${module_name}/g" \
+      -e "s#__REQUIRE_PATH__#${require_path}#g" \
+      -e "s/__DESCRIBE_LABEL__/${describe_label}/g" \
+      -e "s#__PRESENTER_PATH__#${presenter_path}#g" \
+      -e "s/__PRESENTER_NAME__/${PASCAL_NAME}Presenter/g" \
+      -e "s/__FETCH_NAME__/Fetch${PASCAL_NAME}Service/g" \
+      -e "s#__FETCH_PATH__#${fetch_path}#g" \
+      -e "s#__SESSION_MODEL_PATH__#${session_model_path}#g" \
+      -e "s#__SESSION_HELPER_PATH__#${session_helper_path}#g" \
+      "$template" > "$output_path"
+
+  echo "✅ Created $output_path"
+}
+
+generate_controller_snippet() {
+  local snippet_template="templates/helper.js"
+
+  if [ -f "$snippet_template" ]; then
+    echo ""
+    echo "Here is your ready-to-use controller + router snippet:"
+    echo ""
+
+    sed -e "s|__SERVICE_NAME__|${PASCAL_NAME}Service|g" \
+        -e "s|__SUBMIT_NAME__|Submit${PASCAL_NAME}Service|g" \
+        -e "s|__SERVICE_PATH__|${SERVICE_PATH}|g" \
+        -e "s|__NAME__|${PASCAL_NAME}|g" \
+        -e "s|__VIEW_PATH__|${VIEW_PATH}|g" \
+        -e "s|__CONTROLLER_NAME__|${PASCAL_NAME}Controller|g" \
+        -e "s|__CONTROLLER_PATH__|${CONTROLLER_PATH}|g" \
+        -e "s|__SUBMIT_PATH__|${SUBMIT_PATH}|g" \
+        "$snippet_template"
+
+    echo ""
+  else
+    echo "❌ Snippet template not found: $snippet_template"
+  fi
+}
+
+generate_paths() {
+  local type="$1"    # Service, Presenter,
+  local service_variant="$2"  # service, presenter, fetch-presenter
+
+  TYPE="$type"
+  TYPE_LOWER=$(echo "$TYPE" | tr '[:upper:]' '[:lower:]')
+
+  MODULE_NAME="${PASCAL_NAME}${TYPE}"
+  SOURCE_FILE="${RAW_NAME}.${TYPE_LOWER}.js"
+  TEST_FILE="${RAW_NAME}.${TYPE_LOWER}.test.js"
+
+    if [ "$TYPE" = "Service" ]; then
+      SUBFOLDER="services"
+
+      # Additional input
+      FETCH_PATH="./fetch-${RAW_NAME}.service.js"
+
+      case "$service_variant" in
+        service)
+          SOURCE_TEMPLATE="$TEMPLATE_SERVICE"
+          TEST_TEMPLATE="$TEMPLATE_TEST_SERVICE"
+          ;;
+        service-with-fetch)
+          SOURCE_TEMPLATE="$TEMPLATE_SERVICE_FETCH"
+          TEST_TEMPLATE="$TEMPLATE_TEST_SERVICE_FETCH"
+          ;;
+        presenter-session)
+          SOURCE_TEMPLATE="$TEMPLATE_SERVICE_PRESENTER_SESSION"
+          TEST_TEMPLATE="$TEMPLATE_TEST_SERVICE_PRESENTER_SESSION"
+          ;;
+        fetch)
+            SOURCE_TEMPLATE="$TEMPLATE_FETCH"
+            TEST_TEMPLATE="$TEMPLATE_TEST_FETCH"
+            # We prefix our fetch services with 'fetch'
+            MODULE_NAME="Fetch${MODULE_NAME}"
+            SOURCE_FILE="fetch-${SOURCE_FILE}"
+            TEST_FILE="fetch-${TEST_FILE}"
+          ;;
+        submit)
+            SOURCE_TEMPLATE="$TEMPLATE_SUBMIT_SERVICE"
+            TEST_TEMPLATE="$TEMPLATE_TEST_SUBMIT_SERVICE"
+            # We prefix our submit services with 'submit'
+            MODULE_NAME="Submit${MODULE_NAME}"
+            SOURCE_FILE="submit-${SOURCE_FILE}"
+            TEST_FILE="submit-${TEST_FILE}"
+          ;;
+        *)
+          echo "❌ Unknown service variant: $service_variant"
+          exit 1
+          ;;
+      esac
+
+    elif [ "$TYPE" = "Presenter" ]; then
+      SOURCE_TEMPLATE="$TEMPLATE_PRESENTER"
+      TEST_TEMPLATE="$TEMPLATE_TEST_PRESENTER"
+
+      SUBFOLDER="presenters"
+
+    elif [ "$TYPE" = "Validator" ]; then
+        SOURCE_TEMPLATE="$TEMPLATE_VALIDATOR"
+        TEST_TEMPLATE="$TEMPLATE_TEST_VALIDATOR"
+
+        SUBFOLDER="validators"
+
+    elif [ "$TYPE" = "View" ]; then
+        SOURCE_FILE="${RAW_NAME}.${TYPE_LOWER}.njk"
+        SOURCE_TEMPLATE="$TEMPLATE_VIEW"
+
+        SUBFOLDER="views"
+    else
+      echo "❌ Unknown type: $TYPE"
+      exit 1
+    fi
+
+  # When a folder path is supplied
+  if [ -n "$REL_DIR" ]; then
+    SOURCE_OUTPUT="app/${SUBFOLDER}/${REL_DIR}/${SOURCE_FILE}"
+    TEST_OUTPUT="test/${SUBFOLDER}/${REL_DIR}/${TEST_FILE}"
+  else
+    SOURCE_OUTPUT="app/${SUBFOLDER}/${SOURCE_FILE}"
+    TEST_OUTPUT="test/${SUBFOLDER}/${TEST_FILE}"
+  fi
+
+  # Helper
+  RELATIVE_UP_PATH=$(build_up_path)
+
+  # Test
+  DESCRIBE_LABEL="$(echo "$RAW_NAME" | sed -E 's/[-_]+/ /g' | awk '{for(i=1;i<=NF;++i) $i=toupper(substr($i,1,1)) substr($i,2)}1') $TYPE" # Test describe block
path: scaffold.sh
user: login: jonathangoulding
body: This could do with a bit of input
created_at: 2025-04-29T13:47:47Z
author_association: COLLABORATOR
path: README.md
user: login: Cruikshanks
body: Can we please move this into its own `README.md` in the `/templates` folder?

Its a personal opinion, but I don't want all this extra info about scaffolding to detract from the key details on the main `README.md`.

However, please add a `## Scaffolding` section to the main README that gives a brief intro and points to the `/templates/README.md`, but it needs to go before the licence section.

Then final nit-pick: can we drop all the `---` as they I do not believe they are needed to separate the sections. Whitespace is performing adequately for that.
created_at: 2025-04-30T08:59:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,52 @@
+/* eslint-disable camelcase, no-unused-vars, strict */
+
+// Controller code
+const __SERVICE_NAME__ = require('__SERVICE_PATH__')
+const __SUBMIT_NAME__ = require('__SUBMIT_PATH__')
+
+async function view__NAME__(request, h) {
+  const { sessionId } = request.params
+
+  const pageData = await __SERVICE_NAME__.go(sessionId)
+
+  return h.view(`__VIEW_PATH__`, pageData)
+}
+
+async function submit__NAME__(request, h) {
+  const { sessionId } = request.params
+
+  await __SUBMIT_NAME__.go(sessionId)
path: templates/helper.js
user: login: jonathangoulding
body: This needs

```
  const {
    payload,
    params: { sessionId }
  } = request
```
created_at: 2025-04-30T12:36:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,52 @@
+/* eslint-disable camelcase, no-unused-vars, strict */
+
+// Controller code
+const __SERVICE_NAME__ = require('__SERVICE_PATH__')
+const __SUBMIT_NAME__ = require('__SUBMIT_PATH__')
+
+async function view__NAME__(request, h) {
+  const { sessionId } = request.params
+
+  const pageData = await __SERVICE_NAME__.go(sessionId)
+
+  return h.view(`__VIEW_PATH__`, pageData)
+}
+
+async function submit__NAME__(request, h) {
+  const { sessionId } = request.params
+
+  await __SUBMIT_NAME__.go(sessionId)
path: templates/helper.js
user: login: jonathangoulding
body: and the render data for the error

```
  if (pageData.error) {
    return h.view(`${basePath}/ad-hoc-licence.njk`, pageData)
  }
```
created_at: 2025-04-30T12:39:55Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,33 @@
+'use strict'
+
+// Test framework dependencies
+const Lab = require('@hapi/lab')
+const Code = require('@hapi/code')
+
+const { describe, it, beforeEach } = (exports.lab = Lab.script())
+const { expect } = Code
+
+// Thing under test
+const __MODULE_NAME__ = require('__REQUIRE_PATH__')
+
+describe('__DESCRIBE_LABEL__', () => {
+  let session
+
+  beforeEach(async () => {})
+
+  describe('when called with valid data', () => {
+    it('returns with no errors', () => {
+      const result = __MODULE_NAME__.go(session.id)
path: templates/validator.test.js
user: login: jonathangoulding
body: this should be a payload object
created_at: 2025-04-30T12:55:01Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,33 @@
+'use strict'
+
+// Test framework dependencies
+const Lab = require('@hapi/lab')
+const Code = require('@hapi/code')
+
+const { describe, it, beforeEach } = (exports.lab = Lab.script())
+const { expect } = Code
+
+// Thing under test
+const __MODULE_NAME__ = require('__REQUIRE_PATH__')
+
+describe('__DESCRIBE_LABEL__', () => {
+  let session
+
+  beforeEach(async () => {})
+
+  describe('when called with valid data', () => {
+    it('returns with no errors', () => {
+      const result = __MODULE_NAME__.go(session.id)
path: templates/validator.test.js
user: login: jonathangoulding
body: no need for async in the bfe
created_at: 2025-04-30T12:55:35Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,33 @@
+'use strict'
+
+// Test framework dependencies
+const Lab = require('@hapi/lab')
+const Code = require('@hapi/code')
+
+const { describe, it, beforeEach } = (exports.lab = Lab.script())
+const { expect } = Code
+
+// Thing under test
+const __MODULE_NAME__ = require('__REQUIRE_PATH__')
+
+describe('__DESCRIBE_LABEL__', () => {
+  let session
+
+  beforeEach(async () => {})
+
+  describe('when called with valid data', () => {
+    it('returns with no errors', () => {
+      const result = __MODULE_NAME__.go(session.id)
path: templates/validator.test.js
user: login: jonathangoulding
body: test need to be updated to simialr:

```
  describe('when called with valid data', () => {
    it('returns with errors', () => {
      const result = AlertTypeValidator.go(payload)

      expect(result.value).to.exist()
      expect(result.error).not.to.exist()
    })
  })

  describe('when called with valid data', () => {
    it('returns with no errors', () => {
      const result = AlertTypeValidator.go(payload)

      expect(result.value).to.exist()
      expect(result.error).to.exist()
      expect(result.error.details[0].message).to.equal('Enter a licence number')
    })
  })
```
created_at: 2025-04-30T12:59:07Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,40 @@
+'use strict'
+
+// Test framework dependencies
+const Lab = require('@hapi/lab')
+const Code = require('@hapi/code')
+
+const { describe, it, beforeEach } = (exports.lab = Lab.script())
+const { expect } = Code
+
+// Test helpers
+const SessionHelper = require('__SESSION_HELPER_PATH__')
+
+// Thing under test
+const __MODULE_NAME__ = require('__REQUIRE_PATH__')
+
+describe('__DESCRIBE_LABEL__', () => {
+  let session
+  let sessionData
+
+  beforeEach(async () => {
+    sessionData = {}
+    session = await SessionHelper.add({ data: sessionData })
+  })
+
+  describe('when called', () => {
+    it('continues the journey', async () => {
+      const result = await __MODULE_NAME__.go(session.id)
path: templates/submit.service.test.js
user: login: jonathangoulding
body: add a payload object
created_at: 2025-04-30T13:16:10Z
author_association: COLLABORATOR

PR comments: