privaterelay/sp3_plans.py (197 lines of code) (raw):
"""
Paid plans for Relay with SubPlat3 urls.
There is currently a free plan and 3 paid plans:
* free - limited random email masks, one reply
* premium - unlimited email masks, replies, and a custom subdomain
* phones - premium, plus a phone mask
* bundle - premium and phones, plus Mozilla VPN
get_sp3_country_language_mapping gets the details of the paid plans in this structure:
{
"AT": {
"*": {
"monthly": {
"id": "",
"price": 1.99,
"currency": "EUR",
"url": "https://payments-next.stage.fxa.nonprod.webservices.mozgcp.net/relay-premium-127/monthly/landing",
},
"yearly": {
"id": "",
"price": 0.99,
"currency": "EUR",
"url": "https://payments-next.stage.fxa.nonprod.webservices.mozgcp.net/relay-premium-127/yearly/landing",
},
},
},
...
}
This says that Austria (RelayCountryStr "AT") with any language ("*")
has a monthly and a yearly plan. The monthly plan has a Stripe ID of
"price_1LYC79JNcmPzuWtRU7Q238yL", costs €1.99 (CurrencyStr "EUR"), and the sp3 purchase
link url is "https://payments-next.stage.fxa.nonprod.webservices.mozgcp.net/relay-premium-127/monthly/landing".
The yearly plan has a Stripe ID of "price_1LYC7xJNcmPzuWtRcdKXCVZp", costs €11.88 a year
(equivalent to €0.99 a month), and the SP3 purchase link url is
"https://payments-next.stage.fxa.nonprod.webservices.mozgcp.net/relay-premium-127/yearly/landing".
The top-level keys say which countries are supported. The function get_premium_countries
returns these as a set, when the rest of the data is unneeded.
The second-level keys are the languages for that country. When all languages in that
country have the same plan, the single entry is "*". In SubPlat3, all countries have
"*", because Relay does not need to distinguish between the languages in a country:
SubPlat3 does that for us. We have kept the second-level structure for backwards
compatibility with SP2 code while we migrate to SP3. When we have migrated to SP3, we
could refactor the data structure to remove the unneeded 2nd-level language keys.
The third-level keys are the plan periods. Premium and phones are available on
monthly and yearly periods, and bundle is yearly only.
"""
from functools import lru_cache
from typing import Literal, TypedDict
from django.conf import settings
from django.http import HttpRequest
from privaterelay.country_utils import _get_cc_from_request
#
# Public types
#
PlanType = Literal["premium", "phones", "bundle"]
PeriodStr = Literal["monthly", "yearly"]
CurrencyStr = Literal["CHF", "CZK", "DKK", "EUR", "PLN", "USD"]
CountryStr = Literal[
"AT",
"BE",
"BG",
"CA",
"CH",
"CY",
"CZ",
"DE",
"DK",
"EE",
"ES",
"FI",
"FR",
"GB",
"GR",
"HR",
"HU",
"IE",
"IT",
"LT",
"LU",
"LV",
"MT",
"MY",
"NL",
"NZ",
"PL",
"PR",
"PT",
"RO",
"SE",
"SG",
"SI",
"SK",
"US",
]
# See https://docs.google.com/spreadsheets/d/1qThASP94f4KBSwc4pOJRcb09cSInw7vUy_SE8y4KKPc/edit?usp=sharing for valid product keys # noqa: E501 # ignore long line for URL
ProductKey = Literal[
"relay-premium-127",
"relay-premium-127-phone",
"relay-email-phone-protection-127",
"relay-premium-dev",
"relay-email-phone-protection-dev",
"bundle-relay-vpn-dev",
"relaypremiumemailstage",
"relaypremiumphonestage",
"vpnrelaybundlestage",
]
class PlanPricing(TypedDict):
monthly: dict[Literal["price", "currency", "url"], float | CurrencyStr | str]
yearly: dict[Literal["price", "currency", "url"], float | CurrencyStr | str]
SP3PlanCountryLangMapping = dict[CountryStr, dict[Literal["*"], PlanPricing]]
#
# Pricing Data (simplified, no Stripe IDs)
#
PLAN_PRICING: dict[PlanType, dict[CurrencyStr, dict[PeriodStr, float]]] = {
"premium": {
"CHF": {"monthly": 2.00, "yearly": 1.00},
"CZK": {"monthly": 47.0, "yearly": 23.0},
"DKK": {"monthly": 15.0, "yearly": 7.00},
"EUR": {"monthly": 1.99, "yearly": 0.99},
"PLN": {"monthly": 8.00, "yearly": 5.00},
"USD": {"monthly": 1.99, "yearly": 0.99},
},
"phones": {
"USD": {"monthly": 4.99, "yearly": 3.99},
},
"bundle": {
"USD": {"monthly": 6.99, "yearly": 6.99},
},
}
#
# Public functions
#
def get_sp3_country_language_mapping(plan: PlanType) -> SP3PlanCountryLangMapping:
"""Get plan mapping for the given plan type."""
return _cached_country_language_mapping(plan)
def get_supported_countries(plan: PlanType) -> set[CountryStr]:
"""Get the country codes where the plan is available."""
return set(get_sp3_country_language_mapping(plan).keys())
def get_subscription_url(plan: PlanType, period: PeriodStr) -> str:
"""Generate the URL for a given plan and period."""
product_key: ProductKey
settings_attr = f"SUBPLAT3_{plan.upper()}_PRODUCT_KEY"
product_key = getattr(settings, settings_attr)
return f"{settings.SUBPLAT3_HOST}/{product_key}/{period}/landing"
def get_premium_countries() -> set[CountryStr]:
"""Return the merged set of premium, phones, and bundle country codes."""
return (
get_supported_countries("premium")
| get_supported_countries("phones")
| get_supported_countries("bundle")
)
def is_plan_available_in_country(request: HttpRequest, plan: PlanType) -> bool:
country_code = _get_cc_from_request(request)
return country_code in get_supported_countries(plan)
#
# Internal caching
#
@lru_cache
def _cached_country_language_mapping(plan: PlanType) -> SP3PlanCountryLangMapping:
"""Create the plan mapping."""
mapping: SP3PlanCountryLangMapping = {}
for country in _get_supported_countries_by_plan(plan):
currency = _get_country_currency(country)
prices = PLAN_PRICING[plan].get(currency, {"monthly": 0.0, "yearly": 0.0})
mapping[country] = {
"*": {
"monthly": {
"price": prices["monthly"],
"currency": currency,
"url": get_subscription_url(plan, "monthly"),
},
"yearly": {
"price": prices["yearly"],
"currency": currency,
"url": get_subscription_url(plan, "yearly"),
},
}
}
return mapping
def _get_supported_countries_by_plan(plan: PlanType) -> list[CountryStr]:
"""Return the list of supported countries for the given plan."""
plan_countries: dict[PlanType, list[CountryStr]] = {
"premium": [
"AT",
"BE",
"BG",
"CA",
"CH",
"CY",
"CZ",
"DE",
"DK",
"EE",
"ES",
"FI",
"FR",
"GB",
"GR",
"HR",
"HU",
"IE",
"IT",
"LT",
"LU",
"LV",
"MT",
"MY",
"NL",
"NZ",
"PL",
"PR",
"PT",
"RO",
"SE",
"SG",
"SI",
"SK",
"US",
],
"phones": ["US", "CA", "PR"],
"bundle": ["US", "CA", "PR"],
}
return plan_countries.get(plan, [])
def _get_country_currency(country: CountryStr) -> CurrencyStr:
"""Return the default currency for a given country."""
country_currency_map: dict[CountryStr, CurrencyStr] = {
"AT": "EUR",
"BE": "EUR",
"BG": "EUR",
"CA": "USD",
"CH": "CHF",
"CY": "EUR",
"CZ": "CZK",
"DE": "EUR",
"DK": "DKK",
"EE": "EUR",
"ES": "EUR",
"FI": "EUR",
"FR": "EUR",
"GB": "USD",
"GR": "EUR",
"HR": "EUR",
"HU": "EUR",
"IE": "EUR",
"IT": "EUR",
"LT": "EUR",
"LU": "EUR",
"LV": "EUR",
"MT": "EUR",
"MY": "USD",
"NL": "EUR",
"NZ": "USD",
"PL": "PLN",
"PR": "USD",
"PT": "EUR",
"RO": "EUR",
"SE": "EUR",
"SG": "USD",
"SI": "EUR",
"SK": "EUR",
"US": "USD",
}
return country_currency_map.get(country, "USD")