-
Notifications
You must be signed in to change notification settings - Fork 2
Conversation
🦋 Changeset detectedLatest commit: 1e41d04 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds maturity-aware activity filtering and optional PDF statement generation: new React-PDF Statement component, API changes to accept Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
✅ All tests passed.
Summary of ChangesHello @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
Changelog
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this 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.
server/api/activity.ts
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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).
server/api/activity.ts
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
server/utils/Statement.tsx
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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 "
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Read the tsconfig.json file to verify the structure and content
cat -n server/tsconfig.jsonRepository: 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.
server/utils/Statement.tsx
Outdated
There was a problem hiding this comment.
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.
server/utils/Statement.tsx
Outdated
There was a problem hiding this comment.
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.
🤖 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
🤖 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
630b065 to
f2d5d12
Compare
There was a problem hiding this 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
f2d5d12 to
86aef9f
Compare
There was a problem hiding this 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
86aef9f to
60e5c23
Compare
There was a problem hiding this 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
60e5c23 to
5986cd2
Compare
9eaf944 to
072708d
Compare
There was a problem hiding this 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
There was a problem hiding this comment.
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.
5986cd2 to
1e41d04
Compare
Uh oh!
There was an error while loading. Please reload this page.
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Chores