Skip to content

✨ server: forward exchange rate to webhooks#917

Draft
nfmelendez wants to merge 1 commit intobasefrom
exchange-rate
Draft

✨ server: forward exchange rate to webhooks#917
nfmelendez wants to merge 1 commit intobasefrom
exchange-rate

Conversation

@nfmelendez
Copy link
Copy Markdown
Contributor

@nfmelendez nfmelendez commented Mar 26, 2026

Summary by CodeRabbit

  • New Features

    • Exchange rates are now forwarded to webhook payloads for transaction created and completed events when transaction currency differs from local currency.
  • Documentation

    • Webhook event reference tables updated to document the new optional exchangeRate field.
  • Tests

    • Webhook tests updated to assert exchangeRate presence/omission and payload signature correctness.
  • Chores

    • Added release metadata entry and startup warmup gating for background processes.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 26, 2026

🦋 Changeset detected

Latest commit: cdd6a70

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

Walkthrough

Adds forwarding of transaction exchange rates to outgoing webhooks: an optional numeric spend.exchangeRate is included in created/completed transaction webhook payloads when currency !== localCurrency; updates server schema/dispatch logic, tests, docs, and release metadata; also introduces a firehose warmup step in test DB startup.

Changes

Cohort / File(s) Summary
Release metadata
.changeset/shy-foxes-trade.md
Added patch changeset for @exactly/server with release note: “✨ forward exchange rate to webhooks”.
Documentation
docs/src/content/docs/webhooks.md
Documented optional body.spend.exchangeRate? (number) for Transaction created and Transaction completed events.
Webhook implementation
server/hooks/panda.ts
Extended Valibot schemas and webhook payload construction to allow and conditionally include spend.exchangeRate for transaction created/completed when currency !== localCurrency (omit for updated).
Tests
server/test/hooks/panda.test.ts
Updated webhook tests to assert inclusion/omission of body.spend.exchangeRate, added JSON payload parsing and maintained signature (HMAC) checks; renamed/clarified test cases.
Test DB startup / Firehose warmup
server/test/database.ts
Added a firehose warmup phase before main processes: runs fireeth warmup, watches output, races warmup against artifact creation/errors, and surfaces warmup logs on failure with improved abort/cleanup handling.

Sequence Diagram(s)

sequenceDiagram
    participant TxnEvent as Transaction Event
    participant ServerLogic as Server Logic
    participant WebhookBuilder as Webhook Payload Builder
    participant WebhookTarget as External Webhook

    TxnEvent->>ServerLogic: emit created/completed (body.spend)
    activate ServerLogic
    alt body.spend.currency != body.spend.localCurrency
        ServerLogic->>WebhookBuilder: include spend.exchangeRate
    else
        ServerLogic->>WebhookBuilder: omit spend.exchangeRate
    end
    deactivate ServerLogic

    WebhookBuilder->>WebhookTarget: POST payload (with conditional exchangeRate)
    activate WebhookTarget
    WebhookTarget->>WebhookTarget: verify Signature HMAC and process payload
    deactivate WebhookTarget
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • 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 clearly describes the main change: forwarding exchange rate data to webhooks, which is the primary objective reflected across all modified files.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch exchange-rate

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
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the inclusion of exchange rates in webhook payloads when the transaction currency differs from the local currency. The implementation involves updating the transaction and webhook schemas, modifying the publishing logic to conditionally attach the exchange rate, and updating the documentation. Comprehensive tests have been added and updated to verify the new behavior. I have no feedback to provide.

@sentry
Copy link
Copy Markdown

sentry bot commented Mar 26, 2026

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
486 2 484 1
View the top 2 failed test(s) by shortest run time
test/hooks/panda.test.ts > concurrency > handles concurrent authorizations
Stack Traces | 0.174s run time
AssertionError: expected 554 to be 200 // Object.is equality

- Expected
+ Received

- 200
+ 554

 ❯ test/hooks/panda.test.ts:1983:26
test/hooks/activity.test.ts > address activity > calls poke with correct ignore option
Stack Traces | 4s run time
AssertionError: expected "poke" to be called with arguments: [ …(2) ]

Received: 

  1st poke call:

  [
-   "0xF58e914C9692995712Ad3E32b6365f06C6A0d12a",
+   "0xC0aaF0145491DD98CC720D6A232c269760f327B1",
    {
      "ignore": [
-       "NotAllowed(0xF58e914C9692995712Ad3E32b6365f06C6A0d12a)",
+       "NotAllowed(0xC0aaF0145491DD98CC720D6A232c269760f327B1)",
      ],
    },
  ]


Number of calls: 1

 ❯ test/hooks/activity.test.ts:752:21

To view more test analytics, go to the Prevent Tests Dashboard

Copy link
Copy Markdown

@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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 84402000-271a-4d76-8f8b-bd546d875628

📥 Commits

Reviewing files that changed from the base of the PR and between deddc7c and cdd6a70.

📒 Files selected for processing (4)
  • .changeset/shy-foxes-trade.md
  • docs/src/content/docs/webhooks.md
  • server/hooks/panda.ts
  • server/test/hooks/panda.test.ts

@nfmelendez
Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nfmelendez
Copy link
Copy Markdown
Contributor Author

@CodeRabbit what i can do to run a review if i get " Review rate limit exceeded" ? is there a manual way?

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

Tip

For best results, initiate chat on the files or code changes.

@nfmelendez: When you hit the "Review rate limit exceeded" message, here are your options:

  1. Wait for the timeout to reset — In-trial and open-source plans have lower rate limits than paid plans. In all cases, CodeRabbit re-allows further reviews/conversations after a brief timeout. Once the cooldown passes, you can try @coderabbitai review again.

  2. Push a new commit — Incremental reviews for new commits added to the PR also count towards the rate limit, but pushing a meaningful change to the branch can trigger a fresh incremental review automatically once the limit resets.

  3. Mark the PR as "Ready for Review" — Since this PR is currently in draft status, toggling it to ready for review can trigger a new review event.

  4. Upgrade your plan — In-trial and open-source plans have lower rate limits than the paid plan. In all cases, re-allowing occurs after a brief timeout. A paid plan gives you higher limits.

  5. Contact support / Discord — You can visit the CodeRabbit Support page for additional assistance or reach out to the team on Discord.

In short, the most straightforward path is to simply wait a short while and then retry with @coderabbitai review. I can go ahead and trigger a review right now if you'd like — just let me know!

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ed16abbe-f9f6-4d41-98b6-f61ecb01d30f

📥 Commits

Reviewing files that changed from the base of the PR and between cdd6a70 and bc54ccf.

📒 Files selected for processing (5)
  • .changeset/shy-foxes-trade.md
  • docs/src/content/docs/webhooks.md
  • server/hooks/panda.ts
  • server/test/database.ts
  • server/test/hooks/panda.test.ts

| body.spend.authorizedAmount | integer | The authorized amount | 100 |
| body.spend.status | "pending" \| "declined" | Can be pending or declined. In case of declined, the field `declinedReason` has the reason | pending |
| body.spend.declinedReason? | string | Decline message | webhook declined |
| body.spend.exchangeRate? | number | Present when `currency` differs from `localCurrency`. The exchange rate applied to the transaction | 1.1806900825 |
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Define the rate direction.

exchangeRate is still ambiguous here. Consumers need to know whether this is currency/localCurrency or the inverse to reconcile amount with localAmount.

Also applies to: 624-624

...BaseTransaction.entries.spend.entries,
status: v.picklist(["pending", "declined"]),
declinedReason: v.optional(v.string()),
exchangeRate: v.optional(v.number()),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The new webhook contract is not enforced.

exchangeRate is documented as present for cross-currency created/completed webhooks, but these schemas still accept it as optional and this branch only forwards it if Panda already supplied it. A cross-currency event without spend.exchangeRate will still pass validation and emit a payload that contradicts the docs. Either require it in those cases or relax the docs to “may be present”.

Also applies to: 160-160, 1322-1328, 1383-1383, 1422-1422

Comment on lines +103 to +115
} catch (error) {
warmupController.abort();
await warmupLogWatcher.exit.catch(() => undefined);
await warmupLogWatcher.outputFlushed;
const warmupText = await readFile(warmupLog, "utf8").catch(() => "");
const message = error instanceof Error ? error.message : String(error);
throw new Error(`wait firehose warmup: ${message}\nfirehose:\n${warmupText}`, { cause: error });
} finally {
warmupLogWatcher.stopWatchingOutput();
warmupController.abort();
await warmupLogWatcher.exit.catch(() => undefined);
await warmupLogWatcher.outputFlushed;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Redundant cleanup between catch and finally blocks.

The catch block (lines 104-106) performs abort(), await exit, await outputFlushed, and then finally (lines 112-114) repeats the same operations. On the error path, both blocks execute sequentially, causing redundant calls.

Consider consolidating cleanup entirely in finally and capturing the error for deferred handling:

♻️ Proposed consolidation
-    } catch (error) {
-      warmupController.abort();
-      await warmupLogWatcher.exit.catch(() => undefined);
-      await warmupLogWatcher.outputFlushed;
-      const warmupText = await readFile(warmupLog, "utf8").catch(() => "");
-      const message = error instanceof Error ? error.message : String(error);
-      throw new Error(`wait firehose warmup: ${message}\nfirehose:\n${warmupText}`, { cause: error });
+    } catch (error) {
+      warmupLogWatcher.stopWatchingOutput();
+      warmupController.abort();
+      await warmupLogWatcher.exit.catch(() => undefined);
+      await warmupLogWatcher.outputFlushed;
+      const warmupText = await readFile(warmupLog, "utf8").catch(() => "");
+      const message = error instanceof Error ? error.message : String(error);
+      throw new Error(`wait firehose warmup: ${message}\nfirehose:\n${warmupText}`, { cause: error });
     } finally {
       warmupLogWatcher.stopWatchingOutput();
       warmupController.abort();
-      await warmupLogWatcher.exit.catch(() => undefined);
-      await warmupLogWatcher.outputFlushed;
     }

This moves stopWatchingOutput() into catch (where it's needed before reading logs) and removes redundant awaits from finally, matching the pattern used for firehoseLogWatcher at lines 139-148.

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