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

mozilla / fx-private-relay / 74d01625-9661-4180-904e-0bd85eb6c11f

29 May 2025 12:03PM CUT coverage: 85.583% (+0.2%) from 85.353%
74d01625-9661-4180-904e-0bd85eb6c11f

push

circleci

web-flow
Merge pull request #5573 from mozilla/MPP-4155-new-megabundle-banner

MPP-4155 New Megabundle Banner Landing Page

2490 of 3629 branches covered (68.61%)

Branch coverage included in aggregate %.

60 of 62 new or added lines in 8 files covered. (96.77%)

19 existing lines in 6 files now uncovered.

17598 of 19843 relevant lines covered (88.69%)

9.97 hits per line

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

89.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
* megabundle - relay, monitor, and vpn
11

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

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

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

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

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

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

56
from functools import lru_cache
1✔
57
from typing import Literal, TypedDict, assert_never, get_args
1✔
58

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

62
from privaterelay.country_utils import _get_cc_from_request
1✔
63

64
#
65
# Public types
66
#
67

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

122
TEST_PRODUCT_KEYS = [
1✔
123
    "premium-key",
124
    "phones-key",
125
    "bundle-key",
126
    "new-premium-key",
127
    "new-phones-key",
128
    "new-bundle-key",
129
]
130

131

132
class PlanPricing(TypedDict):
1✔
133
    monthly: dict[Literal["price", "currency", "url"], float | CurrencyStr | str]
1✔
134
    yearly: dict[Literal["price", "currency", "url"], float | CurrencyStr | str]
1✔
135

136

137
SP3PlanCountryLangMapping = dict[CountryStr, dict[Literal["*"], PlanPricing]]
1✔
138

139
#
140
# Pricing Data (simplified, no Stripe IDs)
141
#
142

143
PLAN_PRICING: dict[PlanType, dict[CurrencyStr, dict[PeriodStr, float]]] = {
1✔
144
    "premium": {
145
        "CHF": {"monthly": 2.00, "yearly": 1.00},
146
        "CZK": {"monthly": 47.0, "yearly": 23.0},
147
        "DKK": {"monthly": 15.0, "yearly": 7.00},
148
        "EUR": {"monthly": 1.99, "yearly": 0.99},
149
        "PLN": {"monthly": 8.00, "yearly": 5.00},
150
        "USD": {"monthly": 1.99, "yearly": 0.99},
151
    },
152
    "phones": {
153
        "USD": {"monthly": 4.99, "yearly": 3.99},
154
    },
155
    "bundle": {
156
        "USD": {"monthly": 6.99, "yearly": 6.99},
157
    },
158
    "megabundle": {
159
        "USD": {"monthly": 8.25, "yearly": 8.25},
160
    },
161
}
162

163

164
#
165
# Public functions
166
#
167

168

169
def get_sp3_country_language_mapping(plan: PlanType) -> SP3PlanCountryLangMapping:
1✔
170
    """Get plan mapping for the given plan type."""
171
    return _cached_country_language_mapping(plan)
1✔
172

173

174
def get_supported_countries(plan: PlanType) -> set[CountryStr]:
1✔
175
    """Get the country codes where the plan is available."""
176
    return set(get_sp3_country_language_mapping(plan).keys())
×
177

178

179
def get_subscription_url(plan: PlanType, period: PeriodStr) -> str:
1✔
180
    """Generate the URL for a given plan and period."""
181
    product_key: str
182
    match plan:
1✔
183
        case "phones":
1✔
184
            product_key = settings.SUBPLAT3_PHONES_PRODUCT_KEY
1✔
185
        case "premium":
1✔
186
            product_key = settings.SUBPLAT3_PREMIUM_PRODUCT_KEY
1✔
187
        case "bundle":
1✔
188
            product_key = settings.SUBPLAT3_BUNDLE_PRODUCT_KEY
1✔
189
        case "megabundle":
1✔
190
            product_key = settings.SUBPLAT3_MEGABUNDLE_PRODUCT_KEY
1✔
191
        case _ as unreachable:  # pragma: no cover
192
            assert_never(unreachable)
193
    valid_keys = list(get_args(ProductKey))
1✔
194
    if settings.IN_PYTEST:
1!
195
        valid_keys.extend(TEST_PRODUCT_KEYS)
1✔
196
    if product_key not in valid_keys:
1!
NEW
197
        raise ValueError("'{product_key}' is not a ProductKey")
×
198
    return f"{settings.SUBPLAT3_HOST}/{product_key}/{period}/landing"
1✔
199

200

201
def get_premium_countries() -> set[CountryStr]:
1✔
202
    """Return the merged set of premium, phones, and bundle country codes."""
203
    return (
×
204
        get_supported_countries("premium")
205
        | get_supported_countries("phones")
206
        | get_supported_countries("bundle")
207
    )
208

209

210
def is_plan_available_in_country(request: HttpRequest, plan: PlanType) -> bool:
1✔
211
    country_code = _get_cc_from_request(request)
×
212
    return country_code in get_supported_countries(plan)
×
213

214

215
#
216
# Internal caching
217
#
218
@lru_cache
1✔
219
def _cached_country_language_mapping(plan: PlanType) -> SP3PlanCountryLangMapping:
1✔
220
    """Create the plan mapping."""
221
    mapping: SP3PlanCountryLangMapping = {}
1✔
222

223
    for country in _get_supported_countries_by_plan(plan):
1✔
224
        currency = _get_country_currency(country)
1✔
225
        prices = PLAN_PRICING[plan].get(currency, {"monthly": 0.0, "yearly": 0.0})
1✔
226
        mapping[country] = {
1✔
227
            "*": {
228
                "monthly": {
229
                    "price": prices["monthly"],
230
                    "currency": currency,
231
                    "url": get_subscription_url(plan, "monthly"),
232
                },
233
                "yearly": {
234
                    "price": prices["yearly"],
235
                    "currency": currency,
236
                    "url": get_subscription_url(plan, "yearly"),
237
                },
238
            }
239
        }
240

241
    return mapping
1✔
242

243

244
def _get_supported_countries_by_plan(plan: PlanType) -> list[CountryStr]:
1✔
245
    """Return the list of supported countries for the given plan."""
246
    plan_countries: dict[PlanType, list[CountryStr]] = {
1✔
247
        "premium": [
248
            "AT",
249
            "BE",
250
            "BG",
251
            "CA",
252
            "CH",
253
            "CY",
254
            "CZ",
255
            "DE",
256
            "DK",
257
            "EE",
258
            "ES",
259
            "FI",
260
            "FR",
261
            "GB",
262
            "GR",
263
            "HR",
264
            "HU",
265
            "IE",
266
            "IT",
267
            "LT",
268
            "LU",
269
            "LV",
270
            "MT",
271
            "MY",
272
            "NL",
273
            "NZ",
274
            "PL",
275
            "PR",
276
            "PT",
277
            "RO",
278
            "SE",
279
            "SG",
280
            "SI",
281
            "SK",
282
            "US",
283
        ],
284
        "phones": ["US", "CA", "PR"],
285
        "bundle": ["US", "CA", "PR"],
286
        "megabundle": ["US"],
287
    }
288
    return plan_countries.get(plan, [])
1✔
289

290

291
def _get_country_currency(country: CountryStr) -> CurrencyStr:
1✔
292
    """Return the default currency for a given country."""
293
    country_currency_map: dict[CountryStr, CurrencyStr] = {
1✔
294
        "AT": "EUR",
295
        "BE": "EUR",
296
        "BG": "EUR",
297
        "CA": "USD",
298
        "CH": "CHF",
299
        "CY": "EUR",
300
        "CZ": "CZK",
301
        "DE": "EUR",
302
        "DK": "DKK",
303
        "EE": "EUR",
304
        "ES": "EUR",
305
        "FI": "EUR",
306
        "FR": "EUR",
307
        "GB": "USD",
308
        "GR": "EUR",
309
        "HR": "EUR",
310
        "HU": "EUR",
311
        "IE": "EUR",
312
        "IT": "EUR",
313
        "LT": "EUR",
314
        "LU": "EUR",
315
        "LV": "EUR",
316
        "MT": "EUR",
317
        "MY": "USD",
318
        "NL": "EUR",
319
        "NZ": "USD",
320
        "PL": "PLN",
321
        "PR": "USD",
322
        "PT": "EUR",
323
        "RO": "EUR",
324
        "SE": "EUR",
325
        "SG": "USD",
326
        "SI": "EUR",
327
        "SK": "EUR",
328
        "US": "USD",
329
    }
330
    return country_currency_map.get(country, "USD")
1✔
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