next payment

This commit is contained in:
Brandon Thomas committed 2025-09-16 08:46:51 -04:00
1 parent 9835a238ce
commit 0f5ec3c83b
9 files changed
+56 -9

No files matched your search

@@ -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)
@@ -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<DateTime<Utc>>,
/// If the subscription is set to cancel, this is the end date of the subscription.
pub subscription_end_at: Option<DateTime<Utc>>,
///// When the current billing period ends (either
///// auto-renew/rebill date or auto-cancel/cancellation date).
//pub current_billing_period_end_at: DateTime<Utc>,
}
@@ -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)
}
@@ -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<DateTime<Utc>>,
}
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(),
@@ -31,6 +31,9 @@ pub struct UserSubscription {
/// This is the authoritative timestamp for when the subscription expires.
pub subscription_expires_at: DateTime<Utc>,
/// When the current billing period ends (either auto-renew/rebill date or auto-cancel/cancellation date).
pub current_billing_period_end_at: DateTime<Utc>,
}
/// Technically, there may be more than one subscription record.
@@ -85,6 +88,7 @@ fn map_result(result: Result<Option<RawUserSubscription>, 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<Utc>,
subscription_expires_at: DateTime<Utc>,
current_billing_period_end_at: DateTime<Utc>,
}
@@ -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,
}
}),
}))
}
@@ -70,6 +70,11 @@ export const BillingSettingsPane = (args: BillingSettingsPaneProps) => {
{billingInfo.nextPayment.date}
</div>
*/}
<div className="flex items-center gap-2 text-white/50">
<FontAwesomeIcon icon={faInfoCircle} />
Next payment on{" "}
{subscriptionStore.subscriptionInfo?.nextBillAt?.toLocaleDateString()}
</div>
<hr className="border-white/10" />
@@ -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<SubscriptionState>()((set, get) => ({
@@ -37,13 +40,16 @@ export const useSubscriptionState = create<SubscriptionState>()((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,
}));
@@ -13,6 +13,7 @@ export interface ActiveSubscriptionInfo {
subscription_token: string,
product_slug: string,
namespace: string,
next_bill_at?: string,
}
export const ArtcraftGetSubscription = async () : Promise<ArtcraftGetSubscriptionSuccess> => {