Skip to content

✨ server: support multiple cards in statement#920

Draft
nfmelendez wants to merge 1 commit intobasefrom
statement-base
Draft

✨ server: support multiple cards in statement#920
nfmelendez wants to merge 1 commit intobasefrom
statement-base

Conversation

@nfmelendez
Copy link
Contributor

@nfmelendez nfmelendez commented Mar 26, 2026

Summary by CodeRabbit

Release Notes

  • New Features
    • Statements now support display of multiple cards with individual card-specific transaction details
    • Added account summary, due balance, and statement period range information
    • Enhanced payment presentation with conditional discount and penalty indicators

@changeset-bot
Copy link

changeset-bot bot commented Mar 26, 2026

🦋 Changeset detected

Latest commit: 3639b18

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@exactly/server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Mar 26, 2026

Walkthrough

Refactored activity API and statement generation to support multiple cards per account. Changed transaction querying from two-step process to eager loading with filtered joins. Updated statement data structure to include card-specific purchases and payments, and modified PDF generation to handle multiple cards with grouped transaction data.

Changes

Cohort / File(s) Summary
Changeset
.changeset/soft-trains-dig.md
Added patch release entry for @exactly/server declaring support for multiple cards in statements.
Activity API
server/api/activity.ts
Replaced two-step card/transaction fetching with single eager-loaded query using with.transactions.where. Updated PDF statement generation to map transactions to cards via purchases, restructured statement output to include account, cards[] with id/lastFour/purchases, and payments[] filtered from repay items.
Core Tests
server/test/api/activity.test.ts, server/test/utils/statement.test.ts
Updated test fixtures to create multiple card records with alternating card associations. Modified statement test setup to persist PDFs to timestamped files. Changed statement input structure from flat data array to nested cards[]/purchases[] and updated all assertions for card/payment rendering and installment formatting.
Statement Component
server/utils/Statement.tsx
Refactored props from flat data array to structured API with account, cards[] (each containing lastFour and purchases[]), maturity, and payments[]. Reworked rendering to traverse cards for purchases and payments; updated due/period display with computed "Due balance"; changed payment chip visibility logic from percent !== 0 to percent >= 0.01 (discount) or percent <= -0.01 (penalty). Changed exported format signature from format(timestamp: number) to format(date: Date).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Suggested reviewers

  • cruzdanilo
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main objective—adding support for multiple cards in statements. The title clearly identifies the primary change and aligns with the substantial refactoring across multiple files to restructure the statement data model and rendering logic.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch statement-base

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for multiple cards within the generated PDF statements. Key changes include refactoring the activity API to group transactions by card using Map.groupBy, updating the Statement component with a professional table-based layout and SVG branding, and enhancing the test suite to cover multi-card scenarios and financial calculations like discounts and penalties. I have no feedback to provide.

@sentry
Copy link

sentry bot commented Mar 26, 2026

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1 1 0 0
View the top 1 failed test(s) by shortest run time
web::web
Stack Traces | 33s run time
Assertion is false: "Sign in" is not visible

To view more test analytics, go to the Prevent Tests Dashboard

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/test/api/activity.test.ts (1)

257-295: 🧹 Nitpick | 🔵 Trivial

Assert the PDF grouping, not just the MIME type.

The new purchasesByCard logic only runs in this PDF path, but these tests still stop at content-type and byteLength. A regression that drops one card or groups every purchase under the same card would still pass.

Please assert both last-fours are present in the rendered output, or mock Statement and inspect the props passed into renderToBuffer.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 141b917e-cca7-4de8-9649-498061fb41a4

📥 Commits

Reviewing files that changed from the base of the PR and between deddc7c and 3639b18.

📒 Files selected for processing (5)
  • .changeset/soft-trains-dig.md
  • server/api/activity.ts
  • server/test/api/activity.test.ts
  • server/test/utils/statement.test.ts
  • server/utils/Statement.tsx

Comment on lines +268 to +272
const hashes = borrows
.entries()
.filter(([_, { events }]) => events.some(({ maturity: m }) => Number(m) === maturity))
.map(([hash]) => hash)
.toArray();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== runtime targets =="
fd '^(package\.json|\.nvmrc|\.tool-versions)$' -x sh -c 'echo "--- $1"; cat "$1"' sh {}
fd '.*\.(yml|yaml)$' .github/workflows -x sh -c 'echo "--- $1"; rg -n "setup-node|node-version|bun-version|deno-version" "$1" || true' sh {}

echo
echo "== typescript target/lib =="
fd '^tsconfig.*\.json$' -x sh -c 'echo "--- $1"; jq ".compilerOptions | {target, lib, module}" "$1"' sh {}

echo
echo "== usages introduced in this path =="
rg -n '\.toArray\(|\bMap\.groupBy\b' --type ts --type tsx

Repository: exactly/exa

Length of output: 33794


🏁 Script executed:

# Read tsconfig files directly (jq syntax was incorrect)
cat server/tsconfig.json
echo "---"
cat tsconfig.json

Repository: exactly/exa

Length of output: 1627


🏁 Script executed:

# Read the actual code at the specified lines
echo "=== activity.ts lines 265-285 ==="
sed -n '265,285p' server/api/activity.ts
echo ""
echo "=== activity.ts lines 423-533 (first 50 lines) ==="
sed -n '423,473p' server/api/activity.ts

Repository: exactly/exa

Length of output: 3575


🌐 Web query:

Node.js 24.13.0 ES2025 Map.groupBy iterator helpers support

💡 Result:

Node.js 24.13.0 fully supports Map.groupBy as it uses V8 13.6 which includes ES2024 features like Map.groupBy (stable since Node.js 21). However, ES2025 iterator helpers (e.g., Iterator.prototype.map, .filter, etc.) are not supported. V8 shipped iterator helpers in v12.2 (Chrome 122), but Node.js 24's V8 13.6 does not list them as shipped features, and sources indicate no Node.js support as of V8 12.2 docs. No harmony flags needed for Map.groupBy; iterator helpers remain experimental/post-ES2025.

Citations:


Iterator helpers (.entries().filter().map().toArray() and .values().toArray()) are not supported in Node.js 24.13.0 and will fail at runtime.

This code uses ES2025 iterator helpers despite the runtime targeting Node.js 24.13.0. While Map.groupBy() is available (ES2024, stable in Node.js 21+), the iterator methods on lines 268–272 and 429–463 will throw at runtime. TypeScript with target: "ESNext" allows these patterns to type-check but does not transpile them to equivalent ES2024 code.

Rewrite using .entries().toArray().filter().map() (convert iterator to array first) or use traditional Array.from() and array methods.

Comment on lines +517 to +523
cards: purchases
.filter(({ id }) => purchasesByCard.has(id))
.map(({ id, lastFour }) => ({
id,
lastFour,
purchases: (purchasesByCard.get(id) ?? []).map(({ cardId: _, ...rest }) => rest),
})),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Sort statement.cards before rendering the PDF.

Once an account has multiple cards, relying on query result order makes the summary and card sections unstable across requests. Sorting here keeps the PDF deterministic.

Proposed fix
         cards: purchases
           .filter(({ id }) => purchasesByCard.has(id))
+          .toSorted((a, b) => a.lastFour.localeCompare(b.lastFour) || a.id.localeCompare(b.id))
           .map(({ id, lastFour }) => ({
             id,
             lastFour,
             purchases: (purchasesByCard.get(id) ?? []).map(({ cardId: _, ...rest }) => rest),
           })),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cards: purchases
.filter(({ id }) => purchasesByCard.has(id))
.map(({ id, lastFour }) => ({
id,
lastFour,
purchases: (purchasesByCard.get(id) ?? []).map(({ cardId: _, ...rest }) => rest),
})),
cards: purchases
.filter(({ id }) => purchasesByCard.has(id))
.toSorted((a, b) => a.lastFour.localeCompare(b.lastFour) || a.id.localeCompare(b.id))
.map(({ id, lastFour }) => ({
id,
lastFour,
purchases: (purchasesByCard.get(id) ?? []).map(({ cardId: _, ...rest }) => rest),
})),

Comment on lines +34 to +35
const totalPayments = payments.reduce((sum, p) => sum + p.positionAmount, 0);
const dueBalance = totalSpent - totalPayments;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Show the actual payment cash flow, not the relieved balance.

Keep positionAmount for dueBalance, but the rows labeled "Payments" should render amount; otherwise an early-payment discount can show up as a larger payment than the user actually made.

Proposed fix
-  const totalPayments = payments.reduce((sum, p) => sum + p.positionAmount, 0);
-  const dueBalance = totalSpent - totalPayments;
+  const totalApplied = payments.reduce((sum, p) => sum + p.positionAmount, 0);
+  const totalPaid = payments.reduce((sum, p) => sum + p.amount, 0);
+  const dueBalance = totalSpent - totalApplied;
...
-              <Text style={styles.summaryAmount}>-{currency.format(totalPayments)}</Text>
+              <Text style={styles.summaryAmount}>-{currency.format(totalPaid)}</Text>
...
-                  <Text style={styles.colTotal}>-{currency.format(payment.positionAmount)}</Text>
+                  <Text style={styles.colTotal}>-{currency.format(payment.amount)}</Text>
...
-              <Text style={styles.totalAmount}>-{currency.format(totalPayments)}</Text>
+              <Text style={styles.totalAmount}>-{currency.format(totalPaid)}</Text>

Also applies to: 113-118, 170-198

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant