-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Implement currency conversion for SEPA debit payouts #2572
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
… converted total; streamline FX quote request body construction.
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
WalkthroughThis update enhances payout confirmation by adding currency conversion for "sepa_debit" and "acss_debit" payment methods using Stripe FX quotes with a markup. It stores Stripe charge metadata in invoices, updates invoice display to show non-USD amounts, and adjusts webhook handling to save charge details. A new FX quote utility and a forex markup constant are introduced. Payment method UI and API restrict SEPA Debit to Enterprise plans. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant App
participant Stripe
User->>App: Initiate payout confirmation
App->>App: Determine payout currency based on payment method
App->>Stripe: Request FX quote if payment method requires conversion
Stripe-->>App: Return FX quote with exchange rate
App->>App: Calculate converted payout amount with markup
App->>Stripe: Create payment intent with converted amount and currency
Stripe-->>App: Return payment intent details
App->>App: Create invoice with payment intent and charge metadata
App->>User: Complete payout confirmation flow
Possibly related PRs
Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
⏰ Context from checks skipped due to timeout of 90000ms (1)
✨ Finishing Touches
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
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: 7
🔭 Outside diff range comments (1)
apps/web/app/(ee)/api/cron/payouts/confirm/confirm-payouts.ts (1)
142-171: Fix data inconsistency between database and Stripe.The database stores the original
total(line 150) while Stripe usesconvertedTotal(line 159). This creates a mismatch between what's recorded in the database and what's actually charged.Consider storing both values or using the converted total consistently:
const invoice = await tx.invoice.create({ data: { id: createId({ prefix: "inv_" }), number: invoiceNumber, programId: program.id, workspaceId: workspace.id, amount, fee: totalFee, - total, + total: convertedTotal, }, });Or add a new field to track the original USD amount if needed for reporting.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/web/app/(ee)/api/cron/payouts/confirm/confirm-payouts.ts(3 hunks)apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts(2 hunks)apps/web/lib/stripe/create-fx-quote.ts(1 hunks)packages/prisma/schema/invoice.prisma(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/web/app/(ee)/api/cron/payouts/confirm/confirm-payouts.ts (1)
apps/web/lib/stripe/create-fx-quote.ts (1)
createFxQuote(13-41)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: build
🔇 Additional comments (5)
packages/prisma/schema/invoice.prisma (1)
18-18: LGTM! Schema change is well-designed.The optional
stripePaymentIntentIdfield is correctly added to support storing Stripe payment intent identifiers. The optional nature ensures backward compatibility with existing invoices.apps/web/lib/stripe/create-fx-quote.ts (1)
30-30: Verify the Stripe API version and feature flag.The API version
2025-05-28.basil;fx_quote_preview=v1appears to be from the future and uses a preview feature flag. Preview features may not be stable for production use.What is the current stable version of Stripe API that supports FX quotes?apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts (1)
9-9: Extract payment_intent safely.The destructuring looks good, but the variable will need proper validation before use.
apps/web/app/(ee)/api/cron/payouts/confirm/confirm-payouts.ts (2)
17-20: Currency mapping looks correct.The payment method to currency mapping appropriately maps SEPA debit to EUR and ACSS debit to CAD.
105-105: Good use of the currency mapping.The currency determination logic correctly falls back to USD for payment methods not requiring conversion.
…d update currency formatting to support dynamic currencies.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
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: 0
🧹 Nitpick comments (1)
apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts (1)
9-9: Consider removing unused variable or document its purpose.The
payment_intentis destructured from the charge object but doesn't appear to be used anywhere in this function. If it's intended for future use or downstream processing, consider adding a comment to clarify its purpose.- const { id: chargeId, receipt_url, transfer_group, payment_intent } = charge; + const { id: chargeId, receipt_url, transfer_group } = charge;Alternatively, if
payment_intentis needed for future enhancements, add a comment:- const { id: chargeId, receipt_url, transfer_group, payment_intent } = charge; + const { id: chargeId, receipt_url, transfer_group, payment_intent } = charge; // TODO: Use for currency conversion
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts(2 hunks)apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/route.tsx(4 hunks)packages/prisma/schema/invoice.prisma(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/prisma/schema/invoice.prisma
- apps/web/app/(ee)/app.dub.co/invoices/[invoiceId]/route.tsx
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: build
🔇 Additional comments (1)
apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts (1)
60-60: LGTM! Good approach for storing charge metadata.The JSON serialization using
JSON.parse(JSON.stringify(charge))is a solid approach to deep clone the charge object and remove any circular references before storage. This enables downstream currency conversion functionality as described in the PR objectives.
|
@CodeRabbit final review |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 10796 🏁 Script executed: Length of output: 13904 @steven-tey I've completed a comprehensive final review of the currency conversion implementation. Here's my assessment: ✅ Strengths & Well-Implemented FeaturesArchitecture & Code Quality
Currency Conversion Logic
Invoice Display & UX
|
|
@CodeRabbit final review |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chainI'll conduct a comprehensive final review of this currency conversion implementation. 🏁 Script executed: Length of output: 10796 🏁 Script executed: Length of output: 13904 @steven-tey I've completed a comprehensive final review of the currency conversion implementation. Here's my assessment: ✅ Strengths & Well-Implemented FeaturesArchitecture & Code Quality
Currency Conversion Logic
Invoice Display & UX
|
|
@CodeRabbit final review, any logical errors you can find? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 6779 @steven-tey I found several critical logical errors in the currency conversion implementation: 🚨 Critical Logical Errors1. Missing Currency Mapping Definitionconst currency = paymentMethodToCurrency[paymentMethod.type] || "usd";❌ Error: Expected fix: Should be: const PAYMENT_METHOD_CURRENCY_MAPPING = {
sepa_debit: "eur",
acss_debit: "cad"
} as const;
const currency = PAYMENT_METHOD_CURRENCY_MAPPING[paymentMethod.type] || "usd";2. Unsafe Property Accessconst exchangeRate = fxQuote.rates[currency].exchange_rate;❌ Error: No null check for Expected fix: const rateData = fxQuote.rates[currency];
if (!rateData) {
throw new Error(`Exchange rate for ${currency} not found in FX quote response`);
}
const exchangeRate = rateData.exchange_rate;3. Conversion Direction Confusion// Comment says "convert the amount to EUR/CAD"
const fxQuote = await createFxQuote({
fromCurrency: currency, // EUR/CAD
toCurrency: "usd", // USD
});
// But then we divide USD by EUR→USD rate
convertedTotal = Math.round((total / exchangeRate) * (1 + FOREX_MARKUP_RATE));❌ Error: The FX quote direction is backwards. We're requesting EUR→USD rate but need USD→EUR rate for our conversion. Expected fix: const fxQuote = await createFxQuote({
fromCurrency: "usd", // We're converting FROM USD
toCurrency: currency, // TO EUR/CAD
});
const exchangeRate = fxQuote.rates["usd"].exchange_rate; // USD to target currency rate
convertedTotal = Math.round((total * exchangeRate) * (1 + FOREX_MARKUP_RATE));4. Misleading Error Message// Comment says "use the cached rate from Redis" but there's no Redis logic
if (!exchangeRate || exchangeRate <= 0) {
throw new Error(`Failed to get exchange rate from Stripe for ${currency}.`);
}❌ Error: Comment mentions Redis caching that doesn't exist in the code. 🔧 Mathematical VerificationThe current logic:
However, the API request direction is confusing and makes the code harder to understand. 🧪 Edge Cases Missing
📊 Severity Assessment
The most critical issue is the missing currency mapping that will cause immediate runtime failures. 🏁 Script executed: Length of output: 3530 @steven-tey I need to correct my previous analysis after seeing the complete code. Here are the actual logical errors I found: ✅ Corrections to My Previous AssessmentCurrency Mapping DefinitionMy initial assessment was wrong - the mapping IS defined: const paymentMethodToCurrency = {
sepa_debit: "eur",
acss_debit: "cad",
} as const;Conversion Direction LogicThe direction is actually mathematically correct:
🚨 Real Logical Errors Found1. Unsafe Property Access (Critical)const exchangeRate = fxQuote.rates[currency].exchange_rate;❌ Error: No null/undefined check for Fix needed: const rateData = fxQuote.rates[currency];
if (!rateData) {
throw new Error(`Exchange rate for ${currency} not found in FX quote response`);
}
const exchangeRate = rateData.exchange_rate;2. Type Safety Issue (Medium)const currency = paymentMethodToCurrency[paymentMethod.type] || "usd";❌ Error: Better approach: const currency = paymentMethodToCurrency[paymentMethod.type as keyof typeof paymentMethodToCurrency] || "usd";3. Inconsistent Error Message (Low)// Comment says "use the cached rate from Redis" but no Redis logic exists
if (!exchangeRate || exchangeRate <= 0) {
throw new Error(`Failed to get exchange rate from Stripe for ${currency}.`);
}4. Missing Edge Case Handling (Medium)No validation that the currency conversion should actually happen: if (["sepa_debit", "acss_debit"].includes(paymentMethod.type)) {This hardcoded check could get out of sync with the currency mapping. Better approach: const currency = paymentMethodToCurrency[paymentMethod.type] || "usd";
if (currency !== "usd") {
// Perform currency conversion
}🎯 Severity Assessment
The primary concern is the unsafe property access that could cause production failures if Stripe's API response format changes or doesn't include expected currencies. ✅ What's Actually Working Well
The core logic is sound, but needs defensive programming for API response handling. |
Summary by CodeRabbit