The two-code 475/255 filter missed every return on the Jan-Jul statement
(29 ARP refer-to-maker credits, 24 electronic return credits, and the
redeposit cycles), leaving 5 paid-then-returned checks stuck at Cleared
($5,256.62, vendors unpaid). Rewrite fetchBoaTransactions around a pure
reconciliation module (src/boaRecon.js) covered by node:test:
- BAI transaction-code event map (only 475 = check paid is confirmed;
remaining codes pend the enumeration replay) with a description-text
fallback classifier for ARP refer-to-maker, return-of-posted-check
(check-numbered and electronic) and ACH DES:PAYMENTS formats. Unknown
check-shaped transactions are logged and counted, never dropped.
- Matching on check number AND amount over all candidates; wrong-amount
or collapsed-number postings fall back to a unique exact-amount match
within checks issued in the last 120 days; ambiguity means unmatched
with no write.
- Transitions always set BOTH status and clear_status (the missed
returns slipped through the divergence between them). clear_status is
bank truth: returns apply even to Cleared records, a second paid debit
on a Returned check is a redeposit back to Cleared, and a return on a
voided check is terminal voided-and-bounced. Every applied event is
appended to a history list; identical replayed events are noops.
- Optional {fromDate, toDate} replay payload (strictly validated) for
gap replays and the BAI-code enumeration runs; default stays
yesterday.
- Each run writes a boa_recon#<runDate> summary item (90-day TTL) with
per-event counts, unmatched check numbers/amounts, and unknown codes;
unmatched/unknown also console.error (Slack alerting is #71).
ACH transactions are classified but not yet acted on; bank-confirming
ACH lands with #69.
Two agentic-review findings on the payments-dashboard security surface:
- CRITICAL (CWE-862): the /slack/events endpoint (slackAppHome) performed no
Slack signing-secret verification, so an unauthenticated caller could forge
app_home_opened/block_actions events and exfiltrate payment data via
DynamoDB scans + views.publish. Add HMAC-SHA256 signature verification with
a 5-minute replay window over the raw request body, mirroring the existing
expenseReceiver pattern. Requests failing verification get a 401 before any
body parsing, DynamoDB access, or Slack API call. Adds the
SLACK_SIGNING_SECRET_NAME env var and an additive secretsmanager grant for
payments-dashboard/slack-signing-secret.
- HIGH (CWE-532): full BoA CashPro response bodies + headers were logged to
CloudWatch and persisted to DynamoDB (response_body/response_headers), which
could expose account numbers/PII. Drop those fields from both boa_txn#
records and stop logging raw bodies/headers on both the main submit path and
the backfill retry path; log status + txnId only (txnId still correlates to
BoA support for the full body).
Verification: sam validate, node --check both handlers. /sh-security-review
(detector fan-out + verifier) and GPT-4.1 cross-family review run; the
cross-family review confirmed the IAM grant is purely additive.
* Add CloudWatch alarm coverage for payments-dashboard (Wave 1)
Add 22 CloudWatch alarms to round out observability:
- Lambda Errors alarms for the 4 functions that lacked them
(slackAppHome, fetchBoaTransactions, expenseReceiver, expenseProcessor)
- Lambda Throttles alarms for all 6 functions
- Lambda Duration alarms for all 6 functions (~80% of timeout, Maximum)
- DynamoDB throttle/system-error alarms for the PaymentsDashboard table
(ReadThrottleEvents/WriteThrottleEvents/SystemErrors, TableName dim)
- API Gateway v2 alarms on the implicit ServerlessHttpApi
(5xx, 4xx, Latency p99; ApiId dim)
All alarms publish to the site-alerts SNS topic, ALARM-only, missing data
notBreaching, matching the existing payments-<fn>-errors convention from PR #48.
Documents the full alarm inventory under a new README Monitoring section.
* Drop DynamoDB SystemErrors alarm (never fires at TableName dimension)
AWS/DynamoDB SystemErrors does not emit at the TableName-only dimension,
so payments-dashboard-table-system-errors could never fire. Remove the
alarm resource and its README entry; keep Read/WriteThrottleEvents.
Replace the SSM Parameters section with the new Secrets Manager secret
structure (3 grouped secrets) and correct the runtime-config note.
Refs: INFRA-5, #3
* Merge expense-approval-bot into payments-dashboard
Port the Slack reaction-driven expense routing workflow (receiver +
processor) from expense-approval-bot into this stack as JavaScript ESM.
Secrets copied to payments-dashboard/ prefix in Secrets Manager.
* Fix review findings from PR #28
- Add length check before timingSafeEqual to prevent RangeError on
malformed signatures (returns 401 instead of 500)
- Check event.type === reaction_added to prevent reaction_removed
from advancing expenses
- Move getPermalink call behind isOrigin check to skip unnecessary
API call on non-origin stage transitions
- Add OAuth client-credentials token exchange to both Lambda handlers
- Fix sandbox/prod base URL (api-sb.bofa.com / api.bofa.com)
- Fix issueAction casing to add_Issue / cancel_Issue per API docs
- Fix transaction inquiry response parsing (accountTransactions array)
- Move all BoA config (app IDs, bank ID) from env vars to SSM params
- Update template.yaml with correct SSM param names and policies
- Add project README with architecture, API details, and SSM param reference