Skip to content

Conversation

@aguxez
Copy link
Contributor

@aguxez aguxez commented Jan 22, 2026


Open with Devin

Summary by CodeRabbit

  • New Features

    • Automatic provisioning of travel insurance when a user upgrades from Platinum to Signature during card issuance; provisioning failures no longer block card creation.
  • Tests

    • Added comprehensive tests for upgrade flows, integration error handling, missing-account scenarios, and strengthened mocks/typing for related integrations.
  • Chores

    • Added patch release changeset.

@changeset-bot
Copy link

changeset-bot bot commented Jan 22, 2026

🦋 Changeset detected

Latest commit: b65a3b2

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

@gemini-code-assist
Copy link

Summary of Changes

Hello @aguxez, 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 enhances the server's card issuance API by integrating with PAX for users who are upgrading from a Platinum card to a Signature card. The system now identifies these upgrade scenarios, retrieves necessary user information from Persona, and then registers the user with PAX for associated benefits, all while ensuring that the card issuance flow remains resilient to external service failures.

Highlights

  • PAX Integration for Platinum to Signature Upgrades: Implemented logic to automatically enroll users in PAX when they upgrade their card from Platinum to Signature. This involves checking for a previously deleted Platinum card associated with the user's credential.
  • Persona Data Retrieval: A new function, addCapitaForPlatinumUpgrade, was introduced to fetch user KYC (Know Your Customer) data (such as name, birthdate, document ID, email, and phone) from the Persona API. This data is then used to register the user with PAX for services like travel insurance.
  • Robust Error Handling: The integration includes comprehensive error handling for calls to both Persona and PAX APIs. If these external API calls fail or if Persona account data is missing, the core card issuance process will still succeed, and errors are captured via Sentry.
  • Comprehensive Testing: Extensive unit tests have been added to cover various scenarios, including successful upgrades, new card issuances (no upgrade), and error conditions from the PAX and Persona APIs, ensuring the feature's reliability.
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.

@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

 ______________________________________________________________________________________________________________________________________________________________
< Find bugs once. Once a human tester finds a bug, it should be the last time a human tester finds that bug. Automatic tests should check for it from then on. >
 --------------------------------------------------------------------------------------------------------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ

✏️ Tip: You can disable in-progress messages and the fortune message in your review settings.

Tip

CodeRabbit can enforce grammar and style rules using `languagetool`.

Configure languagetool in your project's settings in CodeRabbit enable/disable rules and categories. Refer to the LanguageTool Community to learn more.

Walkthrough

Detects signature-card issuance following a previously deleted platinum card; when detected, fetches persona account, derives an internalId, and calls PAX addCapita during card issuance. Adds typed PAX test mocks, new tests covering upgrade/non-upgrade/error cases, and a changeset entry.

Changes

Cohort / File(s) Summary
Release Metadata
\.changeset/wise-comics-know.md
New changeset file for a patch release of @exactly/server noting the PAX-related platinum→signature upgrade behavior.
Core Implementation
server/api/card.ts
Imports PLATINUM_PRODUCT_ID, detects deleted-platinum→signature upgrade flows, adds addCapitaForPlatinumUpgrade() (persona account lookup, ID doc validation, derive internalId, call addCapita), conditionally invokes it after issuance, and reports errors to Sentry; minor formatting tweaks.
Tests & Mocks
server/test/api/card.test.ts, server/test/mocks/pax.ts
Introduces strongly-typed PAX mocks (addCapita, removeCapita), replaces teardown with vi.resetAllMocks(), and adds extensive tests for upgrade flow, non-upgrade flow, Pax errors, and missing persona account scenarios.

Sequence Diagram

sequenceDiagram
    rect rgba(220,220,255,0.5)
    participant Client as Client
    participant CardAPI as Card API
    participant DB as Database
    participant Persona as Persona Service
    participant PAX as PAX Service
    participant Sentry as Sentry
    end

    Client->>CardAPI: Request signature card (POST/GET)
    CardAPI->>DB: Query cards for credentialId (include deleted/platinum)
    DB-->>CardAPI: Card list (deleted platinum found / not)
    alt Upgrade detected
        CardAPI->>Persona: getAccount(credentialId)
        Persona-->>CardAPI: Account + persona data
        CardAPI->>CardAPI: Validate identity, derive internalId
        CardAPI->>PAX: addCapita({ internalId, travelInsurance, productId })
        alt PAX success
            PAX-->>CardAPI: OK
        else PAX error
            PAX-->>CardAPI: Error
            CardAPI->>Sentry: captureException(error, { credentialId, account, productId })
        end
    end
    CardAPI->>DB: Insert/fetch signature card
    CardAPI-->>Client: Return card data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • nfmelendez
  • 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 title accurately describes the main change: adding pax functionality for platinum card upgrades to signature cards, which aligns with the code changes across card.ts, card.test.ts, and the changeset.

✏️ 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 pax-card-upgrade

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

The pull request introduces functionality to track platinum to signature card upgrades by integrating with the PAX system. The changes include adding logic to detect such upgrades, calling an external API (addCapita) with user data, and comprehensive test cases covering various scenarios, including successful integration, no-upgrade cases, and error handling for external API calls. The addition of vi.clearAllMocks() in the test suite's afterEach hook is a good practice for ensuring test isolation and reliability.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional flags.

Open in Devin Review

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

🤖 Fix all issues with AI agents
In `@server/api/card.ts`:
- Around line 383-385: The call to addCapitaForPlatinumUpgrade(credentialId,
account) is intended as fire-and-forget but currently returns a floating
promise; prefix the call with the void operator to make the intent explicit and
satisfy linters (i.e., change the invocation inside the isUpgradeFromPlatinum
block to void addCapitaForPlatinumUpgrade(credentialId, account)). Ensure you do
not await it and keep any surrounding logic unchanged so card creation remains
non-blocking.

In `@server/test/mocks/pax.ts`:
- Around line 3-4: The imports in server/test/mocks/pax.ts are out of order;
move external-library imports before relative imports so "valibot" (InferInput)
is imported before the local "../../utils/pax" (CapitaRequest), ensuring import
order follows eslint-plugin-import conventions (external libs first, then
relative paths) and updating any surrounding import groups similarly.

@sentry
Copy link

sentry bot commented Jan 22, 2026

Codecov Report

❌ Patch coverage is 88.88889% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.85%. Comparing base (aa89a99) to head (7643409).

Files with missing lines Patch % Lines
server/api/card.ts 88.88% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #670      +/-   ##
==========================================
+ Coverage   65.82%   65.85%   +0.02%     
==========================================
  Files         190      190              
  Lines        5996     6012      +16     
  Branches     1734     1738       +4     
==========================================
+ Hits         3947     3959      +12     
- Misses       1870     1871       +1     
- Partials      179      182       +3     
Flag Coverage Δ
e2e 47.20% <0.00%> (-0.23%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View issue and 5 additional flags in Devin Review.

Open in Devin Review

@aguxez aguxez force-pushed the pax-card-upgrade branch 3 times, most recently from 8f1e6a5 to 1779bbd Compare January 27, 2026 13:05
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: 1

🤖 Fix all issues with AI agents
In `@server/api/card.ts`:
- Around line 334-355: The lookup for deletedPlatinumCard is using the
module-level database client instead of the request context DB; replace the call
to database.query.cards.findFirst with c.var.db.query.cards.findFirst (keeping
the same where/filter using cards, credentialId and PLATINUM_PRODUCT_ID) so the
route uses the injected DB client; also update the other similar lookup(s)
around the later block (the occurrences mentioned at 369-372) to use
c.var.db.query.* instead of database.query.* (ensure you keep the same
columns/where predicates and variable names like deletedPlatinumCard and
isUpgradeFromPlatinum).

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

🤖 Fix all issues with AI agents
In `@server/test/api/card.test.ts`:
- Around line 83-86: The afterEach cleanup currently calls both
vi.restoreAllMocks() and vi.clearAllMocks() in the test file; remove the
redundant vi.clearAllMocks() call and keep only vi.restoreAllMocks() in the
afterEach block so mocks are restored and state cleared by restoreAllMocks()
(update the afterEach function that contains vi.restoreAllMocks() and
vi.clearAllMocks()).

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

🤖 Fix all issues with AI agents
In `@server/test/api/card.test.ts`:
- Around line 366-367: The inline comment above the expectation uses an
uppercase word "NOT" which violates the repo comment casing rule; edit the
comment preceding the expect(pax.addCapita).not.toHaveBeenCalled() assertion so
all words are lowercase (e.g., "pax should not be called for new users (no
upgrade)"), and apply the same lowercase fix to the other offending comment at
the mentioned location (lines around the second occurrence referencing
pax.addCapita).
- Around line 472-474: The test currently mocks persona.getAccount with a null
cast which violates its declared return type; update the vi.spyOn(...,
"getAccount").mockResolvedValueOnce(...) call to pass undefined (no casts) so it
matches Promise<AccountOutput<T> | undefined> and preserves type safety—i.e.,
replace the null as unknown as ... line with mockResolvedValueOnce(undefined).

@aguxez aguxez force-pushed the pax-card-upgrade branch 2 times, most recently from 501509c to 5cef43c Compare January 30, 2026 17:02
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: 1

🤖 Fix all issues with AI agents
In `@server/test/api/card.test.ts`:
- Line 472: The test uses vi.spyOn(persona,
"getAccount").mockResolvedValueOnce() without an argument causing TS2554; update
the mock call to pass the expected return value explicitly (pass undefined) by
changing the mockResolvedValueOnce invocation for persona.getAccount to
mockResolvedValueOnce(undefined) so it matches the function signature
Promise<AccountOutput<T> | undefined>.

@aguxez aguxez force-pushed the pax-card-upgrade branch 4 times, most recently from a8e392a to 353edeb Compare February 2, 2026 13:36
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: 1

🤖 Fix all issues with AI agents
In `@server/test/mocks/pax.ts`:
- Around line 8-11: The mock for removeCapita is a Promise<void> but calls
mockResolvedValue() with no argument, which fails strict TypeScript typing;
update the removeCapita mock (the vi.fn<(internalId: string) => Promise<void>>()
mock) to call .mockResolvedValue(undefined) so the resolved value matches
Awaited<ReturnType<...>>; leave addCapita as-is.

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

🤖 Fix all issues with AI agents
In `@server/test/api/card.test.ts`:
- Around line 440-466: Update the "handles missing persona account during
signature card creation" test to explicitly mock persona.getAccount to return
undefined so the test doesn't depend on default mock behavior: inside that it
block add a spy/mock for getAccount (or the persona service method named
getAccount) and have it resolve to undefined before calling
appClient.index.$post, leaving existing expectations (e.g., vi.spyOn(pax,
"addCapita") and vi.spyOn(panda, "createCard")) unchanged.

Comment on lines +440 to +466
it("handles missing persona account during signature card creation", async () => {
const testCredentialId = "no-account-test";
await database.insert(credentials).values({
id: testCredentialId,
publicKey: new Uint8Array(),
account: padHex("0x666", { size: 20 }),
factory: inject("ExaAccountFactory"),
pandaId: "no-account-panda",
});

await database.insert(cards).values({
id: "old-platinum-card-no-account",
credentialId: testCredentialId,
lastFour: "0000",
status: "DELETED",
productId: PLATINUM_PRODUCT_ID,
});

vi.spyOn(pax, "addCapita").mockResolvedValueOnce({});
vi.spyOn(panda, "createCard").mockResolvedValueOnce({ ...cardTemplate, id: "no-account-card", last4: "7777" });

const response = await appClient.index.$post({ header: { "test-credential-id": testCredentialId } });

expect(response.status).toBe(200);

expect(pax.addCapita).not.toHaveBeenCalled();
});
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Make the missing-persona test explicit about getAccount returning undefined.

This test currently relies on the default persona mock behavior; an explicit mock keeps the intent stable if the default changes.

♻️ Suggested change
   await database.insert(cards).values({
     id: "old-platinum-card-no-account",
     credentialId: testCredentialId,
     lastFour: "0000",
     status: "DELETED",
     productId: PLATINUM_PRODUCT_ID,
   });

+  vi.spyOn(persona, "getAccount").mockResolvedValueOnce(undefined);
   vi.spyOn(pax, "addCapita").mockResolvedValueOnce({});
   vi.spyOn(panda, "createCard").mockResolvedValueOnce({ ...cardTemplate, id: "no-account-card", last4: "7777" });
📝 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
it("handles missing persona account during signature card creation", async () => {
const testCredentialId = "no-account-test";
await database.insert(credentials).values({
id: testCredentialId,
publicKey: new Uint8Array(),
account: padHex("0x666", { size: 20 }),
factory: inject("ExaAccountFactory"),
pandaId: "no-account-panda",
});
await database.insert(cards).values({
id: "old-platinum-card-no-account",
credentialId: testCredentialId,
lastFour: "0000",
status: "DELETED",
productId: PLATINUM_PRODUCT_ID,
});
vi.spyOn(pax, "addCapita").mockResolvedValueOnce({});
vi.spyOn(panda, "createCard").mockResolvedValueOnce({ ...cardTemplate, id: "no-account-card", last4: "7777" });
const response = await appClient.index.$post({ header: { "test-credential-id": testCredentialId } });
expect(response.status).toBe(200);
expect(pax.addCapita).not.toHaveBeenCalled();
});
it("handles missing persona account during signature card creation", async () => {
const testCredentialId = "no-account-test";
await database.insert(credentials).values({
id: testCredentialId,
publicKey: new Uint8Array(),
account: padHex("0x666", { size: 20 }),
factory: inject("ExaAccountFactory"),
pandaId: "no-account-panda",
});
await database.insert(cards).values({
id: "old-platinum-card-no-account",
credentialId: testCredentialId,
lastFour: "0000",
status: "DELETED",
productId: PLATINUM_PRODUCT_ID,
});
vi.spyOn(persona, "getAccount").mockResolvedValueOnce(undefined);
vi.spyOn(pax, "addCapita").mockResolvedValueOnce({});
vi.spyOn(panda, "createCard").mockResolvedValueOnce({ ...cardTemplate, id: "no-account-card", last4: "7777" });
const response = await appClient.index.$post({ header: { "test-credential-id": testCredentialId } });
expect(response.status).toBe(200);
expect(pax.addCapita).not.toHaveBeenCalled();
});
🤖 Prompt for AI Agents
In `@server/test/api/card.test.ts` around lines 440 - 466, Update the "handles
missing persona account during signature card creation" test to explicitly mock
persona.getAccount to return undefined so the test doesn't depend on default
mock behavior: inside that it block add a spy/mock for getAccount (or the
persona service method named getAccount) and have it resolve to undefined before
calling appClient.index.$post, leaving existing expectations (e.g.,
vi.spyOn(pax, "addCapita") and vi.spyOn(panda, "createCard")) unchanged.

@aguxez aguxez force-pushed the pax-card-upgrade branch 2 times, most recently from 04226a8 to 7643409 Compare February 2, 2026 16:23
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View issue and 13 additional flags in Devin Review.

Open in Devin Review

Comment on lines +35 to +36
import { addCapita, deriveAssociateId } from "../utils/pax";
import { getAccount } from "../utils/persona";

Choose a reason for hiding this comment

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

🔴 server can crash on startup because card route now imports pax module with required env vars

server/api/card.ts now imports addCapita/deriveAssociateId from ../utils/pax at module load time (server/api/card.ts:35-36). The pax module performs top-level environment variable checks and throws if any are missing (server/utils/pax.ts:19-26).

Actual behavior: in any environment where PAX_API_URL, PAX_API_KEY, or PAX_ASSOCIATE_ID_KEY are not set, the server process will throw during module evaluation and fail to boot, even if the card issuance flow never reaches the pax integration.

Expected behavior: pax provisioning should be an optional side-effect (as described in the PR) and must not prevent the API server from starting or serving other routes.

click to expand

Relevant code paths:

// server/api/card.ts
import { addCapita, deriveAssociateId } from "../utils/pax";
// server/utils/pax.ts
if (!process.env.PAX_API_URL) throw new Error("missing pax api url");
if (!process.env.PAX_API_KEY) throw new Error("missing pax api key");
if (!process.env.PAX_ASSOCIATE_ID_KEY) throw new Error("missing pax associate id secret");

Impact:

  • Deployment/startup failure if pax env isn’t present.
  • Contradicts the goal of “provisioning failures no longer block card creation”, since lack of configuration blocks the entire server.

Recommendation: Make pax configuration lazy/optional: e.g. move env checks inside addCapita/request, return a rejected promise with a clear error when misconfigured, or dynamically import pax inside addCapitaForPlatinumUpgrade and no-op (with a logged error) when env vars are missing.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

2 participants