Skip to content

Latest commit

 

History

422 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fg-gas-backend

Grant Application Service defines and manages farming grants and applications. It is the source of truth for all grant applications and their status.

Running with other services

If you want to run this service with other farming grants applications see fg-grants-core

User guide

Configure grant actions

As part of the Grant Application Service, you can configure and call actions on grants.

Given a grant definition is stored in GAS as per the following example:

  • Note the $areaId and $segmentId in the URL, these are placeholders that will be replaced with actual values when the action is invoked.
{
  "code": "my-grant-code",
  "actions": [
    {
      "name": "land-area-calculation",
      "method": "POST",
      "url": "https://my-other-server.%ENVIRONMENT%.gov.uk/some-path/area-calc/$areaId/$segmentId"
    }
  ]
}

Note: The %ENVIRONMENT% placeholder will be replaced with the current CDP environment when running in a CDP environment.

This can be called via the following HTTP request:

  • Note the areaId and segmentId query parameters, these will be used to replace the placeholders in the URL.
  • The anotherParam query parameter is an example of additional parameters will be passed on in the URL query string.
POST http://gas-server/grants/my-grant-code/actions/land-area-calculation/invoke?areaId=123&segmentId=456&anotherParam=ABC123
Content-Type: application/json

{
  "someKey": "someValue"
}

This wil result in the following HTTP request being made to the configured URL:

POST https://my-other-server/some-path/area-calc/123/456?code=my-grant-code&anotherParam=ABC123
Content-Type: application/json

{
  "someKey": "someValue"
}

Developer guide

Node.js

Please install Node Version Manager nvm

To use the correct version of Node.js for this application, via nvm:

cd fg-gas-backend
nvm use

Local development

Setup

Install application dependencies:

npm install

Create a .env file in the root of the project. You can use the .env.example file as a template.

cp .env.example .env

VIEW_AGREEMENT_URI is the Agreements UI base used in accepted Agreement lifecycle events. Do not include a trailing slash; the Agreement Number is appended when the event is created.

GAS__SNS__REPORTING_EVENTS_TOPIC_ARN is the Grants Reporting SNS topic. GAS writes Agreement-created and Agreement-status-changed events to its transactional outbox so they are committed atomically with the corresponding Agreement version.

AGREEMENTS_JWT_SECRET (FGP-1307) is the shared HS256 secret GAS uses to verify the caller token (the x-encrypted-auth header) forwarded by Agreements UI on the agreement routes. It must match the secret the producer services sign with (Caseworking frontend, PDF service and Grants UI). It is supplied per environment from the platform secret store and must never be committed. It is optional while verification runs in warn-only mode; when absent the caller token is reported as unverified but requests are not rejected. The producer issuers permitted to mint caller tokens (grants-ui, fg-cw-frontend, agreements-pdf) are a fixed code constant rather than configuration — the same list applies in every environment, so there is no env var to set and no cdp-app-config entry, and the allowlist can never be misconfigured to an empty "accept any issuer" list.

AWS emulation (floci)

SQS and SNS are emulated by floci on localhost:4566, replacing LocalStack. The rest of the stack (fg-grants-core, fg-cw-backend) uses the same emulator.

Queues, topics and subscriptions are created by init scripts mounted into the floci container. They run in lexical order by filename, pooled across every repo that mounts one in:

Script Mounted by
10-setup-resources.sh this repo (compose/floci/start.d/)
99-ready.sh this repo — writes the /tmp/READY marker

The container's healthcheck waits on /tmp/READY rather than on the gateway responding, because floci starts serving HTTP before the init scripts have finished. Services that depends_on floci with condition: service_healthy therefore never start before their queues exist. If you add a script, give it a numeric prefix below 99.

When gas is pulled into another stack that already runs floci, only 10-setup-resources.sh is mounted and the host stack owns the marker — see compose/ext/compose.gas-ext.example.yml.

State is in-memory: every compose up recreates the resources from scratch.

Inspecting queues and topics

The compat image ships the AWS CLI, so the quickest route needs nothing installed on the host:

docker compose exec floci awslocal sqs list-queues
docker compose exec floci awslocal sns list-topics
docker compose exec floci awslocal sqs receive-message \
  --queue-url $(docker compose exec -T floci awslocal sqs get-queue-url \
    --queue-name gas__sqs__update_status_fifo.fifo --output text)

Note that queues dead-letter after a single failed receive by default, so peeking at a queue can consume the message. Set MAX_READS=2 in compose/aws.env and recreate the stack if you need headroom.

Development

To run the application in development mode run:

npm run dev

Testing

Unit tests

To run the unit tests:

npm run test:unit

Integration tests

To run the integration tests:

npm run test:integration

Contract tests

This project uses Pact to verify consumer-provider contracts.

  • To run the contract tests against the Pact broker:
npm run test:contract

Note: This is normally used in CI, but if running locally it requires .env.test to be populated with the broker details and you must be connected to the Azure VPN.

  • To run the contract tests locally against local pact files (helpful when developing without Azure VPN access or iterating quickly), use the provided script which sets PACT_USE_LOCAL=true:
npm run test:contract:local

When PACT_USE_LOCAL=true, tests will read pact files from tmp/pacts (for example tmp/pacts/grants-ui-fg-gas-backend.json).

Service to service authentication

The GAS API uses simple bearer tokens for service access. Tokens are UUIDv4 values whose SHA‑256 hash is stored in MongoDB in the access_tokens collection.

Clients must send the raw token in the Authorization header as Bearer <token>.

Issuing a credential to another service

Deployed environments give nobody direct database access, so tokens for other services are not inserted by hand. GAS seeds one itself on boot from SERVICE_ACCESS_TOKEN_HASH, supplied by the platform's secret store, which makes issuing and rotating a credential a secret change plus a redeploy.

The value is a single client:sha256hex pair:

some-service:4bb35ade...

Only the hash reaches GAS. The raw token lives solely in the calling service's own secrets, so GAS cannot mint or impersonate the credentials it accepts - the same reason access_tokens stores hashes in the first place. Neither value belongs in a repository: this repo is public, and callers' repos may be too.

This is an instruction to issue, not a record of who holds a token:

  • It affects only the client it names. Other services' tokens are never touched, and neither are tokens minted by hand with scripts/mint-access-token.js (reconciliation is scoped to records the seeder created).
  • Unset it and nothing happens. Clearing the secret revokes nobody, so it can be emptied once a credential has been issued.
  • Onboard services one at a time: point it at the next client and redeploy. The previously issued token stays valid.

Seeding never stops GAS starting. A malformed value or a database failure is logged and skipped, leaving that client without a working token until the next deploy, rather than taking the service down for everyone. Check the logs after a deploy - a credential that silently never appeared looks exactly like one that was never set.

Issue or rotate

Run once per environment - never reuse a token across environments, or a dev credential authenticates against prod:

npm run token:new -- <client-name>

Then in the CDP portal for that environment:

  1. fg-gas-backend -> Secrets -> SERVICE_ACCESS_TOKEN_HASH = the printed client:hash pair
  2. Give the raw token to the calling service as its own secret
  3. Redeploy fg-gas-backend, then the caller

Setting a new hash for a client that already has one rotates it: a single seeded record per client is enforced by a unique index, so the new token replaces the previous one on the next boot. Expect 401s between the two redeploys, so deploy GAS first.

Locally, set SERVICE_ACCESS_TOKEN_HASH in .env instead of the portal.

Revoke

There is no revoke-by-omission - clearing the secret deliberately does nothing. To cut a client off, rotate it to a freshly generated hash and discard the raw token: the old token stops working on the next boot and nobody holds the new one. Removing the record outright needs database access. Issuing the new hash and revoking the old one is a single atomic step, so confirm the deploy logged Seeded access token for <client> - a failed seed leaves the old token live.

The client name is the identity. Reconciliation is scoped to the client named in the secret, so renaming one issues a second credential rather than rotating the first, and the original stays valid indefinitely. Cut the old name off the same way - point the secret at <old-name>:<fresh hash>, redeploy, and discard that token - before switching to the new name.

Verify

Check the GAS startup logs, which will show one of:

Log line Meaning
Seeded access token for <client> Issued. A , replacing the previous one suffix means the hash changed; without it, the credential was already in place.
SERVICE_ACCESS_TOKEN_HASH is not a client:sha256hex pair - nothing seeded The value is malformed. Nothing was issued or removed.
Failed to seed access token for <client> The database write failed. GAS started anyway; retry by redeploying.
nothing The secret is empty or unset, so there was nothing to do.

Then confirm the raw token is accepted:

curl -s -o /dev/null -w '%{http_code}\n' \
  -H "Authorization: Bearer <raw token>" \
  https://fg-gas-backend.<env>.cdp-int.defra.cloud/grants

Minting service access tokens

There is a helper script to mint access tokens and optionally write them to MongoDB:

Use scripts/mint-access-token.js to mint a token and, if MONGO_URI and MONGO_DATABASE are set, write the hashed token to Mongo.

Usage:

# Mint and write hashed token to Mongo, print raw token to console
node --env-file=.env scripts/mint-access-token.js [clientName] [expiresISO]
# Mint and print hashed token and raw token to console
node scripts/mint-access-token.js [clientName] [expiresISO]
  • clientName defaults to grants-ui.
  • expiresISO is optional (e.g. 2099-01-01T00:00:00Z).

The script prints:

  • A confirmation message and details.
  • The hashed token if MONGO_URI and MONGO_DATABASE are not set.
  • The raw bearer token on the last line – copy and store it securely. It will not be shown again.

There is also a convenience script to mint and write the raw token to http-client.private.env.json to allow for local testing with api.http:

npm run token:local

This runs: node --env-file=.env scripts/write-http-client-token.js local http-client 2099-01-01T00:00:00Z.

Which uses scripts/write-http-client-token.js to mint a token, store in the local MongoDB and write the raw value into http-client.private.env.json under the chosen environment key, so api.http can use it immediately.

Run with manual vars:

MONGO_URI="mongodb://localhost:27017" MONGO_DATABASE=fg-gas-backend node scripts/mint-access-token.js

Minting access tokens for fg-grants-core

When using grants core with just the case working and gas dev mode the mongo db is available on localhost and does not use a direct connection.

If you're using grants core with external apps (grants-ui for example) then the db connection is through the docker network and direct connection is required. The env-file option won't work here. You have to manually add MONGO_URI and MONGO_DATABASE to the command - see "run with manual vars"

General script usage for any environment if needed:

node --env-file=.env scripts/write-http-client-token.js <envName> [clientName] [expiresISO]

Examples:

# Local environment – also inserts the hash into Mongo (requires MONGO_URI/MONGO_DATABASE)
node --env-file=.env scripts/write-http-client-token.js local http-client 2099-01-01T00:00:00Z
# Non-local environment – only updates http-client.private.env.json
node scripts/write-http-client-token.js dev grants-ui 2026-01-01T00:00:00Z

After running, you should see output similar to:

Updated http-client.private.env.json -> [local].serviceToken

Notes and requirements:

  • For local usage, ensure your .env provides MONGO_URI and MONGO_DATABASE so the hashed token can be stored in MongoDB.
  • http-client.private.env.json is git‑ignored by default. Keep tokens out of source control.

HTTP client and API examples

This repository includes api.http which you can run directly from JetBrains IDEs (HTTP Client) or VS Code (REST Client extension).

  • Base URLs are defined in http-client.env.json (checked in).
  • Private secrets (like the serviceToken) are stored in http-client.private.env.json (git‑ignored). Do not commit this file.

api.http uses these variables:

  • {{base}} – selected environment base URL (for example http://localhost:3000).
  • {{serviceToken}} – a bearer token used by the API for service‑to‑service auth.

To run the requests:

  1. Ensure the API is running locally (see Development section), or pick a non‑local environment.
  2. Make sure http-client.private.env.json has a serviceToken for the environment you want to call. You can generate/populate this token using the scripts above.
  3. Open api.http, select the desired environment from the drop‑down (e.g. local) and send requests.

Test endpoints (non-production only)

GAS exposes two QA-only endpoints for the agreement journey, accessibility and performance suites:

  • POST /api/test/agreements — create a GAS-managed Agreement, returns 201 with agreementData.agreementNumber.
  • POST /api/test/agreements/{agreementNumber}/status — apply a withdrawn, cancelled or terminated transition, returns 200 with the updated Agreement.

Both are registered only when ENABLE_TEST_ENDPOINTS=true (default false), enabled in dev, test, ext-test and perf-test and never in production. They call the same agreement command handlers as normal processing and add no queues.

Full request and response schemas, status codes and the QA repositories that must migrate off the legacy Agreements API queue-message endpoint are documented in docs/TEST_ENDPOINTS.md.

Docker

Launch GAS and dependencies via Docker Compose:

docker compose up --watch

Project structure

Routes can access use cases and schemas. Subscriptions can access use cases. Use cases can access repositories, http clients, domain classes and other use cases. Use cases should export a single function. Repositories can access db.

src/grants/services/ contains both stateless helpers and transactional application services. The application services coordinate repositories and domain objects for a complete operation, own their Mongo transaction, and pass its session to every participating repository call. entitlement.service.js and claims.service.js are the Grants entry points for the Grant Admin inbound adapter. Services may import domain models when coordinating those operations; domain models must not import services.

Cross-module data is obtained through a documented integration seam. In particular, Grants can use the reviewed Agreements reference-context query, which returns a plain context using the active Mongo session. It must not import an Agreements repository or domain model.

Routes and subscriptions should never respond with a domain object. Domain objects should never access use cases, repositories or subscriptions. Repositories should never accept or return db records.

Logging

This application uses Pino for structured logging, configured with ECS (Elastic Common Schema) formatting for better observability and log analysis.

Logging is configured in src/common/logger.js.

Basic Logging

We use paired entry and exit logging patterns for better log correlation.

Entry logs indicate the start of an operation:

logger.info(
  `Adding agreement ${agreementNumber} for application ${clientRef} with code ${code} on ${date}`,
);

Exit logs indicate the completion of an operation:

logger.info(
  `Finished: Adding agreement ${agreementNumber} for application ${clientRef} with code ${code}`,
);

Note: We use consistent entry text to make it easier to correlate logs within OpenSearch.

Conditional Logging

For operations that have conditional logic between entry and exit logs, use logger.debug() or logger.info() based on relevance:

logger.debug(
  `Application updated with agreement for ${clientRef} with code ${code}`,
);

Example implementation: See src/grants/use-cases/add-agreement.use-case.js

Log Levels

Warning logs for recoverable issues:

Debug logs for detailed diagnostic information:

Note: Error logging (logger.error) is typically not required in use cases as errors are thrown and will propagate up the call stack where they can be handled and logged by the error handling middleware.

Best Practices

  • Include relevant identifiers (IDs, codes, references) in log messages when they must be searchable in OpenSearch
  • CDP only indexes the supported ECS field subset, so unknown context-object properties are not available in OpenSearch
  • Use context objects only for supported ECS fields such as event.* and error.*, or for diagnostics that do not need downstream search
  • Keep entry and exit log messages consistent for easier correlation
  • Use appropriate log levels based on the importance of the information

Licence

THIS INFORMATION IS LICENSED UNDER THE CONDITIONS OF THE OPEN GOVERNMENT LICENCE found at:

http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3

The following attribution statement MUST be cited in your products and applications when using this information.

Contains public sector information licensed under the Open Government license v3

About the licence

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.

About

Git repository for service fg-gas-backend

Topics

Resources

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages