* 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
* fix(csv): parse M/D/YY dates with loud rejects; stop canceled rows zeroing stored fields (#65, #68)
Stampli exports switched from MM/DD/YYYY to M/D/YY, which toISODate
mangled into garbage ISO strings and slackAppHome's parseMDYLocal turned
into year-1926 dates. Canceled rows also blank 'Amount in USD' and can
blank dates, which the unconditional upsert wrote over previously stored
real values (116 records at amount_usd=0 as of the 2026-07-21
reconciliation).
- src/dates.js: shared strict parser for M/D/YY + MM/DD/YYYY (real-date
validation, 2-digit years 2000-based, plausibility window helper)
- processPaymentCsv: canonicalize send_payment_on to MM/DD/YYYY; reject
rows with unparseable/implausible dates and fail the invocation after
valid rows are processed (surfaces via errors alarm + async DLQ;
retry-safe since upserts are idempotent and existing checks are not
re-offered to BoA); only overwrite amount_usd/send_payment_on with
real values, falling back to the 'Amount' column on cancels
- BoA add_Issue/cancel_Issue now use stored record values when the CSV
row is blank (cancel_Issue previously sent amount 0.00 for voids) and
skip registration on missing date/amount instead of sending garbage;
same guard on the backfill path
- slackAppHome: use the shared parser (fixes 1926 aging)
* fix(csv): validate BoA registration data before writes; harden logging (security review)
Hardening from the /sh-security-review pass on this branch:
- BoA eligibility (resolvable amount + issue date) is now decided and
validated BEFORE the DDB upsert: a cancel-transition row we cannot act
on is rejected with the record untouched, so the transition gate stays
open for a later clean file instead of silently losing the cancel
forever (the skip guard previously ran after the status write)
- First-seen rows that are already canceled are stored but never
offered to add_Issue — registering a voided check as an active issue
created a live issue with no cancel to follow (worsened by the Amount
fallback making the previously-failing call succeed)
- isPlausibleSendYear window is now relative (year-7..year+2) instead
of hardcoded 2020-2035
- Untrusted CSV values are JSON-encoded and truncated before log
interpolation (quoted CSV cells carry newlines -> CloudWatch log-line
forgery, CWE-117)
- OAuth token-exchange failures no longer throw the raw BoA authn
response body (aligns with the PR #63 log-scrub posture)
Verified: unit smoke on the date module; full replay of the real
2026-07-21 export (1,197 rows) against live table state produces 1,197
upserts, 0 rejects, 0 spurious BoA submissions.
* fix(csv): comma-tolerant amount parsing in backfill (PR #72 review)
Number() on a hand-inserted comma-formatted string amount would NaN and
skip the check during backfill; hoist the CSV path's parseAmount to
module scope and share it. No live records are affected (all amount_usd
values are DDB Number type) — defensive consistency.
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.
API tokens and credentials must live in Secrets Manager per
secrets-and-config.md, but the four original payment Lambdas still
read 10 SecureString SSM params. Move them to three grouped secrets
(slack-bot-token plaintext, boa-check-mgmt and boa-reporting as JSON),
matching the pattern the expense Lambdas already use. IAM is scoped to
secretsmanager:GetSecretValue per secret; the VPC Lambdas reach the
public endpoint over the existing NAT path. Test/reissue scripts and
the client-ssm dependency are updated/removed accordingly.
Refs: #3
* Make dashboard sections collapsible and clean up BoA audit log
- Scheduled Checks, Scheduled ACH, and Outstanding Checks sections
default to collapsed with summary line; Expand/Collapse button
toggles detail view via private_metadata state
- Filter backfill failure records from BoA submissions display
- Limit Recent BoA Submissions to 5 most recent entries
* Share Slack home publish pipeline
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
Capture response headers, body, and transactionId on every check-issue
API call; persist as boa_txn#<ts>#<action> records with 90-day TTL.
Add "Recent BoA Submissions" section to App Home showing the last 10
with click-to-copy transactionId.
Motivation: BoA support asked for a transactionId from a past
successful call and we had no way to recover it from CloudWatch
summary logs alone.
Also adds simulate-csv.cjs (dry-run preview) and stampli-uploader.sh
(launchd-invoked S3 uploader), and gitignores debug artifacts.
- Scheduled section split into Scheduled Checks and Scheduled ACH
- Remove Cleared section from dashboard
- Outstanding renamed to Outstanding Checks with <=90 day and >90 day totals
- processPaymentCsv: switch from BatchWrite to upsert, auto-mark ACH
payments as Cleared when send date has passed
- slackAppHome: categorize payments into Scheduled/Outstanding/Cleared
sections using unified status field
- Add seed scripts for bulk-loading Stampli and bank CSV exports
- VPC with NAT Gateway for static outbound IP (52.86.95.107) for BoA API
- Payments stored as individual DynamoDB items keyed by check number
- Slack bot token fetched from SSM at runtime instead of CF parameter
- Slack Lambda scans payment items instead of reading single blob
Two Lambda functions:
- processPaymentCsv: S3 trigger, parses CSV, stores dashboard in DynamoDB
- slackAppHome: API Gateway endpoint for Slack events, publishes Home tab view