Skip to content

Commit

Permalink
finish "resume subscription" with locks
Browse files Browse the repository at this point in the history
  • Loading branch information
williamstein committed Dec 12, 2024
1 parent fa56fe9 commit d74f84a
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 84 deletions.
123 changes: 77 additions & 46 deletions src/packages/frontend/purchases/resume-subscription.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand All @@ -34,9 +36,9 @@ export default function ResumeSubscription({
const [status, setStatus] = useState<string>(status0);
const [error, setError] = useState<string>("");
const [lineItems, setLineItems] = useState<LineItem[] | null>(null);
const [place, setPlace] = useState<"checkout" | "processing" | "congrats">(
"checkout",
);
const [place, setPlace] = useState<
"checkout" | "processing" | "buying" | "congrats"
>("checkout");
const [disabled, setDisabled] = useState<boolean>(false);
const numPaymentsRef = useRef<number | null>(null);
const [costToResume, setCostToResume] = useState<number | undefined>(
Expand All @@ -47,6 +49,28 @@ export default function ResumeSubscription({
);
const [loading, setLoading] = useState<boolean>(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;
Expand Down Expand Up @@ -107,7 +131,7 @@ export default function ResumeSubscription({
<>
<b>There is no charge</b> 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}.
</>
) : (
Expand Down Expand Up @@ -154,49 +178,55 @@ export default function ResumeSubscription({
</div>
)}
{place == "checkout" && lineItems == null && <Spin />}
{place == "checkout" && lineItems != null && (
<>
{place == "checkout" &&
lineItems != null &&
(costToResume == 0 ? (
<div style={{ textAlign: "center" }}>
<StripePayment
disabled={disabled}
lineItems={lineItems}
description={`Pay fee to resume subscription id ${subscription_id}`}
purpose={RESUME_SUBSCRIPTION}
metadata={{ subscription_id: `${subscription_id}` }}
onFinished={async (total) => {
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);
<Space>
<Button
size="large"
onClick={() => {
setOpen?.(false);
}}
>
Cancel
</Button>
<Button
type="primary"
size="large"
onClick={() => {
directResume();
}}
>
Resume Subscription
</Button>
</Space>
</div>
) : (
<>
<div style={{ textAlign: "center" }}>
<StripePayment
disabled={disabled}
lineItems={lineItems}
description={`Pay fee to resume subscription id ${subscription_id}`}
purpose={RESUME_SUBSCRIPTION}
metadata={{ subscription_id: `${subscription_id}` }}
onFinished={async (total) => {
if (!total) {
directResume();
} else {
setPlace("processing");
await resumeSubscription(subscription_id);
} catch (err) {
setError(`${err}`);
return;
} finally {
setDisabled(false);
}
setPlace("congrats");
return;
}
setPlace("processing");
}}
}}
/>
</div>
<Payments
purpose={RESUME_SUBSCRIPTION}
numPaymentsRef={numPaymentsRef}
limit={5}
/>
</div>
</>
)}
{place != "processing" && (
<Payments
purpose={RESUME_SUBSCRIPTION}
numPaymentsRef={numPaymentsRef}
limit={5}
/>
)}
</>
))}
</div>
);
}
Expand All @@ -214,11 +244,12 @@ export default function ResumeSubscription({
title={
<>
<Icon name="credit-card" style={{ marginRight: "10px" }} />
Pay to Resume Subscription Id {subscription_id}
Resume Subscription Id {subscription_id}
</>
}
>
{body}
{loading && <BigSpin />}
</Modal>
);
}
55 changes: 26 additions & 29 deletions src/packages/frontend/purchases/subscriptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -539,36 +539,33 @@ export default function Subscriptions() {
style={{ marginBottom: "15px" }}
/>
)}
{loading ? (
<Spin />
) : (
<div style={{ overflow: "auto", width: "100%" }}>
<UnpaidSubscriptions
size="large"
style={{ margin: "15px 0", textAlign: "center" }}
showWhen="unpaid"
counter={counter}
refresh={getSubscriptions}
/>
<Table
rowKey={"id"}
pagination={{ hideOnSinglePage: true, defaultPageSize: 25 }}
dataSource={subscriptions ?? undefined}
columns={columns}
{loading && <Spin />}
<div style={{ overflow: "auto", width: "100%" }}>
<UnpaidSubscriptions
size="large"
style={{ margin: "15px 0", textAlign: "center" }}
showWhen="unpaid"
counter={counter}
refresh={getSubscriptions}
/>
<Table
rowKey={"id"}
pagination={{ hideOnSinglePage: true, defaultPageSize: 25 }}
dataSource={subscriptions ?? undefined}
columns={columns}
/>
{current != null && (
<SubscriptionModal
subscription={current}
getSubscriptions={getSubscriptions}
onClose={() => {
setCurrent(undefined);
Fragment.clear();
redux.getActions("account").setFragment(undefined);
}}
/>
{current != null && (
<SubscriptionModal
subscription={current}
getSubscriptions={getSubscriptions}
onClose={() => {
setCurrent(undefined);
Fragment.clear();
redux.getActions("account").setFragment(undefined);
}}
/>
)}
</div>
)}
)}
</div>
</SettingBox>
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/packages/server/purchases/resume-subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default async function resumeSubscription({
account_id,
subscription_id,
}: Options): Promise<number | null | undefined> {
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;
Expand All @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 }],
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/packages/server/purchases/student-pay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`,
);
Expand Down

0 comments on commit d74f84a

Please sign in to comment.