-
Notifications
You must be signed in to change notification settings - Fork 2
✨️ server: add activity and push notification on card decline#622
✨️ server: add activity and push notification on card decline #622
Conversation
🦋 Changeset detectedLatest commit: d907571 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 server-side decline handling: classifies decline reasons, persists declined transaction bodies, routes frozen/non-active/error flows through new decline handlers, triggers push notifications for select decline types, extends activity schema to include "requested"/declined fields, and adds tests for declines and notifications. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant CardSystem as Card System
participant PandaHook as Panda Hook
participant DB as Database
participant Notif as Notification Service
Client->>CardSystem: submit transaction
CardSystem->>PandaHook: webhook / payload
PandaHook->>DB: fetch card & transaction
alt card FROZEN or not ACTIVE
DB-->>PandaHook: card status != ACTIVE
PandaHook->>PandaHook: getDeclineReason()
PandaHook->>PandaHook: handleRejectedTransactionSync()
PandaHook-->>CardSystem: 403 / decline response
else processing error or explicit decline
PandaHook->>PandaHook: getDeclineReason()
PandaHook->>DB: updateTransactionRecord(...) with declined body
DB-->>PandaHook: OK
PandaHook->>PandaHook: handleDeclinedTransaction()
PandaHook->>Notif: sendDeclinedNotification()
Notif-->>Client: push notification delivered
else success
PandaHook-->>CardSystem: success response
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 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 |
Summary of ChangesHello @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 transaction handling by introducing a mechanism to process declined card transactions. The immediate user-facing change is the implementation of push notifications, which will inform users instantly about rejected purchases. Although the foundational code for recording these declined transactions as user activity has been added, this specific database logging functionality is temporarily disabled, pending the development of corresponding user interface elements. Highlights
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
|
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
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: 2
🤖 Fix all issues with AI agents
In @server/hooks/panda.ts:
- Around line 955-998: Remove the large commented-out DB logic inside
handleDeclinedTransaction and track the work in your issue tracker: create an
issue describing the pending UI changes needed to handle declined transactions
and include its ID; then replace the commented block with a single-line comment
in handleDeclinedTransaction referencing that issue (e.g., "See ISSUE-1234:
enable declined-transaction persistence once UI supports it"). Ensure the rest
of the function (push notification and error capture) remains unchanged.
- Line 965: Replace the existing comment "// TODO: Enable once UI has proper
designs to handle declined transactions in activity" with the coding-guideline
compliant format: use uppercase tag, a single space, and a fully lowercase
comment body (e.g. "// TODO enable once ui has proper designs to handle declined
transactions in activity"); update the line containing that TODO comment in the
server/hooks/panda.ts hook to remove the colon and capitalize only the TODO tag.
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (3)
.changeset/honest-peas-stand.mdserver/hooks/panda.tsserver/test/hooks/panda.test.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/.changeset/*.md
📄 CodeRabbit inference engine (.cursor/rules/style.mdc)
Use a lowercase sentence in the imperative present tense for changeset summaries
Files:
.changeset/honest-peas-stand.md
server/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/server.mdc)
server/**/*.ts: Usec.varobject to pass strongly-typed data between Hono middleware and route handlers; do not usec.set
All request validation (headers, body, params) must be handled by@hono/valibot-validatormiddleware; do not perform manual validation inside route handlers
Use Hono's built-in error handling by throwingnew HTTPException()for expected errors; unhandled errors will be caught and logged automatically
Enforce Node.js best practices using ESLintplugin:n/recommendedconfiguration
Enforce Drizzle ORM best practices using ESLintplugin:drizzle/allconfiguration, including requiringwhereclauses forupdateanddeleteoperations
Use Drizzle ORM query builder for all database interactions; do not write raw SQL queries unless absolutely unavoidable
All authentication and authorization logic must be implemented in Hono middleware
Do not accessprocess.envdirectly in application code; load all configuration and secrets once at startup and pass them through dependency injection or context
Avoid long-running, synchronous operations; useasync/awaitcorrectly and be mindful of CPU-intensive tasks to prevent blocking the event loop
Files:
server/hooks/panda.tsserver/test/hooks/panda.test.ts
**/*.{js,ts,tsx,jsx,sol}
📄 CodeRabbit inference engine (AGENTS.md)
Follow linter/formatter (eslint, prettier, solhint) strictly with high strictness level. No
anytype.
Files:
server/hooks/panda.tsserver/test/hooks/panda.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Omit redundant type names in variable declarations - let the type system explain itself
**/*.{ts,tsx}: Use PascalCase for TypeScript types and interfaces
Use valibot for all runtime validation of API inputs, environment variables, and other data; define schemas once and reuse them
Infer TypeScript types from valibot schemas usingtype User = v.Input<typeof UserSchema>instead of manually defining interfaces
Files:
server/hooks/panda.tsserver/test/hooks/panda.test.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx,js,jsx}: Omit contextual names - don't repeat class/module names in members
Omit meaningless words like 'data', 'state', 'manager', 'engine', 'value' from variable and function names unless they add disambiguation
**/*.{ts,tsx,js,jsx}: Prefer function declarations for all multi-line functions; use function expressions or arrow functions only for single-line implementations
Preferconstfor all variable declarations by default; only useletif the variable's value will be reassigned
Declare each variable on its own line with its ownconstorletkeyword, not multiple declarations on one line
Use camelCase for TypeScript variables and functions
Always useimport type { ... }for type imports
Use relative paths for all imports within the project; avoid tsconfig path aliases
Follow eslint-plugin-import order: react, external libraries, then relative paths
Use object and array destructuring to access and use properties
Use object method shorthand syntax when a function is a property of an object
Prefer optional chaining (?.), nullish coalescing (??), object and array spreading (...), andfor...ofloops over traditional syntax
Do not use abbreviations or cryptic names; write out full words likeerror,parameters,requestinstead oferr,params,req
UseNumber.parseInt()instead of the globalparseInt()function when parsing numbers
All classes called withnewmust use PascalCase
UseBuffer.from(),Buffer.alloc(), orBuffer.allocUnsafe()instead of the deprecatednew Buffer()
Use@ts-expect-errorinstead of@ts-ignore; follow it immediately with a single-line lowercase comment explaining why the error is expected, without separators like-or:
Do not include the type in a variable's name; let the static type system do its job (e.g., useconst user: Usernotconst userObject: User)
Do not repeat the name of a class or module within its members; omit contextual names (e.g., use `class User { getProfil...
Files:
server/hooks/panda.tsserver/test/hooks/panda.test.ts
server/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
server/**/*.{ts,tsx}: Server API: implement schema-first approach using OpenAPI via hono with validation via valibot middleware
Server database: drizzle schema is source of truth. Migrations required. No direct database access in handlers - usec.var.db
Files:
server/hooks/panda.tsserver/test/hooks/panda.test.ts
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/style.mdc)
For files with a single
defaultexport, name the file identically to the export; for files with multiple exports, use camelCase with a strong preference for a single word
Files:
server/hooks/panda.tsserver/test/hooks/panda.test.ts
🧠 Learnings (2)
📚 Learning: 2025年12月31日T00:23:55.034Z
Learnt from: cruzdanilo
Repo: exactly/exa PR: 610
File: .changeset/ready-experts-fly.md:1-2
Timestamp: 2025年12月31日T00:23:55.034Z
Learning: In the exactly/exa repository, allow and require empty changeset files (containing only --- separators) when changes are not user-facing and do not warrant a version bump. This is needed because CI runs changeset status --since origin/main and requires a changeset file to exist. Ensure such empty changesets are used only for non-user-facing changes and document the rationale in the commit or changelog notes.
Applied to files:
.changeset/honest-peas-stand.md
📚 Learning: 2025年12月23日T19:58:16.574Z
Learnt from: CR
Repo: exactly/exa PR: 0
File: AGENTS.md:0-0
Timestamp: 2025年12月23日T19:58:16.574Z
Learning: Zero config local dev environment: no `.env` files, mock all external services
Applied to files:
server/test/hooks/panda.test.ts
🧬 Code graph analysis (2)
server/hooks/panda.ts (1)
server/utils/onesignal.ts (1)
sendPushNotification(7-25)
server/test/hooks/panda.test.ts (1)
server/database/schema.ts (1)
transactions(36-43)
🔇 Additional comments (5)
server/test/hooks/panda.test.ts (2)
2-2: LGTM!Import adjustments for mocks are clean and improve organization.
Also applies to: 7-7
1352-1439: Test stubs properly scaffolded for future implementation.The two test cases for declined transaction handling are well-structured and align with the commented-out database logic in
server/hooks/panda.ts. Usingit.todois appropriate while waiting for UI designs.server/hooks/panda.ts (2)
533-533: LGTM!The call to
handleDeclinedTransactionis correctly placed after the mutex is released, and the type cast is necessary due to TypeScript's union type handling.
988-998: LGTM!Push notification implementation properly formats the transaction details and includes error handling consistent with the rest of the codebase.
.changeset/honest-peas-stand.md (1)
1-5: LGTM!Changeset properly documents the feature addition with appropriate version bump and clear description.
118a340 to
dc0ac8a
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@ ## main #622 +/- ## ========================================== + Coverage 69.73% 70.13% +0.39% ========================================== Files 208 208 Lines 7455 7745 +290 Branches 2382 2506 +124 ========================================== + Hits 5199 5432 +233 - Misses 2040 2076 +36 - Partials 216 237 +21
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
2b6c4a8 to
64325f3
Compare
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
64325f3 to
b3814d0
Compare
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
b3814d0 to
8c11c8c
Compare
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
8c11c8c to
77c26c2
Compare
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
98e690f to
a16d92d
Compare
96b7642 to
2ae2bca
Compare
a16d92d to
685fbf3
Compare
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
685fbf3 to
4bcf82f
Compare
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
4bcf82f to
bbacd86
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.
✅ 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.
c7829dc to
5e7861d
Compare
75bcaa2 to
ea2b4f2
Compare
nfmelendez
commented
Feb 10, 2026
@coderabbitai the hooks panda will be sending notification messages to users and this is is super import because it will model the concept and confidence they have in the company so i need a deep research. Please analyze every possible flow of the panda hook and tell me all the notification messages possibility, their text and line number is triggered
Tip
For best results, initiate chat on the files or code changes.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Read the complete panda hook file to analyze notification flows
cat server/hooks/panda.ts
Length of output: 45387
@nfmelendez I've completed a deep analysis of all notification flows in the Panda hook. Here's the comprehensive breakdown:
📬 All Notification Messages & Flows
1. Refund Processed Notification
Lines: 685-692
Heading: "Refund processed"
Message: "${refundAmountUsd} USDC from ${merchantName} have been refunded to your account"
Example: "5.50 USDC from Starbucks have been refunded to your account"
Triggered when:
- Transaction status is
"reversed"(full authorization reversal) - Transaction amount is negative (merchant-initiated refund)
- Partial capture occurs (charged less than authorized)
Flow path: action: "completed" | "updated" → refund amount calculation → keeper.exaSend → Line 685 notification
2. Card Purchase Notification
Lines: 829-839
Heading: "Card purchase"
Message: "${localAmount in local currency} at ${merchantName}. Paid {payment method}"
Payment method variations:
- Mode 0:
"with USDC" - Mode 1:
"with credit" - Mode 2+:
"in {mode} installments"
Examples:
- "12ドル.50 at Amazon. Paid with USDC"
- "45ドル.00 at H&M. Paid with credit"
- "99ドル.99 at Apple Store. Paid in 6 installments"
Triggered when:
action: "created"(successful authorization) ORaction: "completed"with force capture (no prior authorization)
Flow path: action: "created"/"completed" → prepareCollection → keeper.exaSend → Line 829 notification
3. Transaction Declined Notification ⚠️
Lines: 984-992 (function definition), 1027-1029 & 1058-1061 (invocation)
Heading: "Exa Card purchase rejected"
Message: "Transaction rejected: ${reason} - ${formattedAmount} at ${merchantName}"
Examples based on decline reasons:
- "Transaction rejected: insufficient funds - 25ドル.00 at Walmart"
- "Transaction rejected: frozen card - 15ドル.50 at McDonald's"
- "Transaction rejected: merchant blocked - 100ドル.00 at Casino"
Decline reasons that trigger notifications (Lines 55-59):
- ✅
"insufficient funds"- When user lacks sufficient account liquidity - ✅
"frozen card"- When card status is FROZEN - ✅
"merchant blocked"- When merchant is blocked by risk rules
Decline reasons that DON'T trigger notifications:
- ❌
"transaction declined"- Generic decline (not in NOTIFICATION_TRIGGERING_REASONS set)
🔄 Complete Flow Mapping
Declined Notification Flow A: Frozen Card
- Entry: Line 333 -
if (card.status === "FROZEN") - Handler: Line 337 -
handleRejectedTransaction(account, payload, jsonBody, "frozen_card") - Reason mapping: Line 948 - Maps to
DECLINE_REASONS.FROZEN_CARD - Check: Line 1027 - Reason is in NOTIFICATION_TRIGGERING_REASONS ✅
- Send: Line 1028 -
sendDeclinedNotification()fires
Declined Notification Flow B: Insufficient Funds
- Entry: Line 433 -
case "InsufficientAccountLiquidity"error in trace - Throw: Line 434 -
throw new PandaError("InsufficientAccountLiquidity", 557) - Catch: Line 456-465 - Caught as PandaError
- Handler: Line 465 -
handleRejectedTransaction(account, payload, jsonBody, error.message) - Reason mapping: Line 948 - Maps to
DECLINE_REASONS.INSUFFICIENT_FUNDS - Check: Line 1027 - Reason is in NOTIFICATION_TRIGGERING_REASONS ✅
- Send: Line 1028 -
sendDeclinedNotification()fires
Declined Notification Flow C: Merchant Blocked
- Entry: Merchant blocked by risk rules (external to hook)
- Received: Line 495 -
action: "created"withstatus: "declined"anddeclinedReason: "merchant_blocked" - Handler: Line 917 -
await handleDeclinedTransaction(account, payload, jsonBody) - Sync handler: Line 1040 -
handleRejectedTransactionSync() - Reason mapping: Line 948 - Maps to
DECLINE_REASONS.MERCHANT_BLOCKED - Check: Line 1057 - Reason is in NOTIFICATION_TRIGGERING_REASONS ✅
- Send: Line 1058 -
await sendDeclinedNotification()fires
Declined Notification Flow D: Network Declined (Silent)
- Entry: Line 495 -
action: "created"withstatus: "declined"and genericdeclinedReason - Handler: Line 917 -
await handleDeclinedTransaction(account, payload, jsonBody) - Reason mapping: Line 954 - Falls through to
DECLINE_REASONS.TRANSACTION_DECLINED - Check: Line 1057 - Reason is NOT in NOTIFICATION_TRIGGERING_REASONS ❌
- Result: No notification sent (activity recorded only)
Declined Notification Flow E: Unexpected Errors
- Entry: Line 467-475 - Unexpected error during authorization
- Handler: Line 472 -
handleRejectedTransaction(account, payload, jsonBody, error.message) - Reason mapping: Line 948 - Depends on error message (likely falls to generic)
- Check: Line 1027 - Only sends if error message contains key phrases
- Result: Usually no notification unless error message matches a known reason
🎯 Critical Considerations for User Confidence
-
Inconsistent notification coverage: Only 3 of 4 decline reasons trigger notifications. Generic "transaction declined" is silent.
-
Error message formatting variability: Line 976-982 includes fallback formatting if currency formatting fails, ensuring messages always send.
-
Fire-and-forget vs. awaited:
- Lines 1027-1029:
handleRejectedTransactiondoesn't await notification (may fail silently) - Lines 1058-1061:
handleRejectedTransactionSyncawaits notification (errors are logged)
- Lines 1027-1029:
-
Missing transaction ID warning: Lines 993-1001 - If transaction ID is missing, no activity is recorded and no notification is sent (logged as warning).
-
All notifications catch and log errors: Lines 692, 839, 1029, 1061 - Network failures won't crash the flow but may result in users not being notified.
Would you like me to analyze any specific scenario in more detail or suggest improvements to the notification strategy?
ea2b4f2 to
5a7b8ad
Compare
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
4ee1569 to
e91e988
Compare
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
280933f to
517e2cc
Compare
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
517e2cc to
2a6fdab
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.
Devin Review found 1 new potential issue.
View 21 additional findings in Devin Review.
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.
🟡 Duplicate declined records and notifications when body.id is undefined for "requested" action
When the "requested" webhook has body.id as undefined (allowed by the schema at server/hooks/panda.ts:148), the validateTransactionId fallback uses payload.id (the webhook event ID) as the transaction primary key. Later, the "created" declined webhook arrives with body.id set to the actual transaction ID — a different key. Both rejectTx invocations insert separate records with isNewRecord = true, causing a duplicate push notification and two declined activity entries in the user's feed.
Detailed trace of the duplicate flow
- "requested" fails (e.g. InsufficientAccountLiquidity at line 424) →
rejectTxcalled fire-and-forget at line 462 withpayload.body.id = undefined. validateTransactionIdatserver/hooks/panda.ts:1156returnstransactionId = payload.id(e.g."abcdef-123456").updateTransactionRecordINSERTs a row withid = "abcdef-123456",isNewRecord = true→ notification sent.- "created" declined webhook arrives with
body.id = "31eaa81e-..."(actual tx ID). handleDeclinedTransaction→rejectTx→validateTransactionIdreturnstransactionId = "31eaa81e-..."(different key).updateTransactionRecordINSERTs another row withid = "31eaa81e-...",isNewRecord = true→ second notification sent.
The xmax = 0 guard at server/hooks/panda.ts:1121 only prevents duplicates for the same primary key. With two different keys, both upserts are treated as new records.
Impact: The user receives two push notifications for a single decline event, and the activity feed shows two duplicate declined entries.
Was this helpful? React with 👍 or 👎 to provide feedback.
2a6fdab to
914e2c0
Compare
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
914e2c0 to
d907571
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.
Devin Review found 1 new potential issue.
View 23 additional findings in Devin Review.
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.
🚩 Notification suppressed for existing transaction records regardless of reason change
The notification logic at server/hooks/panda.ts:1314 sends notifications only when isNewRecord && NOTIFICATION_TRIGGERING_REASONS.has(reason). This means if a transaction record already exists (e.g., from a prior rejectTx call during the "requested" phase with a non-triggering reason like "transaction declined"), a subsequent decline event with a triggering reason (e.g., "insufficient funds") will NOT send a notification because isNewRecord is false.
Scenario: (1) "requested" fails with "bad collection" → rejectTx inserts record, reason maps to "transaction declined" (not in triggering set), no notification. (2) "created" arrives with status "declined" and declinedReason="insufficient_funds" → rejectTx updates existing record, isNewRecord=false → no notification despite the reason being triggering.
In practice this edge case may be rare (most InsufficientAccountLiquidity errors would trigger notifications in step 1), but it's a design limitation worth understanding.
Was this helpful? React with 👍 or 👎 to provide feedback.
Uh oh!
There was an error while loading. Please reload this page.
closes #114
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Chores