Skip to content

Commit 85f9239

Browse files
authored
Multi-currencies (#18)
* wip * update * update * update * update
1 parent 0b333ff commit 85f9239

File tree

2 files changed

+223
-0
lines changed

2 files changed

+223
-0
lines changed

docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"engineering/design-documents/prorations",
5353
"engineering/design-documents/subscription-retries",
5454
"engineering/design-documents/wallets",
55+
"engineering/design-documents/multi-currency",
5556
"engineering/design-documents/business-entity"
5657
]
5758
},
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
<Info>
2+
**Status**: Draft
3+
4+
**Created**: 2025-11-07
5+
6+
**Last Updated**: 2025-11-10
7+
8+
</Info>
9+
10+
# Multi-Currency Support in Polar
11+
12+
## Problem Statement
13+
14+
Historically, Polar has operated using a single base currency (USD) for all transactions and accounting. However, as our user base expands globally, there is a growing need to support multiple currencies natively within the platform. This design document will explore the requirements, challenges, and proposed solutions for implementing multi-currency support in Polar.
15+
16+
## Background
17+
18+
### Key terms
19+
20+
- Presentment Currency: The currency in which a payment/transaction is done, i.e. the price shown on the checkout page and customer's invoice.
21+
- Settlement Currency: The currency in which funds are shown on the merchant's side.
22+
- Payout Currency: The currency in which funds are paid out to the merchant's bank account.
23+
24+
### Current state
25+
26+
Currently, we only support USD as **presentment** and **settlement** currency. We support multiple **payout** currencies: merchants can connect any bank account supported by Stripe Connect and receive payouts in that currency. Stripe handles the currency conversion internally.
27+
28+
### Comparison with Stripe
29+
30+
Stripe has both the concepts of Presentment and Settlement currencies, with the following capabilities, given that Polar runs a **Stripe US account**.
31+
32+
#### Presentment Currency: 135+ currencies supported
33+
34+
In the current context, if we trigger a payment in a currency different than USD, Stripe will implicitly convert the funds to USD before settling them to our Stripe account.
35+
36+
That said, they still show it up in the transactions list as the original currency. However, balance and analytics are all in USD.
37+
38+
Ref: https://docs.stripe.com/currencies#presentment-currencies
39+
40+
#### Settlement Currency
41+
42+
For US-based Stripe accounts, Stripe allows us to wire bank account for the following settlement currencies:
43+
44+
- CAD
45+
- EUR
46+
- GBP
47+
48+
After enabling another settlement currency, payments made in that currency will be settled in that currency, and funds will be shown in that currency in the balance.
49+
50+
Ref: https://docs.stripe.com/payouts/multicurrency-settlement?account-country=US#attach-bank-accounts-to-receive-payouts-in-local-currencies
51+
52+
## Proposed Solutions
53+
54+
Depending on how far we want to go, given the constraints we have with Stripe, we can consider several scenarios detailed below.
55+
56+
### Scenario 1: Multiple Presentment Currencies, USD Settlement currency
57+
58+
In this scenario, we would allow merchants to set prices in multiple currencies, and customers would be able to pay in their local currency. However, all funds would still be settled in USD.
59+
60+
#### Implementation details
61+
62+
- Allow to set other presentment currencies than `usd` when creating products (only a validation constraint to change).
63+
64+
**Implementation A**
65+
66+
- To allow to display and track the presentment currency in the dashboard, add the following columns to Payment, Order, OrderItem, Refund and Subscription models:
67+
- `presentment_currency`: `str`
68+
- `presentment_amount`: `int`
69+
- When receiving a payment or refund from Stripe, we'll need to get the converted amount in `usd` Stripe calculated (from the balance transaction), to fill our original amount fields.
70+
- Prefer `presentment_amount` and `presentment_currency` when displaying amounts in the dashboard.
71+
72+
Since original amounts field are still using `usd`, analytics and account transactions are working unchanged.
73+
74+
**Implementation B**
75+
76+
- Store presentment amount and currency in the current amount fields.
77+
- When receiving a payment or refund from Stripe, we'll need to get the converted amount in `usd` Stripe calculated (from the balance transaction), to impact the merchant's Account balance.
78+
- Revamp metrics so amount are always computed in `usd`, directly from the Account transactions amount.
79+
80+
This is more or less the Stripe way of doing things.
81+
82+
### Scenario 2: Multiple Presentment Currencies, One Settlement Currency
83+
84+
In this scenario, we would allow merchants to set prices in multiple currencies, and customers would be able to pay in their local currency. However, merchants would choose a single settlement currency for their account.
85+
86+
#### Implementation details
87+
88+
Similar to Scenario 1, but with the ability to set an Organization's settlement currency.
89+
90+
- Add `currency` field to Organization model.
91+
- Only currencies supported by Stripe for settlement can be chosen here (for US-based accounts: USD, CAD, EUR, GBP); provided Polar has enabled the settlement currency in Stripe account (with a proper bank account attached).
92+
- Allow to set other presentment currencies than `usd` when creating products (only a validation constraint to change).
93+
94+
**Implementation A**
95+
96+
- To allow to display and track the presentment currency in the dashboard, add the following columns to Payment, Order, OrderItem, Refund and Subscription models:
97+
- `presentment_currency`: `str`
98+
- `presentment_amount`: `int`
99+
- When receiving a payment or refund from Stripe, we'll need to get the converted amount in `Organization.currency` Stripe calculated (from the balance transaction), to fill our original amount fields.
100+
- Prefer `presentment_amount` and `presentment_currency` when displaying amounts in the dashboard.
101+
102+
Since original amounts field are still using `Organization.currency`, analytics and account transactions are working unchanged.
103+
104+
**Implementation B**
105+
106+
- Store presentment amount and currency in the current amount fields.
107+
- When receiving a payment or refund from Stripe, we'll need to get the converted amount in `usd` Stripe calculated (from the balance transaction), to impact the merchant's Account balance.
108+
- Revamp metrics so amount are always computed in `Organization.currency`, directly from the Account transactions amount.
109+
110+
This is more or less the Stripe way of doing things.
111+
112+
### Scenario 3: Single Presentment and Settlement Currency per Merchant
113+
114+
In this scenario, we would allow merchants to choose a single presentment and settlement currency for their account. All prices would be shown in that currency, and all funds would be settled in that currency. For example, if they choose EUR, all prices would be shown in EUR, and all funds would be settled in EUR.
115+
116+
#### Implementation details
117+
118+
This is somehow a generalization of our current single-currency model.
119+
120+
- Add `currency` field to Organization model.
121+
- Only currencies supported by Stripe for settlement can be chosen here (for US-based accounts: USD, CAD, EUR, GBP); provided Polar has enabled the settlement currency in Stripe account (with a proper bank account attached).
122+
123+
### Scenario 4: Multiple Presentment and Settlement Currencies
124+
125+
In this scenario, we would allow merchants to set prices in multiple currencies, and customers would be able to pay in their local currency. Merchants would also be able to choose multiple settlement currencies for their account.
126+
127+
**Implementation details**
128+
129+
This is the most complex scenario, as it requires significant changes to our data model and business logic. In particular, an Organization should be able to have several Account (one per settlement currency). It means then we'll need to extract the payout logic from that (since merchants will still likely want to have a single bank account for payouts, regardless of settlement currency).
130+
131+
- Allow to create multiple Account in different settlement currencies for an Organization.
132+
- Only currencies supported by Stripe for settlement can be chosen here (for US-based accounts: USD, CAD, EUR, GBP); provided Polar has enabled the settlement currency in Stripe account (with a proper bank account attached).
133+
- Create a new PayoutAccount entity that'll hold all the payout information (Stripe Connect). Payout entity will be linked to this new PayoutAccount entity as well.
134+
- Add a mandatory `currency` parameter when computing metrics _or_ generate metrics in every enabled settlement currency.
135+
- Allow to set other presentment currencies than `usd` when creating products (only a validation constraint to change).
136+
137+
**Variant 1: limited presentment currencies per Organization**
138+
139+
Only allow to set presentment currencies that match one of the settlement currencies defined for the Organization. This is similar to Scenario 3.
140+
141+
- When receiving a payment or refund from Stripe, match the currency with one of the Organization's settlement currencies, and impact it on the corresponding Account.
142+
143+
**Variant 2: unrestricted presentment currencies**
144+
145+
Allow to set any presentment currency when creating products. This is similar to Scenario 2.
146+
147+
**Implementation A**
148+
149+
- To allow to display and track the presentment currency in the dashboard, add the following columns to Payment, Order, OrderItem, Refund and Subscription models:
150+
- `presentment_currency`: `str`
151+
- `presentment_amount`: `int`
152+
- When receiving a payment or refund from Stripe: if it matches one of the Organization's settlement currencies, impact it on the corresponding Account; otherwise, impact it on the currency which Stripe converted it to (most likely `usd`).
153+
- It means Organization will probably always have a `usd` Account by default.
154+
- Prefer `presentment_amount` and `presentment_currency` when displaying amounts in the dashboard.
155+
156+
**Implementation B**
157+
158+
- Store presentment amount and currency in the current amount fields.
159+
- When receiving a payment or refund from Stripe, if it matches one of the Organization's settlement currencies, impact it on the corresponding Account; otherwise, impact it on the currency which Stripe converted it to (most likely `usd`).
160+
- Revamp metrics so amount are always computed using the amounts from Account's transaction.
161+
162+
This is more or less the Stripe way of doing things.
163+
164+
## Scenario Comparison
165+
166+
When we look at those scenarios, one interesting thing to note is that Scenario 1, 2 and 4 Variant 2 are relatively similar from an implementation standpoint. We can almost already see the iterations there:
167+
168+
1. Scenario 1: multiple presentment currencies, single settlement currency (usd)
169+
2. Scenario 2: multiple presentment currencies, single settlement currency (configurable)
170+
3. Scenario 4 Variant 2: multiple presentment currencies, multiple settlement currencies
171+
172+
I think we can quickly rule out Scenario 3 as it's locking too much the merchant to a single currency, which is not what users expect when asking for multi-currency support. Similar for Scenario 4 Variant 1, which is adding complexity without much added value.
173+
174+
#### Implementation A or B
175+
176+
In those three scenarios, I've described two possible implementation strategies: Implementation A (adding presentment fields) and Implementation B (metrics from Account transactions).
177+
178+
Implementation A has the advantage of being simpler to implement, as we don't need to revamp all the metrics calculations. The drawback is that we'll need to introduce a new set of fields in our models, and use them a bit everywhere in the dashboard and in lot of places in the codebase (e.g. refunds creation or invoice generation). It feels a bit weird to have those fields as "second-class citizens".
179+
180+
Implementation B feels cleaner from a data model standpoint and consistency: amounts are shown in the currency the customer paid it. Most of the code we have today should naturally work out-of-the-box (except a few currency formatting quirks). However, it requires to revamp all the metrics calculations to compute them from Account transactions, which is a significant amount of work and might lead to poorer performance as we need to do more joins.
181+
182+
## Recommendation
183+
184+
Given our current time constraints, I would recommend to go with **Scenario 1** as a first step. It would allow us to deliver multi-currency support relatively quickly, while still providing significant value to our merchants.
185+
186+
Then, we can quite quickly iterate to Scenario 2. Longer-term, we still have the possibility to expand easily towards Scenario 4 Variant 2 if we see a strong demand for it.
187+
188+
From a technical standpoint, I would recommend to go with **Implementation B**. It feels cleaner and more consistent, and will save us from having to maintain two sets of amount fields in our models; even if it requires more work upfront.
189+
190+
## Technical Considerations and Open Questions
191+
192+
### Polar fees
193+
194+
Our fees is a percentage + fixed amount per payment. We should define if we want to compute it:
195+
196+
- On the presentment amount
197+
- On the settlement amount
198+
199+
We should also decide if we want to charge conversion fees when the presentment currency is different than the settlement currency.
200+
201+
#### Comparison with Stripe
202+
203+
Here is what Stripe does:
204+
205+
- If the presentment currency is not supported as settlement currency, Stripe first converts to the settlement currency (usd for us), then computes the fee on that basis, and add a conversion fee percentage.
206+
- Conversion: 12 EUR -> 13.89 USD
207+
- Stripe fee: 13.89 \* 2.9% + 0.30 = 0.70 USD
208+
- Conversion fee: 13.89 \* 1% = 0.14 USD
209+
- Total fee: 0.84 USD
210+
- If the presentment currency is supported as settlement currency, Stripe computes the fee directly in that currency.
211+
- Amount: 60 EUR
212+
- Stripe fee: 60 \* 2.9% + 0.30 = 2.04 EUR
213+
214+
### Exchange Rate Management
215+
216+
Whenever we receive a payment in a presentment currency different than the supported settlement currency, we rely on Stripe to perform the currency conversion. The exchange rate applied is opaque and only known by Stripe.
217+
218+
### Exchange Rate and Refunds
219+
220+
When issuing a refund, we'll issue it in the original presentment currency. If the exchange rate has changed since the original payment, we might have to withdwraw more money from the merchant's balance than the original payment amount (in settlement currency).
221+
222+
For example, if a customer paid 100 EUR when the exchange rate was 1 EUR = 1.1 USD, the merchant received 110 USD. If we refund the customer later when the exchange rate is 1 EUR = 1.2 USD, we need to withdraw 120 USD from the merchant's balance to refund the customer.

0 commit comments

Comments
 (0)