Repository: liberapay/liberapay.com

Issue:
Use the "preferred currency" option on /<team>/donate when the donor isn't logged in
When donating to my team ([`/voadi/donate`](https://liberapay.com/voadi/donate)) from a logged-out browser, the currency is in euros:

![Screenshot from 2019-05-03 12 36 03](https://user-images.githubusercontent.com/3639540/57152333-3225a180-6da1-11e9-993d-476e7a6c6392.png)

**This is a big problem for me.** My donors are based in the US, and this GUI is a barrier of entry.

It is true you can change the currency, like so:

![Screenshot from 2019-05-03 12 38 49](https://user-images.githubusercontent.com/3639540/57152417-7c0e8780-6da1-11e9-8c96-974bd9852692.png)

But this has problems:

* It's hard to find.
* You have to reload the entire page. This is the part where I imagine people say "eh, whatever" and close the tab without donating.

## My suggestion

The team can choose a "preferred currency." If a user is logged out, just use the team's preferred currency. It's this setting:

![Screenshot from 2019-05-03 12 43 20](https://user-images.githubusercontent.com/3639540/57152309-1f12d180-6da1-11e9-9e61-3971ff822647.png)

This allows teams to have some control over the default currency shown for their donations page. The behavior for logged-in users stays the same (it takes the user's currency preference).

Thank you!


Discussion:
The currency is (supposed to be) influenced by the visitor's location (country detected by Cloudflare based on the IP address), so a donor in the US should get the page in USD.
My IP address says I'm in: New York, United States (10118). I see euros. Sounds like location detection isn't working.

I like your solution. I think it would still be good to fall back to preferred currency if it fails. And it seems like it's failing quietly too, so maybe it should fail loudly (eg send you an email notification)?

Resolving patch:
diff --git a/liberapay/i18n/base.py b/liberapay/i18n/base.py
--- a/liberapay/i18n/base.py
+++ b/liberapay/i18n/base.py
@@ -290,11 +290,20 @@ def make_sorted_dict(keys, d):
 
 COUNTRIES = make_sorted_dict(COUNTRY_CODES, Locale('en').territories)
 
-CURRENCIES_MAP = {
-    k: v[-1][0]
-    for k, v in babel.core.get_global('territory_currencies').items()
-    if v[-1][0] in CURRENCIES
-}
+CURRENCIES_MAP = {}
+today = utcnow().date()
+for country, currencies in babel.core.get_global('territory_currencies').items():
+    for currency, start_date, end_date, tender in currencies:
+        if currency not in CURRENCIES:
+            continue
+        if start_date:
+            start_date = date(*start_date)
+        if end_date:
+            end_date = date(*end_date)
+        if (start_date is None or start_date <= today) and (end_date is None or end_date >= today):
+            assert country not in CURRENCIES_MAP
+            CURRENCIES_MAP[country] = currency
+del today
 
 LANGUAGE_CODES_2 = """
     aa af ak am ar as az be bg bm bn bo br bs ca cs cy da de dz ee el en eo es
@@ -451,6 +460,16 @@ def add_helpers_to_context(context, loc):
     )
 
 
+class DefaultString(str):
+    __slots__ = ()
+
+    def __bool__(self):
+        return False
+
+
+DEFAULT_CURRENCY = DefaultString('EUR')
+
+
 def add_currency_to_state(request, user):
     qs_currency = request.qs.get('currency')
     if qs_currency in CURRENCIES:
@@ -461,4 +480,4 @@ def add_currency_to_state(request, user):
     if user:
         return {'currency': user.main_currency}
     else:
-        return {'currency': CURRENCIES_MAP.get(request.country) or 'EUR'}
+        return {'currency': CURRENCIES_MAP.get(request.country) or DEFAULT_CURRENCY}
diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py
--- a/liberapay/models/participant.py
+++ b/liberapay/models/participant.py
@@ -2162,7 +2162,7 @@ def set_tip_to(self, tippee, periodic_amount, period='weekly',
     def _zero_tip_dict(tippee, currency=None):
         if not isinstance(tippee, Participant):
             tippee = Participant.from_id(tippee)
-        if not currency or currency not in tippee.accepted_currencies_set:
+        if currency is i18n.DEFAULT_CURRENCY or currency not in tippee.accepted_currencies_set:
             currency = tippee.main_currency
         zero = Money.ZEROS[currency]
         return dict(amount=zero, is_funded=False, tippee=tippee.id,
diff --git a/liberapay/utils/state_chain.py b/liberapay/utils/state_chain.py
--- a/liberapay/utils/state_chain.py
+++ b/liberapay/utils/state_chain.py
@@ -15,7 +15,7 @@
 
 
 def attach_environ_to_request(environ, request):
-    request.country = request.headers.get('CF-IPCountry')
+    request.country = request.headers.get(b'Cf-Ipcountry', b'').decode() or None
     request.environ = environ
 
 


Test patch:
diff --git a/tests/py/test_i18n.py b/tests/py/test_i18n.py
--- a/tests/py/test_i18n.py
+++ b/tests/py/test_i18n.py
@@ -1,10 +1,39 @@
 from liberapay.exceptions import AmbiguousNumber, InvalidNumber
-from liberapay.i18n.base import LOCALE_EN, Money
+from liberapay.i18n.base import CURRENCIES_MAP, DEFAULT_CURRENCY, LOCALE_EN, Money
 from liberapay.testing import Harness
 
 
 class Tests(Harness):
 
+    def test_currencies_map(self):
+        assert CURRENCIES_MAP['BR'] == 'BRL'
+        assert CURRENCIES_MAP['CH'] == 'CHF'
+        assert CURRENCIES_MAP['DE'] == 'EUR'
+        assert CURRENCIES_MAP['JP'] == 'JPY'
+        assert CURRENCIES_MAP['US'] == 'USD'
+        assert CURRENCIES_MAP['ZA'] == 'ZAR'
+
+    def test_request_country(self):
+        request = self.client.GET('/', want='request')
+        assert request.country is None
+        request = self.client.GET('/', HTTP_CF_IPCOUNTRY='US', want='request')
+        assert request.country == 'US'
+
+    def test_state_currency(self):
+        state = self.client.GET('/', want='state')
+        assert state['currency'] is DEFAULT_CURRENCY
+        state = self.client.GET('/', HTTP_CF_IPCOUNTRY='CH', want='state')
+        assert state['currency'] == 'CHF'
+        state = self.client.GET('/', HTTP_CF_IPCOUNTRY='US', want='state')
+        assert state['currency'] == 'USD'
+
+    def test_default_donation_currency(self):
+        alice = self.make_participant('alice', main_currency='KRW', accepted_currencies=None)
+        self.add_payment_account(alice, 'stripe', 'KR')
+        self.add_payment_account(alice, 'paypal', 'KR')
+        r = self.client.GET('/alice/donate')
+        assert '<input type="hidden" name="currency" value="KRW" />' in r.text, r.text
+
     def test_format_currency_without_trailing_zeroes(self):
         expected = '$16'
         actual = LOCALE_EN.format_money(Money(16, 'USD'), trailing_zeroes=False)