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

mozilla / fx-private-relay / 8aed337d-6ae1-4a90-8596-746db75f1871

19 Mar 2025 03:25PM CUT coverage: 85.024% (-0.1%) from 85.137%
8aed337d-6ae1-4a90-8596-746db75f1871

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

2442 of 3580 branches covered (68.21%)

Branch coverage included in aggregate %.

115 of 145 new or added lines in 7 files covered. (79.31%)

13 existing lines in 2 files now uncovered.

17133 of 19443 relevant lines covered (88.12%)

9.84 hits per line

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

90.63
/privaterelay/plans.py
1
"""
2
Paid plans for Relay
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
These functions get the details of the paid plans:
12

13
* get_premium_country_language_mapping
14
  * get_premium_countries
15
* get_phone_country_language_mapping
16
* get_bundle_country_language_mapping
17

18
They all return a PlanCountryLangMapping dict, which has this structure:
19

20
{
21
  "AT": {
22
    "*": {
23
      "monthly": {
24
        "id": "price_1LYC79JNcmPzuWtRU7Q238yL",
25
        "price": 1.99,
26
        "currency": "EUR",
27
      },
28
      "yearly": {
29
        "id": "price_1LYC7xJNcmPzuWtRcdKXCVZp",
30
        "price": 0.99,
31
        "currency": "EUR",
32
      },
33
    },
34
  },
35
  ...
36
}
37

38
This says that Austria (RelayCountryStr "AT") with any language ("*")
39
has a monthly and a yearly plan. The monthly plan has a Stripe ID of
40
"price_1LYC79JNcmPzuWtRU7Q238yL", and costs €1.99 (CurrencyStr "EUR"). The yearly
41
plan has a Stripe ID of "price_1LYC7xJNcmPzuWtRcdKXCVZp", and costs €11.88 a year,
42
equivalent to €0.99 a month.
43

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

47
The second-level keys are the languages for that country. When all languages in that
48
country have the same plan, the single entry is "*". When the country is known but
49
the language is not, or is not one of the listed languages, the first language is the
50
default for that country.
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
The raw data is stored in two dicts:
56
* _STRIPE_PLAN_DATA
57
* _RELAY_PLANS
58

59
These are extended to support more countries, languages, plans, etc. They are parsed
60
on first use to create the PlanCountryLangMapping, and served from cache on later uses.
61
"""
62

63
from copy import deepcopy
1✔
64
from functools import lru_cache
1✔
65
from typing import Literal, TypedDict, get_args
1✔
66

67
from django.conf import settings
1✔
68

69
#
70
# Public types
71
#
72

73
# ISO 4217 currency identifier
74
# See https://en.wikipedia.org/wiki/ISO_4217
75
CurrencyStr = Literal[
1✔
76
    "CHF",  # Swiss Franc, Fr. or fr.
77
    "CZK",  # Czech koruna, Kč
78
    "DKK",  # Danish krone, kr.
79
    "EUR",  # Euro, €
80
    "PLN",  # Polish złoty, zł
81
    "USD",  # US Dollar, $
82
]
83

84
# ISO 639 language codes handled by Relay
85
# These are the 4th-level keys in _RELAY_PLANS[$PLAN][by_country_and_lang][$COUNTRY],
86
# and are unrelated to the supported languages in Pontoon.
87
#
88
# Use the 2-letter ISO 639-1 code if available, otherwise the 3-letter ISO 639-2 code.
89
# See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
90
# and https://www.loc.gov/standards/iso639-2/php/English_list.php
91
LanguageStr = Literal[
1✔
92
    "de",  # German
93
    "fr",  # French
94
    "it",  # Italian
95
    "nl",  # Dutch
96
]
97

98
# ISO 3166 country codes handled by Relay
99
# Specifically, the two-letter ISO 3116-1 alpha-2 codes
100
# See https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes
101
# and https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
102
CountryStr = Literal[
1✔
103
    "AT",  # Austria
104
    "BE",  # Belgium
105
    "BG",  # Bulgaria
106
    "CA",  # Canada
107
    "CH",  # Switzerland
108
    "CY",  # Cyprus
109
    "CZ",  # Czech Republic / Czechia
110
    "DE",  # Germany
111
    "DK",  # Denmark
112
    "EE",  # Estonia
113
    "ES",  # Spain
114
    "FI",  # Finland
115
    "FR",  # France
116
    "GB",  # United Kingdom
117
    "GR",  # Greece
118
    "HR",  # Croatia
119
    "HU",  # Hungary
120
    "IE",  # Ireland
121
    "IT",  # Italy
122
    "LT",  # Lituania
123
    "LU",  # Luxembourg
124
    "LV",  # Latvia
125
    "MT",  # Malta
126
    "MY",  # Malaysia
127
    "NL",  # Netherlands
128
    "NZ",  # New Zealand
129
    "PL",  # Poland
130
    "PR",  # Puerto Rico
131
    "PT",  # Portugal
132
    "RO",  # Romania
133
    "SE",  # Sweden
134
    "SG",  # Singapore
135
    "SI",  # Slovenia
136
    "SK",  # Slovakia
137
    "US",  # United States
138
]
139
relay_countries = set(get_args(CountryStr))
1✔
140

141
# Periodic subscription categories
142
PeriodStr = Literal["monthly", "yearly"]
1✔
143

144

145
# A Stripe Price, along with key details for Relay website
146
# https://stripe.com/docs/api/prices/object
147
class StripePriceDef(TypedDict):
1✔
148
    id: str  # Must start with "price_"
1✔
149
    price: float
1✔
150
    currency: CurrencyStr
1✔
151

152

153
PricesForPeriodDict = dict[PeriodStr, StripePriceDef]
1✔
154
LanguageOrAny = LanguageStr | Literal["*"]
1✔
155
PricePeriodsForLanguageDict = dict[LanguageOrAny, PricesForPeriodDict]
1✔
156
PlanCountryLangMapping = dict[CountryStr, PricePeriodsForLanguageDict]
1✔
157

158
#
159
# Public functions
160
#
161

162

163
def get_premium_country_language_mapping() -> PlanCountryLangMapping:
1✔
164
    """Get mapping for premium countries (unlimited masks, custom subdomain)"""
165
    return _country_language_mapping("premium")
1✔
166

167

168
def get_premium_countries() -> set[CountryStr]:
1✔
169
    """Get the country codes where Relay premium can be sold"""
170
    mapping = get_premium_country_language_mapping()
1✔
171
    return set(mapping.keys())
1✔
172

173

174
def get_phone_country_language_mapping() -> PlanCountryLangMapping:
1✔
175
    """Get mapping for phone countries (premium + phone mask)"""
176
    return _country_language_mapping("phones")
1✔
177

178

179
def get_bundle_country_language_mapping() -> PlanCountryLangMapping:
1✔
180
    """Get mapping for bundle countries (premium + phone mask + VPN)"""
181
    return _country_language_mapping("bundle")
1✔
182

183

184
#
185
# Private types for Selected Stripe data (_STRIPE_PLAN_DATA)
186
#
187

188
# RFC 5646 regional language tags handled by Relay
189
# Typically an ISO 639 language code, a dash, and an ISO 3166 country code
190
_RegionalLanguageStr = Literal[
1✔
191
    "de-CH",  # German (Swiss)
192
    "fr-CH",  # French (Swiss)
193
    "it-CH",  # Italian (Swiss)
194
]
195

196
# Stripe plans are associated with a country or country-language pair
197
_CountryOrRegion = CountryStr | _RegionalLanguageStr
1✔
198

199

200
# Types for _STRIPE_PLAN_DATA
201
class _StripeMonthlyPriceDetails(TypedDict):
1✔
202
    monthly: float
1✔
203
    monthly_when_yearly: float
1✔
204

205

206
class _StripeMonthlyCountryDetails(TypedDict):
1✔
207
    currency: CurrencyStr
1✔
208
    monthly_id: str
1✔
209
    yearly_id: str
1✔
210

211

212
class _StripeMonthlyPlanDetails(TypedDict):
1✔
213
    periods: Literal["monthly_and_yearly"]
1✔
214
    prices: dict[CurrencyStr, _StripeMonthlyPriceDetails]
1✔
215
    countries_and_regions: dict[_CountryOrRegion, _StripeMonthlyCountryDetails]
1✔
216

217

218
class _StripeYearlyPriceDetails(TypedDict):
1✔
219
    monthly_when_yearly: float
1✔
220

221

222
class _StripeYearlyCountryDetails(TypedDict):
1✔
223
    currency: CurrencyStr
1✔
224
    yearly_id: str
1✔
225

226

227
class _StripeYearlyPlanDetails(TypedDict):
1✔
228
    periods: Literal["yearly"]
1✔
229
    prices: dict[CurrencyStr, _StripeYearlyPriceDetails]
1✔
230
    countries_and_regions: dict[_CountryOrRegion, _StripeYearlyCountryDetails]
1✔
231

232

233
class _StripePlanData(TypedDict):
1✔
234
    premium: _StripeMonthlyPlanDetails
1✔
235
    phones: _StripeMonthlyPlanDetails
1✔
236
    bundle: _StripeYearlyPlanDetails
1✔
237

238

239
_StripePlanDetails = _StripeMonthlyPlanDetails | _StripeYearlyPlanDetails
1✔
240

241
# Selected Stripe data
242
# The "source of truth" is the Stripe data, this copy is used for upsell views
243
# and directing users to the correct Stripe purchase page.
244
_STRIPE_PLAN_DATA: _StripePlanData = {
1✔
245
    "premium": {
246
        "periods": "monthly_and_yearly",
247
        "prices": {
248
            "CHF": {"monthly": 2.00, "monthly_when_yearly": 1.00},
249
            "CZK": {"monthly": 47.0, "monthly_when_yearly": 23.0},
250
            "DKK": {"monthly": 15.0, "monthly_when_yearly": 7.00},
251
            "EUR": {"monthly": 1.99, "monthly_when_yearly": 0.99},
252
            "PLN": {"monthly": 8.00, "monthly_when_yearly": 5.00},
253
            "USD": {"monthly": 1.99, "monthly_when_yearly": 0.99},
254
        },
255
        "countries_and_regions": {
256
            "de-CH": {  # German-speaking Switzerland
257
                "currency": "CHF",
258
                "monthly_id": "price_1LYCqOJNcmPzuWtRuIXpQRxi",
259
                "yearly_id": "price_1LYCqyJNcmPzuWtR3Um5qDPu",
260
            },
261
            "fr-CH": {  # French-speaking Switzerland
262
                "currency": "CHF",
263
                "monthly_id": "price_1LYCvpJNcmPzuWtRq9ci2gXi",
264
                "yearly_id": "price_1LYCwMJNcmPzuWtRm6ebmq2N",
265
            },
266
            "it-CH": {  # Italian-speaking Switzerland
267
                "currency": "CHF",
268
                "monthly_id": "price_1LYCiBJNcmPzuWtRxtI8D5Uy",
269
                "yearly_id": "price_1LYClxJNcmPzuWtRWjslDdkG",
270
            },
271
            "BG": {  # Bulgaria
272
                "currency": "EUR",
273
                "monthly_id": "price_1NOSjBJNcmPzuWtRMQwYp5u1",
274
                "yearly_id": "price_1NOSkTJNcmPzuWtRpbKwsLcw",
275
            },
276
            "CY": {  # Cyprus
277
                "currency": "EUR",
278
                "monthly_id": "price_1NH9saJNcmPzuWtRpffF5I59",
279
                "yearly_id": "price_1NH9rKJNcmPzuWtRzDiXCeEG",
280
            },
281
            "CZ": {  # Czech Republic / Czechia
282
                "currency": "CZK",
283
                "monthly_id": "price_1NNkAlJNcmPzuWtRxsfrXacj",
284
                "yearly_id": "price_1NNkDHJNcmPzuWtRHnQmCDGP",
285
            },
286
            "DE": {  # Germany
287
                "currency": "EUR",
288
                "monthly_id": "price_1LYC79JNcmPzuWtRU7Q238yL",
289
                "yearly_id": "price_1LYC7xJNcmPzuWtRcdKXCVZp",
290
            },
291
            "DK": {  # Denmark
292
                "currency": "DKK",
293
                "monthly_id": "price_1NNfPCJNcmPzuWtR3SNA8gqG",
294
                "yearly_id": "price_1NNfLoJNcmPzuWtRpmLc9lst",
295
            },
296
            "EE": {  # Estonia
297
                "currency": "EUR",
298
                "monthly_id": "price_1NHA1tJNcmPzuWtRvSeyiVYH",
299
                "yearly_id": "price_1NHA2TJNcmPzuWtR10yknZHf",
300
            },
301
            "ES": {  # Spain
302
                "currency": "EUR",
303
                "monthly_id": "price_1LYCWmJNcmPzuWtRtopZog9E",
304
                "yearly_id": "price_1LYCXNJNcmPzuWtRu586XOFf",
305
            },
306
            "FI": {  # Finland
307
                "currency": "EUR",
308
                "monthly_id": "price_1LYBn9JNcmPzuWtRI3nvHgMi",
309
                "yearly_id": "price_1LYBq1JNcmPzuWtRmyEa08Wv",
310
            },
311
            "FR": {  # France
312
                "currency": "EUR",
313
                "monthly_id": "price_1LYBuLJNcmPzuWtRn58XQcky",
314
                "yearly_id": "price_1LYBwcJNcmPzuWtRpgoWcb03",
315
            },
316
            "GB": {  # United Kingdom
317
                "currency": "USD",
318
                "monthly_id": "price_1LYCHpJNcmPzuWtRhrhSYOKB",
319
                "yearly_id": "price_1LYCIlJNcmPzuWtRQtYLA92j",
320
            },
321
            "GR": {  # Greece
322
                "currency": "EUR",
323
                "monthly_id": "price_1NHA5CJNcmPzuWtR1JSmxqFA",
324
                "yearly_id": "price_1NHA4lJNcmPzuWtRniS23IuE",
325
            },
326
            "HR": {  # Croatia
327
                "currency": "EUR",
328
                "monthly_id": "price_1NOSznJNcmPzuWtRH7CEeAwA",
329
                "yearly_id": "price_1NOT0WJNcmPzuWtRpeNDEjvC",
330
            },
331
            "HU": {  # Hungary
332
                "currency": "EUR",
333
                "monthly_id": "price_1PYB6XJNcmPzuWtR5Ff9cW3D",
334
                "yearly_id": "price_1NOOKvJNcmPzuWtR2DEWIRE4",
335
            },
336
            "IE": {  # Ireland
337
                "currency": "EUR",
338
                "monthly_id": "price_1LhdrkJNcmPzuWtRvCc4hsI2",
339
                "yearly_id": "price_1LhdprJNcmPzuWtR7HqzkXTS",
340
            },
341
            "IT": {  # Italy
342
                "currency": "EUR",
343
                "monthly_id": "price_1LYCMrJNcmPzuWtRTP9vD8wY",
344
                "yearly_id": "price_1LYCN2JNcmPzuWtRtWz7yMno",
345
            },
346
            "LT": {  # Lithuania
347
                "currency": "EUR",
348
                "monthly_id": "price_1NHACcJNcmPzuWtR5ZJeVtJA",
349
                "yearly_id": "price_1NHADOJNcmPzuWtR2PSMBMLr",
350
            },
351
            "LU": {  # Luxembourg
352
                "currency": "EUR",
353
                "monthly_id": "price_1NHAFZJNcmPzuWtRm5A7w5qJ",
354
                "yearly_id": "price_1NHAF8JNcmPzuWtRG1FiPK0N",
355
            },
356
            "LV": {  # Latvia
357
                "currency": "EUR",
358
                "monthly_id": "price_1NHAASJNcmPzuWtRpcliwx0R",
359
                "yearly_id": "price_1NHA9lJNcmPzuWtRLf7DV6GA",
360
            },
361
            "MT": {  # Malta
362
                "currency": "EUR",
363
                "monthly_id": "price_1NH9yxJNcmPzuWtRChanpIQU",
364
                "yearly_id": "price_1NH9y3JNcmPzuWtRIJkQos9q",
365
            },
366
            "NL": {  # Netherlands
367
                "currency": "EUR",
368
                "monthly_id": "price_1LYCdLJNcmPzuWtR0J1EHoJ0",
369
                "yearly_id": "price_1LYCdtJNcmPzuWtRVm4jLzq2",
370
            },
371
            "PL": {  # Poland
372
                "currency": "PLN",
373
                "monthly_id": "price_1NNKGJJNcmPzuWtRTlP7GKWW",
374
                "yearly_id": "price_1NNfCvJNcmPzuWtRCvFppHqt",
375
            },
376
            "PT": {  # Portugal
377
                "currency": "EUR",
378
                "monthly_id": "price_1NHAI1JNcmPzuWtRx8jXjkrQ",
379
                "yearly_id": "price_1NHAHWJNcmPzuWtRCRMnWyvK",
380
            },
381
            "RO": {  # Romania
382
                "currency": "EUR",
383
                "monthly_id": "price_1NOOEnJNcmPzuWtRicUvOyUy",
384
                "yearly_id": "price_1NOOEJJNcmPzuWtRyHqMe2jb",
385
            },
386
            "SE": {  # Sweden
387
                "currency": "EUR",
388
                "monthly_id": "price_1LYBblJNcmPzuWtRGRHIoYZ5",
389
                "yearly_id": "price_1LYBeMJNcmPzuWtRT5A931WH",
390
            },
391
            "SI": {  # Slovenia
392
                "currency": "EUR",
393
                "monthly_id": "price_1NHALmJNcmPzuWtR2nIoAzEt",
394
                "yearly_id": "price_1NHAL9JNcmPzuWtRSZ3BWQs0",
395
            },
396
            "SK": {  # Slovakia
397
                "currency": "EUR",
398
                "monthly_id": "price_1NHAJsJNcmPzuWtR71WX0Pz9",
399
                "yearly_id": "price_1NHAKYJNcmPzuWtRtETl30gb",
400
            },
401
            "US": {  # United States
402
                "currency": "USD",
403
                "monthly_id": "price_1LXUcnJNcmPzuWtRpbNOajYS",
404
                "yearly_id": "price_1LXUdlJNcmPzuWtRKTYg7mpZ",
405
            },
406
        },
407
    },
408
    "phones": {
409
        "periods": "monthly_and_yearly",
410
        "prices": {
411
            "USD": {"monthly": 4.99, "monthly_when_yearly": 3.99},
412
        },
413
        "countries_and_regions": {
414
            "US": {  # United States
415
                "currency": "USD",
416
                "monthly_id": "price_1Li0w8JNcmPzuWtR2rGU80P3",
417
                "yearly_id": "price_1Li15WJNcmPzuWtRIh0F4VwP",
418
            }
419
        },
420
    },
421
    "bundle": {
422
        "periods": "yearly",
423
        "prices": {
424
            "USD": {"monthly_when_yearly": 6.99},
425
        },
426
        "countries_and_regions": {
427
            "US": {  # United States
428
                "currency": "USD",
429
                "yearly_id": "price_1LwoSDJNcmPzuWtR6wPJZeoh",
430
            }
431
        },
432
    },
433
}
434

435

436
# Private types for _RELAY_PLANS
437
_RelayPlanCategory = Literal["premium", "phones", "bundle"]
1✔
438

439

440
class _RelayPlansByType(TypedDict, total=False):
1✔
441
    by_country_and_lang: dict[CountryStr, dict[LanguageStr, _CountryOrRegion]]
1✔
442
    by_country_override: dict[CountryStr, CountryStr]
1✔
443
    by_country: list[CountryStr]
1✔
444

445

446
_RelayPlans = dict[_RelayPlanCategory, _RelayPlansByType]
1✔
447

448

449
# Map of Relay-supported countries to languages and their plans
450
# The top-level key is the plan type, "premium" or "phones" or "bundle"
451
# The second-level key is a map from criteria to the Stripe plan country index:
452
#   - "by_country": The plan is indexed by the original country code.
453
#   - "by_country_override": The plan is indexed by a different country code.
454
#     For example, the "phones" plan in Canada ("CA") is the same as the United
455
#     States ("US") plan.
456
#   - "by_country_and_lang": The plan is indexed by country and language. For
457
#     example, German speakers in Belgium have a different plan ("DE") than Dutch
458
#     speakers ("NL"). The first language has the default plan index if none match.
459
# The different maps are used to find the CountryStr that is an index into the
460
# _STRIPE_PLAN_DATA for that plan type.
461
_RELAY_PLANS: _RelayPlans = {
1✔
462
    "premium": {
463
        "by_country": [
464
            "BG",  # Bulgaria
465
            "CY",  # Cyprus
466
            "CZ",  # Czech Republic / Czechia
467
            "DE",  # Germany
468
            "DK",  # Denmark
469
            "EE",  # Estonia
470
            "ES",  # Spain
471
            "FI",  # Finland
472
            "FR",  # France
473
            "GB",  # United Kingdom
474
            "GR",  # Greece
475
            "HR",  # Croatia
476
            "HU",  # Hungary
477
            "IE",  # Ireland
478
            "IT",  # Italy
479
            "LT",  # Lithuania
480
            "LU",  # Luxembourg
481
            "LV",  # Latvia
482
            "MT",  # Malta
483
            "NL",  # Netherlands
484
            "PL",  # Poland
485
            "PT",  # Portugal
486
            "RO",  # Romania
487
            "SE",  # Sweden
488
            "SI",  # Slovenia
489
            "SK",  # Slovakia
490
            "US",  # United States
491
        ],
492
        "by_country_override": {
493
            "AT": "DE",  # Austria -> Germany
494
            "CA": "US",  # Canada -> United States
495
            "MY": "GB",  # Malaysia -> United Kingdom
496
            "NZ": "GB",  # New Zealand -> United Kingdom
497
            "PR": "US",  # Puerto Rico -> United States
498
            "SG": "GB",  # Singapore -> United Kingdom
499
        },
500
        "by_country_and_lang": {
501
            "BE": {  # Belgium
502
                "fr": "FR",  # French-speaking Belgium -> France
503
                "de": "DE",  # German-speaking Belgium -> Germany
504
                "nl": "NL",  # Dutch-speaking Belgium -> Netherlands
505
            },
506
            "CH": {  # Switzerland
507
                "fr": "fr-CH",  # French-speaking Swiss
508
                "de": "de-CH",  # Germany-speaking Swiss
509
                "it": "it-CH",  # Italian-speaking Swiss
510
            },
511
        },
512
    },
513
    "phones": {
514
        "by_country": ["US"],  # United States
515
        "by_country_override": {
516
            "CA": "US",  # Canada -> United States
517
            "PR": "US",  # Puerto Rico -> United States
518
        },
519
    },
520
    "bundle": {
521
        "by_country": ["US"],  # United States
522
        "by_country_override": {
523
            "CA": "US",  # Canada -> United States
524
            "PR": "US",  # Puerto Rico -> United States
525
        },
526
    },
527
}
528

529

530
#
531
# Private functions
532
#
533

534

535
def _country_language_mapping(plan: _RelayPlanCategory) -> PlanCountryLangMapping:
1✔
536
    """Get plan mapping with cache parameters"""
537
    return _cached_country_language_mapping(
1✔
538
        plan=plan,
539
        us_premium_monthly_price_id=settings.PREMIUM_PLAN_ID_US_MONTHLY,
540
        us_premium_yearly_price_id=settings.PREMIUM_PLAN_ID_US_YEARLY,
541
        us_phone_monthly_price_id=settings.PHONE_PLAN_ID_US_MONTHLY,
542
        us_phone_yearly_price_id=settings.PHONE_PLAN_ID_US_YEARLY,
543
        us_bundle_yearly_price_id=settings.BUNDLE_PLAN_ID_US,
544
    )
545

546

547
@lru_cache
1✔
548
def _cached_country_language_mapping(
1✔
549
    plan: _RelayPlanCategory,
550
    us_premium_monthly_price_id: str,
551
    us_premium_yearly_price_id: str,
552
    us_phone_monthly_price_id: str,
553
    us_phone_yearly_price_id: str,
554
    us_bundle_yearly_price_id: str,
555
) -> PlanCountryLangMapping:
556
    """Create the plan mapping with settings overrides"""
557
    relay_maps = _RELAY_PLANS[plan]
1✔
558
    stripe_data = _get_stripe_data_with_overrides(
1✔
559
        us_premium_monthly_price_id=us_premium_monthly_price_id,
560
        us_premium_yearly_price_id=us_premium_yearly_price_id,
561
        us_phone_monthly_price_id=us_phone_monthly_price_id,
562
        us_phone_yearly_price_id=us_phone_yearly_price_id,
563
        us_bundle_yearly_price_id=us_bundle_yearly_price_id,
564
    )[plan]
565

566
    mapping: PlanCountryLangMapping = {}
1✔
567
    for relay_country in relay_maps.get("by_country", []):
1✔
568
        if relay_country in mapping:
1!
UNCOV
569
            raise ValueError("relay_country should not be in mapping.")
×
570
        mapping[relay_country] = {"*": _get_stripe_prices(relay_country, stripe_data)}
1✔
571

572
    for relay_country, override in relay_maps.get("by_country_override", {}).items():
1✔
573
        if relay_country in mapping:
1!
UNCOV
574
            raise ValueError("relay_country should not be in mapping.")
×
575
        mapping[relay_country] = {"*": _get_stripe_prices(override, stripe_data)}
1✔
576

577
    for relay_country, languages in relay_maps.get("by_country_and_lang", {}).items():
1✔
578
        if relay_country in mapping:
1!
UNCOV
579
            raise ValueError("relay_country should not be in mapping.")
×
580
        mapping[relay_country] = {
1✔
581
            lang: _get_stripe_prices(stripe_country, stripe_data)
582
            for lang, stripe_country in languages.items()
583
        }
584
    # Sort by country code
585
    return {code: mapping[code] for code in sorted(mapping)}
1✔
586

587

588
def _get_stripe_prices(
1✔
589
    country_or_region: _CountryOrRegion, data: _StripePlanDetails
590
) -> PricesForPeriodDict:
591
    """Return the Stripe monthly and yearly price data for the given country."""
592
    stripe_details = data["countries_and_regions"][country_or_region]
1✔
593
    currency = stripe_details["currency"]
1✔
594
    prices = data["prices"][currency]
1✔
595
    period_to_details: PricesForPeriodDict = {}
1✔
596
    if data["periods"] == "monthly_and_yearly":
1✔
597
        # mypy thinks stripe_details _could_ be _StripeYearlyPriceDetails,
598
        # so extra asserts are needed to make mypy happy.
599
        monthly_id = str(stripe_details.get("monthly_id"))
1✔
600
        if not monthly_id.startswith("price_"):
1!
UNCOV
601
            raise ValueError("monthly_id must start with 'price_'")
×
602
        price = prices.get("monthly", 0.0)
1✔
603
        if not isinstance(price, float):
1!
UNCOV
604
            raise TypeError("price must be of type float.")
×
605
        period_to_details["monthly"] = {
1✔
606
            "id": monthly_id,
607
            "currency": currency,
608
            "price": price,
609
        }
610
    yearly_id = stripe_details["yearly_id"]
1✔
611
    if not yearly_id.startswith("price_"):
1!
UNCOV
612
        raise ValueError("yearly_id must start with 'price_'")
×
613
    period_to_details["yearly"] = {
1✔
614
        "id": yearly_id,
615
        "currency": currency,
616
        "price": prices["monthly_when_yearly"],
617
    }
618
    return period_to_details
1✔
619

620

621
@lru_cache
1✔
622
def _get_stripe_data_with_overrides(
1✔
623
    us_premium_monthly_price_id: str,
624
    us_premium_yearly_price_id: str,
625
    us_phone_monthly_price_id: str,
626
    us_phone_yearly_price_id: str,
627
    us_bundle_yearly_price_id: str,
628
) -> _StripePlanData:
629
    """Returns the Stripe plan data with settings overrides"""
630
    plan_data = deepcopy(_STRIPE_PLAN_DATA)
1✔
631
    plan_data["premium"]["countries_and_regions"]["US"][
1✔
632
        "monthly_id"
633
    ] = us_premium_monthly_price_id
634
    plan_data["premium"]["countries_and_regions"]["US"][
1✔
635
        "yearly_id"
636
    ] = us_premium_yearly_price_id
637
    plan_data["phones"]["countries_and_regions"]["US"][
1✔
638
        "monthly_id"
639
    ] = us_phone_monthly_price_id
640
    plan_data["phones"]["countries_and_regions"]["US"][
1✔
641
        "yearly_id"
642
    ] = us_phone_yearly_price_id
643
    plan_data["bundle"]["countries_and_regions"]["US"][
1✔
644
        "yearly_id"
645
    ] = us_bundle_yearly_price_id
646
    return plan_data
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