diff --git a/src/processPaymentCsv.js b/src/processPaymentCsv.js index 3246432..cc67469 100644 --- a/src/processPaymentCsv.js +++ b/src/processPaymentCsv.js @@ -1,11 +1,29 @@ import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; -import { DynamoDBDocumentClient, PutCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb"; +import { DynamoDBDocumentClient, GetCommand, PutCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb"; +import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm"; import { parse } from "csv-parse/sync"; const s3 = new S3Client(); const ddb = DynamoDBDocumentClient.from(new DynamoDBClient()); +const ssm = new SSMClient(); const TABLE_NAME = process.env.TABLE_NAME; +const BOA_BASE_URL = process.env.BOA_BASE_URL; + +async function getSSMParam(name) { + const { Parameter } = await ssm.send( + new GetParameterCommand({ Name: name, WithDecryption: true }) + ); + return Parameter.Value; +} + +// Convert MM/DD/YYYY to YYYY-MM-DD +function toISODate(mdyDate) { + const parts = String(mdyDate).split("/"); + if (parts.length !== 3) return null; + const [mm, dd, yyyy] = parts; + return `${yyyy}-${mm.padStart(2, "0")}-${dd.padStart(2, "0")}`; +} export const handler = async (event) => { const record = event.Records[0]; @@ -36,12 +54,23 @@ export const handler = async (event) => { return isNaN(num) ? 0 : num; }; - // Upsert each payment, preserving clear_status/bank_reference/cleared_date if they exist + const cancelStatuses = ["voided", "cancelled"]; + const newChecks = []; + const cancelChecks = []; + + // Upsert each payment, tracking new and cancelled checks let count = 0; for (const row of normalizedRows) { const checkNumber = (row["Check Number"] || "").trim(); + const method = (row["Method"] || "").trim(); + const status = (row["Status"] || "").trim(); const pk = `payment#${checkNumber}`; + // Check if record already exists (for detecting new vs updated) + const { Item: existing } = await ddb.send( + new GetCommand({ TableName: TABLE_NAME, Key: { pk } }) + ); + await ddb.send( new UpdateCommand({ TableName: TABLE_NAME, @@ -61,18 +90,92 @@ export const handler = async (event) => { "#status": "status", }, ExpressionAttributeValues: { - ":method": (row["Method"] || "").trim(), + ":method": method, ":payee": (row["Payee"] || "").trim(), ":check_number": checkNumber, ":invoice_numbers": (row["Invoice Numbers"] || "").trim(), ":send_payment_on": (row["Send Payment On"] || "").trim(), ":amount_usd": parseAmount(row["Amount in USD"]), - ":status": (row["Status"] || "").trim(), + ":status": status, ":company_subsidiary": (row["Company/Subsidiary"] || "").trim(), }, }) ); count++; + + // Only process checks for CashPro + if (method !== "Check" || !checkNumber) continue; + + if (!existing) { + // New check → issue + newChecks.push({ + checkNumber, + amount: parseAmount(row["Amount in USD"]).toFixed(2), + issueDate: toISODate(row["Send Payment On"] || ""), + }); + } else if ( + cancelStatuses.includes(status.toLowerCase()) && + !cancelStatuses.includes((existing.status || "").toLowerCase()) + ) { + // Existing check now voided/cancelled → cancel + cancelChecks.push({ + checkNumber, + amount: parseAmount(row["Amount in USD"]).toFixed(2), + issueDate: toISODate(row["Send Payment On"] || ""), + }); + } + } + + // Submit to CashPro if there are any new issues or cancels + if (newChecks.length || cancelChecks.length) { + const [apiToken, accountNumber, companyId] = await Promise.all([ + getSSMParam(process.env.BOA_API_TOKEN_PARAM), + getSSMParam(process.env.BOA_ACCOUNT_NUMBER_PARAM), + getSSMParam(process.env.BOA_COMPANY_ID_PARAM), + ]); + + const submitToBoA = async (items, action) => { + const issueList = items.map((item) => ({ + accountNumber, + checkNumber: item.checkNumber, + amount: item.amount, + issueAction: action, + issueDate: item.issueDate, + })); + + const res = await fetch( + `${BOA_BASE_URL}/cashpro/checkmanagement/v1/check-issues`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiToken}`, + companyId, + }, + body: JSON.stringify({ issueList }), + } + ); + + const data = await res.json(); + + if (!res.ok) { + console.error(`BoA ${action} error ${res.status}:`, JSON.stringify(data)); + throw new Error(`BoA ${action} failed: ${res.status}`); + } + + console.log( + `BoA ${action}: ${data.processedItems}/${data.totalItems} processed, ${data.unprocessedItems} failed` + ); + return data; + }; + + if (newChecks.length) { + await submitToBoA(newChecks, "add_issue"); + } + + if (cancelChecks.length) { + await submitToBoA(cancelChecks, "cancel_issue"); + } } // Update metadata record @@ -88,6 +191,11 @@ export const handler = async (event) => { }) ); - console.log(`Upserted ${count} payments from ${key}`); - return { statusCode: 200, body: `Upserted ${count} payments` }; + console.log( + `Upserted ${count} payments, ${newChecks.length} issued, ${cancelChecks.length} cancelled` + ); + return { + statusCode: 200, + body: `Upserted ${count}, issued ${newChecks.length}, cancelled ${cancelChecks.length}`, + }; }; diff --git a/template.yaml b/template.yaml index c8389f9..5c7febd 100644 --- a/template.yaml +++ b/template.yaml @@ -131,6 +131,13 @@ Resources: Properties: FunctionName: payments-processPaymentCsv Handler: src/processPaymentCsv.handler + Timeout: 120 + Environment: + Variables: + BOA_BASE_URL: https://sandbox.cashpro.bankofamerica.com + BOA_API_TOKEN_PARAM: /payments-dashboard/boa-api-token + BOA_ACCOUNT_NUMBER_PARAM: /payments-dashboard/boa-account-number + BOA_COMPANY_ID_PARAM: /payments-dashboard/boa-company-id VpcConfig: SubnetIds: - !Ref PrivateSubnet @@ -152,6 +159,12 @@ Resources: BucketName: !Sub seahaven-payments-csv-${AWS::AccountId} - DynamoDBCrudPolicy: TableName: !Ref DashboardTable + - SSMParameterReadPolicy: + ParameterName: payments-dashboard/boa-api-token + - SSMParameterReadPolicy: + ParameterName: payments-dashboard/boa-account-number + - SSMParameterReadPolicy: + ParameterName: payments-dashboard/boa-company-id - Version: "2012-10-17" Statement: - Effect: Allow @@ -210,6 +223,7 @@ Resources: BOA_BASE_URL: https://sandbox.cashpro.bankofamerica.com BOA_API_TOKEN_PARAM: /payments-dashboard/boa-api-token BOA_ACCOUNT_NUMBER_PARAM: /payments-dashboard/boa-account-number + BOA_COMPANY_ID_PARAM: /payments-dashboard/boa-company-id Events: DailySchedule: Type: Schedule @@ -224,6 +238,8 @@ Resources: ParameterName: payments-dashboard/boa-api-token - SSMParameterReadPolicy: ParameterName: payments-dashboard/boa-account-number + - SSMParameterReadPolicy: + ParameterName: payments-dashboard/boa-company-id - Version: "2012-10-17" Statement: - Effect: Allow