diff --git a/_sql/migrations_squashed/user_tables/user_subscriptions.sql b/_sql/migrations_squashed/user_tables/user_subscriptions.sql index 68c3118bf5..fae704bf31 100644 --- a/_sql/migrations_squashed/user_tables/user_subscriptions.sql +++ b/_sql/migrations_squashed/user_tables/user_subscriptions.sql @@ -84,7 +84,10 @@ CREATE TABLE user_subscriptions ( -- This may predate the Stripe object `created` timestamp due to backdating. subscription_start_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - -- We'll use this to determine if the subscription is active. + -- Our synthetic "expire" date. Moved forward for active subscriptions, + -- backdated for canceled subscriptions. + -- + -- TODO: We'll (maybe?) use this to determine if the subscription is active. -- -- Always compare against MySQL's clock rather than the app's clock so that we -- don't get weird clock skew behaviors across multiple requests. @@ -94,8 +97,12 @@ CREATE TABLE user_subscriptions ( subscription_expires_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Billing periods. - -- Maybe useful for debugging. + -- Stripe API: subscription.items[0].data.current_period_start current_billing_period_start_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + -- Stripe API: subscription.items[0].data.current_period_end + -- current_period_end is when the subscription renews (or cancels, if the + -- subscription is set to cancel). current_billing_period_end_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Subscription cancellation (future and past) diff --git a/crates/api_clients/artcraft_api_defs/src/subscriptions/get_session_subscription.rs b/crates/api_clients/artcraft_api_defs/src/subscriptions/get_session_subscription.rs index 26f91b5c01..aa7a0088dd 100644 --- a/crates/api_clients/artcraft_api_defs/src/subscriptions/get_session_subscription.rs +++ b/crates/api_clients/artcraft_api_defs/src/subscriptions/get_session_subscription.rs @@ -1,3 +1,4 @@ +use chrono::{DateTime, Utc}; use enums::common::payments_namespace::PaymentsNamespace; use serde_derive::{Deserialize, Serialize}; use tokens::tokens::user_subscriptions::UserSubscriptionToken; @@ -23,4 +24,14 @@ pub struct SubscriptionInfo { /// Local key for the subscription. pub product_slug: String, + + /// If the subscription is currently active, this is the next billing date. + pub next_bill_at: Option>, + + /// If the subscription is set to cancel, this is the end date of the subscription. + pub subscription_end_at: Option>, + + ///// When the current billing period ends (either + ///// auto-renew/rebill date or auto-cancel/cancellation date). + //pub current_billing_period_end_at: DateTime, } diff --git a/crates/api_clients/storyteller_client/src/subscriptions/get_session_subscription.rs b/crates/api_clients/storyteller_client/src/subscriptions/get_session_subscription.rs index b177731990..7b44a8ee87 100644 --- a/crates/api_clients/storyteller_client/src/subscriptions/get_session_subscription.rs +++ b/crates/api_clients/storyteller_client/src/subscriptions/get_session_subscription.rs @@ -22,9 +22,7 @@ pub async fn get_session_subscription( ).await?) } - fn get_url_path(api_host: &ApiHost, payments_namespace: PaymentsNamespace) -> String { let payments_namespace = payments_namespace.to_str(); format!("/v1/subscriptions/namespace/{}", payments_namespace) } - diff --git a/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_get_subscription_command.rs b/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_get_subscription_command.rs index a1d3e06ee1..68f41321e8 100644 --- a/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_get_subscription_command.rs +++ b/crates/desktop/artcraft/src/services/storyteller/commands/storyteller_get_subscription_command.rs @@ -1,3 +1,4 @@ +use chrono::{DateTime, Utc}; use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::{ContextualImageEditModel, EditImageQuality, EditImageSize}; use crate::core::commands::providers::get_provider_order_command::GetProviderOrderResponse; use crate::core::commands::response::shorthand::ResponseOrErrorMessage; @@ -35,6 +36,7 @@ pub struct ActiveSubscriptionInfo { pub subscription_token: UserSubscriptionToken, pub product_slug: ArtcraftSubscriptionSlug, pub namespace: PaymentsNamespace, + pub next_bill_at: Option>, } impl SerializeMarker for GetSubscriptionResponse {} @@ -86,6 +88,7 @@ async fn get( subscription_token: sub.subscription_token, product_slug, namespace: sub.namespace, + next_bill_at: sub.next_bill_at, }) }) .flatten(), diff --git a/crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/find_subscription_for_owner_user.rs b/crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/find_subscription_for_owner_user.rs index 47f4ed8a1a..5feed02607 100644 --- a/crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/find_subscription_for_owner_user.rs +++ b/crates/schema/database/mysql_queries/src/queries/users/user_subscriptions/find_subscription_for_owner_user.rs @@ -31,6 +31,9 @@ pub struct UserSubscription { /// This is the authoritative timestamp for when the subscription expires. pub subscription_expires_at: DateTime, + + /// When the current billing period ends (either auto-renew/rebill date or auto-cancel/cancellation date). + pub current_billing_period_end_at: DateTime, } /// Technically, there may be more than one subscription record. @@ -85,6 +88,7 @@ fn map_result(result: Result, sqlx::Error>) -> Resul stripe_invoice_is_paid: nullable_i8_to_bool_default_false(record.maybe_stripe_invoice_is_paid), subscription_start_at: record.subscription_start_at, subscription_expires_at: record.subscription_expires_at, + current_billing_period_end_at: record.current_billing_period_end_at, })), Ok(None) => Ok(None), Err(e) => Err(e.into()), @@ -112,7 +116,8 @@ SELECT maybe_stripe_subscription_status, maybe_stripe_invoice_is_paid, subscription_start_at, - subscription_expires_at + subscription_expires_at, + current_billing_period_end_at FROM user_subscriptions @@ -150,4 +155,6 @@ struct RawUserSubscription { subscription_start_at: DateTime, subscription_expires_at: DateTime, + + current_billing_period_end_at: DateTime, } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/get_session_subscription_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/get_session_subscription_handler.rs index 4d7b31f873..ff04df0656 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/get_session_subscription_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/subscriptions/get_session_subscription_handler.rs @@ -79,10 +79,19 @@ pub async fn get_session_subscription_handler( Ok(Json(GetSessionSubscriptionResponse { success: true, - active_subscription: maybe_subscription.map(|sub| SubscriptionInfo { - namespace: sub.subscription_namespace, - product_slug: sub.subscription_product_slug, - subscription_token: sub.token, + active_subscription: maybe_subscription.map(|sub| { + let mut next_bill_date = None; + + // TODO: Suppress if canceled. + next_bill_date = Some(sub.current_billing_period_end_at); + + SubscriptionInfo { + namespace: sub.subscription_namespace, + product_slug: sub.subscription_product_slug, + subscription_token: sub.token, + next_bill_at: next_bill_date, + subscription_end_at: None, + } }), })) } diff --git a/frontend/libs/components/settings-modal/src/lib/panes/BillingSettingsPane.tsx b/frontend/libs/components/settings-modal/src/lib/panes/BillingSettingsPane.tsx index 07f247d7e0..fa1d063a2b 100644 --- a/frontend/libs/components/settings-modal/src/lib/panes/BillingSettingsPane.tsx +++ b/frontend/libs/components/settings-modal/src/lib/panes/BillingSettingsPane.tsx @@ -70,6 +70,11 @@ export const BillingSettingsPane = (args: BillingSettingsPaneProps) => { {billingInfo.nextPayment.date} */} +
+ + Next payment on{" "} + {subscriptionStore.subscriptionInfo?.nextBillAt?.toLocaleDateString()} +

diff --git a/frontend/libs/state/subscription/src/lib/subscription-state.ts b/frontend/libs/state/subscription/src/lib/subscription-state.ts index 9219c7fea5..5d44b82d65 100644 --- a/frontend/libs/state/subscription/src/lib/subscription-state.ts +++ b/frontend/libs/state/subscription/src/lib/subscription-state.ts @@ -23,6 +23,9 @@ interface SubscriptionInfo { /// The namespace for the subscription ("artcraft" or "fakeyou") namespace: string, + + /// The next billing date for the subscription + nextBillAt?: Date, } export const useSubscriptionState = create()((set, get) => ({ @@ -37,13 +40,16 @@ export const useSubscriptionState = create()((set, get) => ({ const data = await ArtcraftGetSubscription(); if (!!data.payload) { let activeSubscription = undefined; + console.log(">>> subscription data.payload", data.payload); if (!!data.payload.active_subscription) { activeSubscription = { subscriptionToken: data.payload.active_subscription.subscription_token, productSlug: data.payload.active_subscription.product_slug, namespace: data.payload.active_subscription.namespace, + nextBillAt: data.payload.active_subscription.next_bill_at ? new Date(data.payload.active_subscription.next_bill_at) : undefined, }; } + console.log(">>> activeSubscription", activeSubscription); set((state) => ({ subscriptionInfo: activeSubscription, })); diff --git a/frontend/libs/tauri-api/src/lib/artcraft/ArtcraftGetSubscription.ts b/frontend/libs/tauri-api/src/lib/artcraft/ArtcraftGetSubscription.ts index c0daa1647c..f51e4cc117 100644 --- a/frontend/libs/tauri-api/src/lib/artcraft/ArtcraftGetSubscription.ts +++ b/frontend/libs/tauri-api/src/lib/artcraft/ArtcraftGetSubscription.ts @@ -13,6 +13,7 @@ export interface ActiveSubscriptionInfo { subscription_token: string, product_slug: string, namespace: string, + next_bill_at?: string, } export const ArtcraftGetSubscription = async () : Promise => {