Skip to content

Refactor USCR beacon dispatcher and poller lambdas to be multi-tenanted - #1124

Open
stephenhand with Copilot wants to merge 18 commits into
masterfrom
copilot/refactor-beacon-dispatcher-poller-lambdas
Open

Refactor USCR beacon dispatcher and poller lambdas to be multi-tenanted#1124
stephenhand with Copilot wants to merge 18 commits into
masterfrom
copilot/refactor-beacon-dispatcher-poller-lambdas

Conversation

Copilot AI commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Renames and refactors two custom-integration lambdas from USCR-specific to multi-tenant, supporting arbitrary helplines via SSM-driven configuration discovery.

Description

Renamed modules:

  • uscr-beacon-pollerbeacon-poller
  • uscr-dispatcherbeacon-dispatcher

beacon-dispatcher

Converted from an ALB handler with Twilio token auth to an EventBridge handler secured by IAM.

  • Accepts { helplineShortCode, casePayload, contactId } — no longer receives accountSid from the caller
  • Resolves accountSid from SSM at runtime: /{env}/twilio/{helplineShortCode}/account_sid
  • Beacon credentials looked up per-helpline: /{env}/hrm/custom-integration/{helplineShortCode}/beacon_{api_key,base_url}
  • Removed: Twilio auth, ALB handler wrapper, hardcoded alb-handler/twilio-worker-auth dependencies

beacon-poller

Converted from single-tenant (hardcoded ACCOUNT_SID env var + uscr SSM prefix) to scanning all configured helplines at runtime.

  • On each invocation, calls loadSsmCache on /{env}/hrm/custom-integration/ and discovers helplines that have both beacon_api_key and beacon_base_url; logs a warning and skips any with only one
  • Resolves accountSid from SSM for each helpline; skips with a warning if not found
  • SSM tracking keys updated to: /{env}/hrm/custom-integration/{helpline}/{accountSid}/beacon/latest_*_seen
  • addSectionToAseloCase, addDependentSectionToAseloCase, updateAseloCaseOverview/Status now accept accountSid as a parameter instead of reading a module-level constant
  • addIncidentReportSectionsToAseloCase / addCaseReportSectionsToAseloCase replaced with factory functions createIncidentReportProcessor(accountSid) / createCaseReportProcessor(accountSid)

Checklist

  • Corresponding issue has been opened
  • New tests added
  • Feature flags / configuration added

Other Related Issues

None

Verification steps

Deploy to staging and verify:

  1. SSM params exist for at least one helpline under /{env}/hrm/custom-integration/{helpline}/beacon_api_key and beacon_base_url, plus /{env}/twilio/{helpline}/account_sid
  2. Trigger the beacon-poller lambda — confirm it polls for each configured helpline and logs per-helpline traces
  3. Trigger the beacon-dispatcher via EventBridge with { helplineShortCode, casePayload, contactId } — confirm incident is created in Beacon and case section updated in HRM
  4. Configure a helpline with only one of the two beacon SSM params — confirm warning is logged and that helpline is skipped without error

AFTER YOU MERGE

  1. Cut a release tag using the GitHub workflow. Wait for it to complete and notify in the #aselo-deploys Slack channel.
  2. Comment on the ticket with the release tag version AND any additional instructions required to configure an environment to test the changes.
  3. Only then move the ticket into the QA column in JIRA

You are responsible for ensuring the above steps are completed. If you move a ticket into QA without advising what version to test, the QA team will assume the latest tag has the changes. If it does not, the following confusion is on you! :-P

Copilot AI and others added 2 commits August 25, 2026 18:38
…tcher; make multi-tenant

Co-authored-by: stephenhand <1694716+stephenhand@users.noreply.github.com>
Co-authored-by: stephenhand <1694716+stephenhand@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor USCR beacon dispatcher and poller lambdas for multi-tenancy Refactor USCR beacon dispatcher and poller lambdas to be multi-tenanted Aug 25, 2026
Copilot AI requested a review from stephenhand August 25, 2026 18:48

@stephenhand stephenhand left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot - see comments for requested changes

const beaconHeaders = { [BEACON_API_KEY_HEADER]: beaconApiKey };

const lastIncidentReportUpdateSeenSsmKey = `/${environment}/hrm/custom-integration/${helplineShortCode}/${accountSid}/beacon/latest_incident_report_seen`;
const lastCaseReportUpdateSeenSsmKey = `/${environment}/hrm/custom-integration/${helplineShortCode}/${accountSid}/beacon/latest_case_report_seen`;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This SSM key does not include the helplineShortCode, we should change the naming to reflect beacon being used in multiple helplines

Suggested change
const lastCaseReportUpdateSeenSsmKey = `/${environment}/hrm/custom-integration/${helplineShortCode}/${accountSid}/beacon/latest_case_report_seen`;
const lastCaseReportUpdateSeenSsmKey = `/${environment}/hrm/custom-integration/beacon/${accountSid}/latest_case_report_seen`;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the latest commit. SSM key updated to /${environment}/hrm/custom-integration/beacon/${accountSid}/latest_incident_report_seen.

Comment thread hrm-domain/lambdas/custom-integrations/beacon-poller/src/index.ts Outdated
contactId?: string;
};

export const handler = async (event: BeaconDispatcherEvent) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue desciption was mistaken, this still needs to be an ALB event. The only difference from the original code is that the account SID should now be extracted from a last, additional section of the path rather than the environment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted to ALB handler pattern. The handler now uses handleAlbEvent with Twilio worker token validation, and extracts accountSid from the last path segment. The body contains { helplineShortCode, casePayload, contactId } for Beacon credential lookup and payload validation. Added @tech-matters/alb-handler and @tech-matters/twilio-worker-auth back to dependencies.

…Twilio auth in dispatcher

Co-authored-by: stephenhand <1694716+stephenhand@users.noreply.github.com>

@stephenhand stephenhand left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Align the lambda docker container build workflows with the updated lambda module patha

Co-authored-by: stephenhand <1694716+stephenhand@users.noreply.github.com>

Copilot AI commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Align the lambda docker container build workflows with the updated lambda module patha

Updated hrm-lambda-deploy-all.yml, hrm-ci.yml, hrm-lambda-deploy.yml, lambda-specific-region-map.json, tsconfig.json, and package-lock.json to use beacon-dispatcher and beacon-poller in place of the old uscr-dispatcher and uscr-beacon-poller paths.

@stephenhand stephenhand left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Fix the beacon poller service tests by mocking the required SSM parameters

@stephenhand

Copy link
Copy Markdown
Collaborator

@copilot fix the linter issues causing the tests to fail

…ution

Co-authored-by: stephenhand <1694716+stephenhand@users.noreply.github.com>

Copilot AI commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Fix the beacon poller service tests by mocking the required SSM parameters

Fixed in the latest commit. Two issues were addressed:

  1. beforeAll ordering: mockLastUpdateSeenParameter is now called before mockBeacon, so the SSM mock handler is set up before mockBeacon calls putSsmParameter for beacon_base_url.

  2. SSM cache pollution: The putSsmParameter calls in beforeEach and mockBeacon now use { cacheValue: false }. Previously they populated ssmCache.values, causing loadSsmCache to short-circuit (isConfigNotEmpty() && !hasCacheExpired() was always true), so the path-based parameter scan never ran and no helplines were discovered.

Co-authored-by: stephenhand <1694716+stephenhand@users.noreply.github.com>

Copilot AI commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

@copilot fix the linter issues causing the tests to fail

Fixed in the latest commit. Ran eslint --fix on the three affected unit test files (caseUpdater.test.ts, incidentReport.test.ts, caseReport.test.ts) to auto-correct the prettier formatting errors. All unit tests still pass.

@stephenhand
stephenhand requested a review from gpaoloni September 2, 2026 17:28
@stephenhand
stephenhand marked this pull request as ready for review September 2, 2026 17:28
Copilot AI lite review requested due to automatic review settings September 2, 2026 17:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The dispatcher and poller implementations materially diverge from the stated multi-tenant/EventBridge behavior and include confirmed functional issues (e.g., helpline code extraction).

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR renames the USCR-specific Beacon “dispatcher” and “poller” lambdas to generic beacon-* modules and begins refactoring them toward multi-tenant operation by resolving helpline-specific configuration from SSM and updating associated build/test/deploy wiring.

Changes:

  • Renamed uscr-* custom-integration lambdas to beacon-dispatcher and beacon-poller, updating TS project references and GitHub Actions matrices.
  • Updated beacon-dispatcher to derive Beacon credentials from SSM per helpline (but still implemented as an ALB + Twilio-token-auth handler).
  • Updated beacon-poller to resolve helpline config from SSM and added/updated unit + service tests around the refactored processors and service behavior (but still runs per-helpline invocation rather than discovering all helplines).
File summaries
File Description
tsconfig.json Updates TS project references to renamed beacon lambdas.
package.json Adds ms and @types/ms override/dependency entries.
hrm-domain/lambdas/custom-integrations/uscr-dispatcher/index.ts Removes legacy USCR dispatcher ALB implementation.
hrm-domain/lambdas/custom-integrations/uscr-dispatcher/authentication.ts Removes legacy Twilio auth helper (migrated/refactored elsewhere).
hrm-domain/lambdas/custom-integrations/uscr-beacon-poller/src/index.ts Removes legacy single-tenant poller entrypoint.
hrm-domain/lambdas/custom-integrations/beacon-poller/tsconfig.json Adds TS config for renamed poller package.
hrm-domain/lambdas/custom-integrations/beacon-poller/tsconfig.build.json Updates build references for the renamed poller package.
hrm-domain/lambdas/custom-integrations/beacon-poller/tests/unit/verifyAddSectionRequest.ts Adds unit-test helper for verifying HRM section writes.
hrm-domain/lambdas/custom-integrations/beacon-poller/tests/unit/responder.test.ts Adds unit tests for responder-to-case-section mapping.
hrm-domain/lambdas/custom-integrations/beacon-poller/tests/unit/incidentReport.test.ts Updates unit tests to use the new incident report processor factory.
hrm-domain/lambdas/custom-integrations/beacon-poller/tests/unit/caseUpdater.test.ts Updates tests for new accountSid-parameterized case updater APIs.
hrm-domain/lambdas/custom-integrations/beacon-poller/tests/unit/caseReport/caseReport.test.ts Updates case-report tests to use the processor factory.
hrm-domain/lambdas/custom-integrations/beacon-poller/tests/unit/caseReport/apiPayload.test.ts Adds tests for case report API payload restructuring.
hrm-domain/lambdas/custom-integrations/beacon-poller/tests/service/docker-compose.yml Adds docker compose setup for poller service tests.
hrm-domain/lambdas/custom-integrations/beacon-poller/tests/service/docker-compose-ci.yml Adds CI compose configuration for service tests.
hrm-domain/lambdas/custom-integrations/beacon-poller/tests/service/dbConnection.ts Adds DB connection helper for service tests.
hrm-domain/lambdas/custom-integrations/beacon-poller/tests/service/beaconPoller.test.ts Updates service tests for SSM-driven config + helpline short code input.
hrm-domain/lambdas/custom-integrations/beacon-poller/tests/service/.env.service-test Updates comments to reflect renamed poller.
hrm-domain/lambdas/custom-integrations/beacon-poller/tests/mockGenerators.ts Adds generators for incident/case report test payloads.
hrm-domain/lambdas/custom-integrations/beacon-poller/tests/fixtures.ts Adds large fixtures for payload restructuring tests.
hrm-domain/lambdas/custom-integrations/beacon-poller/src/types.ts Introduces shared types for poller item processors and case sections.
hrm-domain/lambdas/custom-integrations/beacon-poller/src/responder.ts Adds responder mapping logic used by incident processing.
hrm-domain/lambdas/custom-integrations/beacon-poller/src/index.ts New poller entrypoint: per-helpline SSM lookups + chunked polling.
hrm-domain/lambdas/custom-integrations/beacon-poller/src/incidentReport.ts Refactors incident processing into an accountSid-scoped factory.
hrm-domain/lambdas/custom-integrations/beacon-poller/src/config.ts Removes old module-level accountSid / headers config.
hrm-domain/lambdas/custom-integrations/beacon-poller/src/caseUpdater.ts Refactors HRM update helpers to accept accountSid as an argument.
hrm-domain/lambdas/custom-integrations/beacon-poller/src/caseReport/index.ts Refactors case report processing into an accountSid-scoped factory.
hrm-domain/lambdas/custom-integrations/beacon-poller/src/caseReport/apiPayload.ts Adds raw→processed case report payload restructuring.
hrm-domain/lambdas/custom-integrations/beacon-poller/src/apiChunkReader.ts Adds chunked Beacon API polling + last-seen tracking in SSM.
hrm-domain/lambdas/custom-integrations/beacon-poller/setTestEnvVars.js Adds Jest setup to seed env vars for unit/service tests.
hrm-domain/lambdas/custom-integrations/beacon-poller/package.json Renames package, updates scripts, and adds test dependencies.
hrm-domain/lambdas/custom-integrations/beacon-poller/jest.config.js Adds Jest config for the renamed poller.
hrm-domain/lambdas/custom-integrations/beacon-dispatcher/validation.ts Removes accountSid and header validation from dispatcher payload validation.
hrm-domain/lambdas/custom-integrations/beacon-dispatcher/tsconfig.json Adds TS config for renamed dispatcher package.
hrm-domain/lambdas/custom-integrations/beacon-dispatcher/tsconfig.build.json Updates build references for renamed dispatcher package.
hrm-domain/lambdas/custom-integrations/beacon-dispatcher/package.json Renames dispatcher package and docker build script.
hrm-domain/lambdas/custom-integrations/beacon-dispatcher/mapping.ts Avoids mutating category arrays when selecting a category.
hrm-domain/lambdas/custom-integrations/beacon-dispatcher/index.ts New dispatcher handler with SSM-driven helpline config (still ALB/Twilio-token based).
hrm-domain/lambdas/custom-integrations/beacon-dispatcher/hrm-service/index.ts Renames/changes helper for detecting an existing pending incident attempt.
hrm-domain/lambdas/custom-integrations/beacon-dispatcher/hrm-service/contactService.ts Adds HRM contact API helpers (get + connectToCase).
hrm-domain/lambdas/custom-integrations/beacon-dispatcher/hrm-service/caseService.ts Adds HRM case/section API helpers used by dispatcher.
hrm-domain/lambdas/custom-integrations/beacon-dispatcher/beacon-service/index.ts Adds beacon-service barrel exports.
hrm-domain/lambdas/custom-integrations/beacon-dispatcher/beacon-service/incidentService.ts Updates Beacon incident creation to read creds from helpline-scoped SSM keys.
.github/workflows/hrm-lambda-deploy.yml Updates deployment matrix entries to renamed lambdas.
.github/workflows/hrm-lambda-deploy-all.yml Updates “deploy all” matrix entries to renamed lambdas.
.github/workflows/hrm-ci.yml Updates CI matrix entries to renamed lambdas.
.github/workflows/config/lambda-specific-region-map.json Updates region map keys to renamed lambdas.
Review details

Suppressed comments (3)

hrm-domain/lambdas/custom-integrations/beacon-dispatcher/index.ts:83

  • If helplineCode is missing (or regex fails) and the legacy request body also omits accountSid, accountSid becomes undefined and the subsequent SSM lookups will query .../twilio/undefined/... (and token validation will behave unpredictably). Add an explicit validation guard after resolving accountSid.
  const accountSid = helplineCode
    ? await getSsmParameter(
        `/${environment}/twilio/${helplineCode.toUpperCase()}/account_sid`,
      )
    : body.accountSid;

hrm-domain/lambdas/custom-integrations/beacon-poller/src/index.ts:49

  • The PR description/issue state beacon-poller should scan SSM (loadSsmCache) and run for every configured helpline each invocation. This handler instead requires a single helplineShortCode input and does not perform any discovery/iteration across helplines.
export const handler = async ({
  apiType,
  helplineShortCode,
}: {
  apiType: 'incidentReport' | 'caseReport';

hrm-domain/lambdas/custom-integrations/beacon-poller/src/index.ts:78

  • The SSM tracking keys are written under /${env}/hrm/custom-integration/beacon/${accountSid}/latest_*_seen, but the PR description/issue call for /${env}/hrm/custom-integration/{helpline}/{accountSid}/beacon/latest_*_seen to avoid ambiguity and keep state namespaced per helpline.
  const lastIncidentReportUpdateSeenSsmKey = `/${environment}/hrm/custom-integration/beacon/${accountSid}/latest_incident_report_seen`;
  const lastCaseReportUpdateSeenSsmKey = `/${environment}/hrm/custom-integration/beacon/${accountSid}/latest_case_report_seen`;
  • Files reviewed: 27/48 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +36 to +37
const EXTRACT_HELPLINE_CODE_FROM_PATH =
/.*custom-integrations\/beacon\/dispatcher\/(?<helplineCode>[A-Za-z0-9]+)/g;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the fixed part of the URL is known, can't we use length + substring rather than a regex?

Comment on lines +24 to +28
handleAlbEvent,
AlbHandlerEvent,
AlbHandlerResult,
} from '@tech-matters/alb-handler';
import { twilioTokenValidator } from '@tech-matters/twilio-worker-auth';
Comment on lines +17 to +31
/**
* Copyright (C) 2021-2023 Technology Matters
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@gpaoloni gpaoloni left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment on lines +289 to +296
export const existingPendingIncident = (timeline: TimelineApiResponse) =>
Boolean(timeline.activities.length) &&
timeline.activities.some(
(timeline.activities.find(
t =>
(t.activity.sectionTypeSpecificData as IncidentReportAttempt)?.incidentId !== null,
);
)?.activity.sectionTypeSpecificData as
| (IncidentReportAttempt & { incidentId: number })
| undefined);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this type casting necessary here? 🤔

Comment on lines +36 to +37
const EXTRACT_HELPLINE_CODE_FROM_PATH =
/.*custom-integrations\/beacon\/dispatcher\/(?<helplineCode>[A-Za-z0-9]+)/g;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the fixed part of the URL is known, can't we use length + substring rather than a regex?

Comment on lines +95 to +103
// Validate Twilio worker token
const authHeader = event.headers?.authorization || event.headers?.Authorization;
const token = authHeader?.replace(/^Bearer\s+/i, '');
if (!token) {
return newErr({
error: 'AuthenticationError',
message: 'Missing Authorization header',
});
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe move this up so we avoid all the processing above if the request is not authorized in first place?

'sudSurvey',
caseReportToSudSurveyCaseSection,
);
export const createCaseReportProcessor = (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this generic? Asking cause I'm not sure if this is targeting a Beacon structure or the intended Aselo case shape (e.g. creating custom case sections that might differ across accounts).

Comment on lines +17 to +31
/**
* Copyright (C) 2021-2023 Technology Matters
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor USCR beacon dispatcher and poller lambdas to be multi tenanted

4 participants