5 days subscription interval, metadata

This commit is contained in:
fetch
2025-08-04 21:42:53 -04:00
parent 158f5171fc
commit 84cfd21920
4 changed files with 81 additions and 9 deletions

View File

@@ -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<Option<DBProductPriceId>, 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)))
}

View File

@@ -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<String> },
Medal { id: String },
}
#[derive(Serialize, Deserialize)]

View File

@@ -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::<Vec<ActiveServer>>();
@@ -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,

View File

@@ -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())
}