Internal payments tracking dashboard
Find a file
dependabot[bot] 46fb143d94
Bump the minor-and-patch group with 7 updates
Bumps the minor-and-patch group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@aws-sdk/client-dynamodb](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-dynamodb) | `3.1075.0` | `3.1077.0` |
| [@aws-sdk/client-lambda](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-lambda) | `3.1075.0` | `3.1077.0` |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1075.0` | `3.1077.0` |
| [@aws-sdk/client-secrets-manager](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-secrets-manager) | `3.1075.0` | `3.1077.0` |
| [@aws-sdk/client-sqs](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-sqs) | `3.1075.0` | `3.1077.0` |
| [@aws-sdk/lib-dynamodb](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-dynamodb) | `3.1075.0` | `3.1077.0` |
| [mailparser](https://github.com/nodemailer/mailparser) | `3.9.11` | `3.9.12` |


Updates `@aws-sdk/client-dynamodb` from 3.1075.0 to 3.1077.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-dynamodb/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1077.0/clients/client-dynamodb)

Updates `@aws-sdk/client-lambda` from 3.1075.0 to 3.1077.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-lambda/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1077.0/clients/client-lambda)

Updates `@aws-sdk/client-s3` from 3.1075.0 to 3.1077.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1077.0/clients/client-s3)

Updates `@aws-sdk/client-secrets-manager` from 3.1075.0 to 3.1077.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-secrets-manager/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1077.0/clients/client-secrets-manager)

Updates `@aws-sdk/client-sqs` from 3.1075.0 to 3.1077.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-sqs/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1077.0/clients/client-sqs)

Updates `@aws-sdk/lib-dynamodb` from 3.1075.0 to 3.1077.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-dynamodb/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1077.0/lib/lib-dynamodb)

Updates `mailparser` from 3.9.11 to 3.9.12
- [Release notes](https://github.com/nodemailer/mailparser/releases)
- [Changelog](https://github.com/nodemailer/mailparser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/mailparser/compare/v3.9.11...v3.9.12)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-dynamodb"
  dependency-version: 3.1077.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: "@aws-sdk/client-lambda"
  dependency-version: 3.1077.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1077.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: "@aws-sdk/client-secrets-manager"
  dependency-version: 3.1077.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: "@aws-sdk/client-sqs"
  dependency-version: 3.1077.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: "@aws-sdk/lib-dynamodb"
  dependency-version: 3.1077.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: mailparser
  dependency-version: 3.9.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-30 22:16:06 +00:00
.claude Add .claude configuration directory 2026-04-27 19:14:54 -04:00
.github Repo hygiene: PR labeler + README badges (INFRA-56/57) (#45) 2026-06-11 14:13:31 -04:00
scripts Migrate secrets from SSM to Secrets Manager 2026-06-02 20:29:18 -04:00
src Migrate secrets from SSM to Secrets Manager 2026-06-02 20:29:18 -04:00
.gitignore Gitignore .env for compliance (#33) 2026-06-02 19:56:42 -04:00
package-lock.json Bump the minor-and-patch group with 7 updates 2026-06-30 22:16:06 +00:00
package.json Bump the minor-and-patch group with 7 updates 2026-06-30 22:16:06 +00:00
README.md Add CloudWatch alarm coverage (payments-dashboard) (#51) 2026-06-17 14:51:59 -04:00
samconfig.toml.example Add samconfig.toml.example for onboarding 2026-06-02 20:29:29 -04:00
template.yaml Add messages-present alarms on async-invoke DLQs (#48) 2026-06-17 15:09:17 -04:00

Payments Dashboard

JavaScript AWS SAM Slack CI

AWS SAM application that ingests payment CSVs, syncs check data with Bank of America CashPro APIs, processes Gusto payroll confirmation emails into Slack notifications, surfaces an outstanding-payments dashboard in Slack, and routes expense approvals through a Slack reaction-driven workflow.

Architecture

  • ProcessPayrollEmail — Lambda triggered by S3 (inbound email) and SQS (batch timer). SES receives Gusto payroll emails at payroll@int.seahaven.com, stores them to S3, and this Lambda parses the email body, extracts financial data, and posts a combined Slack notification (employee payroll + contractor payments) after a 10-minute batching window. Runs outside VPC.

  • ProcessPaymentCsv — Lambda triggered by S3 CSV upload. Parses Stampli payment exports, upserts to DynamoDB, and submits new/cancelled checks to the CashPro Check Management API.

  • FetchBoaTransactions — Scheduled Lambda (weekdays 9am ET). Calls the CashPro Previous Day Transaction Inquiry API and matches cleared/returned checks back to DynamoDB records.

  • SlackAppHome — Lambda behind API Gateway. Renders the payments dashboard on the Slack App Home tab with outstanding aging buckets and drill-down modals.

  • ExpenseReceiver — Lambda behind API Gateway (POST /slack/expense-events). Verifies the Slack signing secret (HMAC-SHA256), handles URL verification challenges, and async-invokes ExpenseProcessor. Runs outside VPC.

  • ExpenseProcessor — Async Lambda invoked by ExpenseReceiver. Processes :white_check_mark: reactions to advance expense messages through a four-stage Slack channel pipeline: Submitted → Processed → Authorized → Matched. Runs outside VPC.

ProcessPaymentCsv, FetchBoaTransactions, and SlackAppHome run inside a VPC with a NAT Gateway for a static outbound IP (required by BoA IP whitelisting). ProcessPayrollEmail, ExpenseReceiver, and ExpenseProcessor run outside the VPC.

Expense Approval Bot

Reaction-driven workflow that routes expense submissions through four Slack channels. A separate Slack app ("Expense Approval Bot") posts to a Submitted channel. Users react with ✅ to advance the message to the next stage.

Channel pipeline:

Stage Channel ID Action on ✅
Submitted C0AQ2AWLNEN Thread reply on original, copy to Processed
Processed C0APLSGABAB Delete from Processed, post to Authorized
Authorized C0AQ09CDJH4 Delete from Authorized, post to Matched
Matched C0APYUM1JFP Terminal stage (no further routing)

Architecture: Two Lambdas — ExpenseReceiver (HTTP endpoint, signature verification, async invoke) and ExpenseProcessor (business logic). This is the same receiver/processor pattern used for Slack's 3-second timeout requirement.

Secrets (Secrets Manager):

Secret Purpose
payments-dashboard/expense-slack-token Slack Bot token for the Expense Approval Bot app
payments-dashboard/expense-slack-signing-secret Slack signing secret for request verification

Payroll Email Pipeline

Gusto sends payroll confirmation emails when payroll is run. A Gmail filter on adam@seahavenind.com auto-forwards emails from automated@gusto.com and gustonoreply@gusto.com to payroll@int.seahaven.com.

Flow: Gmail forward → SES receipt rule → S3 bucket → Lambda parses email → DynamoDB (pending) → SQS delay queue (10 min) → Lambda batches all pending items for that date → single Slack message → DynamoDB (notified)

Deduplication: Each email is deduplicated by DynamoDB key (PAYROLL_EMAIL#employee#<date> or PAYROLL_EMAIL#contractor#<date>#<bank-suffix>). The batch post is deduplicated by PAYROLL_BATCH#<date>. All items have a 90-day TTL.

BoA CashPro API Integration

Two separate CashPro APIs are used, each with its own OAuth credentials:

API Purpose Endpoint
Check Management Issue and cancel checks /cashpro/checkmanagement/v1/check-issues
Reporting (Transaction Inquiry) Fetch previous-day transactions /cashpro/reporting/v1/transaction-inquiries/previous-day

Authentication flow:

  1. POST to /authn/v1/client-authentication with applicationID, client_id, and client_secret
  2. Receive a Bearer access_token (valid 1 hour)
  3. Pass the token in the Authorization header for subsequent API calls

Base URLs:

  • Production: https://api.bofa.com
  • Sandbox: https://api-sb.bofa.com

Secrets

All BoA and Slack credentials are stored in AWS Secrets Manager (per engineering-handbook/secrets-and-config.md). The Slack token is a plaintext secret; the two BoA secrets are JSON grouping each API's credentials:

Secret Type Contents
payments-dashboard/slack-bot-token plaintext Slack Bot OAuth token (used by processPayrollEmail, slackAppHome)
payments-dashboard/boa-check-mgmt JSON appId, clientId, token, accountNumber, companyId — Check Management API (processPaymentCsv)
payments-dashboard/boa-reporting JSON appId, clientId, token, accountNumber, bankId — Reporting API (fetchBoaTransactions)

boa-account-number is duplicated into both BoA secrets. Each Lambda is granted secretsmanager:GetSecretValue scoped to only the secret it needs. The Expense Approval Bot uses two additional secrets (payments-dashboard/expense-slack-token, payments-dashboard/expense-slack-signing-secret).

Monitoring & Alarms

All CloudWatch alarms publish to the shared site-alerts SNS topic (arn:aws:sns:us-east-1:328440206208:site-alerts). Alarms are ALARM-only by convention (no OK/recovery action) and treat missing data as notBreaching. Each alarm evaluates a single 5-minute period.

SQS dead-letter queues (messages-present, Maximum > 0):

Alarm Source
payments-payroll-batch-dlq-messages payments-payroll-batch-dlq
payments-processPaymentCsv-async-dlq-messages async-invoke OnFailure DLQ
payments-processPayrollEmail-async-dlq-messages async-invoke OnFailure DLQ

Lambda (per function — payments-<fn>-...):

Type Metric / Statistic Threshold
-errors (all 6) Errors / Sum > 0
-throttles (all 6) Throttles / Sum > 0
-duration (all 6) Duration / Maximum ~80% of each function's timeout

Duration thresholds (ms): processPaymentCsv 96000, processPayrollEmail 48000, fetchBoaTransactions 48000, slackAppHome 24000, expenseProcessor 12000, expenseReceiver 4000.

DynamoDB (PaymentsDashboard table, TableName dimension, Sum > 0): payments-dashboard-table-read-throttle (ReadThrottleEvents), payments-dashboard-table-write-throttle (WriteThrottleEvents). The table is PAY_PER_REQUEST; these metrics emit only when a throttle occurs. SystemErrors is intentionally not alarmed because it does not emit at the TableName-only dimension.

API Gateway (implicit HTTP API v2 ServerlessHttpApi, ApiId dimension): payments-dashboard-api-5xx (5xx Sum > 0), payments-dashboard-api-4xx (4xx Sum > 10, client-error noise floor), payments-dashboard-api-latency-p99 (Latency p99 > 3000 ms).

Scripts

Script Purpose
scripts/test-boa-sandbox.js One-off sandbox connectivity test for both CashPro APIs
scripts/seed-from-csv.js Seed DynamoDB from a local CSV file
scripts/seed-bank-status.js Seed bank clear status data into DynamoDB

Deployment

sam build
sam deploy --guided

The BOA_BASE_URL environment variable in template.yaml controls whether Lambdas hit production (https://api.bofa.com) or sandbox (https://api-sb.bofa.com). All other BoA config is read from Secrets Manager at runtime.