diff --git a/.eslintrc.json b/.eslintrc.json
index b8472af..499b2b7 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,14 +1,14 @@
{
"env": {
"es6": true,
- "node": true,
- "sourceType": "module"
+ "node": true
},
"extends": [
"eslint:recommended",
"plugin:jest/recommended"
],
"parserOptions": {
- "ecmaVersion": "latest"
+ "ecmaVersion": "latest",
+ "sourceType": "module"
}
}
\ No newline at end of file
diff --git a/src/actions/ebs-sync/ebs.js b/src/actions/ebs-sync/ebs.js
index d5a5414..29b2905 100644
--- a/src/actions/ebs-sync/ebs.js
+++ b/src/actions/ebs-sync/ebs.js
@@ -14,7 +14,7 @@
*
* ── Payment data sources ─────────────────────────────────────────────────────
* Chase (provider='chase') payment_completed:
- * transactionId, approvalCode, cardType (brand), cardNumber (masked mPAN),
+ * transactionId, approvalCode, cardType (brand), cardLast4,
* cardExpiry (MMYY), avsMatch, cvvMatch, amount, currency
* providerData.safetechResponse (pipe-delimited, optional)
* fraud_evaluated: decision ('approve'|'decline'|'not_reviewed'|'pending')
@@ -53,6 +53,10 @@ import { proxyFetch } from '../../proxy.js';
const TRANSIENT_ERROR_PATTERN = /timeout|timed out|aborted|fetch failed|network|ECONNRESET|ENOTFOUND|ETIMEDOUT|EAI_AGAIN/i;
+const CHASE_AVS_UNVALIDATED_CODES = new Set([
+ '1', '2', '3', '4', '5', '6', '8', 'G', 'J', 'M1', 'M8', 'N4', 'N6', 'R', 'UK',
+]);
+
/**
* Classify whether an error from syncOrderToEbs is worth retrying.
*
@@ -149,6 +153,8 @@ function extractEbsErrorMessage(xml) {
* - The fraud_evaluated journal entry (fraud decision for Chase order state)
* - order.estimates (fallback for taxAmount/shippingCost/subtotal when
* the payment_completed entry was logged without locked-in estimates)
+ * - payment_completed.cardLast4 or order.payment.cardLastFour for EBS
+ * CreditCard/@CardLast4Digits
*
* Returns null when no payment_completed entry is present in the journal.
*
@@ -220,8 +226,8 @@ export function buildPaymentSnapshot(order, orderJournal) {
approvalCode: completed.approvalCode || '',
// cardType from Chase queryTransaction is the card brand (e.g. "Visa")
cardBrand: completed.cardType || '',
- // cardNumber is the masked mPAN (e.g. "401288XXXXXX1881") — last 4 for EBS
- last4: (completed.cardNumber || '').slice(-4),
+ // Magento sends getCcLast4(); Edge stores only the final four digits.
+ last4: resolveCardLast4(completed, order),
// cardExpiry from Chase in MMYY format (e.g. "0127" = January 2027)
expiration: cardExpiryToDate(completed.cardExpiry || ''),
approvalDate: completed.timestamp,
@@ -237,6 +243,7 @@ export function buildPaymentSnapshot(order, orderJournal) {
fraudAutoDecisionResponse: safetech.AutoDecisionResponse || '',
// Forter / fraud provider decision from fraud_evaluated entry
fraudDecision,
+ avsMatch: completed.avsMatch || '',
// MISSING: storedCredentials (payment plan indicator not in journal) — 'N'
storedCredentials: 'N',
// MISSING: mitMsgType (not in journal) — 'CGEN' (standard one-time payment)
@@ -283,9 +290,8 @@ export function buildPaymentSnapshot(order, orderJournal) {
...base,
transactionId: completed.transactionId || '',
approvalCode: completed.approvalCode || '',
- // cardBrand and last4 not yet available in chase-wallet journal entries
- cardBrand: completed.cardType || '',
- last4: (completed.cardNumber || '').slice(-4),
+ cardBrand: completed.cardBrand || completed.cardType || '',
+ last4: (completed.cardLast4 || '').slice(-4),
nameOnCard: completed.nameOnCard || '',
approvalDate: completed.timestamp,
fraudDecision,
@@ -362,7 +368,7 @@ function buildCreateOrderXml(order, paymentSnapshot) {
ReferrerCode="${escapeXml(referrerCode)}"
Key="${escapeXml(orderKey)}"
Created="${created}">
- ${buildCustomerXml(order)}
+ ${buildCustomerXml(order, paymentSnapshot)}
${buildPaymentXml(paymentSnapshot, order)}
${giftMessage}
@@ -376,7 +382,7 @@ function buildCreateOrderXml(order, paymentSnapshot) {
`;
}
-function buildCustomerXml(order) {
+function buildCustomerXml(order, paymentSnapshot) {
const email = escapeXml(order.customer?.email || '');
const firstName = escapeXml(sanitizeName(order.customer?.firstName || ''));
const lastName = escapeXml(sanitizeName(order.customer?.lastName || ''));
@@ -385,8 +391,14 @@ function buildCustomerXml(order) {
);
// billing falls back to shipping when absent
- const billing = order.billing || order.shipping || {};
- const shipping = order.shipping || {};
+ const billing = {
+ ...(order.billing || order.shipping || {}),
+ isValidated: resolveBillingIsValidated(paymentSnapshot),
+ };
+ const shipping = {
+ ...(order.shipping || {}),
+ isValidated: resolveShippingIsValidated(order),
+ };
return ``;
}
+/**
+ * Resolve ShipTo validation from the stored shipping address flag.
+ * Missing flags default to true for older and express-checkout orders.
+ *
+ * @param {object} order
+ * @returns {boolean}
+ */
+function resolveShippingIsValidated(order) {
+ return order.shipping?.isValidated !== false;
+}
+
+/**
+ * Resolve BillTo validation using Magento's Chase AVS rules.
+ * Non-Chase methods and zero-total Chase orders are always considered validated.
+ *
+ * @param {object} paymentSnapshot
+ * @returns {boolean}
+ */
+function resolveBillingIsValidated(paymentSnapshot) {
+ if (paymentSnapshot?.method !== 'chasehpp') return true;
+ if (Number(paymentSnapshot.amount || 0) === 0) return true;
+ return !CHASE_AVS_UNVALIDATED_CODES.has(String(paymentSnapshot.avsMatch || '').trim());
+}
+
/**
* Build a BillTo or ShipTo address block.
* Uses OrderAddress field names: address1, address2, state, zip.
@@ -858,6 +894,18 @@ function parseSafetech(response) {
return result;
}
+/**
+ * Resolve the final four credit card digits for EBS.
+ * Magento sends getCcLast4(); Edge stores only last-four values for PCI scope.
+ *
+ * @param {object} completed - payment_completed journal entry
+ * @param {object} order - stored order document
+ * @returns {string}
+ */
+function resolveCardLast4(completed, order) {
+ return String(completed.cardLast4 || order.payment?.cardLastFour || '').slice(-4);
+}
+
/**
* Convert a Chase card expiry in MMYY format to EBS date format.
* EBS expects YYYY-MM-DD-hh:mm (12-hour, no AM/PM) — we use the 28th at midnight.
diff --git a/src/actions/ebs-sync/types.d.ts b/src/actions/ebs-sync/types.d.ts
index f98ffcb..1d12386 100644
--- a/src/actions/ebs-sync/types.d.ts
+++ b/src/actions/ebs-sync/types.d.ts
@@ -22,7 +22,7 @@ export interface JournalPaymentData {
/** EBS-facing method identifier */
method: 'chasehpp' | 'paypal' | 'affirm' | 'applepay';
/** Raw provider name from the payment_completed journal entry */
- provider: 'chase' | 'paypal' | 'affirm';
+ provider: 'chase' | 'chase-wallet' | 'paypal' | 'affirm';
/** Charged total as a decimal string (e.g. "913.05") */
amount: string;
/** Tax amount as a decimal string — from journal or order.estimates fallback */
@@ -41,7 +41,7 @@ export interface JournalPaymentData {
approvalCode?: string;
/** Card brand from Chase cardType field (e.g. "Visa", "Mastercard") */
cardBrand?: string;
- /** Last 4 digits sliced from the masked mPAN (e.g. "1881" from "401288XXXXXX1881") */
+ /** Last 4 card digits sent to EBS */
last4?: string;
/** Card expiry converted from Chase MMYY format to EBS date (e.g. "2027-01-28T00:00") */
expiration?: string;
@@ -57,6 +57,8 @@ export interface JournalPaymentData {
fraudAutoDecisionResponse?: string;
/** Decision from fraud_evaluated journal entry (e.g. 'approve', 'decline', 'not_reviewed') */
fraudDecision?: string | null;
+ /** Chase AVSMatch from queryTransaction; drives EBS BillTo IsValidated for chasehpp only */
+ avsMatch?: string;
/** MISSING: StoredCredentials flag — not logged; always 'N' */
storedCredentials?: string;
/** MISSING: MIT message type — not logged; always 'CGEN' */
@@ -121,7 +123,7 @@ export interface StoredOrderAddress {
company?: string;
phone?: string;
isDefault?: boolean;
- /** When false, EBS IsValidated attribute is set to 'false' */
+ /** Stored address metadata; ShipTo uses shipping.isValidated, while BillTo is resolved from payment AVS rules */
isValidated?: boolean;
}
@@ -132,6 +134,15 @@ export interface StoredOrderCustomer {
phone?: string;
}
+export interface StoredOrderPayment {
+ method?: string;
+ transactionId?: string;
+ /** Final four card digits stored by Edge Commerce; fallback source for EBS CreditCard/@CardLast4Digits */
+ cardLastFour?: string;
+ amount?: string;
+ currency?: string;
+}
+
export interface StoredOrderItem {
sku: string;
quantity: number;
@@ -179,6 +190,8 @@ export interface JournalOrderData {
/** Billing address — falls back to shipping when absent */
billing?: StoredOrderAddress;
shipping: StoredOrderAddress;
+ /** Stored order payment metadata; payment_completed journal remains the primary EBS payment source */
+ payment?: StoredOrderPayment;
items: StoredOrderItem[];
/** Locked-in estimate snapshot — present when estimateToken was provided at order creation */
estimates?: StoredOrderEstimates;
diff --git a/src/actions/submit/index.js b/src/actions/submit/index.js
index 9c0c252..1068c7a 100644
--- a/src/actions/submit/index.js
+++ b/src/actions/submit/index.js
@@ -10,7 +10,7 @@ const MAX_PAYLOAD_SIZE = 16_000; // 16KB
* alphanumeric, underscores, hyphens, slashes allowed
* but no trailing/leading slash, hyphen or underscore
*/
-const FORM_ID_PATTERN = /^[a-zA-Z0-9]+[\/a-zA-Z0-9_-]*[a-zA-Z0-9]+$/;
+const FORM_ID_PATTERN = /^[a-zA-Z0-9]+[/a-zA-Z0-9_-]*[a-zA-Z0-9]+$/;
/**
* Origin of the production site, as seen in referer header
diff --git a/test/ebs-sync/ebs-e2e.test.js b/test/ebs-sync/ebs-e2e.test.js
index da7bdb4..cdbec8f 100644
--- a/test/ebs-sync/ebs-e2e.test.js
+++ b/test/ebs-sync/ebs-e2e.test.js
@@ -82,6 +82,13 @@ const CC_APPROVED_ORDER = {
},
tax: { country: 'CA', state: 'ON', rate: 13, id: 'CA-ON-*-Rate1' },
},
+ payment: {
+ method: 'card',
+ transactionId: '69F8F8131B061CE600000FFA0000796C41565367',
+ cardLastFour: '1881',
+ amount: '540.15',
+ currency: 'CAD',
+ },
};
/** Order matching journal-cc-decline.ndjson (Chase, Forter declined → cancelled). */
@@ -397,13 +404,13 @@ const MOCK_CTX = {
describe('ebs-sync e2e', () => {
let capturedXml;
- const origFetch = globalThis.fetch;
+ const origFetch = global.fetch;
beforeAll(() => {
// proxyFetch wraps the destination request inside a JSON envelope:
// POST body: { url, method, headers, body: }
// Tests assert against the inner SOAP XML.
- globalThis.fetch = async (_url, opts) => {
+ global.fetch = async (_url, opts) => {
const wrapped = JSON.parse(opts.body);
capturedXml = wrapped.body;
return { ok: true, status: 200, text: async () => '' };
@@ -411,7 +418,7 @@ describe('ebs-sync e2e', () => {
});
afterAll(() => {
- globalThis.fetch = origFetch;
+ global.fetch = origFetch;
});
beforeEach(() => {
@@ -431,6 +438,20 @@ describe('ebs-sync e2e', () => {
expect(xml).toBe(loadFixture(fixtureName));
}
+ const clone = (value) => JSON.parse(JSON.stringify(value));
+
+ const withCompletedPayment = (journal, fields) => journal.map((entry) => (
+ entry.event === 'payment_completed' ? { ...entry, ...fields } : entry
+ ));
+
+ const addressValidation = (xml, tag) => xml.match(new RegExp(`<${tag} IsValidated="([^"]+)"`))?.[1];
+
+ async function buildXml(order, journal) {
+ await syncOrderToEbs(MOCK_CTX, MOCK_PARAMS, order, journal);
+ expect(capturedXml).toBeTruthy();
+ return capturedXml;
+ }
+
// ── Chase credit card — approved by Forter ──────────────────────────────
describe('CC approved (journal-cc-approved)', () => {
@@ -451,6 +472,7 @@ describe('ebs-sync e2e', () => {
expect(snap.last4).toBe('1881');
expect(snap.expiration).toBe('2028-01-28-12:00');
expect(snap.fraudDecision).toBe('approve');
+ expect(snap.avsMatch).toBe('2 ');
// No SafeTech data in this journal (Forter-only environment)
expect(snap.fraudScore).toBe('');
expect(snap.fraudStatusCode).toBe('');
@@ -461,6 +483,14 @@ describe('ebs-sync e2e', () => {
expect(capturedXml).toBeTruthy();
assertXmlFixture('expected-cc-approved.xml', capturedXml);
});
+
+ test('CreditCard CardLast4Digits falls back to order payment cardLastFour', async () => {
+ const journalWithoutCardLast4 = withCompletedPayment(journal, { cardLast4: undefined });
+
+ const xml = await buildXml(CC_APPROVED_ORDER, journalWithoutCardLast4);
+
+ expect(xml).toContain('CardLast4Digits="1881"');
+ });
});
// ── Chase credit card — declined by Forter ──────────────────────────────
@@ -564,6 +594,70 @@ describe('ebs-sync e2e', () => {
});
});
+ // ── Address validation state ───────────────────────────────────────────
+
+ describe('address validation state', () => {
+ test('ShipTo IsValidated follows shipping.isValidated false', async () => {
+ const order = clone(PP_APPROVED_ORDER);
+ order.shipping.isValidated = false;
+ order.billing.isValidated = true;
+
+ const xml = await buildXml(order, loadJournal('journal-pp-approved.ndjson'));
+
+ expect(addressValidation(xml, 'ShipTo')).toBe('false');
+ expect(addressValidation(xml, 'BillTo')).toBe('true');
+ });
+
+ test.each([undefined, true])('ShipTo IsValidated defaults to true for %s', async (isValidated) => {
+ const order = clone(PP_APPROVED_ORDER);
+ if (isValidated !== undefined) order.shipping.isValidated = isValidated;
+
+ const xml = await buildXml(order, loadJournal('journal-pp-approved.ndjson'));
+
+ expect(addressValidation(xml, 'ShipTo')).toBe('true');
+ });
+
+ test.each([
+ ['paypal', PP_APPROVED_ORDER, 'journal-pp-approved.ndjson'],
+ ['affirm', AFFIRM_APPROVED_ORDER, 'journal-affirm-approved.ndjson'],
+ ['applepay', AP_APPROVED_ORDER, 'journal-ap-approved.ndjson'],
+ ])('BillTo IsValidated stays true for non-Chase method %s', async (_method, baseOrder, fixture) => {
+ const order = clone(baseOrder);
+ order.billing.isValidated = false;
+
+ const xml = await buildXml(order, loadJournal(fixture));
+
+ expect(addressValidation(xml, 'BillTo')).toBe('true');
+ });
+
+ test.each(['1', '2 ', 'G', 'M1', 'UK'])('BillTo IsValidated is false for Chase AVS %s', async (avsMatch) => {
+ const xml = await buildXml(
+ clone(CC_APPROVED_ORDER),
+ withCompletedPayment(loadJournal('journal-cc-approved.ndjson'), { avsMatch }),
+ );
+
+ expect(addressValidation(xml, 'BillTo')).toBe('false');
+ });
+
+ test.each(['', 'Y', ' ', undefined])('BillTo IsValidated is true for Chase AVS %s', async (avsMatch) => {
+ const xml = await buildXml(
+ clone(CC_APPROVED_ORDER),
+ withCompletedPayment(loadJournal('journal-cc-approved.ndjson'), { avsMatch }),
+ );
+
+ expect(addressValidation(xml, 'BillTo')).toBe('true');
+ });
+
+ test('BillTo IsValidated is true for zero-total Chase orders regardless of AVS', async () => {
+ const xml = await buildXml(
+ clone(CC_APPROVED_ORDER),
+ withCompletedPayment(loadJournal('journal-cc-approved.ndjson'), { amount: '0.00', avsMatch: '2' }),
+ );
+
+ expect(addressValidation(xml, 'BillTo')).toBe('true');
+ });
+ });
+
// ── PayPal — bundle + extended warranty + free shipping ─────────────────
describe('PP bundle + warranty (journal-pp-bundle-warranty)', () => {
diff --git a/test/ebs-sync/journal.test.js b/test/ebs-sync/journal.test.js
index b8b50b7..874053f 100644
--- a/test/ebs-sync/journal.test.js
+++ b/test/ebs-sync/journal.test.js
@@ -91,8 +91,7 @@ const CHASE_FORTER_ENTRIES = [
transactionId: '69D6844E106ACBB3000004420000559C4156542B',
approvalCode: 'tst387',
cardType: 'Visa',
- cardNumber: '401288XXXXXX1881',
- cardBin: '401288',
+ cardLast4: '1881',
cardExpiry: '0127',
avsMatch: '2 ',
cvvMatch: 'M',
@@ -370,12 +369,25 @@ describe('buildPaymentSnapshot', () => {
expect(cardBrand).toBe('Visa');
});
- test('derives last4 from masked mPAN', () => {
- // cardNumber "401288XXXXXX1881" → last 4 → "1881"
+ test('derives last4 from journal cardLast4', () => {
const { last4 } = buildPaymentSnapshot(mockOrder(), CHASE_FORTER_ENTRIES);
expect(last4).toBe('1881');
});
+ test('falls back to order payment cardLastFour when journal cardLast4 is absent', () => {
+ const entries = CHASE_FORTER_ENTRIES.map((entry) => {
+ if (entry.event !== 'payment_completed') return entry;
+ const rest = { ...entry };
+ delete rest.cardLast4;
+ return rest;
+ });
+ const { last4 } = buildPaymentSnapshot(
+ mockOrder({ payment: { method: 'card', cardLastFour: '1881' } }),
+ entries,
+ );
+ expect(last4).toBe('1881');
+ });
+
test('converts cardExpiry from MMYY to EBS date format', () => {
// cardExpiry "0127" (January 2027) → "2027-01-28-12:00" (midnight in 12-hour form)
const { expiration } = buildPaymentSnapshot(mockOrder(), CHASE_FORTER_ENTRIES);
@@ -387,6 +399,22 @@ describe('buildPaymentSnapshot', () => {
expect(fraudDecision).toBe('approve');
});
+ test('captures AVS match from payment_completed', () => {
+ const { avsMatch } = buildPaymentSnapshot(mockOrder(), CHASE_FORTER_ENTRIES);
+ expect(avsMatch).toBe('2 ');
+ });
+
+ test('defaults missing AVS match to empty string', () => {
+ const entries = CHASE_FORTER_ENTRIES.map((entry) => {
+ if (entry.event !== 'payment_completed') return entry;
+ const rest = { ...entry };
+ delete rest.avsMatch;
+ return rest;
+ });
+ const { avsMatch } = buildPaymentSnapshot(mockOrder(), entries);
+ expect(avsMatch).toBe('');
+ });
+
test('defaults SafeTech fields to empty string when providerData absent', () => {
// This order has no providerData.safetechResponse
const snapshot = buildPaymentSnapshot(mockOrder(), CHASE_FORTER_ENTRIES);
diff --git a/test/fixtures/expected-cc-approved.xml b/test/fixtures/expected-cc-approved.xml
index 28b0d8b..b987d0e 100644
--- a/test/fixtures/expected-cc-approved.xml
+++ b/test/fixtures/expected-cc-approved.xml
@@ -29,7 +29,7 @@
xsi:type="Person"
Key="approve@forter.com"
Action="U">
-
+
123 Main St
Toronto
diff --git a/test/fixtures/journal-cc-approved.ndjson b/test/fixtures/journal-cc-approved.ndjson
index 02e170d..c94003d 100644
--- a/test/fixtures/journal-cc-approved.ndjson
+++ b/test/fixtures/journal-cc-approved.ndjson
@@ -4,5 +4,5 @@
{"id":"d3a1655c-0743-4f4a-a7ae-f24badd05025","timestamp":"2026-05-04T19:48:36.329Z","org":"aemsites","site":"vitamix","journal":"orders","event":"http_request","service":"chase-query","method":"POST","url":"https://testvitamix.chasepaymentechhostedpay-var.com/direct/services/request/query/","orderId":"2026-05-04T19-47-57.113Z-M4Z6Y6PC","attemptId":"066f0a52-7ed2-43cb-b8fd-a1c2be73b2c6","ok":true,"statusCode":200,"duration":283}
{"id":"67d1ccee-bc70-4027-8ad4-f2bbf9da066a","timestamp":"2026-05-04T19:48:36.904Z","org":"aemsites","site":"vitamix","journal":"orders","event":"fraud_evaluated","orderId":"2026-05-04T19-47-57.113Z-M4Z6Y6PC","attemptId":"066f0a52-7ed2-43cb-b8fd-a1c2be73b2c6","provider":"forter","decision":"approve","reasonCodes":["Test"]}
{"id":"f098ed10-5a9f-444b-a08b-68549844cbed","timestamp":"2026-05-04T19:48:36.904Z","org":"aemsites","site":"vitamix","journal":"orders","event":"http_request","service":"forter","method":"POST","url":"https://f2bc58b1eab5.api.forter-secure.com/v2/orders/2026-05-04T19-47-57.113Z-M4Z6Y6PC","orderId":"2026-05-04T19-47-57.113Z-M4Z6Y6PC","ok":true,"statusCode":200,"duration":351}
-{"id":"e4d5d03f-cd24-485a-9366-fab042e14778","timestamp":"2026-05-04T19:48:37.350Z","org":"aemsites","site":"vitamix","journal":"orders","event":"payment_completed","orderId":"2026-05-04T19-47-57.113Z-M4Z6Y6PC","attemptId":"066f0a52-7ed2-43cb-b8fd-a1c2be73b2c6","provider":"chase","transactionId":"69F8F8131B061CE600000FFA0000796C41565367","approvalCode":"tst401","cardType":"Visa","cardNumber":"401288XXXXXX1881","cardBin":"401288","cardExpiry":"0128","avsMatch":"2 ","cvvMatch":"M","amount":"540.15","currency":"CAD","subtotal":449.95,"taxAmount":58.4935,"shippingCost":31.71}
+{"id":"e4d5d03f-cd24-485a-9366-fab042e14778","timestamp":"2026-05-04T19:48:37.350Z","org":"aemsites","site":"vitamix","journal":"orders","event":"payment_completed","orderId":"2026-05-04T19-47-57.113Z-M4Z6Y6PC","attemptId":"066f0a52-7ed2-43cb-b8fd-a1c2be73b2c6","provider":"chase","transactionId":"69F8F8131B061CE600000FFA0000796C41565367","approvalCode":"tst401","cardType":"Visa","cardLast4":"1881","cardExpiry":"0128","avsMatch":"2 ","cvvMatch":"M","amount":"540.15","currency":"CAD","subtotal":449.95,"taxAmount":58.4935,"shippingCost":31.71}
{"id":"42508d74-e653-4938-b287-d0c07d9f8728","timestamp":"2026-05-04T19:48:39.631Z","org":"aemsites","site":"vitamix","journal":"orders","event":"email_outcome","kind":"email","type":"order-confirmation","orderId":"2026-05-04T19-47-57.113Z-M4Z6Y6PC","jobId":"email_2026-05-04T19-48-39.018Z-94a33a88","outcome":"sent","attempts":1,"toEmail":"approve@forter.com","fromEmail":"noreply@mail.adobecommerce.live","sesMessageId":"0100019df4891b01-732ba0f2-8fcc-4669-9f11-7e01851ef4cf-000000","sentAt":"2026-05-04T19:48:39.423Z"}