Merge pull request #13 from storytold/premium-plans

Premium plan definitions
This commit is contained in:
Brandon Thomas authored and GitHub committed 2022-10-23 00:44:34 -04:00
commit e8d376a3eb
7 files changed
+559 -209

No files matched your search

+1 -1
View File
@@ -76,4 +76,4 @@ STRIPE_CHECKOUT_SUCCESS_URL=http://localhost:12345/stripe/checkout/success
STRIPE_CHECKOUT_CANCEL_URL=http://localhost:12345/stripe/checkout/cancel
STRIPE_PUBLISHABLE_KEY=pk_test_51KZvWXEU5se17Mekx6e0gw32ovDF78k2eeaGctLIs8oQVxN6DerEp1BDqh6gLMYKli4VsTZoVGHoxq5RmwKMlxtP00r59UEEad
STRIPE_SECRET_KEY=sk_test_51KZvWXEU5se17Mek11ZdnWr4frnyw7tWOPuvl3pRzL290zvkT6KqcbGUl3fcvc3hA8oH6lWXz2kGWXXUlNcCuDxB003uVT9JX0
STRIPE_SECRET_WEBHOOK_SIGNING_KEY=whsec_40b60bd75768052532b63dbeefa7a1b163e182c3e657fb27fa906ef49a2cd59f
STRIPE_SECRET_WEBHOOK_SIGNING_KEY=whsec_40b60bd75768052532b63dbeefa7a1b163e182c3e657fb27fa906ef49a2cd59f
Generated
+64
View File
@@ -2854,6 +2854,32 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "num"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e"
dependencies = [
"num-bigint 0.1.44",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1"
dependencies = [
"num-integer",
"num-traits",
"rand 0.4.6",
"rustc-serialize",
]
[[package]]
name = "num-bigint"
version = "0.3.2"
@@ -2894,6 +2920,16 @@ dependencies = [
"zeroize",
]
[[package]]
name = "num-complex"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b288631d7878aaf59442cffd36910ea604ecd7745c36054328595114001c9656"
dependencies = [
"num-traits",
"rustc-serialize",
]
[[package]]
name = "num-integer"
version = "0.1.44"
@@ -2915,6 +2951,18 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e"
dependencies = [
"num-bigint 0.1.44",
"num-integer",
"num-traits",
"rustc-serialize",
]
[[package]]
name = "num-traits"
version = "0.2.14"
@@ -3633,6 +3681,12 @@ dependencies = [
"url 2.2.2",
]
[[package]]
name = "rustc-serialize"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
[[package]]
name = "rustc_version"
version = "0.2.3"
@@ -3998,6 +4052,15 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "speculoos"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bcff6a72e2ddfb09ce0064568857e70d3cf6f4342244ebb1f84fa6f89b16c26"
dependencies = [
"num",
]
[[package]]
name = "spin"
version = "0.5.2"
@@ -4252,6 +4315,7 @@ dependencies = [
"serde_derive",
"serde_json",
"sha2 0.9.5",
"speculoos",
"sqlx",
"storage_buckets_common",
"subprocess",
@@ -143,3 +143,6 @@ pulldown-cmark = "0.8.0"
[build-dependencies]
# NB: Only using diesel for migrations support
diesel = { version = "1.4.6", features = ["mysql"] }
[dev-dependencies]
speculoos = "0.9.0" # Fluent assertions
@@ -1,4 +1,3 @@
pub mod plan;
pub mod plan_builder;
pub mod plan_category;
pub mod plan_list;
@@ -1,14 +1,24 @@
use chrono::Duration;
use crate::configs::plans::plan_builder::PlanBuilder;
use crate::configs::plans::plan_category::PlanCategory;
const TTS_DEFAULT_PRIORITY_LEVEL : u8 = 0;
const TTS_DEFAULT_DURATION_SECONDS : i64 = 12;
const VC_DEFAULT_MAX_CONCURRENT_MODELS : u32 = 5;
const VC_DEFAULT_PRE_RECORDED_TIME_LIMIT_SECONDS : i64 = 20;
const VC_DEFAULT_REAL_TIME_TIME_LIMIT_SECONDS : i64 = 20;
const W2L_DEFAULT_TIME_LIMIT_SECONDS : i64 = 20;
/// A Plan is either a free or premium plan.
/// Each plan corresponds to a certain level of service.
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct Plan {
/// Name of the plan
plan_name: String,
/// Name of the plan in URL-friendly "slug" form (human readable, no spaces, underscores),
/// eg. "plan_1", "en_basic", etc.
plan_slug: String,
/// Whether the plan is free, paid, or loyalty-based
plan_category: PlanCategory,
@@ -29,6 +39,93 @@ pub struct Plan {
// ========== Features for TTS ==========
tts_base_priority_level: u8,
tts_max_duration: Duration,
tts_can_generate_mp3: bool, // TODO: This feature doesn't exist yet.
tts_can_upload_private_models: bool,
tts_can_share_private_models: bool,
// ========== Features for Voice Conversion ==========
vc_max_concurrent_models: u32,
vc_pre_recorded_time_limit: Duration,
vc_pre_recorded_time_is_unlimited: bool,
vc_real_time_time_limit: Duration,
vc_real_time_time_is_unlimited: bool,
// ========== Features for W2L ==========
w2l_max_duration: Duration,
w2l_can_turn_off_watermark: bool,
}
impl Plan {
pub fn from_builder(builder: &PlanBuilder) -> Self {
Self {
plan_slug: builder.plan_slug.clone(),
plan_category: builder.plan_category,
cost_per_month_dollars: builder.cost_per_month_dollars.clone(),
stripe_product_id: builder.stripe_product_id.clone(),
stripe_price_id: builder.stripe_price_id.clone(),
is_development_plan: builder.is_development_plan,
tts_base_priority_level: builder.tts_base_priority_level,
tts_max_duration: builder.tts_max_duration,
tts_can_generate_mp3: builder.tts_can_generate_mp3,
tts_can_upload_private_models: builder.tts_can_upload_private_models,
tts_can_share_private_models: builder.tts_can_share_private_models,
vc_max_concurrent_models: builder.vc_max_concurrent_models,
vc_pre_recorded_time_limit: builder.vc_pre_recorded_time_limit,
vc_pre_recorded_time_is_unlimited: builder.vc_pre_recorded_time_is_unlimited,
vc_real_time_time_limit: builder.vc_real_time_time_limit,
vc_real_time_time_is_unlimited: builder.vc_real_time_time_is_unlimited,
w2l_max_duration: builder.w2l_max_duration,
w2l_can_turn_off_watermark: builder.w2l_can_turn_off_watermark,
}
}
pub fn plan_slug(&self) -> &str {
&self.plan_slug
}
pub fn plan_category(&self) -> PlanCategory {
self.plan_category
}
pub fn stripe_product_id(&self) -> Option<&str> {
self.stripe_product_id.as_deref()
}
pub fn stripe_price_id(&self) -> Option<&str> {
self.stripe_price_id.as_deref()
}
pub fn is_development_plan(&self) -> bool {
self.is_development_plan
}
pub fn tts_base_priority_level(&self) -> u8 {
self.tts_base_priority_level
}
pub fn tts_max_duration(&self) -> Duration {
self.tts_max_duration
}
}
/// Builder pattern for Plans.
#[derive(Clone)]
pub struct PlanBuilder {
plan_slug: String,
plan_category: PlanCategory,
cost_per_month_dollars: Option<u32>,
stripe_product_id: Option<String>,
stripe_price_id: Option<String>,
is_development_plan: bool,
// ========== Features for TTS ==========
tts_base_priority_level: u8,
tts_max_duration: Duration,
tts_can_generate_mp3: bool,
@@ -51,27 +148,131 @@ pub struct Plan {
w2l_can_turn_off_watermark: bool,
}
impl Plan {
pub fn from_builder(builder: &PlanBuilder) -> Self {
Self {
plan_name: builder.plan_name.clone(),
plan_category: builder.plan_category,
cost_per_month_dollars: builder.cost_per_month_dollars.clone(),
stripe_product_id: builder.stripe_product_id.clone(),
stripe_price_id: builder.stripe_price_id.clone(),
is_development_plan: builder.is_development_plan,
tts_base_priority_level: builder.tts_base_priority_level,
tts_max_duration: builder.tts_max_duration,
tts_can_generate_mp3: builder.tts_can_generate_mp3,
tts_can_upload_private_models: builder.tts_can_upload_private_models,
tts_can_share_private_models: builder.tts_can_share_private_models,
vc_max_concurrent_models: builder.vc_max_concurrent_models,
vc_pre_recorded_time_limit: builder.vc_pre_recorded_time_limit,
vc_pre_recorded_time_is_unlimited: builder.vc_pre_recorded_time_is_unlimited,
vc_real_time_time_limit: builder.vc_real_time_time_limit,
vc_real_time_time_is_unlimited: builder.vc_real_time_time_is_unlimited,
w2l_max_duration: builder.w2l_max_duration,
w2l_can_turn_off_watermark: builder.w2l_can_turn_off_watermark,
impl PlanBuilder {
pub fn new(plan_slug: &str) -> Self {
// NB: Not using default() since that seems dangerous when we want to be explicit.
PlanBuilder {
plan_slug: plan_slug.to_string(),
plan_category : PlanCategory::Free,
cost_per_month_dollars: None,
stripe_product_id: None,
stripe_price_id: None,
is_development_plan: false,
// TTS
tts_base_priority_level : TTS_DEFAULT_PRIORITY_LEVEL,
tts_max_duration: Duration::seconds(TTS_DEFAULT_DURATION_SECONDS),
tts_can_generate_mp3 : false,
tts_can_upload_private_models: false,
tts_can_share_private_models : false,
// VC
vc_max_concurrent_models: VC_DEFAULT_MAX_CONCURRENT_MODELS,
vc_pre_recorded_time_limit: Duration::seconds(VC_DEFAULT_PRE_RECORDED_TIME_LIMIT_SECONDS),
vc_pre_recorded_time_is_unlimited: false,
vc_real_time_time_limit: Duration::seconds(VC_DEFAULT_REAL_TIME_TIME_LIMIT_SECONDS),
vc_real_time_time_is_unlimited: false,
// W2L
w2l_max_duration: Duration::seconds(W2L_DEFAULT_TIME_LIMIT_SECONDS),
w2l_can_turn_off_watermark: false
}
}
}
pub fn build(&self) -> Plan {
Plan::from_builder(self)
}
pub fn plan_category(mut self, value: PlanCategory) -> Self {
self.plan_category = value;
self
}
pub fn cost_per_month_dollars(mut self, value: u32) -> Self {
self.cost_per_month_dollars = Some(value);
self
}
pub fn stripe_product_id(mut self, value: &str) -> Self {
self.stripe_product_id = Some(value.to_string());
self
}
pub fn stripe_price_id(mut self, value: &str) -> Self {
self.stripe_price_id = Some(value.to_string());
self
}
pub fn is_development_plan(mut self, value: bool) -> Self {
self.is_development_plan = value;
self
}
pub fn tts_base_priority_level(mut self, value: u8) -> Self {
self.tts_base_priority_level = value;
self
}
pub fn tts_max_duration_seconds(mut self, value: i64) -> Self {
self.tts_max_duration = Duration::seconds(value);
self
}
pub fn tts_can_generate_mp3(mut self, value: bool) -> Self {
self.tts_can_generate_mp3 = value;
self
}
pub fn tts_can_upload_private_models(mut self, value: bool) -> Self {
self.tts_can_upload_private_models = value;
self
}
pub fn tts_can_share_private_models(mut self, value: bool) -> Self {
self.tts_can_share_private_models = value;
self
}
pub fn vc_max_concurrent_models(mut self, value: u32) -> Self {
self.vc_max_concurrent_models = value;
self
}
pub fn vc_pre_recorded_time_limit_seconds(mut self, value: i64) -> Self {
self.vc_pre_recorded_time_limit = Duration::seconds(value);
self
}
pub fn vc_pre_recorded_time_is_unlimited(mut self, value: bool) -> Self {
self.vc_pre_recorded_time_is_unlimited = value;
if value {
self.vc_pre_recorded_time_limit = Duration::seconds(60 * 60 * 24 * 7);
}
self
}
pub fn vc_real_time_time_limit_seconds(mut self, value: i64) -> Self {
self.vc_real_time_time_limit = Duration::seconds(value);
self
}
pub fn vc_real_time_time_is_unlimited(mut self, value: bool) -> Self {
self.vc_real_time_time_is_unlimited = value;
if value {
self.vc_real_time_time_limit = Duration::seconds(60 * 60 * 24 * 7);
}
self
}
pub fn w2l_time_limit_seconds(mut self, value: i64) -> Self {
self.w2l_max_duration = Duration::seconds(value);
self
}
pub fn w2l_can_turn_off_watermark(mut self, value: bool) -> Self {
self.w2l_can_turn_off_watermark = value;
self
}
}
// TODO: Add tests
@@ -1,173 +0,0 @@
use chrono::Duration;
use crate::configs::plans::plan::Plan;
use crate::configs::plans::plan_category::PlanCategory;
const TTS_DEFAULT_PRIORITY_LEVEL : u8 = 0;
const TTS_DEFAULT_DURATION_SECONDS : i64 = 12;
const VC_DEFAULT_MAX_CONCURRENT_MODELS : u32 = 5;
const VC_DEFAULT_PRE_RECORDED_TIME_LIMIT_SECONDS : i64 = 20;
const VC_DEFAULT_REAL_TIME_TIME_LIMIT_SECONDS : i64 = 20;
const W2L_DEFAULT_TIME_LIMIT_SECONDS : i64 = 20;
#[derive(Clone)]
pub struct PlanBuilder {
pub plan_name: String,
pub plan_category: PlanCategory,
pub cost_per_month_dollars: Option<u32>,
pub stripe_product_id: Option<String>,
pub stripe_price_id: Option<String>,
pub is_development_plan: bool,
// ========== Features for TTS ==========
pub tts_base_priority_level: u8,
pub tts_max_duration: Duration,
pub tts_can_generate_mp3: bool,
pub tts_can_upload_private_models: bool,
pub tts_can_share_private_models: bool,
// ========== Features for Voice Conversion ==========
pub vc_max_concurrent_models: u32,
pub vc_pre_recorded_time_limit: Duration,
pub vc_pre_recorded_time_is_unlimited: bool,
pub vc_real_time_time_limit: Duration,
pub vc_real_time_time_is_unlimited: bool,
// ========== Features for W2L ==========
pub w2l_max_duration: Duration,
pub w2l_can_turn_off_watermark: bool,
}
impl PlanBuilder {
pub fn new(plan_name: &str) -> Self {
// NB: Not using default() since that seems dangerous when we want to be explicit.
PlanBuilder {
plan_name : plan_name.to_string(),
plan_category : PlanCategory::Free,
cost_per_month_dollars: None,
stripe_product_id: None,
stripe_price_id: None,
is_development_plan: false,
// TTS
tts_base_priority_level : TTS_DEFAULT_PRIORITY_LEVEL,
tts_max_duration: Duration::seconds(TTS_DEFAULT_DURATION_SECONDS),
tts_can_generate_mp3 : false,
tts_can_upload_private_models: false,
tts_can_share_private_models : false,
// VC
vc_max_concurrent_models: VC_DEFAULT_MAX_CONCURRENT_MODELS,
vc_pre_recorded_time_limit: Duration::seconds(VC_DEFAULT_PRE_RECORDED_TIME_LIMIT_SECONDS),
vc_pre_recorded_time_is_unlimited: false,
vc_real_time_time_limit: Duration::seconds(VC_DEFAULT_REAL_TIME_TIME_LIMIT_SECONDS),
vc_real_time_time_is_unlimited: false,
// W2L
w2l_max_duration: Duration::seconds(W2L_DEFAULT_TIME_LIMIT_SECONDS),
w2l_can_turn_off_watermark: false
}
}
pub fn build(&self) -> Plan {
Plan::from_builder(self)
}
pub fn plan_category(mut self, value: PlanCategory) -> Self {
self.plan_category = value;
self
}
pub fn cost_per_month_dollars(mut self, value: u32) -> Self {
self.cost_per_month_dollars = Some(value);
self
}
pub fn stripe_product_id(mut self, value: &str) -> Self {
self.stripe_product_id = Some(value.to_string());
self
}
pub fn stripe_price_id(mut self, value: &str) -> Self {
self.stripe_price_id = Some(value.to_string());
self
}
pub fn is_development_plan(mut self, value: bool) -> Self {
self.is_development_plan = value;
self
}
pub fn tts_base_priority_level(mut self, value: u8) -> Self {
self.tts_base_priority_level = value;
self
}
pub fn tts_max_duration_seconds(mut self, value: i64) -> Self {
self.tts_max_duration = Duration::seconds(value);
self
}
pub fn tts_can_generate_mp3(mut self, value: bool) -> Self {
self.tts_can_generate_mp3 = value;
self
}
pub fn tts_can_upload_private_models(mut self, value: bool) -> Self {
self.tts_can_upload_private_models = value;
self
}
pub fn tts_can_share_private_models(mut self, value: bool) -> Self {
self.tts_can_share_private_models = value;
self
}
pub fn vc_max_concurrent_models(mut self, value: u32) -> Self {
self.vc_max_concurrent_models = value;
self
}
pub fn vc_pre_recorded_time_limit_seconds(mut self, value: i64) -> Self {
self.vc_pre_recorded_time_limit = Duration::seconds(value);
self
}
pub fn vc_pre_recorded_time_is_unlimited(mut self, value: bool) -> Self {
self.vc_pre_recorded_time_is_unlimited = value;
if value {
self.vc_pre_recorded_time_limit = Duration::seconds(60 * 60 * 24 * 7);
}
self
}
pub fn vc_real_time_time_limit_seconds(mut self, value: i64) -> Self {
self.vc_real_time_time_limit = Duration::seconds(value);
self
}
pub fn vc_real_time_time_is_unlimited(mut self, value: bool) -> Self {
self.vc_real_time_time_is_unlimited = value;
if value {
self.vc_real_time_time_limit = Duration::seconds(60 * 60 * 24 * 7);
}
self
}
pub fn w2l_time_limit_seconds(mut self, value: i64) -> Self {
self.w2l_max_duration = Duration::seconds(value);
self
}
pub fn w2l_can_turn_off_watermark(mut self, value: bool) -> Self {
self.w2l_can_turn_off_watermark = value;
self
}
}
@@ -1,10 +1,12 @@
use std::collections::{HashMap, HashSet};
use once_cell::sync::Lazy;
use tts_common::priority::{FAKEYOU_ANONYMOUS_PRIORITY_LEVEL, FAKEYOU_LOGGED_IN_PRIORITY_LEVEL};
use crate::configs::plans::plan::Plan;
use crate::configs::plans::plan_builder::PlanBuilder;
use crate::configs::plans::plan::PlanBuilder;
use crate::configs::plans::plan_category::PlanCategory;
use once_cell::sync::Lazy;
use std::collections::{HashMap, HashSet};
use tts_common::priority::{FAKEYOU_ANONYMOUS_PRIORITY_LEVEL, FAKEYOU_LOGGED_IN_PRIORITY_LEVEL};
/// The plan for free logged-out users.
/// This is declared at the top level because it may need to be referenced directly.
pub static FREE_LOGGED_OUT_PLAN : Lazy<Plan> = Lazy::new(|| {
PlanBuilder::new("free_logged_out")
.plan_category(PlanCategory::Free)
@@ -12,6 +14,17 @@ pub static FREE_LOGGED_OUT_PLAN : Lazy<Plan> = Lazy::new(|| {
.build()
});
/// A plan for free logged-out users on their first few attempts at using the website.
/// This is declared at the top level because it may need to be referenced directly.
pub static FREE_LOGGED_OUT_FIRST_TRY_PLAN : Lazy<Plan> = Lazy::new(|| {
PlanBuilder::new("free_logged_out_first_try")
.plan_category(PlanCategory::Free)
.tts_base_priority_level(FAKEYOU_LOGGED_IN_PRIORITY_LEVEL) // NB: Same as logged-in free users.
.build()
});
/// The plan for free logged-in users.
/// This is declared at the top level because it may need to be referenced directly.
pub static FREE_LOGGED_IN_PLAN : Lazy<Plan> = Lazy::new(|| {
PlanBuilder::new("free_logged_in")
.plan_category(PlanCategory::Free)
@@ -20,45 +33,288 @@ pub static FREE_LOGGED_IN_PLAN : Lazy<Plan> = Lazy::new(|| {
});
/// This plan is for users that create models for us but don't pay.
/// This is declared at the top level because it may need to be referenced directly.
pub static LOYALTY_PLAN : Lazy<Plan> = Lazy::new(|| {
PlanBuilder::new("loyalty_plan")
.plan_category(PlanCategory::LoyaltyReward)
.tts_base_priority_level(2)
.tts_max_duration_seconds(30)
.build()
});
// TODO: Add by-[stripe-product] lookup HashSets.
pub static DEVELOPMENT_PLANS : Lazy<HashSet<Plan>> = Lazy::new(|| {
let mut plans = HashSet::new();
// ========== English plans ==========
plans.insert(PlanBuilder::new("development_fakeyou_en_plus")
.is_development_plan(true)
.plan_category(PlanCategory::Paid)
.stripe_product_id("prod_MMxi2J5y69VPbO")
.stripe_price_id("price_1LeDnKEU5se17MekVr1iYYNf")
.cost_per_month_dollars(5)
.tts_base_priority_level(10)
.tts_max_duration_seconds(30)
.build());
plans.insert(PlanBuilder::new("development_fakeyou_en_pro")
.is_development_plan(true)
.plan_category(PlanCategory::Paid)
.stripe_product_id("prod_MScAZa5uk5TfDY")
.stripe_price_id("price_1LjgwIEU5se17MekzQZUHl9W")
.cost_per_month_dollars(15)
.tts_base_priority_level(20)
.tts_max_duration_seconds(120)
.build());
plans
});
/// All premium / paid-for plans in the system.
pub static PREMIUM_PLANS : Lazy<HashSet<Plan>> = Lazy::new(|| {
let mut plans = HashSet::new();
// ========== English plans ==========
plans.insert(PlanBuilder::new("fakeyou_en_basic")
plans.insert(PlanBuilder::new("fakeyou_en_plus")
.plan_category(PlanCategory::Paid)
.cost_per_month_dollars(5)
.cost_per_month_dollars(7)
.tts_base_priority_level(10)
.tts_max_duration_seconds(30)
.build());
plans.insert(PlanBuilder::new("fakeyou_en_pro")
.plan_category(PlanCategory::Paid)
.cost_per_month_dollars(15)
.tts_base_priority_level(20)
.tts_max_duration_seconds(120)
.build());
// ========== Spanish plans ==========
plans.insert(PlanBuilder::new("fakeyou_es_basic")
plans.insert(PlanBuilder::new("fakeyou_es_plus")
.plan_category(PlanCategory::Paid)
.cost_per_month_dollars(3)
.tts_base_priority_level(5)
.tts_max_duration_seconds(30)
.build());
plans.insert(PlanBuilder::new("fakeyou_es_pro")
.plan_category(PlanCategory::Paid)
.cost_per_month_dollars(7)
.tts_base_priority_level(11)
.tts_base_priority_level(15)
.tts_max_duration_seconds(45)
.build());
plans
});
/// Every single plan in the system: free, paid, loyalty-based, and Stripe development plans.
pub static ALL_PLANS_BY_SLUG : Lazy<HashMap<String, Plan>> = Lazy::new(|| {
let mut plans = HashMap::new();
fn add_plan(plans: &mut HashMap<String, Plan>, plan: &Plan) {
plans.insert(plan.plan_slug().to_string(), plan.clone());
}
add_plan(&mut plans, &FREE_LOGGED_OUT_PLAN);
add_plan(&mut plans, &FREE_LOGGED_OUT_FIRST_TRY_PLAN);
add_plan(&mut plans, &FREE_LOGGED_IN_PLAN);
add_plan(&mut plans, &LOYALTY_PLAN);
PREMIUM_PLANS.iter().for_each(|plan| add_plan(&mut plans, plan));
DEVELOPMENT_PLANS .iter().for_each(|plan| add_plan(&mut plans, plan));
plans
});
/// Every Stripe premium product by its Stripe product id.
pub static PLANS_BY_STRIPE_PRODUCT_ID : Lazy<HashMap<String, Plan>> = Lazy::new(|| {
let mut plans = HashMap::new();
ALL_PLANS_BY_SLUG.values()
.for_each(|plan| {
if let Some(stripe_product_id) = plan.stripe_product_id() {
plans.insert(stripe_product_id.to_string(), plan.clone());
}
});
plans
});
/// Every Stripe premium product by its Stripe price id.
pub static PLANS_BY_STRIPE_PRICE_ID : Lazy<HashMap<String, Plan>> = Lazy::new(|| {
let mut plans = HashMap::new();
ALL_PLANS_BY_SLUG.values()
.for_each(|plan| {
if let Some(stripe_price_id) = plan.stripe_price_id() {
plans.insert(stripe_price_id.to_string(), plan.clone());
}
});
plans
});
// TODO: Add tests to assert our expectations of each plan.
#[cfg(test)]
mod test {
use crate::configs::plans::plan::Plan;
use crate::configs::plans::plan_category::PlanCategory;
use crate::configs::plans::plan_list::{ALL_PLANS_BY_SLUG, DEVELOPMENT_PLANS, PLANS_BY_STRIPE_PRICE_ID, PLANS_BY_STRIPE_PRODUCT_ID, PREMIUM_PLANS};
use speculoos::prelude::*;
// NB: We're being extremely careful in this test and all those that follow, essentially
// making ourselves check twice when we make additions, removals, or other changes.
#[test]
fn test_number_of_plans_is_expected() {
assert_eq!(10, ALL_PLANS_BY_SLUG.len());
}
#[test]
fn test_number_of_production_plans_is_expected() {
let production_plans = ALL_PLANS_BY_SLUG.values()
.filter(|plan| !plan.is_development_plan())
.collect::<Vec<&Plan>>();
assert_eq!(8, production_plans.len());
}
#[test]
fn test_number_of_development_plans_is_expected() {
let development_plans = ALL_PLANS_BY_SLUG.values()
.filter(|plan| plan.is_development_plan())
.collect::<Vec<&Plan>>();
assert_eq!(2, development_plans.len());
}
#[test]
fn test_plans_by_stripe_product_id_are_expected() {
let product_ids = PLANS_BY_STRIPE_PRODUCT_ID.keys()
.map(|product_id| product_id.to_string())
.collect::<Vec<String>>();
let expected_product_ids = vec![
"prod_MMxi2J5y69VPbO".to_string(), // NB: Development
"prod_MScAZa5uk5TfDY".to_string(), // NB: Development
];
assert_that(&product_ids).contains_all_of(&expected_product_ids.iter());
assert_eq!(product_ids.len(), expected_product_ids.len());
}
#[test]
fn test_plans_by_stripe_price_id_are_expected() {
let price_ids = PLANS_BY_STRIPE_PRICE_ID.keys()
.map(|price_id| price_id.to_string())
.collect::<Vec<String>>();
let expected_price_ids = vec![
"price_1LjgwIEU5se17MekzQZUHl9W".to_string(), // NB: Development
"price_1LeDnKEU5se17MekVr1iYYNf".to_string(), // NB: Development
];
assert_that(&price_ids).contains_all_of(&expected_price_ids.iter());
assert_eq!(price_ids.len(), expected_price_ids.len());
}
#[test]
fn test_tts_base_priority_levels_are_expected() {
// Free
assert_eq!(0, ALL_PLANS_BY_SLUG.get("free_logged_out").unwrap().tts_base_priority_level());
assert_eq!(1, ALL_PLANS_BY_SLUG.get("free_logged_out_first_try").unwrap().tts_base_priority_level());
assert_eq!(1, ALL_PLANS_BY_SLUG.get("free_logged_in").unwrap().tts_base_priority_level());
// Loyalty-based
assert_eq!(2, ALL_PLANS_BY_SLUG.get("loyalty_plan").unwrap().tts_base_priority_level());
// Premium
assert_eq!(5, ALL_PLANS_BY_SLUG.get("fakeyou_es_plus").unwrap().tts_base_priority_level());
assert_eq!(10, ALL_PLANS_BY_SLUG.get("fakeyou_en_plus").unwrap().tts_base_priority_level());
assert_eq!(15, ALL_PLANS_BY_SLUG.get("fakeyou_es_pro").unwrap().tts_base_priority_level());
assert_eq!(20, ALL_PLANS_BY_SLUG.get("fakeyou_en_pro").unwrap().tts_base_priority_level());
}
#[test]
fn test_tts_max_durations_are_expected() {
// Free
assert_eq!(12, ALL_PLANS_BY_SLUG.get("free_logged_out").unwrap().tts_max_duration().num_seconds());
assert_eq!(12, ALL_PLANS_BY_SLUG.get("free_logged_out_first_try").unwrap().tts_max_duration().num_seconds());
assert_eq!(12, ALL_PLANS_BY_SLUG.get("free_logged_in").unwrap().tts_max_duration().num_seconds());
// Loyalty-based
assert_eq!(30, ALL_PLANS_BY_SLUG.get("loyalty_plan").unwrap().tts_max_duration().num_seconds());
// Premium
assert_eq!(30, ALL_PLANS_BY_SLUG.get("fakeyou_es_plus").unwrap().tts_max_duration().num_seconds());
assert_eq!(30, ALL_PLANS_BY_SLUG.get("fakeyou_en_plus").unwrap().tts_max_duration().num_seconds());
assert_eq!(45, ALL_PLANS_BY_SLUG.get("fakeyou_es_pro").unwrap().tts_max_duration().num_seconds());
assert_eq!(120, ALL_PLANS_BY_SLUG.get("fakeyou_en_pro").unwrap().tts_max_duration().num_seconds());
}
// The following tests should not need to change much
#[test]
fn test_assert_all_premium_plans_are_paid() {
let mut test_count = 0;
PREMIUM_PLANS.iter().for_each(|plan| {
assert_eq!(plan.plan_category(), PlanCategory::Paid);
test_count += 1;
});
assert!(test_count > 1);
}
#[test]
fn test_assert_all_development_stripe_plans_are_marked_development() {
let mut test_count = 0;
DEVELOPMENT_PLANS.iter().for_each(|plan| {
assert!(plan.is_development_plan());
test_count += 1;
});
assert!(test_count > 1);
}
#[test]
fn test_assert_correct_key_for_plans_indexed_by_slug() {
let mut test_count = 0;
ALL_PLANS_BY_SLUG.iter().for_each(|(slug, plan)| {
assert_eq!(slug, plan.plan_slug());
test_count += 1;
});
assert!(test_count > 1);
}
#[test]
fn test_assert_correct_key_for_plans_indexed_by_product_id() {
let mut test_count = 0;
PLANS_BY_STRIPE_PRODUCT_ID.iter().for_each(|(product_id, plan)| {
assert_eq!(product_id, plan.stripe_product_id().unwrap());
test_count += 1;
});
assert!(test_count > 1);
}
#[test]
fn test_assert_correct_key_for_plans_indexed_by_price_id() {
let mut test_count = 0;
PLANS_BY_STRIPE_PRICE_ID.iter().for_each(|(price_id, plan)| {
assert_eq!(price_id, plan.stripe_price_id().unwrap());
test_count += 1;
});
assert!(test_count > 1);
}
}