From 84cfd21920d5c9589f28c99751ed513a44a495ab Mon Sep 17 00:00:00 2001 From: fetch Date: Mon, 4 Aug 2025 21:42:53 -0400 Subject: [PATCH] 5 days subscription interval, metadata --- .../src/database/models/product_item.rs | 26 +++++++++++++ apps/labrinth/src/models/v3/billing.rs | 6 +++ apps/labrinth/src/routes/internal/billing.rs | 20 ++++++---- apps/labrinth/src/routes/internal/medal.rs | 38 ++++++++++++++++++- 4 files changed, 81 insertions(+), 9 deletions(-) diff --git a/apps/labrinth/src/database/models/product_item.rs b/apps/labrinth/src/database/models/product_item.rs index dc7187992..d1fe68b38 100644 --- a/apps/labrinth/src/database/models/product_item.rs +++ b/apps/labrinth/src/database/models/product_item.rs @@ -269,3 +269,29 @@ impl DBProductPrice { Ok(prices) } } + +/// Queries a single price ID for a product by type. +pub async fn unique_price_id_of_product_by_type<'a, E>( + exec: E, + r#type: &str, +) -> Result, DatabaseError> +where + E: sqlx::PgExecutor<'a>, +{ + let maybe_row = sqlx::query!( + " + SELECT + products_prices.id + FROM + products_prices + INNER JOIN + products ON products.metadata ->> 'type' = $1 + LIMIT 1 + ", + r#type, + ) + .fetch_optional(exec) + .await?; + + Ok(maybe_row.map(|x| DBProductPriceId(x.id))) +} diff --git a/apps/labrinth/src/models/v3/billing.rs b/apps/labrinth/src/models/v3/billing.rs index f99fab33a..d5f786f73 100644 --- a/apps/labrinth/src/models/v3/billing.rs +++ b/apps/labrinth/src/models/v3/billing.rs @@ -54,6 +54,7 @@ pub enum Price { #[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Debug, Copy, Clone)] #[serde(rename_all = "kebab-case")] pub enum PriceDuration { + FiveDays, Monthly, Quarterly, Yearly, @@ -62,6 +63,7 @@ pub enum PriceDuration { impl PriceDuration { pub fn duration(&self) -> chrono::Duration { match self { + PriceDuration::FiveDays => chrono::Duration::days(5), PriceDuration::Monthly => chrono::Duration::days(30), PriceDuration::Quarterly => chrono::Duration::days(90), PriceDuration::Yearly => chrono::Duration::days(365), @@ -70,6 +72,7 @@ impl PriceDuration { pub fn from_string(string: &str) -> PriceDuration { match string { + "five-days" => PriceDuration::FiveDays, "monthly" => PriceDuration::Monthly, "quarterly" => PriceDuration::Quarterly, "yearly" => PriceDuration::Yearly, @@ -82,6 +85,7 @@ impl PriceDuration { PriceDuration::Monthly => "monthly", PriceDuration::Quarterly => "quarterly", PriceDuration::Yearly => "yearly", + PriceDuration::FiveDays => "five-days", } } @@ -90,6 +94,7 @@ impl PriceDuration { PriceDuration::Monthly, PriceDuration::Quarterly, PriceDuration::Yearly, + PriceDuration::FiveDays, ] .into_iter() } @@ -152,6 +157,7 @@ impl SubscriptionStatus { #[serde(tag = "type", rename_all = "kebab-case")] pub enum SubscriptionMetadata { Pyro { id: String, region: Option }, + Medal { id: String }, } #[derive(Serialize, Deserialize)] diff --git a/apps/labrinth/src/routes/internal/billing.rs b/apps/labrinth/src/routes/internal/billing.rs index f5eddd856..c20a8ed85 100644 --- a/apps/labrinth/src/routes/internal/billing.rs +++ b/apps/labrinth/src/routes/internal/billing.rs @@ -950,14 +950,17 @@ pub async fn active_servers( let server_ids = servers .into_iter() .filter_map(|x| { - x.metadata.as_ref().map(|metadata| match metadata { - SubscriptionMetadata::Pyro { id, region } => ActiveServer { - user_id: x.user_id.into(), - server_id: id.clone(), - price_id: x.price_id.into(), - interval: x.interval, - region: region.clone(), - }, + x.metadata.as_ref().and_then(|metadata| match metadata { + SubscriptionMetadata::Pyro { id, region } => { + Some(ActiveServer { + user_id: x.user_id.into(), + server_id: id.clone(), + price_id: x.price_id.into(), + interval: x.interval, + region: region.clone(), + }) + } + SubscriptionMetadata::Medal { .. } => None, }) }) .collect::>(); @@ -1846,6 +1849,7 @@ pub async fn stripe_webhook( "region": server_region, "source": source, "payment_interval": metadata.charge_item.subscription_interval.map(|x| match x { + PriceDuration::FiveDays => 1, PriceDuration::Monthly => 1, PriceDuration::Quarterly => 3, PriceDuration::Yearly => 12, diff --git a/apps/labrinth/src/routes/internal/medal.rs b/apps/labrinth/src/routes/internal/medal.rs index a4e2a113a..26e7f20a9 100644 --- a/apps/labrinth/src/routes/internal/medal.rs +++ b/apps/labrinth/src/routes/internal/medal.rs @@ -4,9 +4,13 @@ use chrono::Utc; use serde::{Deserialize, Serialize}; use sqlx::PgPool; +use crate::database::models::generate_user_subscription_id; +use crate::database::models::product_item; +use crate::database::models::user_subscription_item::DBUserSubscription; use crate::database::models::users_redeemals::{ Offer, RedeemalLookupFields, Status, UserRedeemal, }; +use crate::models::v3::billing::{PriceDuration, SubscriptionStatus}; use crate::routes::ApiError; use crate::util::guards::medal_key_guard; @@ -64,7 +68,7 @@ pub async fn redeem( ) .await?; - let _redeemal = match maybe_fields { + let redeemal = match maybe_fields { None => return Err(ApiError::NotFound), Some(fields) => { if fields.redeemal_status.is_some() { @@ -88,5 +92,37 @@ pub async fn redeem( txn.commit().await?; + // TODO: Provision server (send archon request) THEN add subscription to DB + + let mut txn = pool.begin().await?; + + // Find the Medal product price + let maybe_price_id = + product_item::unique_price_id_of_product_by_type(&mut *txn, "medal") + .await?; + + let Some(medal_price_id) = maybe_price_id else { + return Ok(HttpResponse::NotImplemented() + .body("Missing price ID for Medal subscription")); + }; + + // Build a subscription using this price ID. + let subscription = DBUserSubscription { + id: generate_user_subscription_id(&mut txn).await?, + user_id: redeemal.user_id, + price_id: medal_price_id, + interval: PriceDuration::FiveDays, + created: Utc::now(), + status: SubscriptionStatus::Unprovisioned, + metadata: None, // TODO: Provision server, then add metadata + }; + + // TODO: Insert a cancelled charge in 5 days time, `index_subscriptions` will unprovision + // the subscription. + + subscription.upsert(&mut txn).await?; + + txn.commit().await?; + Ok(HttpResponse::Ok().finish()) }