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")