Commit graph

6 commits

Author SHA1 Message Date
6f32f3bfcd
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.
2026-09-16 13:41:47 -04:00
seahaven-openswe[bot]
bf235e70d7
feat: add cash-position tile from boa_balance snapshots to App Home (#80)
Some checks are pending
Deploy / deploy (push) Waiting to run
* feat: add cash-position tile from boa_balance snapshots to App Home

Read the latest previous-day boa_balance snapshot (authoritative)
to display current ledger and available balance on the App Home
summary bar. Optionally surfaces today's intraday snapshot as
provisional. Walks backward up to 14 days to handle weekend/holiday
gaps, using targeted GetItem by computed key instead of a scan.

Refs: #77

* fix: null-guard cash-position tile, add error logging, drop UTC date skew and serial GetItems

- Null-guard current_ledger on the authoritative cash-position tile so a
  missing ledger renders 'Not available' instead of the false '/bin/bash.00'
- Log GetCommand failures with console.error + logSafe instead of silent
  catch, matching the rest of the codebase
- Derive dates from local midnight instead of toISOString UTC, so the
  current-day lookup doesn't miss from 8pm ET to midnight
- Fetch all 14 balance keys in parallel with Promise.allSettled instead of
  14 serial GetCommands; start previous-day walk at i=1 since today's
  previous-day snapshot can't exist until tomorrow
- Use Item.as_of_date directly instead of a synthetic _asOfDate field
- Remove _asOfDate from test fixtures; add test for the null-ledger guard

---------

Co-authored-by: amoussa1229 <166072409+amoussa1229@users.noreply.github.com>
2026-07-22 18:41:32 -04: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