mirror of
https://github.com/Sea-Haven-Industries/payments-dashboard.git
synced 2026-09-30 08:53:12 +00:00
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
This commit is contained in:
parent
d8a2412d75
commit
ffab1c8f7b
9 changed files with 4892 additions and 30 deletions
|
|
@ -11,7 +11,7 @@ AWS SAM application that ingests payment CSVs, syncs check data with Bank of Ame
|
|||
|
||||
- **ProcessPayrollEmail** — Lambda triggered by S3 (inbound email) and SQS (batch timer). SES receives Gusto payroll emails at `payroll@int.seahaven.com`, stores them to S3, and this Lambda parses the email body, extracts financial data, and posts a combined Slack notification (employee payroll + contractor payments) after a 10-minute batching window. Runs outside VPC.
|
||||
- **ProcessPaymentCsv** — Lambda triggered by S3 CSV upload. Parses Stampli payment exports, upserts to DynamoDB, and submits new/cancelled checks to the CashPro Check Management API.
|
||||
- **FetchBoaTransactions** — Scheduled Lambda (weekdays 9am ET). Calls the CashPro Previous Day Transaction Inquiry API, classifies each transaction (paid checks, ARP refer-to-maker and return-of-posted-check credits, electronic returns), and reconciles them onto DynamoDB payment records. See [Bank reconciliation](#bank-reconciliation-fetchboatransactions).
|
||||
- **FetchBoaTransactions** — Scheduled Lambda. Weekdays 9am ET it calls the CashPro **previous-day** Transaction Inquiry (authoritative sweep, trailing 7 days); weekdays at 16:00/19:00/22:00 UTC (~12/3/6pm ET, fixed-UTC so it drifts an hour in winter) it calls the **current-day** inquiry for same-day visibility (EventBridge `Input: {"endpoint":"current-day"}`, today-only, staleness sweep skipped). Every run archives the exact raw response to the `seahaven-payments-boa-raw-*` bucket (`raw/<endpoint>/<fromDate>_<toDate>/<runAt>.json`, SSE-S3, 730-day lifecycle, PutObject-only grant; Retain-protected — decommission goes through the CFN decommission runbook) and upserts per-date `boa_balance#<asOfDate>#<endpoint>` snapshots (latest-wins on `run_at`, no TTL) from the Summary rows. Classifies each transaction and reconciles onto DynamoDB payment records. Event payload: `{fromDate?, toDate?, endpoint?}` (endpoint allowlisted and validated; unknown fields ignored). Intraday runs are disable-able as a unit via the `IntradaySchedule` rule. See [Bank reconciliation](#bank-reconciliation-fetchboatransactions).
|
||||
- **SlackAppHome** — Lambda behind API Gateway (`POST /slack/events`). Verifies the Slack signing secret (HMAC-SHA256, 5-minute replay window) before processing, then renders the payments dashboard on the Slack App Home tab with outstanding aging buckets and drill-down modals.
|
||||
|
||||
- **ExpenseReceiver** — Lambda behind API Gateway (`POST /slack/expense-events`). Verifies the Slack signing secret (HMAC-SHA256), handles URL verification challenges, and async-invokes ExpenseProcessor. Runs outside VPC.
|
||||
|
|
@ -136,6 +136,8 @@ The `PaymentsDashboard` DynamoDB table (`AWS::DynamoDB::Table`, `TableName: Paym
|
|||
|
||||
The table is imported by name, so there is no compile-time link between the stacks: any change to the table name, `pk` format, these attribute names, the encryption key, or the table's lifecycle policy will silently break the Bedrock agent at runtime. Coordinate such changes with `seahaven-slack-bot` before shipping (INFRA-138).
|
||||
|
||||
**Key prefixes in this table** (all owned by this stack): `payment#<check_number>` (payment records), `metadata` (ingest metadata), `boa_txn#<ts>#<action>` (BoA submission journal, 90d TTL), `boa_recon#<from>_<to>#<runAt>` (reconciliation run summaries, 90d TTL), `boa_balance#<asOfDate>#<endpoint>` (daily balance snapshots, latest-wins, no TTL), `PAYROLL_*` (payroll pipeline, 90d TTL). New prefixes are invisible to `seahaven-slack-bot`'s `begins_with(pk, "payment#")` scan — no consumer coordination needed when adding one.
|
||||
|
||||
## Monitoring & Alarms
|
||||
|
||||
All CloudWatch alarms publish to the shared `site-alerts` SNS topic (`arn:aws:sns:us-east-1:328440206208:site-alerts`). Alarms are ALARM-only by convention (no OK/recovery action) and treat missing data as `notBreaching`. Each alarm evaluates a single 5-minute period.
|
||||
|
|
|
|||
|
|
@ -433,7 +433,7 @@ const RETURN_EVENTS = new Set(["check_return", "electronic_return", "ach_return"
|
|||
// paid debit against a canceled record (a voided check the bank paid —
|
||||
// expected without Positive Pay), the cancel status is likewise preserved:
|
||||
// the ARP return that follows lands as terminal voided-and-bounced.
|
||||
export function applyEvent(payment, classified, eventDateISO) {
|
||||
export function applyEvent(payment, classified, eventDateISO, via = null) {
|
||||
const { event, amount, bankReference } = classified;
|
||||
const canceled = isCancelStatus(payment.status);
|
||||
const historyEvent = {
|
||||
|
|
@ -441,6 +441,9 @@ export function applyEvent(payment, classified, eventDateISO) {
|
|||
date: eventDateISO,
|
||||
bankRef: bankReference,
|
||||
amount,
|
||||
// Audit tag only (which endpoint applied this): deliberately excluded
|
||||
// from the replay-idempotence identity below.
|
||||
...(via ? { via } : {}),
|
||||
};
|
||||
|
||||
// Replay idempotence: event identity is {event, date, amount} — bankRef
|
||||
|
|
@ -520,9 +523,27 @@ export function isValidISODate(s) {
|
|||
// days are safe — event identity makes replays idempotent. Strings are
|
||||
// validated strictly so a malformed payload fails loudly instead of
|
||||
// querying a garbage range.
|
||||
export function resolveDateRange(event, now = new Date()) {
|
||||
// Endpoint allowlist: the value is interpolated into the request URL, so
|
||||
// anything outside the two known inquiry endpoints throws loudly (path
|
||||
// injection guard; also catches typo'd EventBridge Input).
|
||||
const ENDPOINTS = new Set(["previous-day", "current-day"]);
|
||||
export function resolveEndpoint(event) {
|
||||
const endpoint = event?.endpoint ?? "previous-day";
|
||||
if (!ENDPOINTS.has(endpoint)) {
|
||||
throw new Error(`endpoint must be one of ${[...ENDPOINTS].join(", ")}: ${logSafe(endpoint)}`);
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
export function resolveDateRange(event, now = new Date(), endpoint = "previous-day") {
|
||||
const hasFrom = event?.fromDate != null;
|
||||
const hasTo = event?.toDate != null;
|
||||
if (!hasFrom && !hasTo && endpoint === "current-day") {
|
||||
// Intraday default: today only. Explicit ranges are allowed (the API
|
||||
// accepts them — verified 2026-07-22) but the schedule never sends one.
|
||||
const today = now.toISOString().split("T")[0];
|
||||
return { fromDate: today, toDate: today };
|
||||
}
|
||||
if (!hasFrom && !hasTo) {
|
||||
// Trailing 7 days ending yesterday: self-healing across missed runs,
|
||||
// outages, and holiday gaps — replays are idempotent (event identity
|
||||
|
|
@ -614,3 +635,61 @@ export function buildEventUpdate(tableName, payment, applied) {
|
|||
ExpressionAttributeValues: values,
|
||||
};
|
||||
}
|
||||
|
||||
// Balance/summary extraction (#improvement-plan PR 2). Summary rows carry
|
||||
// the account's balance and total lines per asOfDate; one output object per
|
||||
// date so multi-day replay windows produce one snapshot each. Unrecognized
|
||||
// summary codes land in `other` — never dropped.
|
||||
const BALANCE_FIELDS = {
|
||||
"010": "opening_ledger",
|
||||
"030": "current_ledger",
|
||||
"040": "opening_available",
|
||||
"060": "current_available",
|
||||
"072": "float_one_day",
|
||||
"074": "float_two_day",
|
||||
"100": "total_credits",
|
||||
"400": "total_debits",
|
||||
"450": "total_ach_debits",
|
||||
};
|
||||
const BALANCE_COUNT_FIELDS = new Set(["100", "400", "450"]);
|
||||
|
||||
// Caps: a well-formed response has ~46 summary codes per date and windows
|
||||
// are at most a month; anything beyond is a malformed/hostile feed and must
|
||||
// not balloon DDB items or write counts (counted, never silent).
|
||||
const BALANCE_OTHER_CAP = 50;
|
||||
const BALANCE_DATES_CAP = 31;
|
||||
|
||||
export function extractBalances(transactions) {
|
||||
const byDate = new Map();
|
||||
let datesTruncated = 0;
|
||||
for (const txn of transactions ?? []) {
|
||||
if (String(txn?.transactionType ?? "").trim().toLowerCase() !== "summary") continue;
|
||||
const date = String(txn.asOfDate ?? "").trim();
|
||||
if (!isValidISODate(date)) continue;
|
||||
if (!byDate.has(date)) {
|
||||
if (byDate.size >= BALANCE_DATES_CAP) {
|
||||
datesTruncated++;
|
||||
continue;
|
||||
}
|
||||
byDate.set(date, { as_of_date: date, other: {} });
|
||||
}
|
||||
const snap = byDate.get(date);
|
||||
const code = String(txn.transactionCode ?? "").trim();
|
||||
const amount = parseAmount(txn.amount);
|
||||
if (!Number.isFinite(amount)) continue; // "Infinity" survives parseFloat; unmarshallable
|
||||
const field = BALANCE_FIELDS[Object.hasOwn(BALANCE_FIELDS, code) ? code : ""];
|
||||
if (field) {
|
||||
snap[field] = amount;
|
||||
if (BALANCE_COUNT_FIELDS.has(code) && txn.itemCount != null) {
|
||||
const n = Number(txn.itemCount);
|
||||
snap[`${field}_count`] = Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
} else if (/^[0-9]{1,16}$/.test(code)) {
|
||||
if (Object.keys(snap.other).length < BALANCE_OTHER_CAP) snap.other[code] = amount;
|
||||
else snap.other_truncated = (snap.other_truncated || 0) + 1;
|
||||
}
|
||||
}
|
||||
const out = [...byDate.values()];
|
||||
if (datesTruncated) out.forEach((s) => (s.dates_truncated = datesTruncated));
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
||||
import { DynamoDBDocumentClient, GetCommand, PutCommand, ScanCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb";
|
||||
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
|
||||
|
|
@ -5,20 +6,24 @@ import {
|
|||
applyEvent,
|
||||
buildEventUpdate,
|
||||
classifyTransaction,
|
||||
extractBalances,
|
||||
isValidISODate,
|
||||
logSafe,
|
||||
matchAchTransaction,
|
||||
matchCheckTransaction,
|
||||
matchElectronicReturn,
|
||||
resolveDateRange,
|
||||
resolveEndpoint,
|
||||
sweepStalePayments,
|
||||
} from "./boaRecon.js";
|
||||
import { toISODate } from "./dates.js";
|
||||
|
||||
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient());
|
||||
const secrets = new SecretsManagerClient();
|
||||
const s3 = new S3Client();
|
||||
const TABLE_NAME = process.env.TABLE_NAME;
|
||||
const BOA_BASE_URL = process.env.BOA_BASE_URL;
|
||||
const BOA_RAW_BUCKET = process.env.BOA_RAW_BUCKET;
|
||||
|
||||
const UNMATCHED_LIST_CAP = 50;
|
||||
const UNKNOWN_CODES_CAP = 20;
|
||||
|
|
@ -57,10 +62,13 @@ async function getReportingCreds() {
|
|||
}
|
||||
|
||||
// Parse a BoA response body without ever surfacing a body snippet — a
|
||||
// malformed 200 must not leak account data into logs/errors.
|
||||
// malformed 200 must not leak account data into logs/errors. Returns the
|
||||
// exact raw body text alongside the parse so the archive stores the bytes
|
||||
// the bank actually sent.
|
||||
async function parseJsonResponse(res, label) {
|
||||
const rawBody = await res.text();
|
||||
try {
|
||||
return await res.json();
|
||||
return { data: JSON.parse(rawBody), rawBody };
|
||||
} catch {
|
||||
throw new Error(`BoA ${label} response not JSON: HTTP ${res.status}`);
|
||||
}
|
||||
|
|
@ -80,23 +88,25 @@ async function getAccessToken(applicationID, clientId, clientSecret) {
|
|||
throw new Error(`OAuth token exchange failed: HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
const data = await parseJsonResponse(res, "authn");
|
||||
const { data } = await parseJsonResponse(res, "authn");
|
||||
return data.access_token;
|
||||
}
|
||||
|
||||
const directionRank = (d) => (d === "debit" ? 0 : d === "credit" ? 1 : 2);
|
||||
|
||||
export const handler = async (event) => {
|
||||
// Optional replay payload {fromDate, toDate}; default is the trailing
|
||||
// 7-day window ending yesterday (self-healing across missed runs).
|
||||
const { fromDate, toDate } = resolveDateRange(event ?? {});
|
||||
// Optional payload {fromDate, toDate, endpoint}; endpoint defaults to
|
||||
// previous-day (the authoritative sweep). The intraday schedule passes
|
||||
// {"endpoint":"current-day"} and defaults to today..today; previous-day
|
||||
// defaults to the trailing 7-day window ending yesterday (self-healing).
|
||||
const endpoint = resolveEndpoint(event ?? {});
|
||||
const { fromDate, toDate } = resolveDateRange(event ?? {}, new Date(), endpoint);
|
||||
|
||||
const { appId, clientId, token: clientSecret, accountNumber, bankId } = await getReportingCreds();
|
||||
|
||||
const bearerToken = await getAccessToken(appId, clientId, clientSecret);
|
||||
|
||||
// Call CashPro Previous Day Transaction Inquiry
|
||||
const res = await fetch(`${BOA_BASE_URL}/cashpro/reporting/v1/transaction-inquiries/previous-day`, {
|
||||
const res = await fetch(`${BOA_BASE_URL}/cashpro/reporting/v1/transaction-inquiries/${endpoint}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -113,7 +123,35 @@ export const handler = async (event) => {
|
|||
throw new Error(`BoA API error: HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
const data = await parseJsonResponse(res, "reporting");
|
||||
// Archive the exact response bytes BEFORE parsing or classification: even
|
||||
// a malformed-200 (which throws below) or a crash mid-reconciliation
|
||||
// leaves the raw feed recoverable/replayable. runAt is minted at response
|
||||
// receipt so the balance latest-wins comparison orders by data freshness,
|
||||
// not invocation start (overlapping runs). Archive failure never fails
|
||||
// the reconciliation — flagged on the run summary.
|
||||
const rawBody = await res.text();
|
||||
const runAt = new Date().toISOString();
|
||||
let archiveError = false;
|
||||
try {
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: BOA_RAW_BUCKET,
|
||||
Key: `raw/${endpoint}/${fromDate}_${toDate}/${runAt}.json`,
|
||||
Body: rawBody,
|
||||
ContentType: "application/json",
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
archiveError = true;
|
||||
console.error(`Raw archive put failed: ${logSafe(err?.name)} ${logSafe(err?.message)}`);
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(rawBody);
|
||||
} catch {
|
||||
throw new Error(`BoA reporting response not JSON: HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
// Response shape: { accountTransactions: [{ accountNumber, bankId, currency, transactions: [...] }] }
|
||||
const allTransactions = (data.accountTransactions || []).flatMap(
|
||||
|
|
@ -242,7 +280,7 @@ export const handler = async (event) => {
|
|||
|
||||
summary.matched++;
|
||||
const target = match.payment;
|
||||
let applied = applyEvent(target, classified, eventDate);
|
||||
let applied = applyEvent(target, classified, eventDate, endpoint);
|
||||
if (applied.kind === "noop") {
|
||||
summary.already_applied++;
|
||||
continue;
|
||||
|
|
@ -267,7 +305,7 @@ export const handler = async (event) => {
|
|||
// payments array) and re-derive the transition.
|
||||
for (const k of Object.keys(target)) delete target[k];
|
||||
Object.assign(target, fresh);
|
||||
applied = applyEvent(target, classified, eventDate);
|
||||
applied = applyEvent(target, classified, eventDate, endpoint);
|
||||
if (applied.kind === "noop") {
|
||||
outcome = "noop";
|
||||
break;
|
||||
|
|
@ -302,25 +340,59 @@ export const handler = async (event) => {
|
|||
);
|
||||
}
|
||||
|
||||
// Staleness sweep: payments the bank has never confirmed (#69).
|
||||
const todayISO = new Date().toISOString().split("T")[0];
|
||||
const stale = sweepStalePayments(payments, todayISO);
|
||||
if (stale.staleAchCount) {
|
||||
console.error(
|
||||
`Stale ACH (no bank settlement, sent > 16 days ago): ${stale.staleAchCount} records, ` +
|
||||
`checks=${logSafe(stale.staleAch.map((s) => s.check_number).join(","))}`
|
||||
);
|
||||
// Staleness sweep: payments the bank has never confirmed (#69). Gated to
|
||||
// the authoritative previous-day runs — intraday runs would re-alert the
|
||||
// same backlog several times a day with no new information.
|
||||
let stale = null;
|
||||
if (endpoint === "previous-day") {
|
||||
const todayISO = new Date().toISOString().split("T")[0];
|
||||
stale = sweepStalePayments(payments, todayISO);
|
||||
if (stale.staleAchCount) {
|
||||
console.error(
|
||||
`Stale ACH (no bank settlement, sent > 16 days ago): ${stale.staleAchCount} records, ` +
|
||||
`checks=${logSafe(stale.staleAch.map((s) => s.check_number).join(","))}`
|
||||
);
|
||||
}
|
||||
if (stale.staleChecksCount) {
|
||||
console.error(
|
||||
`Stale checks (no bank activity, issued > 60 days ago): ${stale.staleChecksCount} records`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (stale.staleChecksCount) {
|
||||
console.error(
|
||||
`Stale checks (no bank activity, issued > 60 days ago): ${stale.staleChecksCount} records`
|
||||
);
|
||||
|
||||
// Balance snapshots: one boa_balance#<asOfDate>#<endpoint> item per date,
|
||||
// latest-wins (run_at conditioned so a stale replay never clobbers fresher
|
||||
// data). Intraday snapshots are provisional and keyed separately from the
|
||||
// authoritative previous-day figures. Write failures are informational.
|
||||
const balances = extractBalances(allTransactions);
|
||||
let balanceWriteErrors = 0;
|
||||
const balanceDatesWritten = [];
|
||||
for (const snap of balances) {
|
||||
try {
|
||||
await ddb.send(
|
||||
new PutCommand({
|
||||
TableName: TABLE_NAME,
|
||||
Item: {
|
||||
pk: `boa_balance#${snap.as_of_date}#${endpoint}`,
|
||||
endpoint,
|
||||
run_at: runAt,
|
||||
...snap,
|
||||
},
|
||||
ConditionExpression: "attribute_not_exists(run_at) OR run_at <= :runAt",
|
||||
ExpressionAttributeValues: { ":runAt": runAt },
|
||||
})
|
||||
);
|
||||
balanceDatesWritten.push(snap.as_of_date);
|
||||
} catch (err) {
|
||||
if (err?.name === "ConditionalCheckFailedException") continue; // stale replay, fresher data kept
|
||||
balanceWriteErrors++;
|
||||
console.error(`Balance write failed for ${logSafe(snap.as_of_date)}: ${logSafe(err?.name)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Run summary record for auditing/alerting (Slack wiring is #71). The pk
|
||||
// is append-only (run timestamp suffix) so re-runs never overwrite a
|
||||
// prior run's record.
|
||||
const runAt = new Date().toISOString();
|
||||
const runKey = `boa_recon#${fromDate}_${toDate}#${runAt}`;
|
||||
await ddb.send(
|
||||
new PutCommand({
|
||||
|
|
@ -330,6 +402,10 @@ export const handler = async (event) => {
|
|||
run_at: runAt,
|
||||
from_date: fromDate,
|
||||
to_date: toDate,
|
||||
endpoint,
|
||||
archive_error: archiveError,
|
||||
balance_write_errors: balanceWriteErrors,
|
||||
balance_dates_written: balanceDatesWritten,
|
||||
transactions_seen: summary.transactions_seen,
|
||||
classified: summary.classified,
|
||||
matched: summary.matched,
|
||||
|
|
@ -342,9 +418,13 @@ export const handler = async (event) => {
|
|||
unmatched: summary.unmatched,
|
||||
unknown_count: summary.unknown_count,
|
||||
unknown_codes: summary.unknown_codes,
|
||||
stale_ach: stale.staleAch,
|
||||
stale_ach_count: stale.staleAchCount,
|
||||
stale_checks_count: stale.staleChecksCount,
|
||||
...(stale
|
||||
? {
|
||||
stale_ach: stale.staleAch,
|
||||
stale_ach_count: stale.staleAchCount,
|
||||
stale_checks_count: stale.staleChecksCount,
|
||||
}
|
||||
: {}),
|
||||
ttl: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60,
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -168,6 +168,58 @@ Resources:
|
|||
BlockPublicPolicy: true
|
||||
RestrictPublicBuckets: true
|
||||
|
||||
# Raw archive of every BoA reporting API response (exact bytes, keyed
|
||||
# raw/<endpoint>/<fromDate>_<toDate>/<runAt>.json). Replayable corpus for
|
||||
# parser changes + audit trail. Retain: a template revert must never
|
||||
# attempt to delete a bank-data bucket; decommission goes through the CFN
|
||||
# decommission runbook (inventory, purge, deliberate deletion).
|
||||
# Retain + fixed name = rollback-orphan hazard (same class as the RETAIN
|
||||
# secret deadlock): if a failed deploy orphans the bucket, ADOPT it back
|
||||
# with a CloudFormation resource import — never delete-and-recreate.
|
||||
BoaRawBucket:
|
||||
Type: AWS::S3::Bucket
|
||||
DeletionPolicy: Retain
|
||||
UpdateReplacePolicy: Retain
|
||||
Properties:
|
||||
BucketName: !Sub seahaven-payments-boa-raw-${AWS::AccountId}
|
||||
PublicAccessBlockConfiguration:
|
||||
BlockPublicAcls: true
|
||||
IgnorePublicAcls: true
|
||||
BlockPublicPolicy: true
|
||||
RestrictPublicBuckets: true
|
||||
BucketEncryption:
|
||||
ServerSideEncryptionConfiguration:
|
||||
- ServerSideEncryptionByDefault:
|
||||
SSEAlgorithm: AES256
|
||||
LifecycleConfiguration:
|
||||
Rules:
|
||||
- Id: expire-raw-responses
|
||||
Status: Enabled
|
||||
ExpirationInDays: 730
|
||||
|
||||
# Defense-in-depth for bank data: deny any non-TLS access. No Allow
|
||||
# statements — access is IAM-only (the Lambda's PutObject grant); unlike
|
||||
# PayrollEmailBucket there is no cross-service principal here. The Deny
|
||||
# covers bucket-level actions too, which is safe because nothing is
|
||||
# granted List/Get — revisit if read access is ever added.
|
||||
BoaRawBucketPolicy:
|
||||
Type: AWS::S3::BucketPolicy
|
||||
Properties:
|
||||
Bucket: !Ref BoaRawBucket
|
||||
PolicyDocument:
|
||||
Version: "2012-10-17"
|
||||
Statement:
|
||||
- Sid: DenyInsecureTransport
|
||||
Effect: Deny
|
||||
Principal: "*"
|
||||
Action: s3:*
|
||||
Resource:
|
||||
- !GetAtt BoaRawBucket.Arn
|
||||
- !Sub "${BoaRawBucket.Arn}/*"
|
||||
Condition:
|
||||
Bool:
|
||||
aws:SecureTransport: "false"
|
||||
|
||||
DashboardTable:
|
||||
Type: AWS::DynamoDB::Table
|
||||
Properties:
|
||||
|
|
@ -1015,6 +1067,7 @@ Resources:
|
|||
Variables:
|
||||
BOA_BASE_URL: https://api.bofa.com
|
||||
BOA_REPORTING_SECRET_NAME: payments-dashboard/boa-reporting
|
||||
BOA_RAW_BUCKET: !Ref BoaRawBucket
|
||||
Events:
|
||||
DailySchedule:
|
||||
Type: Schedule
|
||||
|
|
@ -1022,9 +1075,28 @@ Resources:
|
|||
Schedule: cron(0 13 ? * MON-FRI *)
|
||||
Description: Fetch BoA previous day transactions at 9am ET (13:00 UTC)
|
||||
Enabled: true
|
||||
# One rule for all intraday runs so they can be disabled as a unit
|
||||
# (aws events disable-rule) without touching the authoritative 9am
|
||||
# previous-day sweep. Fixed UTC: ~12/3/6pm ET in DST, 11/2/5pm in
|
||||
# winter (accepted drift, documented in README).
|
||||
IntradaySchedule:
|
||||
Type: Schedule
|
||||
Properties:
|
||||
Schedule: cron(0 16,19,22 ? * MON-FRI *)
|
||||
Description: Intraday BoA current-day sweep (~12pm/3pm/6pm ET)
|
||||
Enabled: true
|
||||
Input: '{"endpoint":"current-day"}'
|
||||
Policies:
|
||||
- DynamoDBCrudPolicy:
|
||||
TableName: !Ref DashboardTable
|
||||
- Version: "2012-10-17"
|
||||
Statement:
|
||||
# Archive writes only: no read, no list, no other principal.
|
||||
# Derived from the bucket resource so a rename can't silently
|
||||
# detach the grant.
|
||||
- Effect: Allow
|
||||
Action: s3:PutObject
|
||||
Resource: !Sub "${BoaRawBucket.Arn}/*"
|
||||
- Version: "2012-10-17"
|
||||
Statement:
|
||||
- Effect: Allow
|
||||
|
|
|
|||
142
tests/boaReconIntraday.test.js
Normal file
142
tests/boaReconIntraday.test.js
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import {
|
||||
classifyTransaction,
|
||||
extractBalances,
|
||||
resolveDateRange,
|
||||
resolveEndpoint,
|
||||
} from "../src/boaRecon.js";
|
||||
|
||||
const fixture = (name) =>
|
||||
JSON.parse(readFileSync(new URL(`./fixtures/${name}.json`, import.meta.url)))
|
||||
.accountTransactions[0].transactions;
|
||||
|
||||
const histogram = (txns) => {
|
||||
const h = {};
|
||||
for (const t of txns) {
|
||||
const e = classifyTransaction(t).event;
|
||||
h[e] = (h[e] || 0) + 1;
|
||||
}
|
||||
return h;
|
||||
};
|
||||
|
||||
// --- resolveEndpoint ---
|
||||
|
||||
test("resolveEndpoint: defaults to previous-day; accepts current-day", () => {
|
||||
assert.equal(resolveEndpoint({}), "previous-day");
|
||||
assert.equal(resolveEndpoint(undefined), "previous-day");
|
||||
assert.equal(resolveEndpoint({ endpoint: "current-day" }), "current-day");
|
||||
});
|
||||
|
||||
test("resolveEndpoint: rejects anything else (URL injection guard)", () => {
|
||||
for (const bad of ["previous-day/../admin", "current", "", "PREVIOUS-DAY", 42]) {
|
||||
assert.throws(() => resolveEndpoint({ endpoint: bad }), /endpoint must be one of/);
|
||||
}
|
||||
});
|
||||
|
||||
// --- endpoint-aware resolveDateRange ---
|
||||
|
||||
test("resolveDateRange: current-day default pins today..today", () => {
|
||||
const r = resolveDateRange({}, new Date("2026-07-22T16:00:00Z"), "current-day");
|
||||
assert.deepEqual(r, { fromDate: "2026-07-22", toDate: "2026-07-22" });
|
||||
});
|
||||
|
||||
test("resolveDateRange: current-day explicit range passes through (API accepts ranges)", () => {
|
||||
const r = resolveDateRange({ fromDate: "2026-07-21", toDate: "2026-07-22" }, new Date(), "current-day");
|
||||
assert.deepEqual(r, { fromDate: "2026-07-21", toDate: "2026-07-22" });
|
||||
});
|
||||
|
||||
test("resolveDateRange: previous-day default unchanged by endpoint param", () => {
|
||||
const r = resolveDateRange({}, new Date("2026-07-21T13:00:00Z"), "previous-day");
|
||||
assert.deepEqual(r, { fromDate: "2026-07-14", toDate: "2026-07-20" });
|
||||
});
|
||||
|
||||
// --- extractBalances ---
|
||||
|
||||
test("extractBalances: current-day fixture yields one snapshot with named fields", () => {
|
||||
const snaps = extractBalances(fixture("current-day"));
|
||||
assert.equal(snaps.length, 1);
|
||||
const s = snaps[0];
|
||||
assert.equal(s.as_of_date, "2026-07-22");
|
||||
assert.equal(s.opening_ledger, 209541.48);
|
||||
assert.equal(s.current_ledger, 203890.83);
|
||||
assert.equal(s.opening_available, 209541.48);
|
||||
assert.equal(s.current_available, 203890.83);
|
||||
assert.equal(s.total_credits, 15000);
|
||||
assert.equal(s.total_credits_count, 1);
|
||||
assert.equal(s.total_debits, 20650.65);
|
||||
assert.equal(s.total_debits_count, 5);
|
||||
assert.equal(s.total_ach_debits, 20650.65);
|
||||
// Unrecognized summary codes captured, never dropped.
|
||||
assert.ok(Object.keys(s.other).length > 0);
|
||||
assert.equal(s.other["200"], 15000);
|
||||
});
|
||||
|
||||
test("extractBalances: multi-day window groups one snapshot per asOfDate", () => {
|
||||
const snaps = extractBalances(fixture("previous-day-week"));
|
||||
const dates = snaps.map((s) => s.as_of_date).sort();
|
||||
assert.deepEqual(dates, ["2026-07-15", "2026-07-16", "2026-07-17", "2026-07-20", "2026-07-21"]);
|
||||
for (const s of snaps) assert.ok(typeof s.opening_ledger === "number");
|
||||
});
|
||||
|
||||
test("extractBalances: Detail rows and garbage dates are excluded", () => {
|
||||
const snaps = extractBalances([
|
||||
{ transactionType: "Detail", transactionCode: "010", asOfDate: "2026-07-22", amount: "5" },
|
||||
{ transactionType: "Summary", transactionCode: "010", asOfDate: "garbage", amount: "5" },
|
||||
]);
|
||||
assert.deepEqual(snaps, []);
|
||||
});
|
||||
|
||||
// --- fixture-driven classification histograms (real captured responses) ---
|
||||
|
||||
test("fixture: previous-day returns window classifies exactly as the live replay did", () => {
|
||||
const h = histogram(fixture("previous-day-returns"));
|
||||
assert.deepEqual(h, {
|
||||
summary: 100,
|
||||
ignored: 3,
|
||||
check_paid: 32,
|
||||
check_return: 10,
|
||||
electronic_return: 4,
|
||||
ach_debit: 4,
|
||||
});
|
||||
});
|
||||
|
||||
test("fixture: quiet previous-day week has zero unknowns and full summary coverage", () => {
|
||||
const h = histogram(fixture("previous-day-week"));
|
||||
assert.equal(h.unknown ?? 0, 0);
|
||||
assert.equal(h.summary, 250);
|
||||
assert.equal(Object.values(h).reduce((a, b) => a + b, 0), 259);
|
||||
});
|
||||
|
||||
test("fixture: current-day classifies the five live ACH settlements", () => {
|
||||
const h = histogram(fixture("current-day"));
|
||||
assert.equal(h.ach_debit, 5);
|
||||
assert.equal(h.unknown ?? 0, 0);
|
||||
assert.equal(h.summary, 46);
|
||||
});
|
||||
|
||||
test("fixture: current-day range covers both days without unknowns", () => {
|
||||
const h = histogram(fixture("current-day-range"));
|
||||
assert.equal(h.unknown ?? 0, 0);
|
||||
assert.equal(h.ach_debit, 5);
|
||||
});
|
||||
|
||||
test("extractBalances: hostile responses hit caps and finite guards, counted not silent", () => {
|
||||
const rows = [];
|
||||
for (let i = 0; i < 60; i++)
|
||||
rows.push({ transactionType: "Summary", transactionCode: String(500 + i), asOfDate: "2026-07-22", amount: "1" });
|
||||
rows.push({ transactionType: "Summary", transactionCode: "010", asOfDate: "2026-07-22", amount: "Infinity" });
|
||||
rows.push({ transactionType: "Summary", transactionCode: "100", asOfDate: "2026-07-22", amount: "5", itemCount: "Infinity" });
|
||||
for (const month of ["05", "06"])
|
||||
for (let d = 1; d <= 28; d++)
|
||||
rows.push({ transactionType: "Summary", transactionCode: "030", asOfDate: `2026-${month}-${String(d).padStart(2, "0")}`, amount: "1" });
|
||||
const snaps = extractBalances(rows);
|
||||
const s = snaps.find((x) => x.as_of_date === "2026-07-22");
|
||||
assert.ok(Object.keys(s.other).length <= 50);
|
||||
assert.ok(s.other_truncated > 0);
|
||||
assert.equal(s.opening_ledger, undefined); // Infinity amount skipped
|
||||
assert.equal(s.total_credits_count, 0); // Infinity itemCount zeroed
|
||||
assert.ok(snaps.length <= 31);
|
||||
assert.ok(snaps[0].dates_truncated > 0);
|
||||
});
|
||||
538
tests/fixtures/current-day-range.json
vendored
Normal file
538
tests/fixtures/current-day-range.json
vendored
Normal file
|
|
@ -0,0 +1,538 @@
|
|||
{
|
||||
"accountTransactions": [
|
||||
{
|
||||
"accountNumber": "000000000000",
|
||||
"bankId": "000000000",
|
||||
"currency": "USD",
|
||||
"transactions": [
|
||||
{
|
||||
"asOfDate": "2026-07-21",
|
||||
"transactionCode": "010",
|
||||
"transactionDescription": "Opening Ledger Balance",
|
||||
"transactionType": "Summary",
|
||||
"amount": "194541.48"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-21",
|
||||
"transactionCode": "030",
|
||||
"transactionDescription": "Current Ledger Balance",
|
||||
"transactionType": "Summary",
|
||||
"amount": "209541.48"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-21",
|
||||
"transactionCode": "040",
|
||||
"transactionDescription": "Opening Avail Balance",
|
||||
"transactionType": "Summary",
|
||||
"amount": "194541.48"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-21",
|
||||
"transactionCode": "060",
|
||||
"transactionDescription": "Current Available Balance",
|
||||
"transactionType": "Summary",
|
||||
"amount": "209541.48"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-21",
|
||||
"transactionCode": "072",
|
||||
"transactionDescription": "1 Day Float",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-21",
|
||||
"transactionCode": "074",
|
||||
"transactionDescription": "2 or More Days Float",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-21",
|
||||
"transactionCode": "906",
|
||||
"transactionDescription": "Today Opening 1 Day Float",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-21",
|
||||
"transactionCode": "907",
|
||||
"transactionDescription": "Today Opening 2+ Day Float",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-21",
|
||||
"transactionCode": "100",
|
||||
"transactionDescription": "Total Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "15000.00",
|
||||
"availabilityImmediateAmount": "15000.00",
|
||||
"availability1DayAmount": "0.00",
|
||||
"availability2PlusDayAmount": "0.00",
|
||||
"itemCount": 1
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-21",
|
||||
"transactionCode": "200",
|
||||
"transactionDescription": "Total Automatic Transfer CR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "15000.00",
|
||||
"itemCount": 1
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-21",
|
||||
"transactionCode": "201",
|
||||
"transactionDescription": "Individual Auto Transfer CR",
|
||||
"transactionType": "Detail",
|
||||
"amount": "15000.00",
|
||||
"creditDebitIndicator": "Credit",
|
||||
"availabilityImmediateAmount": "15000.00",
|
||||
"availability1DayAmount": "0.00",
|
||||
"availability2PlusDayAmount": "0.00",
|
||||
"customerReference": "07520",
|
||||
"bankReference": "200009387568",
|
||||
"detailText": "ACCOUNT TRANSFER TRSF FROM 483096772516"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "010",
|
||||
"transactionDescription": "Opening Ledger Balance",
|
||||
"transactionType": "Summary",
|
||||
"amount": "209541.48"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "030",
|
||||
"transactionDescription": "Current Ledger Balance",
|
||||
"transactionType": "Summary",
|
||||
"amount": "203890.83"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "040",
|
||||
"transactionDescription": "Opening Avail Balance",
|
||||
"transactionType": "Summary",
|
||||
"amount": "209541.48"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "060",
|
||||
"transactionDescription": "Current Available Balance",
|
||||
"transactionType": "Summary",
|
||||
"amount": "203890.83"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "072",
|
||||
"transactionDescription": "1 Day Float",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "074",
|
||||
"transactionDescription": "2 or More Days Float",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "906",
|
||||
"transactionDescription": "Today Opening 1 Day Float",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "907",
|
||||
"transactionDescription": "Today Opening 2+ Day Float",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "100",
|
||||
"transactionDescription": "Total Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "15000.00",
|
||||
"availabilityImmediateAmount": "15000.00",
|
||||
"availability1DayAmount": "0.00",
|
||||
"availability2PlusDayAmount": "0.00",
|
||||
"itemCount": 1
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "110",
|
||||
"transactionDescription": "Total Lockbox Deposit CR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"availabilityImmediateAmount": "0.00",
|
||||
"availability1DayAmount": "0.00",
|
||||
"availability2PlusDayAmount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "120",
|
||||
"transactionDescription": "Total EDI Transaction Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "140",
|
||||
"transactionDescription": "Total ACH Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "150",
|
||||
"transactionDescription": "Total Preauthorized Payment CR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "159",
|
||||
"transactionDescription": "Total Real Time Payment Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "170",
|
||||
"transactionDescription": "Total Other Check Deposits CR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "180",
|
||||
"transactionDescription": "Total Loan Proceed Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "186",
|
||||
"transactionDescription": "Total Cash Letter Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "190",
|
||||
"transactionDescription": "Total Incoming Money Trnsfr CR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"availabilityImmediateAmount": "0.00",
|
||||
"availability1DayAmount": "0.00",
|
||||
"availability2PlusDayAmount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "200",
|
||||
"transactionDescription": "Total Automatic Transfer CR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "15000.00",
|
||||
"itemCount": 1
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "210",
|
||||
"transactionDescription": "Total FX/International Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "239",
|
||||
"transactionDescription": "Total Bankers Acceptance CR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "250",
|
||||
"transactionDescription": "Total Deposited Items Ret CR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "260",
|
||||
"transactionDescription": "Total Rejected Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "294",
|
||||
"transactionDescription": "Total ATM Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "310",
|
||||
"transactionDescription": "Total Commercial Deposit CR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "350",
|
||||
"transactionDescription": "Total Sweep Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "390",
|
||||
"transactionDescription": "Total Miscellaneous Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "400",
|
||||
"transactionDescription": "Total Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "20650.65",
|
||||
"itemCount": 5
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "412",
|
||||
"transactionDescription": "Total Debits Less Return Items",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "416",
|
||||
"transactionDescription": "Total Lockbox Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "420",
|
||||
"transactionDescription": "Total EDI Transaction Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "450",
|
||||
"transactionDescription": "Total ACH Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "20650.65",
|
||||
"itemCount": 5
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "459",
|
||||
"transactionDescription": "Total Real Time Payment Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "470",
|
||||
"transactionDescription": "Total Checks Paid Debit",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "480",
|
||||
"transactionDescription": "Total Loan Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "486",
|
||||
"transactionDescription": "Total Cash Letter Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "490",
|
||||
"transactionDescription": "Total Outgoing Money Trnsfr DR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "500",
|
||||
"transactionDescription": "Total Automatic Transfer DR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "510",
|
||||
"transactionDescription": "Total FX/International Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "530",
|
||||
"transactionDescription": "Total Security Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "539",
|
||||
"transactionDescription": "Total Bankers Acceptance DR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "550",
|
||||
"transactionDescription": "Total Deposited Items Ret DR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "594",
|
||||
"transactionDescription": "Total ATM Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "596",
|
||||
"transactionDescription": "Total ARP Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "650",
|
||||
"transactionDescription": "Total Investments Purchased DR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "690",
|
||||
"transactionDescription": "Total Miscellaneous Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "201",
|
||||
"transactionDescription": "Individual Auto Transfer CR",
|
||||
"transactionType": "Detail",
|
||||
"amount": "15000.00",
|
||||
"creditDebitIndicator": "Credit",
|
||||
"availabilityImmediateAmount": "15000.00",
|
||||
"availability1DayAmount": "0.00",
|
||||
"availability2PlusDayAmount": "0.00",
|
||||
"customerReference": "073032",
|
||||
"bankReference": "200009933345",
|
||||
"detailText": "ACCOUNT TRANSFER TRSF FROM 483096772516"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "455",
|
||||
"transactionDescription": "Preauthorized ACH Debit",
|
||||
"transactionType": "Detail",
|
||||
"amount": "14516.84",
|
||||
"creditDebitIndicator": "Debit",
|
||||
"customerReference": "000000000000",
|
||||
"bankReference": "02023379940",
|
||||
"detailText": "ALLIANCE SANITAT DES:PAYMENTS ID:PMT 7854980 INDN:Sea Haven Industries CO ID:1811679038 CCD PMT INFO:Alliance Sanitation LLC 21222000280"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "455",
|
||||
"transactionDescription": "Preauthorized ACH Debit",
|
||||
"transactionType": "Detail",
|
||||
"amount": "3988.81",
|
||||
"creditDebitIndicator": "Debit",
|
||||
"customerReference": "000000000000",
|
||||
"bankReference": "02023379931",
|
||||
"detailText": "SUMMIT FIRE & SE DES:PAYMENTS ID:PMT 7854978 INDN:Sea Haven Industries CO ID:1811679038 CCD PMT INFO:Summit Fire & Security LLC 21222000281"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "455",
|
||||
"transactionDescription": "Preauthorized ACH Debit",
|
||||
"transactionType": "Detail",
|
||||
"amount": "1400.00",
|
||||
"creditDebitIndicator": "Debit",
|
||||
"customerReference": "000000000000",
|
||||
"bankReference": "02023379928",
|
||||
"detailText": "AWT NORTH AMERIC DES:PAYMENTS ID:PMT 7854976 INDN:Sea Haven Industries CO ID:1811679038 CCD PMT INFO:AWT North America 21222000283"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "455",
|
||||
"transactionDescription": "Preauthorized ACH Debit",
|
||||
"transactionType": "Detail",
|
||||
"amount": "385.00",
|
||||
"creditDebitIndicator": "Debit",
|
||||
"customerReference": "000000000000",
|
||||
"bankReference": "02023379934",
|
||||
"detailText": "SASSI INC DES:PAYMENTS ID:PMT 7854974 INDN:Sea Haven Industries CO ID:1811679038 CCD PMT INFO:SASSI INC 21222000285"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "455",
|
||||
"transactionDescription": "Preauthorized ACH Debit",
|
||||
"transactionType": "Detail",
|
||||
"amount": "360.00",
|
||||
"creditDebitIndicator": "Debit",
|
||||
"customerReference": "000000000000",
|
||||
"bankReference": "02023379937",
|
||||
"detailText": "SKYWOOD MAINTENA DES:PAYMENTS ID:PMT 7854973 INDN:Sea Haven Industries CO ID:1811679038 CCD PMT INFO:SKYWOOD MAINTENANCE LLC 21222000282"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
449
tests/fixtures/current-day.json
vendored
Normal file
449
tests/fixtures/current-day.json
vendored
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
{
|
||||
"accountTransactions": [
|
||||
{
|
||||
"accountNumber": "000000000000",
|
||||
"bankId": "000000000",
|
||||
"currency": "USD",
|
||||
"transactions": [
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "010",
|
||||
"transactionDescription": "Opening Ledger Balance",
|
||||
"transactionType": "Summary",
|
||||
"amount": "209541.48"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "030",
|
||||
"transactionDescription": "Current Ledger Balance",
|
||||
"transactionType": "Summary",
|
||||
"amount": "203890.83"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "040",
|
||||
"transactionDescription": "Opening Avail Balance",
|
||||
"transactionType": "Summary",
|
||||
"amount": "209541.48"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "060",
|
||||
"transactionDescription": "Current Available Balance",
|
||||
"transactionType": "Summary",
|
||||
"amount": "203890.83"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "072",
|
||||
"transactionDescription": "1 Day Float",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "074",
|
||||
"transactionDescription": "2 or More Days Float",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "906",
|
||||
"transactionDescription": "Today Opening 1 Day Float",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "907",
|
||||
"transactionDescription": "Today Opening 2+ Day Float",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "100",
|
||||
"transactionDescription": "Total Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "15000.00",
|
||||
"availabilityImmediateAmount": "15000.00",
|
||||
"availability1DayAmount": "0.00",
|
||||
"availability2PlusDayAmount": "0.00",
|
||||
"itemCount": 1
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "110",
|
||||
"transactionDescription": "Total Lockbox Deposit CR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"availabilityImmediateAmount": "0.00",
|
||||
"availability1DayAmount": "0.00",
|
||||
"availability2PlusDayAmount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "120",
|
||||
"transactionDescription": "Total EDI Transaction Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "140",
|
||||
"transactionDescription": "Total ACH Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "150",
|
||||
"transactionDescription": "Total Preauthorized Payment CR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "159",
|
||||
"transactionDescription": "Total Real Time Payment Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "170",
|
||||
"transactionDescription": "Total Other Check Deposits CR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "180",
|
||||
"transactionDescription": "Total Loan Proceed Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "186",
|
||||
"transactionDescription": "Total Cash Letter Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "190",
|
||||
"transactionDescription": "Total Incoming Money Trnsfr CR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"availabilityImmediateAmount": "0.00",
|
||||
"availability1DayAmount": "0.00",
|
||||
"availability2PlusDayAmount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "200",
|
||||
"transactionDescription": "Total Automatic Transfer CR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "15000.00",
|
||||
"itemCount": 1
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "210",
|
||||
"transactionDescription": "Total FX/International Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "239",
|
||||
"transactionDescription": "Total Bankers Acceptance CR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "250",
|
||||
"transactionDescription": "Total Deposited Items Ret CR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "260",
|
||||
"transactionDescription": "Total Rejected Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "294",
|
||||
"transactionDescription": "Total ATM Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "310",
|
||||
"transactionDescription": "Total Commercial Deposit CR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "350",
|
||||
"transactionDescription": "Total Sweep Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "390",
|
||||
"transactionDescription": "Total Miscellaneous Credits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "400",
|
||||
"transactionDescription": "Total Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "20650.65",
|
||||
"itemCount": 5
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "412",
|
||||
"transactionDescription": "Total Debits Less Return Items",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "416",
|
||||
"transactionDescription": "Total Lockbox Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "420",
|
||||
"transactionDescription": "Total EDI Transaction Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "450",
|
||||
"transactionDescription": "Total ACH Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "20650.65",
|
||||
"itemCount": 5
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "459",
|
||||
"transactionDescription": "Total Real Time Payment Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "470",
|
||||
"transactionDescription": "Total Checks Paid Debit",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "480",
|
||||
"transactionDescription": "Total Loan Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "486",
|
||||
"transactionDescription": "Total Cash Letter Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "490",
|
||||
"transactionDescription": "Total Outgoing Money Trnsfr DR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "500",
|
||||
"transactionDescription": "Total Automatic Transfer DR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "510",
|
||||
"transactionDescription": "Total FX/International Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "530",
|
||||
"transactionDescription": "Total Security Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "539",
|
||||
"transactionDescription": "Total Bankers Acceptance DR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "550",
|
||||
"transactionDescription": "Total Deposited Items Ret DR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "594",
|
||||
"transactionDescription": "Total ATM Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "596",
|
||||
"transactionDescription": "Total ARP Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "650",
|
||||
"transactionDescription": "Total Investments Purchased DR",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "690",
|
||||
"transactionDescription": "Total Miscellaneous Debits",
|
||||
"transactionType": "Summary",
|
||||
"amount": "0.00",
|
||||
"itemCount": 0
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "201",
|
||||
"transactionDescription": "Individual Auto Transfer CR",
|
||||
"transactionType": "Detail",
|
||||
"amount": "15000.00",
|
||||
"creditDebitIndicator": "Credit",
|
||||
"availabilityImmediateAmount": "15000.00",
|
||||
"availability1DayAmount": "0.00",
|
||||
"availability2PlusDayAmount": "0.00",
|
||||
"customerReference": "073032",
|
||||
"bankReference": "200009933345",
|
||||
"detailText": "ACCOUNT TRANSFER TRSF FROM 483096772516"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "455",
|
||||
"transactionDescription": "Preauthorized ACH Debit",
|
||||
"transactionType": "Detail",
|
||||
"amount": "14516.84",
|
||||
"creditDebitIndicator": "Debit",
|
||||
"customerReference": "000000000000",
|
||||
"bankReference": "02023379940",
|
||||
"detailText": "ALLIANCE SANITAT DES:PAYMENTS ID:PMT 7854980 INDN:Sea Haven Industries CO ID:1811679038 CCD PMT INFO:Alliance Sanitation LLC 21222000280"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "455",
|
||||
"transactionDescription": "Preauthorized ACH Debit",
|
||||
"transactionType": "Detail",
|
||||
"amount": "3988.81",
|
||||
"creditDebitIndicator": "Debit",
|
||||
"customerReference": "000000000000",
|
||||
"bankReference": "02023379931",
|
||||
"detailText": "SUMMIT FIRE & SE DES:PAYMENTS ID:PMT 7854978 INDN:Sea Haven Industries CO ID:1811679038 CCD PMT INFO:Summit Fire & Security LLC 21222000281"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "455",
|
||||
"transactionDescription": "Preauthorized ACH Debit",
|
||||
"transactionType": "Detail",
|
||||
"amount": "1400.00",
|
||||
"creditDebitIndicator": "Debit",
|
||||
"customerReference": "000000000000",
|
||||
"bankReference": "02023379928",
|
||||
"detailText": "AWT NORTH AMERIC DES:PAYMENTS ID:PMT 7854976 INDN:Sea Haven Industries CO ID:1811679038 CCD PMT INFO:AWT North America 21222000283"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "455",
|
||||
"transactionDescription": "Preauthorized ACH Debit",
|
||||
"transactionType": "Detail",
|
||||
"amount": "385.00",
|
||||
"creditDebitIndicator": "Debit",
|
||||
"customerReference": "000000000000",
|
||||
"bankReference": "02023379934",
|
||||
"detailText": "SASSI INC DES:PAYMENTS ID:PMT 7854974 INDN:Sea Haven Industries CO ID:1811679038 CCD PMT INFO:SASSI INC 21222000285"
|
||||
},
|
||||
{
|
||||
"asOfDate": "2026-07-22",
|
||||
"transactionCode": "455",
|
||||
"transactionDescription": "Preauthorized ACH Debit",
|
||||
"transactionType": "Detail",
|
||||
"amount": "360.00",
|
||||
"creditDebitIndicator": "Debit",
|
||||
"customerReference": "000000000000",
|
||||
"bankReference": "02023379937",
|
||||
"detailText": "SKYWOOD MAINTENA DES:PAYMENTS ID:PMT 7854973 INDN:Sea Haven Industries CO ID:1811679038 CCD PMT INFO:SKYWOOD MAINTENANCE LLC 21222000282"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
1370
tests/fixtures/previous-day-returns.json
vendored
Normal file
1370
tests/fixtures/previous-day-returns.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
2130
tests/fixtures/previous-day-week.json
vendored
Normal file
2130
tests/fixtures/previous-day-week.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue