Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

✨ server: implement card statement#770

Draft
nfmelendez wants to merge 11 commits intomain from
statement
Draft

✨ server: implement card statement #770
nfmelendez wants to merge 11 commits intomain from
statement

Conversation

@nfmelendez
Copy link
Contributor

@nfmelendez nfmelendez commented Feb 12, 2026
edited by coderabbitai bot
Loading

Summary by CodeRabbit

  • New Features

    • Generate and download card statements as PDF
    • Filter activity by maturity date for clearer transaction views
    • Activity responses now include maturity-aware data
  • Bug Fixes

    • Fixed iterator consumption issue in activity processing
  • Tests

    • Added tests for maturity filtering and PDF statement rendering
  • Chores

    • Project config updated to enable server-side PDF rendering, JSX on the server, ESLint React integration, and workspace catalog entries; added release changesets

Copy link

changeset-bot bot commented Feb 12, 2026
edited
Loading

🦋 Changeset detected

Latest commit: 1e41d04

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

Copy link

coderabbitai bot commented Feb 12, 2026
edited
Loading

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds maturity-aware activity filtering and optional PDF statement generation: new React-PDF Statement component, API changes to accept maturity and return JSON or application/pdf, tests, server TSX/JSX config and ESLint React override, plus workspace and server dependency updates. (30 words)

Changes

Cohort / File(s) Summary
Changesets
\.changeset/stale-owls-work.md, \.changeset/upset-eels-ask.md
Two new changeset entries for @exactly/server (card statement addition; iterator-consumption bugfix).
Workspace & Root Manifest
package.json, pnpm-workspace.yaml
Root package.json modified to use workspace/catalog anchors for React; workspace catalog entries for react/react-dom added.
Server deps & config
server/package.json, server/tsconfig.json, server/eslint.config.mjs
Added @react-pdf/renderer and react to server deps; enabled "jsx": "react" in server tsconfig; added ESLint override for TSX with React config.
API Implementation
server/api/activity.ts
Accepts optional maturity query param; maturity-driven card transaction lookup, aggregation, transforms and type updates; supports JSON and application/pdf responses (PDF via Statement + renderToBuffer); changes to onchain activity id format.
PDF Component
server/utils/Statement.tsx
New React-PDF Statement component (default export) rendering card statement PDF from provided data, lastFour, and maturity; computes statement/due dates and renders Payments/Purchases sections.
Server tests
server/test/api/activity.test.ts, server/test/utils/statement.test.ts
Tests added for maturity filtering and PDF response in activity API; unit tests for rendering Statement across scenarios.
Client parsing change
src/utils/server.ts
getActivity refactored to use parseResponse from hono/client and return parsed arrays with fallback to empty array.

Sequence Diagram

sequenceDiagram
 participant Client
 participant API as "API Endpoint\nserver/api/activity.ts"
 participant DB as "Card DB / Transactions"
 participant PDF as "PDF Renderer\nStatement + `@react-pdf/renderer`"
 participant Response
 Client->>API: GET /activity?maturity={m}\nAccept: application/pdf or application/json
 API->>API: validate/parse `maturity`
 alt maturity provided
 API->>DB: fetch card purchases & related transactions (filter by maturity)
 DB-->>API: return card + txn data
 API->>API: aggregate, transform & sort transactions (include maturity)
 alt Accept: application/pdf
 API->>PDF: render Statement(data,lastFour,maturity) via renderToBuffer
 PDF-->>API: return PDF buffer
 API->>Response: respond Content-Type: application/pdf\nwith buffer
 else
 API->>Response: respond JSON including cardPurchases
 end
 else
 API->>Response: respond JSON without maturity-specific cardPurchases
 end
 Response-->>Client: deliver response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • cruzdanilo
  • franm91
🚥 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 title 'server: implement card statement' accurately and concisely describes the primary change—adding card statement functionality to the server, with supporting PDF rendering capability.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch statement

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.

@cruzdanilo cruzdanilo changed the title (削除) Statement (削除ここまで) (追記) ✨ server: implement card statement (追記ここまで) Feb 12, 2026
Copy link

sentry bot commented Feb 12, 2026

✅ All tests passed.

Copy link

Summary of Changes

Hello @nfmelendez, I'm Gemini Code Assist1 ! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant new capability by enabling the generation of detailed PDF card statements directly from the activity API. Users can now request a statement for a specific maturity period, receiving a well-formatted PDF document that summarizes their purchases and repayments. The changes also streamline dependency management for React across the project and include a minor fix for iterator handling.

Highlights

  • PDF Card Statement Generation: Implemented a new feature to generate and serve PDF card statements, accessible via the activity API when a 'maturity' query parameter and 'application/pdf' Accept header are provided.
  • Activity API Enhancements: Extended the activity API to allow filtering of card-related activities by 'maturity' and to support the new PDF statement output.
  • Dependency Management Update: Centralized React and React-DOM dependency versions across the monorepo by utilizing pnpm workspace catalog features.
  • Bug Fix: Iterator Consumption: Addressed a bug to ensure iterators are consumed only once, improving data processing reliability.
Changelog
  • .changeset/stale-owls-work.md
    • Added a new changeset entry for the card statement feature.
  • .changeset/upset-eels-ask.md
    • Added a new changeset entry for consuming iterators once.
  • package.json
    • Updated 'react' and 'react-dom' dependencies to reference the pnpm workspace catalog.
  • pnpm-workspace.yaml
    • Defined 'react' and 'react-dom' versions within the workspace catalog for centralized management.
  • server/api/activity.ts
    • Imported PDF rendering utilities and the new Statement component.
    • Added 'maturity' as an optional query parameter for activity filtering.
    • Modified card activity queries to filter by 'maturity' when specified.
    • Integrated logic to generate and return a PDF statement when 'maturity' is present and the 'Accept' header is 'application/pdf'.
    • Enhanced 'transformBorrow' function to include 'assets' and 'maturity' in the transformed output.
    • Removed redundant 'String()' cast for 'blockNumber' in activity ID generation.
    • Added a 'toArrayBuffer' utility function for buffer conversion.
  • server/package.json
    • Added '@react-pdf/renderer' dependency for PDF generation.
    • Added 'react' as a catalog dependency for server-side rendering.
  • server/test/api/activity.test.ts
    • Added new test cases to verify activity filtering by 'maturity'.
    • Added new test cases to confirm the successful generation and return of PDF statements.
  • server/test/utils/statement.test.ts
    • Added a new test file to validate the rendering of the Statement React component into a PDF buffer with various data scenarios.
  • server/tsconfig.json
    • Enabled JSX support for React components in the server's TypeScript configuration.
  • server/utils/Statement.tsx
    • Added a new React component that utilizes '@react-pdf/renderer' to structure and render card statement data into a PDF document.
  • src/utils/server.ts
    • Imported 'parseResponse' from 'hono/client' for improved response handling.
    • Updated the 'getActivity' function to use 'parseResponse' and ensure the returned data is an array.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

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 a new feature to generate card statements as PDFs, involving changes like adding a maturity filter to the activity endpoint, handling application/pdf requests to render and return a PDF statement using @react-pdf/renderer, and creating a new React component for the statement layout. A security review found no specific vulnerabilities in the analyzed code changes, including a SAST Reconnaissance Pass on server/api/activity.ts where no issues were identified. The suggestions to improve readability, maintainability, and robustness of the new data transformation logic in server/api/activity.ts and date formatting in the PDF statement remain valid. Overall, great work on implementing this feature.

Comment on lines 255 to 269
!ignore("card") && maturity
? await (async function () {
if (!borrows) return [];
const hashes = borrows
.entries()
.filter(([_, { events }]) => events.some(({ maturity: m }) => m === BigInt(maturity)))
.map(([hash]) => hash)
.toArray();
if (hashes.length === 0) return [];
const transactions = await database.query.transactions.findMany({
where: arrayOverlaps(transactionsSchema.hashes, hashes),
columns: { hashes: true, payload: true },
});
if (panda.success) return panda.output;

if (hashes.length !== 1) throw new Error("cryptomate transactions need to have only one hash");
const hash = hashes[0];
const borrow = borrows?.get(hash as Hash);
const cryptomate = safeParse(
{ 0: DebitActivity, 1: CreditActivity }[borrow?.events.length ?? 0] ?? InstallmentsActivity,
{
...(payload as object),
hash,
events: borrow?.events,
blockTimestamp: borrow?.blockNumber && timestamps.get(borrow.blockNumber),
},
);
if (cryptomate.success) return cryptomate.output;
captureException(new Error("bad transaction"), { level: "error", contexts: { cryptomate, panda } });
}),
),
...[...deposits, ...repays, ...withdraws].map(({ blockNumber, ...event }) => {
const timestamp = timestamps.get(blockNumber);
if (timestamp) return { ...event, timestamp: new Date(Number(timestamp) * 1000).toISOString() };
captureException(new Error("block not found"), {
level: "error",
contexts: { event: { ...event, timestamp } },
return [{ transactions }];
})()
Copy link

@gemini-code-assist gemini-code-assist bot Feb 12, 2026

Choose a reason for hiding this comment

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

medium

This immediately-invoked async function expression (IIAFE) contains significant logic for fetching card purchases based on maturity. To improve readability and separation of concerns, consider extracting this logic into a dedicated async function, for example getCardPurchasesForMaturity(borrows, maturity).

Comment on lines 325 to 371
data: response
.map((item) => {
if (item.type === "panda") {
return {
timestamp: item.timestamp,
description: `${item.merchant.name}${item.merchant.city ? `,${item.merchant.city}` : ""}`,
installments: item.operations
.reduce((accumulator, operation) => {
if ("borrow" in operation) {
if ("installments" in operation.borrow) {
const installments = operation.borrow.installments;
const n = installments.findIndex((installment) => installment.maturity === Number(maturity));
const progress = `${n + 1}/${installments.length}`;
const status = accumulator.get(progress) ?? {
current: n + 1,
total: installments.length,
amount: 0,
};
status.amount += (installments[n]?.assets ?? 0) + (installments[n]?.fee ?? 0);
accumulator.set(progress, status);
} else {
const status = accumulator.get("1/1") ?? {
current: 1,
total: 1,
amount: 0,
};
status.amount += operation.borrow.assets + operation.borrow.fee;
accumulator.set("1/1", status);
}
}
return accumulator;
}, new Map<string, { amount: number; current: number; total: number }>())
.values()
.toArray(),
};
}
if (item.type === "repay") {
return {
timestamp: item.timestamp,
positionAmount: item.positionAmount,
amount: item.amount,
discount: ((item.positionAmount - item.amount) / item.positionAmount) * 100,
};
}
})
.filter((item) => item !== undefined),
};
Copy link

@gemini-code-assist gemini-code-assist bot Feb 12, 2026

Choose a reason for hiding this comment

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

medium

The logic to transform the response data into the format required for the statement is quite complex, especially for 'panda' type items, and is embedded within the main handler. To improve readability and testability, consider extracting the transformation logic for each activity type into separate helper functions. You could then use flatMap to build the final data array, which would make the code cleaner and easier to maintain.

Comment on lines 92 to 93
const dueDate = new Date(statement.maturity * 1000).toLocaleDateString("en-CA");
const statementDate = new Date((statement.maturity - MATURITY_INTERVAL) * 1000).toLocaleDateString("en-CA");
Copy link

@gemini-code-assist gemini-code-assist bot Feb 12, 2026

Choose a reason for hiding this comment

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

medium

Using toLocaleDateString('en-CA') to format dates as YYYY-MM-DD is a clever trick, but it relies on locale-specific behavior that might not be consistent across all environments, making it potentially fragile. For more robust and explicit date formatting, consider a manual approach, for example by constructing the date string from getFullYear(), getMonth(), and getDate(). This would be more resilient to environment differences.

Copy link
Contributor Author

@nfmelendez nfmelendez Feb 13, 2026

Choose a reason for hiding this comment

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

could you explain more what you mean by " locale-specific behavior "

Copy link
Member

@cruzdanilo cruzdanilo Feb 13, 2026

Choose a reason for hiding this comment

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

@gemini-code-assist could you explain more what you mean by "locale-specific behavior"

Copy link

@gemini-code-assist gemini-code-assist bot Feb 13, 2026

Choose a reason for hiding this comment

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

You're right to ask for clarification! When I refer to "locale-specific behavior" with toLocaleDateString(), I mean that the exact output format can vary depending on the JavaScript engine, operating system, or even user settings where the code is executed.

While "en-CA" is generally understood to produce YYYY-MM-DD (e.g., 2026年02月13日), this is not a strict guarantee by the ECMAScript specification. The specification allows implementations to define their own locale data. For example:

  • Different JavaScript engines: A date formatted in Node.js might look slightly different than in a browser (Chrome, Firefox, Safari) or on a different operating system (Windows, macOS, Linux).
  • Version differences: Newer versions of Node.js or browsers might update their locale data, potentially leading to subtle changes in formatting.
  • User settings: In some environments, user-level locale preferences might override the "en-CA" locale's default date format.

This means that while it works as expected in most common scenarios, there's a small but non-zero risk that in a specific, less common environment, toLocaleDateString("en-CA") might produce something like YYYY/MM/DD or DD-MM-YYYY, which would break the expected YYYY-MM-DD format. For a critical output like a PDF statement, where consistency is paramount, relying on such an implicit behavior can be fragile.

That's why a manual approach, constructing the date string explicitly from getFullYear(), getMonth(), and getDate(), offers more control and guarantees the exact YYYY-MM-DD format regardless of the execution environment.

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: 12

🤖 Fix all issues with AI agents
In `@pnpm-workspace.yaml`:
- Around line 14-15: The react version is pinned to "19.1.0" while other
packages use caret ranges; either make the pin intentional or change to a caret
range—update the react entry (and by extension react-dom which references it) in
pnpm-workspace.yaml to use a caret range (e.g., "^19.1.0") if you want normal
semver updates, or add a short comment/docstring indicating that keeping react
at the exact "19.1.0" is deliberate to prevent accidental upgrades.
In `@server/api/activity.ts`:
- Around line 326-370: The map callback for the array mapping (the .map(...)
that checks item.type === "panda" and "repay") can return undefined for other
item types; replace this .map with .flatMap and ensure the callback returns an
array: return [pandaObject] for "panda", [repayObject] for "repay", and [] for
all other cases so no implicit undefineds remain; keep the existing logic
building the panda object (including the installments accumulator and
Map.values().toArray()) and the repay object (positionAmount/amount/discount)
but wrap each in an array when returning from the callback.
- Around line 361-367: The repay branch returns a discount computed as
((item.positionAmount - item.amount) / item.positionAmount) * 100 which can
divide by zero; update the repay case in the activity mapping (the branch
checking item.type === "repay") to guard against positionAmount === 0 by
computing discount only when positionAmount is nonzero (e.g., set discount to 0
or null when item.positionAmount === 0) and otherwise calculate the percentage
as before; ensure you reference item.positionAmount and item.amount in the
guarded expression so no division by zero occurs.
- Around line 69-72: The request schema allows any string for the "maturity"
field which later is passed to BigInt() and Number(), so validate it at the
schema level: update the optional(object(...)) schema that defines maturity to
use a numeric-string validator (e.g., replace optional(string()) with a
regex/number-string validator) so only decimal-integer strings (or an accepted
numeric format) are allowed; also add the regex import to the valibot imports
(per comment) and keep the field name "maturity" and surrounding schema
structure intact so BigInt(maturity) and Number(maturity) calls downstream won't
throw on invalid input.
- Around line 334-343: The code uses findIndex to locate an installment (in
server/api/activity.ts) and then builds progress and accesses installments[n]
without checking for a not-found (-1) result; update the block that computes n
(the findIndex on operation.borrow.installments) to detect n === -1 and skip
processing or continue (i.e., do not add a progress entry or modify the
accumulator) when no matching maturity is found, ensuring you do not compute
progress = `${n+1}/${...}` or access installments[n] when n is -1; apply this
guard around the logic that creates/updates status and amount so only valid
installment matches are aggregated.
- Around line 272-315: The map callbacks inside the response construction (the
cardPurchases.flatMap(... transactions.map(...)) branch and the [...deposits,
...repays, ...withdraws].map(...) branch) sometimes fall through to
captureException and implicitly return undefined, triggering the linter; change
those inner .map usages to .flatMap and, where you currently call
captureException, return an empty array ([]) instead of falling through so the
flatMap removes the bad item inline; keep the outer cardPurchases.flatMap and
replace the deposits/repays/withdraws .map with .flatMap as well, using the same
pattern (return the mapped object on success or [] after captureException) so
the subsequent filter step becomes unnecessary.
In `@server/test/api/activity.test.ts`:
- Around line 213-235: The test duplicates maturity extraction and risks
producing "undefined" by using String(withBorrow && withBorrow.borrow.maturity);
change to assert and use a non-null assertion when reading the maturity (e.g.,
use withBorrow!.borrow.maturity) or extract a small helper
(getBorrowMaturity(from activity) or a local const getMaturity) to compute
maturity once, then pass String(getMaturity(withBorrow!)) into the request;
update the occurrences around the withBorrow variable and the maturity const in
the tests (the "filters by maturity" and the similar "returns statement pdf"
block) so you no longer use the && pattern and avoid duplicated extraction
logic.
In `@server/tsconfig.json`:
- Line 3: The tsconfig currently sets "jsx": "react" but the "include" globs
only match *.ts so .tsx files like server/utils/Statement.tsx are excluded;
update the tsconfig "include" array to add matching .tsx patterns (e.g., add
"*.tsx" or directory-specific "*.tsx" entries for any folders that contain React
components) so the TypeScript compiler picks up .tsx files, ensuring the "jsx"
option is effective and future .tsx files are compiled.
In `@server/utils/Statement.tsx`:
- Around line 79-91: The default-exported React component Statement is currently
defined below supporting details (like styles); move the Statement
declaration/export so the default export is at the top of the file above any
helpers/styles. Specifically, place the Statement function (the component that
accepts props { data, lastFour, maturity }) and its export before the styles
object and any helper functions, and apply the same change for the other default
export referenced around line ~199 so all default exports appear at the top with
supporting details below.
- Around line 94-112: Extract the duplicated inline union member types into
named types (e.g., RepaymentItem and PurchaseItem) and use those types both in
the component props union and in the type guard predicates used for repayments
and purchases; update the predicates that currently assert "positionAmount" in
item and "description" in item to be typed as (item): item is RepaymentItem and
(item): item is PurchaseItem respectively, and replace the repeated inline shape
definitions wherever they appear (including the props union and the
repayments/purchases filters) to reference the new named types.
- Around line 152-156: The discount chip currently renders even when
item.discount is 0; update the JSX that renders the <View
style={styles.discountChip}> / <Text style={styles.discountText}> so it is
conditionally rendered only when item.discount is greater than 0 (e.g., guard
with item.discount > 0 or Boolean(item.discount) around that block). Locate the
rendering block in Statement.tsx (the View with flexDirection row that contains
styles.discountChip and styles.discountText) and wrap it in a conditional check
to prevent "0.00% discount applied" from showing.
In `@src/utils/server.ts`:
- Around line 240-241: The current code calls parseResponse(response) and then
converts any non-array result to [] which can silently swallow errors and bypass
the file's usual APIError handling; update the flow to first check response.ok
and, if false, parse the error body and throw an APIError (preserving
status/message), otherwise call parseResponse(response) and return the parsed
value only if Array.isArray(parsed) (or throw/convert explicitly), referencing
the existing APIError class, the parseResponse function, and the response.ok
boolean so callers receive consistent structured errors instead of opaque or
empty arrays.

@@ -1,5 +1,6 @@
{
"compilerOptions": {
"jsx": "react",
Copy link

@coderabbitai coderabbitai bot Feb 12, 2026

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:

# Read the tsconfig.json file to verify the structure and content
cat -n server/tsconfig.json

Repository: exactly/exa

Length of output: 981


🏁 Script executed:

# Check for any .tsx files in the server directory that would be missed
fd -e tsx . server/

Repository: exactly/exa

Length of output: 81


.tsx files are excluded from compilation.

"jsx": "react" is added but every include glob uses *.ts, which does not match *.tsx. server/utils/Statement.tsx (and any future .tsx files) will be invisible to tsc.

Proposed fix — add tsx globs
 "include": [
 "api/**/*.ts",
+ "api/**/*.tsx",
 "database/**/*.ts",
 "hooks/**/*.ts",
 "middleware/**/*.ts",
 "utils/**/*.ts",
+ "utils/**/*.tsx",
 "script/**/*.ts",
 "test/**/*.ts",
+ "test/**/*.tsx",
 "index.ts",
 "instrument.cjs",
 "*.config.ts",

Alternatively, a single wildcard per directory (e.g. "utils/**/*.{ts,tsx}") keeps it compact, but tsconfig doesn't support brace expansion — so individual entries are needed.

🤖 Prompt for AI Agents
In `@server/tsconfig.json` at line 3, The tsconfig currently sets "jsx": "react"
but the "include" globs only match *.ts so .tsx files like
server/utils/Statement.tsx are excluded; update the tsconfig "include" array to
add matching .tsx patterns (e.g., add "*.tsx" or directory-specific "*.tsx"
entries for any folders that contain React components) so the TypeScript
compiler picks up .tsx files, ensuring the "jsx" option is effective and future
.tsx files are compiled.

Comment on lines 79 to 91
const Statement = (statement: {
data: (
| {
amount: number;
discount: number;
positionAmount: number;
timestamp: string;
}
| { description: string; installments: { amount: number; current: number; total: number }[]; timestamp: string }
)[];
lastFour: string;
maturity: number;
}) => {
Copy link

@coderabbitai coderabbitai bot Feb 12, 2026

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Default export should be at the top of the file.

Per coding guidelines, the default export goes at the top with supporting details (styles, helpers) below. Move Statement above styles.

As per coding guidelines: "place the default export at the top of the file; place supporting details and extracted functions at the bottom."

Also applies to: 199-199

🤖 Prompt for AI Agents
In `@server/utils/Statement.tsx` around lines 79 - 91, The default-exported React
component Statement is currently defined below supporting details (like styles);
move the Statement declaration/export so the default export is at the top of the
file above any helpers/styles. Specifically, place the Statement function (the
component that accepts props { data, lastFour, maturity }) and its export before
the styles object and any helper functions, and apply the same change for the
other default export referenced around line ~199 so all default exports appear
at the top with supporting details below.

Comment on lines 94 to 112
const repayments = statement.data.filter(
(
item,
): item is {
amount: number;
discount: number;
positionAmount: number;
timestamp: string;
} => "positionAmount" in item,
);
const purchases = statement.data.filter(
(
item,
): item is {
description: string;
installments: { amount: number; current: number; total: number }[];
timestamp: string;
} => "description" in item,
);
Copy link

@coderabbitai coderabbitai bot Feb 12, 2026

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Duplicated type definitions — extract shared types.

The union members in the props type (lines 80-88) are repeated verbatim in the type guard predicates (lines 96-102, 107-110). Extract them once and reference in both places.

Proposed approach
+interface Repayment { amount: number; discount: number; positionAmount: number; timestamp: string }
+interface Purchase { description: string; installments: { amount: number; current: number; total: number }[]; timestamp: string }
+
 const Statement = (statement: {
- data: (
- | {
- amount: number;
- discount: number;
- positionAmount: number;
- timestamp: string;
- }
- | { description: string; installments: { amount: number; current: number; total: number }[]; timestamp: string }
- )[];
+ data: (Repayment | Purchase)[];
 lastFour: string;
 maturity: number;
 }) => {
 ...
- const repayments = statement.data.filter(
- (
- item,
- ): item is {
- amount: number;
- discount: number;
- positionAmount: number;
- timestamp: string;
- } => "positionAmount" in item,
- );
- const purchases = statement.data.filter(
- (
- item,
- ): item is {
- description: string;
- installments: { amount: number; current: number; total: number }[];
- timestamp: string;
- } => "description" in item,
- );
+ const repayments = statement.data.filter((item): item is Repayment => "positionAmount" in item);
+ const purchases = statement.data.filter((item): item is Purchase => "description" in item);
📝 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
const repayments = statement.data.filter(
(
item,
): item is {
amount: number;
discount: number;
positionAmount: number;
timestamp: string;
} => "positionAmount" in item,
);
const purchases = statement.data.filter(
(
item,
): item is {
description: string;
installments: { amount: number; current: number; total: number }[];
timestamp: string;
} => "description" in item,
);
interface Repayment { amount: number; discount: number; positionAmount: number; timestamp: string }
interface Purchase { description: string; installments: { amount: number; current: number; total: number }[]; timestamp: string }
const Statement = (statement: {
data: (Repayment | Purchase)[];
lastFour: string;
maturity: number;
}) => {
// ... existing code ...
const repayments = statement.data.filter((item): item is Repayment => "positionAmount" in item);
const purchases = statement.data.filter((item): item is Purchase => "description" in item);
🤖 Prompt for AI Agents
In `@server/utils/Statement.tsx` around lines 94 - 112, Extract the duplicated
inline union member types into named types (e.g., RepaymentItem and
PurchaseItem) and use those types both in the component props union and in the
type guard predicates used for repayments and purchases; update the predicates
that currently assert "positionAmount" in item and "description" in item to be
typed as (item): item is RepaymentItem and (item): item is PurchaseItem
respectively, and replace the repeated inline shape definitions wherever they
appear (including the props union and the repayments/purchases filters) to
reference the new named types.

Comment on lines +152 to +156
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
<View style={styles.discountChip}>
<Text style={styles.discountText}>{item.discount.toFixed(2)}% discount applied</Text>
</View>
</View>
Copy link

@coderabbitai coderabbitai bot Feb 12, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Discount chip renders even when discount is 0.

When item.discount is 0, this renders "0.00% discount applied" — likely not the desired UX. Guard on a non-zero discount.

Proposed fix
- <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
- <View style={styles.discountChip}>
- <Text style={styles.discountText}>{item.discount.toFixed(2)}% discount applied</Text>
+ {item.discount > 0 && (
+ <View style={styles.discountChip}>
+ <Text style={styles.discountText}>{item.discount.toFixed(2)}% discount applied</Text>
 </View>
- </View>
+ )}
📝 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
<Viewstyle={{flexDirection: "row",alignItems: "center",gap: 6}}>
<View style={styles.discountChip}>
<Text style={styles.discountText}>{item.discount.toFixed(2)}% discount applied</Text>
</View>
</View>
{item.discount>0&&(
<View style={styles.discountChip}>
<Text style={styles.discountText}>{item.discount.toFixed(2)}% discount applied</Text>
</View>
)}
🤖 Prompt for AI Agents
In `@server/utils/Statement.tsx` around lines 152 - 156, The discount chip
currently renders even when item.discount is 0; update the JSX that renders the
<View style={styles.discountChip}> / <Text style={styles.discountText}> so it is
conditionally rendered only when item.discount is greater than 0 (e.g., guard
with item.discount > 0 or Boolean(item.discount) around that block). Locate the
rendering block in Statement.tsx (the View with flexDirection row that contains
styles.discountChip and styles.discountText) and wrap it in a conditional check
to prevent "0.00% discount applied" from showing.

Comment on lines +240 to +241
const parsed = await parseResponse(response);
return Array.isArray(parsed) ? parsed : [];
Copy link

@coderabbitai coderabbitai bot Feb 12, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Errors are silently swallowed.

Every other function in this file throws APIError when response.ok is false. Here, parseResponse on a non-ok response will either throw internally (Hono's behavior) or return a non-array error body — which the Array.isArray guard silently converts to []. If parseResponse does throw, the caller gets an opaque error instead of a structured APIError.

Consider preserving the existing error-handling pattern for consistency and debuggability:

Proposed approach
 const response = await api.activity.$get(
 parameters?.include === undefined ? undefined : { query: { include: parameters.include } },
 );
- const parsed = await parseResponse(response);
- return Array.isArray(parsed) ? parsed : [];
+ if (!response.ok) throw new APIError(response.status, stringOrLegacy(await response.json()));
+ return response.json();

If parseResponse is specifically needed for the new response shape, at minimum guard response.ok first.

🤖 Prompt for AI Agents
In `@src/utils/server.ts` around lines 240 - 241, The current code calls
parseResponse(response) and then converts any non-array result to [] which can
silently swallow errors and bypass the file's usual APIError handling; update
the flow to first check response.ok and, if false, parse the error body and
throw an APIError (preserving status/message), otherwise call
parseResponse(response) and return the parsed value only if
Array.isArray(parsed) (or throw/convert explicitly), referencing the existing
APIError class, the parseResponse function, and the response.ok boolean so
callers receive consistent structured errors instead of opaque or empty arrays.

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: 4

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: 4

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

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: 4

Comment on lines +7 to +68
describe("statement rendering", () => {
it("renders with purchases", async () => {
const pdf = await renderToBuffer(
Statement({
data: [
{
id: "purchase-1",
description: "grocery store",
installments: [{ amount: 50.25, current: 1, total: 3 }],
timestamp: "2025年12月19日T11:35:11.030Z",
},
{
id: "purchase-2",
description: "gas station",
installments: [{ amount: 30.5, current: 2, total: 2 }],
timestamp: "2025年12月19日T11:22:49.412Z",
},
],
lastFour: "1234",
maturity: 1_768_435_200,
}),
);
expect(pdf.byteLength).toBeGreaterThan(0);
});

it("renders with repayments", async () => {
const pdf = await renderToBuffer(
Statement({
data: [
{ id: "repay-1", amount: 100, discount: 5.5, positionAmount: 105.82, timestamp: "2025年12月19日T11:35:11.030Z" },
],
lastFour: "1234",
maturity: 1_768_435_200,
}),
);
expect(pdf.byteLength).toBeGreaterThan(0);
});

it("renders with empty data", async () => {
const pdf = await renderToBuffer(Statement({ data: [], lastFour: "", maturity: 1_768_435_200 }));
expect(pdf.byteLength).toBeGreaterThan(0);
});

it("renders with both purchases and repayments", async () => {
const pdf = await renderToBuffer(
Statement({
data: [
{
id: "purchase-3",
description: "online purchase",
installments: [{ amount: 75, current: 1, total: 1 }],
timestamp: "2025年12月19日T11:35:11.030Z",
},
{ id: "repay-2", amount: 200, discount: 3.2, positionAmount: 206.6, timestamp: "2025年12月20日T10:00:00.000Z" },
],
lastFour: "5678",
maturity: 1_768_435_200,
}),
);
expect(pdf.byteLength).toBeGreaterThan(0);
});
});
Copy link

@coderabbitai coderabbitai bot Feb 17, 2026

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Tests are smoke tests only — consider adding at least one content assertion.

All four tests only check pdf.byteLength > 0. While PDF content validation is harder, you could parse the buffer with a PDF library (e.g., pdf-parse) to verify that expected text like "Card Statement", "Payments", or "Purchases" appears in the output. This would catch rendering regressions where the component silently produces an empty or broken document.

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

Reviewers

@cruzdanilo cruzdanilo cruzdanilo left review comments

@aguxez aguxez aguxez left review comments

@coderabbitai coderabbitai[bot] coderabbitai[bot] left review comments

@dieguezguille dieguezguille Awaiting requested review from dieguezguille dieguezguille will be requested when the pull request is marked ready for review dieguezguille is a code owner

@franm91 franm91 Awaiting requested review from franm91 franm91 will be requested when the pull request is marked ready for review franm91 is a code owner

+1 more reviewer

@gemini-code-assist gemini-code-assist[bot] gemini-code-assist[bot] left review comments

Reviewers whose approvals may not affect merge requirements

At least 2 approving reviews are required to merge this pull request.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

AltStyle によって変換されたページ (->オリジナル) /