Commit graph

15 commits

Author SHA1 Message Date
Adam Moussa
0e3e95c240
feat(infra): migrate payments-dashboard to HCP Terraform (PLAT-79) (#109)
Some checks are pending
Deploy / Deploy to prod (push) Waiting to run
* feat(infra): migrate payments-dashboard to HCP Terraform (PLAT-79)

Replace the mgmt SAM stack with a prod-only HCP workspace using the afterhours stub-plus-zip-CD seam so GitHub Actions owns function code and Terraform owns infrastructure.

* fix(infra): pin secret and CMK ARNs for bootstrap-plan

hcptf-bootstrap-plan cannot ssm:GetParameter or DescribeSecret, so the first plan must not data-source those values.

* fix(infra): add EIP describe and DynamoDB CMK grants for first apply

Scoped apply missed ec2:DescribeAddressesAttribute and kms Encrypt/Decrypt/GenerateDataKey on the table CMK.
2026-09-16 18:29:01 +00:00
Adam Moussa
3f2aa692c3
chore(payroll): remove deprecated Gusto email pipeline (#105)
Some checks failed
Deploy / deploy (push) Has been cancelled
2026-09-01 20:54:38 +00:00
Adam Moussa
a6a8132a69
feat(apphome): surface returned payments as a needs-action queue (#74)
* feat(apphome): surface returned payments as a needs-action queue

Bank-returned payments carry status "Cleared" (the CSV ladder has no
Returned rung), so the status skip-list hid them from every App Home
bucket; five Feb-Apr returns ($5,256.62) surfaced only via a manual
statement reconciliation. Route on clear_status ahead of the status
skip-list: non-canceled Returned records render in an always-visible
action queue (oldest return first) plus a summary tile, and are
excluded from Outstanding totals. Terminal voided-and-bounced records
stay out of both (audit trail only). Membership keys off clear_status,
not returned_date, so redeposited checks drop back out of the queue.

Refs: #70

* docs(apphome): restore returned-queue README bullet dropped in rebase
2026-07-22 16:38:56 -04:00
Adam Moussa
ffab1c8f7b
feat(fetchboa): intraday current-day runs, raw S3 archive, balance records (#76)
Same-day settlement visibility plus maximal data capture from the
reporting feed. The handler gains an allowlisted endpoint parameter
(EventBridge passes {"endpoint":"current-day"} on a new 16/19/22 UTC
weekday rule; the 9am previous-day sweep is unchanged and remains
authoritative). Every response's exact bytes archive to a new
Retain-protected bucket before classification, so the feed is
replayable and auditable even across parser changes. Summary rows
become per-date boa_balance# snapshots (latest-wins on run_at, no
TTL) instead of being discarded. The staleness sweep is gated to
previous-day runs so intraday runs don't re-alert the backlog three
times a day. History events carry a via:<endpoint> audit tag outside
the replay-idempotence identity. Fixtures are sanitized real API
captures; classification histograms assert against live-verified
counts.

Refs: #66, #69
2026-07-22 16:02:48 -04:00
Adam Moussa
d8a2412d75
fix(fetchboa): read real CashPro field names; extend BAI code map; 7-day window (#75)
Some checks are pending
Deploy / deploy (push) Waiting to run
The deployed v2 pipeline was fully inert: the classifier never read
detailText (where the live API carries the ACH DES:PAYMENTS text) and
the handler keyed event dates off valueDate, which the API does not
send (it sends asOfDate) — so ACH could not classify and no event of
any kind could apply. Both proven against real captured responses.

- classifyTransaction: detailText first in the description chain;
  Summary-row guard (new "summary" event); all-zeros customerReference
  normalizes to empty instead of fabricating check number 0.
- BAI_CODE_EVENTS: empirical enumeration lands — 255 + 252 are both
  check-numbered return credits, 266 is the no-number return credit
  (text disambiguates ach_return vs electronic_return via new
  fallbackEvent semantics), 455 is ach_debit via DES text or ignored
  third-party autopay, 170/201/470/481 are noise.
- postingDate helper (asOfDate ?? valueDate) drives both the sort and
  event dates; unmatched reason is now "missing posting date".
- Default replay window widened to trailing 7 days ending yesterday:
  self-healing across missed runs; responses verified pagination-free.

Refs: #66, #69
2026-07-22 15:40:49 -04:00
Adam Moussa
f7adb6e8b8
feat(fetchboa): bank-truth reconciliation v2 — returns, redeposits, ACH confirmation (#73)
Some checks are pending
Deploy / deploy (push) Waiting to run
* feat(fetchboa): v2 return/redeposit-aware reconciliation (#66)

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.

* feat(ach): bank-confirm ACH, drop CSV-date auto-clear (#69)

processPaymentCsv auto-cleared ACH the moment send_payment_on passed,
but the bank disagrees often enough to matter: settlement lags the send
date by up to 8 days, settled ACH can bounce and re-debit (Uline
$19,281.12, Heights $10,000.00 on the 5/26 overdrawn week), and voided
ACH that settled anyway returned via electronic credits invisible to a
Check-only feed. Move ACH to bank confirmation:

- processPaymentCsv: ACH rows keep their Stampli status; the send-date
  auto-clear is removed. The bankConfirmed protection is unchanged, so
  bank-cleared records still cannot be moved backward by the CSV.
- fetchBoaTransactions scans all payments (method filter dropped) and
  processes ACH CCD lines: match by stored pmt_id first (reversals
  reuse the original PMT id), then by the Stampli payment number
  embedded in PMT INFO (internal spaces stripped, amount must agree),
  then vendor + exact amount within send_payment_on -2..+14 days.
  Settled debit -> Cleared/Cleared with dates, pmt_id persisted on the
  record; credit reusing the pmt_id (or a unique same-amount return
  credit against a bank-confirmed ACH) -> clear_status=Returned +
  returned_date + history event. Ambiguity is unmatched, no write.

Existing DDB ACH records already Cleared by the old auto-clear are
unaffected: bank confirmation is additive and the CSV path never moves
a record backward.

Handler event contract extended (optional fromDate/toDate); cross-family
review required before merge.

* fix(fetchboa): close review findings — coverage gap, matcher corroboration, replay identity (#66)

Security-review gate (detector fan-out + verifier + GPT-4.1 cross-family)
blocked on F6 and confirmed six lower findings; all are closed here.

- F6 (HIGH): the default run now queries a trailing 3-day window
  (today-3..today-1) so the Monday run covers Friday-Sunday postings the
  previous-day cadence silently skipped. A per-run staleness sweep flags
  never-bank-confirmed payments (ACH > 16 days, checks > 60 days) in the
  summary and via console.error.
- F2: replay identity is {event, date, amount} — bankRef excluded (feeds
  omit/reformat it between runs); a paid event dated on/before the
  latest return in history is a replayed original, never a redeposit;
  rows without a valid valueDate are routed to unmatched instead of
  being applied under a substituted date; within a day, debits sort
  before credits.
- F1: a check-number match with the wrong amount no longer falls
  through to the amount fallback (altered-check/collapsed-posting is a
  human-review case); the amount fallback requires digit-subsequence
  corroboration between posting and candidate numbers; check_return
  fallback requires a bank-confirmed candidate.
- F4: the ach_return amount fallback requires cleared_date within 45
  days before the credit; an unattributable PMT id is an unmatched
  alert, never an amount guess.
- F5: the pmt_id rung requires amount equality; mismatch is the
  partial-reversal human case.
- F3: vendor prefix matching requires >= 10 normalized chars (short
  names must match exactly); candidates with a conflicting stored
  pmt_id are excluded; vendor+amount matches are audit-logged.
- F7: payment writes are conditioned on the snapshot's
  status/clear_status and retried once against a fresh read; residual
  conflicts are counted, not silently interleaved.
- F9/summary bounds: run summary pk is append-only
  (boa_recon#<from>_<to>#<runAt>); unmatched list capped at 50, unknown
  codes at 20 keys/16 chars, stale list at 50 (full counts kept).
- ReDoS: description bounded to 500 chars before classification;
  CHECK/embedded-number regexes use bounded quantifiers.
- FBOA2-1: both BoA res.json() parses are wrapped so a malformed 200
  throws a status-only error and cannot leak a body snippet.

Handler event contract extended (optional fromDate/toDate); cross-family
review required before merge.

* fix(boaRecon): add method=Check filter to matchElectronicReturn

Electronic returns only apply to checks, but the matcher was not
filtering by method, so an electronic return could incorrectly
match against a same-amount, bank-confirmed ACH record.
2026-07-21 22:07:37 -04:00
Adam Moussa
0eeca1e7a5
fix(security): verify Slack signatures on /slack/events and stop logging raw BoA responses (#63)
Some checks failed
Deploy / deploy (push) Has been cancelled
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.
2026-07-10 17:04:35 -04:00
Adam Moussa
a30003d725
docs: document cross-stack DynamoDB data contracts (INFRA-138) (#60) 2026-07-08 16:20:05 -04:00
Adam Moussa
3f46bdb57c
docs: link Confluence AWS Architecture Map (INFRA-53) (#57)
Some checks are pending
Deploy / deploy (push) Waiting to run
2026-07-06 17:44:01 -04:00
Adam Moussa
fe386ee33f
Add CloudWatch alarm coverage (payments-dashboard) (#51)
Some checks are pending
Deploy / deploy (push) Waiting to run
* 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.
2026-06-17 14:51:59 -04:00
Adam Moussa
fd9a63371d
Repo hygiene: PR labeler + README badges (INFRA-56/57) (#45)
Some checks failed
Deploy / deploy (push) Has been cancelled
- Add callable PR labeler workflow (.github/workflows/labeler.yml)
- Add README status badges (JavaScript, AWS SAM, Slack, CI)

Part of INFRA-47 (INFRA-56, INFRA-57).
2026-06-11 14:13:31 -04:00
Adam Moussa
4b6c973ac7 Update README for Secrets Manager migration
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
2026-06-02 20:39:35 -04:00
Adam Moussa
5330c3f88a
Merge expense-approval-bot into payments-dashboard (#28)
Some checks failed
Deploy / deploy (push) Has been cancelled
* 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
2026-05-12 13:08:42 -04:00
Adam Moussa
df14849dbb Update README with payroll email pipeline documentation 2026-04-30 14:19:06 -04:00
Adam Moussa
c445fa947a Update integration code with correct BoA CashPro API specs and add README
- 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
2026-04-13 16:24:20 -04:00