• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / fx-private-relay / 0d156135-cca1-4c97-8cdc-123caf66a5c1

19 Mar 2025 03:45PM CUT coverage: 85.014% (-0.1%) from 85.137%
0d156135-cca1-4c97-8cdc-123caf66a5c1

Pull #5456

circleci

groovecoder
fix MPP-4020: update getPlan.get__SubscribeLink functions to use SP3 url when available
Pull Request #5456: for MPP-4020: add sp3_plans to backend and API

2441 of 3580 branches covered (68.18%)

Branch coverage included in aggregate %.

114 of 144 new or added lines in 7 files covered. (79.17%)

1 existing line in 1 file now uncovered.

17131 of 19442 relevant lines covered (88.11%)

9.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

54.55
/privaterelay/sp3_plans.py
1
"""
2
Paid plans for Relay with SubPlat3 urls.
3

4
There is currently a free plan and 3 paid plans:
5

6
* free - limited random email masks, one reply
7
* premium - unlimited email masks, replies, and a custom subdomain
8
* phones - premium, plus a phone mask
9
* bundle - premium and phones, plus Mozilla VPN
10

11
get_sp3_country_language_mapping gets the details of the paid plans in this structure:
12

13
{
14
  "AT": {
15
    "*": {
16
      "monthly": {
17
        "id": "price_1LYC79JNcmPzuWtRU7Q238yL",
18
        "price": 1.99,
19
        "currency": "EUR",
20
        "url": "https://payments-next.stage.fxa.nonprod.webservices.mozgcp.net/relay-premium-127/monthly/landing",
21
      },
22
      "yearly": {
23
        "id": "price_1LYC7xJNcmPzuWtRcdKXCVZp",
24
        "price": 0.99,
25
        "currency": "EUR",
26
        "url": "https://payments-next.stage.fxa.nonprod.webservices.mozgcp.net/relay-premium-127/yearly/landing",
27
      },
28
    },
29
  },
30
  ...
31
}
32

33
This says that Austria (RelayCountryStr "AT") with any language ("*")
34
has a monthly and a yearly plan. The monthly plan has a Stripe ID of
35
"price_1LYC79JNcmPzuWtRU7Q238yL", costs €1.99 (CurrencyStr "EUR"), and the sp3 purchase
36
link url is "https://payments-next.stage.fxa.nonprod.webservices.mozgcp.net/relay-premium-127/monthly/landing".
37
The yearly plan has a Stripe ID of "price_1LYC7xJNcmPzuWtRcdKXCVZp", costs €11.88 a year
38
(equivalent to €0.99 a month), and the SP3 purchase link url is
39
"https://payments-next.stage.fxa.nonprod.webservices.mozgcp.net/relay-premium-127/yearly/landing".
40

41
The top-level keys say which countries are supported. The function get_premium_countries
42
returns these as a set, when the rest of the data is unneeded.
43

44
The second-level keys are the languages for that country. When all languages in that
45
country have the same plan, the single entry is "*". In SubPlat3, all countries have
46
"*", because Relay does not need to distinguish between the languages in a country:
47
SubPlat3 does that for us. We have kept the second-level structure for backwards
48
compatibility with SP2 code while we migrate to SP3. When we have migrated to SP3, we
49
could refactor the data structure to remove the unneeded 2nd-level language keys.
50

51
The third-level keys are the plan periods. Premium and phones are available on
52
monthly and yearly periods, and bundle is yearly only.
53
"""
54

55
from functools import lru_cache
1✔
56
from typing import Literal, TypedDict
1✔
57

58
from django.conf import settings
1✔
59
from django.http import HttpRequest
1✔
60

61
from privaterelay.country_utils import _get_cc_from_request
1✔
62

63
#
64
# Public types
65
#
66

67
PlanType = Literal["premium", "phones", "bundle"]
1✔
68
PeriodStr = Literal["monthly", "yearly"]
1✔
69
CurrencyStr = Literal["CHF", "CZK", "DKK", "EUR", "PLN", "USD"]
1✔
70
CountryStr = Literal[
1✔
71
    "AT",
72
    "BE",
73
    "BG",
74
    "CA",
75
    "CH",
76
    "CY",
77
    "CZ",
78
    "DE",
79
    "DK",
80
    "EE",
81
    "ES",
82
    "FI",
83
    "FR",
84
    "GB",
85
    "GR",
86
    "HR",
87
    "HU",
88
    "IE",
89
    "IT",
90
    "LT",
91
    "LU",
92
    "LV",
93
    "MT",
94
    "MY",
95
    "NL",
96
    "NZ",
97
    "PL",
98
    "PR",
99
    "PT",
100
    "RO",
101
    "SE",
102
    "SG",
103
    "SI",
104
    "SK",
105
    "US",
106
]
107
# See https://docs.google.com/spreadsheets/d/1qThASP94f4KBSwc4pOJRcb09cSInw7vUy_SE8y4KKPc/edit?usp=sharing for valid product keys  # noqa: E501  # ignore long line for URL
108
ProductKey = Literal[
1✔
109
    "relay-premium-127",
110
    "relay-premium-127-phone",
111
    "relay-email-phone-protection-127",
112
    "relay-premium-dev",
113
    "relay-email-phone-protection-dev",
114
    "bundle-relay-vpn-dev",
115
    "relaypremiumemailstage",
116
    "relaypremiumphonestage",
117
    "vpnrelaybundlestage",
118
]
119

120

121
class PlanPricing(TypedDict):
1✔
122
    monthly: dict[Literal["price", "currency", "url"], float | CurrencyStr | str]
1✔
123
    yearly: dict[Literal["price", "currency", "url"], float | CurrencyStr | str]
1✔
124

125

126
SP3PlanCountryLangMapping = dict[CountryStr, dict[Literal["*"], PlanPricing]]
1✔
127

128
#
129
# Pricing Data (simplified, no Stripe IDs)
130
#
131

132
PLAN_PRICING: dict[PlanType, dict[CurrencyStr, dict[PeriodStr, float]]] = {
1✔
133
    "premium": {
134
        "CHF": {"monthly": 2.00, "yearly": 1.00},
135
        "CZK": {"monthly": 47.0, "yearly": 23.0},
136
        "DKK": {"monthly": 15.0, "yearly": 7.00},
137
        "EUR": {"monthly": 1.99, "yearly": 0.99},
138
        "PLN": {"monthly": 8.00, "yearly": 5.00},
139
        "USD": {"monthly": 1.99, "yearly": 0.99},
140
    },
141
    "phones": {
142
        "USD": {"monthly": 4.99, "yearly": 3.99},
143
    },
144
    "bundle": {
145
        "USD": {"monthly": 6.99, "yearly": 6.99},
146
    },
147
}
148

149

150
#
151
# Public functions
152
#
153

154

155
def get_sp3_country_language_mapping(plan: PlanType) -> SP3PlanCountryLangMapping:
1✔
156
    """Get plan mapping for the given plan type."""
NEW
157
    return _cached_country_language_mapping(plan)
×
158

159

160
def get_supported_countries(plan: PlanType) -> set[CountryStr]:
1✔
161
    """Get the country codes where the plan is available."""
NEW
162
    return set(get_sp3_country_language_mapping(plan).keys())
×
163

164

165
def get_subscription_url(plan: PlanType, period: PeriodStr) -> str:
1✔
166
    """Generate the URL for a given plan and period."""
167
    product_key: ProductKey
NEW
168
    settings_attr = f"SUBPLAT3_{plan.upper()}_PRODUCT_KEY"
×
NEW
169
    product_key = getattr(settings, settings_attr)
×
NEW
170
    return f"{settings.SUBPLAT3_HOST}/{product_key}/{period}/landing"
×
171

172

173
def get_premium_countries() -> set[CountryStr]:
1✔
174
    """Return the merged set of premium, phones, and bundle country codes."""
NEW
175
    return (
×
176
        get_supported_countries("premium")
177
        | get_supported_countries("phones")
178
        | get_supported_countries("bundle")
179
    )
180

181

182
def is_plan_available_in_country(request: HttpRequest, plan: PlanType) -> bool:
1✔
NEW
183
    country_code = _get_cc_from_request(request)
×
NEW
184
    return country_code in get_supported_countries(plan)
×
185

186

187
#
188
# Internal caching
189
#
190
@lru_cache
1✔
191
def _cached_country_language_mapping(plan: PlanType) -> SP3PlanCountryLangMapping:
1✔
192
    """Create the plan mapping."""
NEW
193
    mapping: SP3PlanCountryLangMapping = {}
×
194

NEW
195
    for country in _get_supported_countries_by_plan(plan):
×
NEW
196
        currency = _get_country_currency(country)
×
NEW
197
        prices = PLAN_PRICING[plan].get(currency, {"monthly": 0.0, "yearly": 0.0})
×
NEW
198
        mapping[country] = {
×
199
            "*": {
200
                "monthly": {
201
                    "price": prices["monthly"],
202
                    "currency": currency,
203
                    "url": get_subscription_url(plan, "monthly"),
204
                },
205
                "yearly": {
206
                    "price": prices["yearly"],
207
                    "currency": currency,
208
                    "url": get_subscription_url(plan, "yearly"),
209
                },
210
            }
211
        }
212

NEW
213
    return mapping
×
214

215

216
def _get_supported_countries_by_plan(plan: PlanType) -> list[CountryStr]:
1✔
217
    """Return the list of supported countries for the given plan."""
NEW
218
    plan_countries: dict[PlanType, list[CountryStr]] = {
×
219
        "premium": [
220
            "AT",
221
            "BE",
222
            "BG",
223
            "CA",
224
            "CH",
225
            "CY",
226
            "CZ",
227
            "DE",
228
            "DK",
229
            "EE",
230
            "ES",
231
            "FI",
232
            "FR",
233
            "GB",
234
            "GR",
235
            "HR",
236
            "HU",
237
            "IE",
238
            "IT",
239
            "LT",
240
            "LU",
241
            "LV",
242
            "MT",
243
            "MY",
244
            "NL",
245
            "NZ",
246
            "PL",
247
            "PR",
248
            "PT",
249
            "RO",
250
            "SE",
251
            "SG",
252
            "SI",
253
            "SK",
254
            "US",
255
        ],
256
        "phones": ["US", "CA", "PR"],
257
        "bundle": ["US", "CA", "PR"],
258
    }
NEW
259
    return plan_countries.get(plan, [])
×
260

261

262
def _get_country_currency(country: CountryStr) -> CurrencyStr:
1✔
263
    """Return the default currency for a given country."""
NEW
264
    country_currency_map: dict[CountryStr, CurrencyStr] = {
×
265
        "AT": "EUR",
266
        "BE": "EUR",
267
        "BG": "EUR",
268
        "CA": "USD",
269
        "CH": "CHF",
270
        "CY": "EUR",
271
        "CZ": "CZK",
272
        "DE": "EUR",
273
        "DK": "DKK",
274
        "EE": "EUR",
275
        "ES": "EUR",
276
        "FI": "EUR",
277
        "FR": "EUR",
278
        "GB": "USD",
279
        "GR": "EUR",
280
        "HR": "EUR",
281
        "HU": "EUR",
282
        "IE": "EUR",
283
        "IT": "EUR",
284
        "LT": "EUR",
285
        "LU": "EUR",
286
        "LV": "EUR",
287
        "MT": "EUR",
288
        "MY": "USD",
289
        "NL": "EUR",
290
        "NZ": "USD",
291
        "PL": "PLN",
292
        "PR": "USD",
293
        "PT": "EUR",
294
        "RO": "EUR",
295
        "SE": "EUR",
296
        "SG": "USD",
297
        "SI": "EUR",
298
        "SK": "EUR",
299
        "US": "USD",
300
    }
NEW
301
    return country_currency_map.get(country, "USD")
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc