diff --git a/src/packages/frontend/purchases/resume-subscription.tsx b/src/packages/frontend/purchases/resume-subscription.tsx index 743566a86d..8d5a696aee 100644 --- a/src/packages/frontend/purchases/resume-subscription.tsx +++ b/src/packages/frontend/purchases/resume-subscription.tsx @@ -1,4 +1,4 @@ -import { Alert, Button, Divider, Modal, Spin } from "antd"; +import { Alert, Button, Divider, Modal, Space, Spin } from "antd"; import { useEffect, useRef, useState } from "react"; import { Icon } from "@cocalc/frontend/components"; import { zIndexPayAsGo } from "./zindex"; @@ -8,7 +8,9 @@ import { resumeSubscription, getSubscription, } from "./api"; -import StripePayment from "@cocalc/frontend/purchases/stripe-payment"; +import StripePayment, { + BigSpin, +} from "@cocalc/frontend/purchases/stripe-payment"; import type { LineItem } from "@cocalc/util/stripe/types"; import { currency, round2up } from "@cocalc/util/misc"; import { decimalSubtract } from "@cocalc/util/stripe/calc"; @@ -34,9 +36,9 @@ export default function ResumeSubscription({ const [status, setStatus] = useState(status0); const [error, setError] = useState(""); const [lineItems, setLineItems] = useState(null); - const [place, setPlace] = useState<"checkout" | "processing" | "congrats">( - "checkout", - ); + const [place, setPlace] = useState< + "checkout" | "processing" | "buying" | "congrats" + >("checkout"); const [disabled, setDisabled] = useState(false); const numPaymentsRef = useRef(null); const [costToResume, setCostToResume] = useState( @@ -47,6 +49,28 @@ export default function ResumeSubscription({ ); const [loading, setLoading] = useState(false); + const directResume = async () => { + // user is paying entirely using their credit on file, so we need to get + // the purchase to happen via the API. Otherwise, they paid and metadata + // got setup so when that payment intent is processed, their item gets + // allocated. + try { + setError(""); + setDisabled(true); + setPlace("buying"); + await resumeSubscription(subscription_id); + setPlace("congrats"); + const { status } = await getSubscription(subscription_id); + setStatus(status); + } catch (err) { + setPlace("checkout"); + setError(`${err}`); + return; + } finally { + setDisabled(false); + } + }; + const update = async () => { if (!open) { return; @@ -107,7 +131,7 @@ export default function ResumeSubscription({ <> There is no charge to resume your subscription, since your license is still active. Your subscription will resume - at the current rate, which is + at the current rate, which is{" "} {currency(round2up(periodicCost))}/{interval}. ) : ( @@ -154,49 +178,55 @@ export default function ResumeSubscription({ )} {place == "checkout" && lineItems == null && } - {place == "checkout" && lineItems != null && ( - <> + {place == "checkout" && + lineItems != null && + (costToResume == 0 ? (
- { - if (!total) { - console.log("total = 0 so pay directly"); - // user is paying entirely using their credit on file, so we need to get - // the purchase to happen via the API. Otherwise, they paid and metadata - // got setup so when that payment intent is processed, their item gets - // allocated. - try { - setError(""); - setDisabled(true); + + + + +
+ ) : ( + <> +
+ { + if (!total) { + directResume(); + } else { setPlace("processing"); - await resumeSubscription(subscription_id); - } catch (err) { - setError(`${err}`); - return; - } finally { - setDisabled(false); } - setPlace("congrats"); - return; - } - setPlace("processing"); - }} + }} + /> +
+ - - - )} - {place != "processing" && ( - - )} + + ))} ); } @@ -214,11 +244,12 @@ export default function ResumeSubscription({ title={ <> - Pay to Resume Subscription Id {subscription_id} + Resume Subscription Id {subscription_id} } > {body} + {loading && } ); } diff --git a/src/packages/frontend/purchases/subscriptions.tsx b/src/packages/frontend/purchases/subscriptions.tsx index dcfd9e44fc..e5734bb3de 100644 --- a/src/packages/frontend/purchases/subscriptions.tsx +++ b/src/packages/frontend/purchases/subscriptions.tsx @@ -539,36 +539,33 @@ export default function Subscriptions() { style={{ marginBottom: "15px" }} /> )} - {loading ? ( - - ) : ( -
- - } +
+ +
+ {current != null && ( + { + setCurrent(undefined); + Fragment.clear(); + redux.getActions("account").setFragment(undefined); + }} /> - {current != null && ( - { - setCurrent(undefined); - Fragment.clear(); - redux.getActions("account").setFragment(undefined); - }} - /> - )} - - )} + )} + ); } diff --git a/src/packages/server/purchases/resume-subscription.ts b/src/packages/server/purchases/resume-subscription.ts index 8f4c3b8160..1bc9329257 100644 --- a/src/packages/server/purchases/resume-subscription.ts +++ b/src/packages/server/purchases/resume-subscription.ts @@ -31,7 +31,7 @@ export default async function resumeSubscription({ account_id, subscription_id, }: Options): Promise { - const { license_id, start, end, current_period_end } = + const { license_id, start, end, current_period_end, periodicCost } = await getSubscriptionRenewalData(subscription_id); const client = await getTransactionClient(); let purchase_id: number | undefined = undefined; @@ -46,6 +46,7 @@ export default async function resumeSubscription({ note: `This is to pay for subscription id=${subscription_id}. The owner of the subscription manually resumed it. This purchase pays for the cost of one period of the subscription.`, isSubscriptionRenewal: true, client, + cost: periodicCost, }) ).purchase_id; diff --git a/src/packages/server/purchases/stripe/create-subscription-payment.ts b/src/packages/server/purchases/stripe/create-subscription-payment.ts index f73890f180..e6e050a8d2 100644 --- a/src/packages/server/purchases/stripe/create-subscription-payment.ts +++ b/src/packages/server/purchases/stripe/create-subscription-payment.ts @@ -242,7 +242,7 @@ export async function processSubscriptionRenewal({ const end = new Date(payment.new_expires_ms); logger.debug( - "processSubscriptionRenewal: extend the license to payment.new_expires_ms", + `processSubscriptionRenewal: extend the license to ${payment.new_expires_ms}`, ); const { purchase_id } = await editLicense({ account_id, @@ -369,23 +369,27 @@ export async function resumeSubscriptionSetPaymentIntent({ }) { const pool = getPool(); const { rows } = await pool.query( - "SELECT resume_payment_intent FROM subscriptions WHERE id=$1", + "SELECT resume_payment_intent, interval FROM subscriptions WHERE id=$1", [subscription_id], ); - if (rows[0]?.resume_payment_intent) { + if (rows.length == 0) { + throw Error(`no such subscription id=${subscription_id}`); + } + if (rows[0].resume_payment_intent) { const stripe = await getConn(); const intent = await stripe.paymentIntents.retrieve( - rows[0]?.resume_payment_intent, + rows[0].resume_payment_intent, ); - if (intent.status != "canceled") { + if (intent.status != "canceled" && intent.status != "succeeded") { throw Error( `There is an outstanding payment to resume this subscription. Pay that invoice or cancel it.`, ); } } + const new_expires_ms = addInterval(new Date(), rows[0].interval).valueOf(); await pool.query( - "UPDATE subscriptions SET resume_payment_intent=$2 WHERE id=$1", - [subscription_id, paymentIntentId], + "UPDATE subscriptions SET resume_payment_intent=$2, payment=$3 WHERE id=$1", + [subscription_id, paymentIntentId, { new_expires_ms }], ); } diff --git a/src/packages/server/purchases/student-pay.ts b/src/packages/server/purchases/student-pay.ts index a67477c1c2..2e14429c79 100644 --- a/src/packages/server/purchases/student-pay.ts +++ b/src/packages/server/purchases/student-pay.ts @@ -279,7 +279,7 @@ export async function studentPayAssertNotPaying({ project_id }) { if (payment_intent_id) { const stripe = await getConn(); const intent = await stripe.paymentIntents.retrieve(payment_intent_id); - if (intent.status != "canceled") { + if (intent.status != "canceled" && intent.status != "succeeded") { throw Error( `There is an outstanding payment for this course right now. Pay that invoice or cancel it.`, );