From 92a0674776ea762858661402e98fdcbc460b5295 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 16 Sep 2025 09:11:13 -0400 Subject: [PATCH] end and bill dates --- .../storyteller_get_subscription_command.rs | 6 ++++ .../find_subscription_for_owner_user.rs | 19 +++++++++- .../get_session_subscription_handler.rs | 35 ++++++++++++++++--- .../src/lib/panes/BillingSettingsPane.tsx | 24 ++++++++++--- .../src/lib/subscription-state.ts | 6 +++- .../lib/artcraft/ArtcraftGetSubscription.ts | 1 + 6 files changed, 79 insertions(+), 12 deletions(-) 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 68f41321e8..38d0d9c102 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 @@ -36,7 +36,12 @@ pub struct ActiveSubscriptionInfo { pub subscription_token: UserSubscriptionToken, pub product_slug: ArtcraftSubscriptionSlug, pub namespace: PaymentsNamespace, + + /// If the subscription is active, this is the next bill date pub next_bill_at: Option>, + + /// If the subscription is expired or is set to expire, this is the end date of the subscription. + pub subscription_end_at: Option>, } impl SerializeMarker for GetSubscriptionResponse {} @@ -89,6 +94,7 @@ async fn get( product_slug, namespace: sub.namespace, next_bill_at: sub.next_bill_at, + subscription_end_at: sub.subscription_end_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 5feed02607..d35fbf4a5e 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 @@ -34,6 +34,16 @@ pub struct UserSubscription { /// When the current billing period ends (either auto-renew/rebill date or auto-cancel/cancellation date). pub current_billing_period_end_at: DateTime, + + /// If the subscription is set to expire (perhaps in the future), this is the date it will expire. + /// TODO: Determine if this is set on expired, payment invalid, or canceled subscriptions where the date + /// is backdated ahead of `maybe_canceled_at`. + pub maybe_cancel_at: Option>, + + /// This might be *before* the subscription is canceled, depending on stripe setup and + /// the cancellation algorithm used. Eg. a year-long subscription that ends next year + /// might be maybe_canceled_at a few days ago, but set to `maybe_cancel_at` next year. + pub maybe_canceled_at: Option>, } /// Technically, there may be more than one subscription record. @@ -89,6 +99,8 @@ fn map_result(result: Result, sqlx::Error>) -> Resul 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, + maybe_cancel_at: record.maybe_cancel_at, + maybe_canceled_at: record.maybe_canceled_at, })), Ok(None) => Ok(None), Err(e) => Err(e.into()), @@ -117,7 +129,9 @@ SELECT maybe_stripe_invoice_is_paid, subscription_start_at, subscription_expires_at, - current_billing_period_end_at + current_billing_period_end_at, + maybe_cancel_at, + maybe_canceled_at FROM user_subscriptions @@ -157,4 +171,7 @@ struct RawUserSubscription { subscription_expires_at: DateTime, current_billing_period_end_at: DateTime, + + maybe_cancel_at: Option>, + maybe_canceled_at: Option>, } 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 ff04df0656..00b76fc782 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 @@ -81,16 +81,41 @@ pub async fn get_session_subscription_handler( success: true, 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); - + let mut subscription_end_date = None; + + // cancel_at > canceled_at - [normal] subscription was terminated and expires (after the canceled_at date, perhaps in the future) + // cancel_at < canceled_at - [???] - ??? + + match (sub.maybe_cancel_at, sub.maybe_canceled_at) { + (Some(cancel_at), Some(canceled_at)) => { + // NB: `canceled_at` is when the user canceled the subscription, not necessarily when it expires. + // TODO: Not sure this is the correct logic. + subscription_end_date = Some(cancel_at); + }, + (Some(cancel_at), None) => { + // Canceling at end of period. + subscription_end_date = Some(cancel_at); + }, + (None, Some(canceled_at)) => { + // Canceled already. + subscription_end_date = Some(canceled_at); + }, + (None, None) => { + // Active subscription. + next_bill_date = Some(sub.current_billing_period_end_at); + }, + } + + //if sub.maybe_cancel_at.is_none() && sub.maybe_canceled_at.is_none() { + // 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, + subscription_end_at: subscription_end_date, } }), })) 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 fa1d063a2b..945018f2d8 100644 --- a/frontend/libs/components/settings-modal/src/lib/panes/BillingSettingsPane.tsx +++ b/frontend/libs/components/settings-modal/src/lib/panes/BillingSettingsPane.tsx @@ -31,6 +31,9 @@ export const BillingSettingsPane = (args: BillingSettingsPaneProps) => { const hasPaidPlan = subscriptionStore.hasPaidPlan(); + const nextBillAt = subscriptionStore.subscriptionInfo?.nextBillAt?.toLocaleDateString(); + const subscriptionEndAt = subscriptionStore.subscriptionInfo?.subscriptionEndAt?.toLocaleDateString(); + useEffect(() => { creditsStore.fetchFromServer(); subscriptionStore.fetchFromServer(); @@ -70,11 +73,22 @@ export const BillingSettingsPane = (args: BillingSettingsPaneProps) => { {billingInfo.nextPayment.date} */} -
- - Next payment on{" "} - {subscriptionStore.subscriptionInfo?.nextBillAt?.toLocaleDateString()} -
+ + {subscriptionEndAt && ( +
+ + Subscription ends on{" "} + {subscriptionEndAt} +
+ )} + + {nextBillAt && ( +
+ + Next payment on{" "} + {nextBillAt} +
+ )}
diff --git a/frontend/libs/state/subscription/src/lib/subscription-state.ts b/frontend/libs/state/subscription/src/lib/subscription-state.ts index 5d44b82d65..7b16811e59 100644 --- a/frontend/libs/state/subscription/src/lib/subscription-state.ts +++ b/frontend/libs/state/subscription/src/lib/subscription-state.ts @@ -24,8 +24,11 @@ interface SubscriptionInfo { /// The namespace for the subscription ("artcraft" or "fakeyou") namespace: string, - /// The next billing date for the subscription + /// The next billing date for the subscription (if it's active and not set to expire/cancel) nextBillAt?: Date, + + /// The end date for the subscription (if it's expired or set to expire/cancel) + subscriptionEndAt?: Date, } export const useSubscriptionState = create()((set, get) => ({ @@ -47,6 +50,7 @@ export const useSubscriptionState = create()((set, get) => ({ 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, + subscriptionEndAt: data.payload.active_subscription.subscription_end_at ? new Date(data.payload.active_subscription.subscription_end_at) : undefined, }; } console.log(">>> activeSubscription", activeSubscription); diff --git a/frontend/libs/tauri-api/src/lib/artcraft/ArtcraftGetSubscription.ts b/frontend/libs/tauri-api/src/lib/artcraft/ArtcraftGetSubscription.ts index f51e4cc117..c8b6baeb37 100644 --- a/frontend/libs/tauri-api/src/lib/artcraft/ArtcraftGetSubscription.ts +++ b/frontend/libs/tauri-api/src/lib/artcraft/ArtcraftGetSubscription.ts @@ -14,6 +14,7 @@ export interface ActiveSubscriptionInfo { product_slug: string, namespace: string, next_bill_at?: string, + subscription_end_at?: string, } export const ArtcraftGetSubscription = async () : Promise => {