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

mozilla / fx-private-relay / a3eebf4c-c61c-4db7-816c-fdac1fa36466

19 Mar 2025 02:25PM CUT coverage: 85.075% (-0.06%) from 85.137%
a3eebf4c-c61c-4db7-816c-fdac1fa36466

Pull #5456

circleci

groovecoder
for MPP-4020: add sp3_plans to backend and API
Pull Request #5456: for MPP-4020: add sp3_plans to backend and API

2439 of 3571 branches covered (68.3%)

Branch coverage included in aggregate %.

109 of 130 new or added lines in 6 files covered. (83.85%)

2 existing lines in 1 file now uncovered.

17130 of 19431 relevant lines covered (88.16%)

9.84 hits per line

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

55.56
/privaterelay/sp3_plans.py
1
from functools import lru_cache
1✔
2
from typing import Literal, TypedDict
1✔
3

4
from django.conf import settings
1✔
5
from django.http import HttpRequest
1✔
6

7
from privaterelay.country_utils import _get_cc_from_request
1✔
8

9
#
10
# Public types
11
#
12

13
CurrencyStr = Literal["CHF", "CZK", "DKK", "EUR", "PLN", "USD"]
1✔
14
LanguageStr = Literal["de", "fr", "it", "nl"]
1✔
15
CountryStr = Literal[
1✔
16
    "AT",
17
    "BE",
18
    "BG",
19
    "CA",
20
    "CH",
21
    "CY",
22
    "CZ",
23
    "DE",
24
    "DK",
25
    "EE",
26
    "ES",
27
    "FI",
28
    "FR",
29
    "GB",
30
    "GR",
31
    "HR",
32
    "HU",
33
    "IE",
34
    "IT",
35
    "LT",
36
    "LU",
37
    "LV",
38
    "MT",
39
    "MY",
40
    "NL",
41
    "NZ",
42
    "PL",
43
    "PR",
44
    "PT",
45
    "RO",
46
    "SE",
47
    "SG",
48
    "SI",
49
    "SK",
50
    "US",
51
]
52
PeriodStr = Literal["monthly", "yearly"]
1✔
53
PlanType = Literal["premium", "phones", "bundle"]
1✔
54
# See https://docs.google.com/spreadsheets/d/1qThASP94f4KBSwc4pOJRcb09cSInw7vUy_SE8y4KKPc/edit?usp=sharing for valid product keys  # noqa: E501  # ignore long line for URL
55
ProductKey = Literal[
1✔
56
    "relay-premium-127",
57
    "relay-premium-127-phone",
58
    "relay-email-phone-protection-127",
59
    "relay-premium-dev",
60
    "relay-email-phone-protection-dev",
61
    "bundle-relay-vpn-dev",
62
    "relaypremiumemailstage",
63
    "relaypremiumphonestage",
64
    "vpnrelaybundlestage",
65
]
66

67

68
class PlanPricing(TypedDict):
1✔
69
    monthly: dict[Literal["price", "currency", "url"], float | CurrencyStr | str]
1✔
70
    yearly: dict[Literal["price", "currency", "url"], float | CurrencyStr | str]
1✔
71

72

73
SP3PlanCountryLangMapping = dict[
1✔
74
    CountryStr, dict[LanguageStr | Literal["*"], PlanPricing]
75
]
76

77
#
78
# Pricing Data (simplified, no Stripe IDs)
79
#
80

81
PLAN_PRICING: dict[PlanType, dict[CurrencyStr, dict[PeriodStr, float]]] = {
1✔
82
    "premium": {
83
        "CHF": {"monthly": 2.00, "yearly": 1.00},
84
        "CZK": {"monthly": 47.0, "yearly": 23.0},
85
        "DKK": {"monthly": 15.0, "yearly": 7.00},
86
        "EUR": {"monthly": 1.99, "yearly": 0.99},
87
        "PLN": {"monthly": 8.00, "yearly": 5.00},
88
        "USD": {"monthly": 1.99, "yearly": 0.99},
89
    },
90
    "phones": {
91
        "USD": {"monthly": 4.99, "yearly": 3.99},
92
    },
93
    "bundle": {
94
        "USD": {"monthly": 6.99, "yearly": 6.99},
95
    },
96
}
97

98

99
#
100
# Public functions
101
#
102

103

104
def get_sp3_country_language_mapping(plan: PlanType) -> SP3PlanCountryLangMapping:
1✔
105
    """Get plan mapping for the given plan type."""
NEW
106
    return _cached_country_language_mapping(plan)
×
107

108

109
def get_supported_countries(plan: PlanType) -> set[CountryStr]:
1✔
110
    """Get the country codes where the plan is available."""
NEW
111
    return set(get_sp3_country_language_mapping(plan).keys())
×
112

113

114
def get_subscription_url(plan: PlanType, period: PeriodStr) -> str:
1✔
115
    """Generate the URL for a given plan and period."""
116
    product_key: ProductKey
NEW
117
    settings_attr = f"SUBPLAT3_{plan.upper()}_PRODUCT_KEY"
×
NEW
118
    product_key = getattr(settings, settings_attr)
×
NEW
119
    return f"{settings.SUBPLAT3_HOST}/{product_key}/{period}/landing"
×
120

121

122
def get_premium_countries() -> set[CountryStr]:
1✔
123
    """Return the merged set of premium, phones, and bundle country codes."""
NEW
124
    return (
×
125
        get_supported_countries("premium")
126
        | get_supported_countries("phones")
127
        | get_supported_countries("bundle")
128
    )
129

130

131
def is_plan_available_in_country(request: HttpRequest, plan: PlanType) -> bool:
1✔
NEW
132
    country_code = _get_cc_from_request(request)
×
NEW
133
    return country_code in get_supported_countries(plan)
×
134

135

136
#
137
# Internal caching
138
#
139
@lru_cache
1✔
140
def _cached_country_language_mapping(plan: PlanType) -> SP3PlanCountryLangMapping:
1✔
141
    """Create the plan mapping."""
NEW
142
    mapping: SP3PlanCountryLangMapping = {}
×
143

NEW
144
    for country in _get_supported_countries_by_plan(plan):
×
NEW
145
        currency = _get_country_currency(country)
×
NEW
146
        prices = PLAN_PRICING[plan].get(currency, {"monthly": 0.0, "yearly": 0.0})
×
NEW
147
        mapping[country] = {
×
148
            "*": {
149
                "monthly": {
150
                    "price": prices["monthly"],
151
                    "currency": currency,
152
                    "url": get_subscription_url(plan, "monthly"),
153
                },
154
                "yearly": {
155
                    "price": prices["yearly"],
156
                    "currency": currency,
157
                    "url": get_subscription_url(plan, "yearly"),
158
                },
159
            }
160
        }
161

NEW
162
    return mapping
×
163

164

165
def _get_supported_countries_by_plan(plan: PlanType) -> list[CountryStr]:
1✔
166
    """Return the list of supported countries for the given plan."""
NEW
167
    plan_countries: dict[PlanType, list[CountryStr]] = {
×
168
        "premium": [
169
            "AT",
170
            "BE",
171
            "BG",
172
            "CA",
173
            "CH",
174
            "CY",
175
            "CZ",
176
            "DE",
177
            "DK",
178
            "EE",
179
            "ES",
180
            "FI",
181
            "FR",
182
            "GB",
183
            "GR",
184
            "HR",
185
            "HU",
186
            "IE",
187
            "IT",
188
            "LT",
189
            "LU",
190
            "LV",
191
            "MT",
192
            "MY",
193
            "NL",
194
            "NZ",
195
            "PL",
196
            "PR",
197
            "PT",
198
            "RO",
199
            "SE",
200
            "SG",
201
            "SI",
202
            "SK",
203
            "US",
204
        ],
205
        "phones": ["US", "CA", "PR"],
206
        "bundle": ["US", "CA", "PR"],
207
    }
NEW
208
    return plan_countries.get(plan, [])
×
209

210

211
def _get_country_currency(country: CountryStr) -> CurrencyStr:
1✔
212
    """Return the default currency for a given country."""
NEW
213
    country_currency_map: dict[CountryStr, CurrencyStr] = {
×
214
        "AT": "EUR",
215
        "BE": "EUR",
216
        "BG": "EUR",
217
        "CA": "USD",
218
        "CH": "CHF",
219
        "CY": "EUR",
220
        "CZ": "CZK",
221
        "DE": "EUR",
222
        "DK": "DKK",
223
        "EE": "EUR",
224
        "ES": "EUR",
225
        "FI": "EUR",
226
        "FR": "EUR",
227
        "GB": "USD",
228
        "GR": "EUR",
229
        "HR": "EUR",
230
        "HU": "EUR",
231
        "IE": "EUR",
232
        "IT": "EUR",
233
        "LT": "EUR",
234
        "LU": "EUR",
235
        "LV": "EUR",
236
        "MT": "EUR",
237
        "MY": "USD",
238
        "NL": "EUR",
239
        "NZ": "USD",
240
        "PL": "PLN",
241
        "PR": "USD",
242
        "PT": "EUR",
243
        "RO": "EUR",
244
        "SE": "EUR",
245
        "SG": "USD",
246
        "SI": "EUR",
247
        "SK": "EUR",
248
        "US": "USD",
249
    }
NEW
250
    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