Commit graph

16 commits

Author SHA1 Message Date
a49b91a25f
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.
2026-07-21 20:30:03 -04:00
Adam Moussa
77fb8e4ad0
fix(csv): M/D/YY date parsing with loud rejects; stop canceled rows zeroing stored fields (#72)
Some checks are pending
Deploy / deploy (push) Waiting to run
* 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.
2026-07-22 00:28:13 +00: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
90accf2f39 Migrate secrets from SSM to Secrets Manager
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
2026-06-02 20:29:18 -04:00
Adam Moussa
491d29de1e
Fix BoA CashPro batch limit for large CSV exports (#21)
* Fix BoA CashPro API rejection when CSV has >100 checks

BoA check-issues endpoint has a 100-item limit per call. Large Stampli
exports were failing with error 10102. Batch submissions into chunks of
100 for both add_Issue and cancel_Issue actions.

* Add backfill handler and filter invalid check numbers

- Add backfill mode (event.backfill=true) that scans DDB for checks not
  yet submitted to BoA and submits them in batches, handling duplicates
  gracefully by retrying without already-submitted items
- Skip check numbers > 10 digits (BoA rejects them with 10092)

* Handle non-JSON BoA backfill errors

Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>

* Handle numeric BoA duplicate statuses

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>
2026-05-07 20:44:14 -04:00
Adam Moussa
b242df15b5 Log BoA submissions to DDB and surface in Slack App Home
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.
2026-04-20 17:40:55 -04:00
Adam Moussa
c849280705 Improve BoA API error logging and update .gitignore
Read response as text before JSON parsing to capture non-JSON error
responses from BoA API. Add .DS_Store, BofA API Resources, and CSV
files to .gitignore.
2026-04-14 20:15:41 -04:00
Adam Moussa
ffd46c4bda Fix BoA transaction matching, add status progression protection, enable daily schedule
- Fix transaction code mapping (475=Cleared, 255=Returned) in fetchBoaTransactions
- Match on customerReference instead of bankReference for check number matching
- Add bank-confirmed protection: CSV cannot override status once bank confirms Cleared
- Add status progression guard: CSV cannot regress status backward in lifecycle
- Cleared status is permanent — cannot be voided, cancelled, or changed
- Enable daily fetchBoaTransactions schedule (9am ET weekdays)
- Add production test script and dry run simulation script
2026-04-14 17:43:13 -04:00
Adam Moussa
d065b320eb Merge master into feature/boa-api-integration
Brings in dashboard UI improvements (drill-down modals, redesign,
ACH reference labels) while preserving BoA OAuth integration code.
2026-04-13 16:28:17 -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
Adam Moussa
3b8c201444 Add ACH auto-clear and expanded cancel status list
ACH payments auto-marked as Cleared when send date has passed.
Cancel statuses expanded to include "canceled" and "marked as void".
2026-04-10 17:31:27 -04:00
Adam Moussa
c43228831d Add unified payment status, ACH auto-clear, and categorized dashboard
- 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
2026-04-10 17:23:54 -04:00
Adam Moussa
33cd9759b4 Add CashPro check issue/cancel on CSV upload
- CSV processor detects new checks and submits add_issue to CashPro
- Checks updated to voided/cancelled trigger cancel_issue to CashPro
- Added SSM params for boa-api-token, boa-account-number, boa-company-id
  to both processPaymentCsv and fetchBoaTransactions Lambdas
- Increased CSV processor timeout to 120s for API calls
2026-04-09 15:14:51 -04:00
Adam Moussa
44a6cd1b63 Add BoA CashPro integration and payment status tracking
- New fetchBoaTransactions Lambda: daily 9am ET schedule (disabled until API key set)
  Calls CashPro Previous Day Transaction Inquiry, filters for codes 255/475,
  matches bankReference to check_number, updates clear_status in DynamoDB
- CSV processor switched to UpdateCommand to preserve clearing data on re-upload
- Slack dashboard now shows Scheduled, Outstanding, Cleared, and Returned sections
- ACH payments auto-assumed cleared once past due date
2026-04-09 14:59:39 -04:00
Adam Moussa
7150028bd3 Add VPC/NAT for static IP, dedup payments by check number, SSM token
- 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
2026-04-09 14:27:34 -04:00
Adam Moussa
f1c2063f52 Initial SAM stack: payments CSV to Slack App Home
Two Lambda functions:
- processPaymentCsv: S3 trigger, parses CSV, stores dashboard in DynamoDB
- slackAppHome: API Gateway endpoint for Slack events, publishes Home tab view
2026-04-09 13:29:28 -04:00