Skip to content

Commit

Permalink
subscriptions: automatic grace period, and improving communication
Browse files Browse the repository at this point in the history
  • Loading branch information
williamstein committed Dec 5, 2024
1 parent 9455d76 commit c110c65
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 11 deletions.
5 changes: 4 additions & 1 deletion src/packages/frontend/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ export function notifyCount() {
const mentions = redux.getStore("mentions");
const account = redux.getStore("account");
const news = redux.getStore("news");
// we always count balance_alert as "1", since we don't even
// know how many of them there are until querying stripe.
return (
(mentions?.getUnreadSize() ?? 0) +
(account?.get("unread_message_count") ?? 0) +
(news?.get("unread") ?? 0)
(news?.get("unread") ?? 0) +
(account?.get("balance_alert") ? 1 : 0)
);
}

Expand Down
3 changes: 3 additions & 0 deletions src/packages/frontend/messages/counter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export default function Counter({
set_window_title();
}, [unread_message_count]);
if (minimal) {
if (!unread_message_count) {
return null;
}
return <span style={style}>{unread_message_count}</span>;
}
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ export const SiteLicensePublicInfo: React.FC<Props> = (
// a subscription will extend it (unless it is canceled)
return null;
}
const word = expired ? "EXPIRED" : "Paid through";
const word = expired ? "EXPIRED" : "Valid through";
const when =
info?.expires != null ? (
<TimeAgo date={info.expires} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ export const SiteLicensePublicInfoTable: React.FC<PropsTable> = (

return (
<>
{runLimitTxt} Paid through {when}.
{runLimitTxt} Valid through {when}.
</>
);
}
Expand Down
69 changes: 64 additions & 5 deletions src/packages/server/purchases/maintain-subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,20 @@ CREATE PAYMENTS:
for that subscription, we do the following:
- Create a payment intent for the amount to renew the subscription for the next
period. The metadata says exactly what this payment is for and what should happen
when payment is processed.
period. The metadata says what this payment is for and what should happen
when payment is processed. If user has selected to pay from credit on file
and they have enough to covert the entire renewal, subscription is immediately
renewed using available credit.
- Also, when making the payment intent, extend the license expire date for 3 days,
as a free automatic grace period, since payments can take a while to complete.
- After successfully making the payment intent, extend the license so it does not
expires 5 days from now (3 days after expire), as a free automatic grace
period, since payments can take a while to complete.
ABUSE POTENTIAL: this is slightly DANGEROUS!! The user could maybe cancel everything and
get a prorated refund on the license that would give them credit for these 3
days. It's not too dangerous though, since this only happens automatically
on the subscription renewal and there is no way for the user to trigger it.
We might want to not allow this... We'll see.
- Send message about subscription renewal payment. Including invoice
payment link from stripe in that message.
Expand Down Expand Up @@ -90,6 +99,7 @@ import { getUser } from "@cocalc/server/purchases/statements/email-statement";
import basePath from "@cocalc/backend/base-path";
import { join } from "path";
import { currency } from "@cocalc/util/misc";
import dayjs from "dayjs";

const logger = getLogger("purchases:maintain-subscriptions");

Expand Down Expand Up @@ -210,7 +220,7 @@ export async function createPayments() {
SELECT id as subscription_id, account_id FROM subscriptions WHERE
status != 'canceled' AND
current_period_end <= NOW() + interval '48 hours' AND
payment#>>'{status}' != 'active'
coalesce(payment#>>'{status}','') != 'active'
`,
);
logger.debug(
Expand All @@ -231,6 +241,55 @@ export async function createPayments() {
`createPayments -- ERROR billing subscription id ${subscription_id} -- ${err}`,
);
}
await gracePeriod({
subscription_id,
until: dayjs().add(5, "days").toDate(),
});
}
}

export async function gracePeriod({
subscription_id,
until,
}: {
subscription_id: number;
until: Date;
}) {
logger.debug("gracePeriod", { subscription_id, until });
// Check to ensure the license does not expire until after "until".
// It might have been renewed already by the time we get here.
const pool = getPool();
const { rows: subscriptions } = await pool.query(
"SELECT metadata FROM subscriptions WHERE id=$1",
[subscription_id],
);
const license_id = subscriptions[0]?.metadata?.license_id;
if (!license_id) {
logger.debug("gracePeriod: no license_id");
return;
}
logger.debug("gracePeriod:", { license_id });
const { rows: licenses } = await pool.query(
"SELECT expires FROM site_licenses WHERE id=$1",
[license_id],
);
if (licenses.length == 0) {
logger.debug("gracePeriod: no such license", { license_id });
return;
}
const expires = licenses[0].expires;
if (expires == null) {
logger.debug("gracePeriod: suspicious license - no expires set (?)", {
license_id,
});
return;
}
if (expires < until) {
logger.debug("gracePeriod: adding grace period to license.");
await pool.query("UPDATE site_licenses SET expires=$1 WHERE id=$2", [
until,
license_id,
]);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import getBalance from "@cocalc/server/purchases/get-balance";
import send from "@cocalc/server/messages/send";
import adminAlert from "@cocalc/server/messages/admin-alert";
import { getServerSettings } from "@cocalc/database/settings/server-settings";
import base_path from "@cocalc/backend/base-path";

// nothing should ever be this small, but just in case:
const MIN_SUBSCRIPTION_AMOUNT = 1;
Expand Down Expand Up @@ -94,7 +95,7 @@ export default async function createSubscriptionPayment({
}
}

const { site_name } = await getServerSettings();
const { site_name, dns } = await getServerSettings();

if (payNow) {
// Instead of trying to charge their credit card (etc.), we just
Expand Down Expand Up @@ -167,15 +168,16 @@ export default async function createSubscriptionPayment({
new_expires_ms,
};

const site = `https://${dns}${base_path}`;

await pool.query("UPDATE subscriptions SET payment=$1 WHERE id=$2", [
payment1,
subscription_id,
]);
await send({
to_ids: [account_id],
subject: `${site_name} Subscription Renewal: Id ${subscription_id}`,
body: `${site_name} has started renewing your subscription (id=${subscription_id}).\n\n
Invoice: ${hosted_invoice_url}`,
body: `${site_name} has started renewing your subscription (id=${subscription_id}).\n\n- [Subscription Status](${site}/subscriptions/${subscription_id})\n\n- Your Account: [Subscriptions](${site}/settings/subscriptions), [Payments](${site}/settings/payments) and [Purchases](${site}/settings/purchases)\n\n- Hosted Invoice: ${hosted_invoice_url}`,
});
}

Expand Down

0 comments on commit c110c65

Please sign in to comment.