Skip to content

✨ server: add card limit update handler#898

Open
aguxez wants to merge 2 commits intomainfrom
kyc-limit
Open

✨ server: add card limit update handler#898
aguxez wants to merge 2 commits intomainfrom
kyc-limit

Conversation

@aguxez
Copy link
Contributor

@aguxez aguxez commented Mar 19, 2026

Summary by CodeRabbit

  • New Features

    • Added a card-limit case: approved cases can trigger automatic card limit updates (amount applied in cents, weekly frequency) by resolving related inquiry references and updating active cards.
    • Added a new KYC scope "cardLimit" and inquiry flow to create or resume card-limit inquiries.
  • Bug Fixes / Reliability

    • Clear response codes and graceful handling for missing data or lookup failures; errors are captured without breaking the webhook.
  • Tests

    • Extensive tests covering card-limit flows, ignored inquiries, edge cases, and failure modes.
  • Chores

    • Added patch release metadata.

Open with Devin

This is part 1 of 2 in a stack made with GitButler:

@changeset-bot
Copy link

changeset-bot bot commented Mar 19, 2026

🦋 Changeset detected

Latest commit: 1dfb827

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, 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 new server-side capability to automatically adjust user card limits based on approved cases from the Persona identity verification platform. By integrating a dedicated webhook handler, the system can now process specific Persona events, validate the incoming data, and programmatically update card limits through an external API, streamlining the process of managing user financial controls.

Highlights

  • New Persona Webhook Handler: Implemented a new webhook handler for Persona events specifically designed to process 'card limit' cases. This handler will update card limits in the system based on approved Persona cases.
  • Card Limit Update Logic: Added logic to extract the new card limit from approved Persona cases, retrieve associated credential and card information from the database, and then call an external service (updateCard in Panda) to apply the new limit.
  • Enhanced Validation and Error Handling: Introduced new validation schemas for the 'card limit' case template and inquiry template. The handler includes robust error handling for scenarios like missing card limits, credentials, or active cards, and captures exceptions using Sentry.
  • Comprehensive Test Coverage: Added extensive unit tests to cover various scenarios for the new card limit update handler, including successful updates, handling of declined cases, missing data, and API failures.
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.

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 Mar 19, 2026

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 a new "cardLimit" Persona case flow: validation and template checks, inquiry fetch to obtain a reference-id, DB lookups for credential and active card, and conditional Panda card limit updates; includes new persona constants, helper, API handling, tests, and a changeset.

Changes

Cohort / File(s) Summary
Changesets & Release
\.changeset/clear-cobras-sip.md
New changeset declaring a patch release for @exactly/server with note "add card limit case update".
Persona Hook
server/hooks/persona.ts
Added cardLimit payload schema branch and handler: Sentry op, short-circuit responses for non-Approved statuses, extract inquiry id, call getInquiryById, DB queries for credential and ACTIVE card, conditional panda.updateCard with amount = cardLimitUsd * 100, and error capture. Also updated ignored-template allowlist handling.
Persona Utilities
server/utils/persona.ts
Added exports CARD_LIMIT_CASE_TEMPLATE, CARD_LIMIT_TEMPLATE, and getInquiryById(inquiryId) which performs Persona GET /inquiries/:id and validates presence of data.attributes["reference-id"].
KYC API
server/api/kyc.ts
Extended POST body validation to accept scope: "cardLimit" and added control flow for creating/resuming/failing card-limit inquiries using CARD_LIMIT_TEMPLATE. GET query validation also allows "cardLimit".
Tests — Persona Hook
server/test/hooks/persona.test.ts
Added comprehensive card limit case test suite, extended "with reference" tests, added mocks (e.g., persona.addDocument), updated invalid-payload expectations, and cleaned up cards rows in afterEach. Covers success, no-card, no-limit, no-credential, no-panda, updateCard failure, and ignored-template paths.
Tests — KYC API
server/test/api/kyc.test.ts
Added cardLimit scope tests for POST flow: creating new inquiry, resuming valid statuses, rejecting terminal/failed statuses, unknown credential case, and forwarding redirectURI to createInquiry.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Hook as Persona Hook
    participant Persona as Persona API
    participant DB as Database
    participant Panda as Panda Service

    Client->>Hook: POST webhook (data.type="case", template: cardLimit, status: Approved)
    Hook->>Hook: Validate payload & template
    Hook->>Persona: GET /inquiries/{inquiryId}
    Persona-->>Hook: { data.attributes["reference-id"]: referenceId }
    Hook->>DB: Query credentials where credentials.id = referenceId
    DB-->>Hook: credential { pandaId? }
    Hook->>DB: Query cards where cards.credentialId = referenceId AND status = "ACTIVE"
    DB-->>Hook: card { id } or none
    alt card exists
      Hook->>Panda: updateCard(cardId, { limit: { amount, frequency: "per7DayPeriod" } })
      Panda-->>Hook: success / error
    end
    Hook-->>Client: 200 { code: "ok" } or short-circuit codes ("no limit","no inquiry","no credential","no panda") / 500 on update error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • cruzdanilo
  • nfmelendez
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the primary change: adding a card limit update handler to the server.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch kyc-limit

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.

gemini-code-assist[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@sentry
Copy link

sentry bot commented Mar 20, 2026

Codecov Report

❌ Patch coverage is 91.66667% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.98%. Comparing base (d62c2a3) to head (1dfb827).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
server/api/kyc.ts 87.71% 3 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #898      +/-   ##
==========================================
+ Coverage   71.46%   71.98%   +0.52%     
==========================================
  Files         225      225              
  Lines        8165     8368     +203     
  Branches     2605     2703      +98     
==========================================
+ Hits         5835     6024     +189     
- Misses       2103     2108       +5     
- Partials      227      236       +9     
Flag Coverage Δ
e2e 71.95% <91.66%> (+0.50%) ⬆️

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:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@aguxez aguxez marked this pull request as ready for review March 20, 2026 11:28
chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

sentry[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: faf0a9e659

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


if (payload.template === "cardLimit") {
getActiveSpan()?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, "persona.case.card-limit");
if (payload.data.attributes.status !== "Approved") return c.json({ code: "ok" }, 200);

Choose a reason for hiding this comment

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

P1 Badge Persist non-approved card-limit case outcomes

Returning 200 for every non-Approved case here drops the only signal about the review result. The new /kyc card-limit endpoints still infer state only from getInquiry() (server/utils/persona.ts:47-57), so once the underlying inquiry becomes approved they report ok/already approved even if the case is still Open/Pending or was later Declined, and the card limit was never changed. In those review outcomes, the user gets a false success state and no way to start a replacement limit-increase flow.

Useful? React with 👍 / 👎.

Comment on lines +261 to +263
} catch (error: unknown) {
captureException(error, { level: "error", contexts: { details: { inquiryId } } });
return c.json({ code: "ok" }, 200);

Choose a reason for hiding this comment

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

P1 Badge Retry card-limit webhooks when inquiry lookup fails

This catch acknowledges the webhook on any getInquiryById() failure, including transient Persona 5xx/timeouts. That lookup is the only way this handler recovers the credential reference from the case payload, and the approved limit is not persisted anywhere else, so a temporary Persona outage here permanently drops the limit increase: Persona stops retrying, the card stays unchanged, and later /kyc calls still only see the historical inquiry. This path needs to stay retryable or persist the case for replay.

Useful? React with 👍 / 👎.

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 2 new potential issues.

View 7 additional findings in Devin Review.

Open in Devin Review

Comment on lines +261 to +263
} catch (error: unknown) {
captureException(error, { level: "error", contexts: { details: { inquiryId } } });
return c.json({ code: "ok" }, 200);

Choose a reason for hiding this comment

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

🚩 Webhook silently drops card limit update when getInquiryById fails

In the card limit webhook handler at server/hooks/persona.ts:261-263, when getInquiryById throws (e.g. transient network error), the error is caught, captured in Sentry, and a 200 response is returned. This tells Persona the webhook was processed successfully, so it won't retry. The card limit update is permanently lost unless manually addressed. By contrast, updateCard failures at line 281 intentionally propagate as 500s, triggering Persona retries. This asymmetry appears deliberate (perhaps because an invalid inquiry ID wouldn't benefit from retries), but a transient Persona API timeout would also be silently dropped.

Open in Devin Review

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

Comment on lines +77 to +79
case "failed":
case "declined":
return c.json({ code: "failed" }, 200);

Choose a reason for hiding this comment

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

🚩 GET and POST handlers return different status codes for failed/declined cardLimit inquiries

The GET handler at server/api/kyc.ts:77-79 returns status 200 for failed/declined cardLimit inquiries, while the POST handler at server/api/kyc.ts:201-202 returns 400 via the default case. This differs from non-cardLimit scopes where both GET and POST return 400 for failed/declined. The tests confirm this is intentional (GET tests assert 200, POST tests assert 400), but this asymmetry could confuse API consumers who expect consistent status codes across GET/POST for the same inquiry state.

Open in Devin Review

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

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 8 additional findings in Devin Review.

Open in Devin Review

Comment on lines +201 to +202
default:
return c.json({ code: "failed" }, 400);

Choose a reason for hiding this comment

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

🟡 POST cardLimit switch uses default as catch-all instead of explicitly listing failed/declined statuses

The POST handler's cardLimit switch statement (lines 185-203) uses default to handle failed and declined statuses, unlike every other inquiry-status switch in the same file which explicitly lists all 8 known statuses and reserves default for throwing on unknown values. Compare with the GET handler for cardLimit at server/api/kyc.ts:67-82 (explicitly lists failed/declined, throws on default) and the non-cardLimit POST handler at server/api/kyc.ts:226-247 (same pattern). This means an unexpected future status value would silently return { code: "failed" } instead of surfacing as an error. This violates the codebase's established consistency pattern per AGENTS.md.

Suggested change
default:
return c.json({ code: "failed" }, 400);
case "failed":
case "declined":
return c.json({ code: "failed" }, 400);
default:
throw new Error("unknown inquiry status");
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.

1 participant