diff --git a/src/packages/frontend/browser.ts b/src/packages/frontend/browser.ts
index ac8a5dccfb..c2ec74dda0 100644
--- a/src/packages/frontend/browser.ts
+++ b/src/packages/frontend/browser.ts
@@ -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)
);
}
diff --git a/src/packages/frontend/messages/counter.tsx b/src/packages/frontend/messages/counter.tsx
index 019da0c613..231f475d09 100644
--- a/src/packages/frontend/messages/counter.tsx
+++ b/src/packages/frontend/messages/counter.tsx
@@ -17,6 +17,9 @@ export default function Counter({
set_window_title();
}, [unread_message_count]);
if (minimal) {
+ if (!unread_message_count) {
+ return null;
+ }
return {unread_message_count};
}
return (
diff --git a/src/packages/frontend/site-licenses/site-license-public-info-component.tsx b/src/packages/frontend/site-licenses/site-license-public-info-component.tsx
index 62fd3b320f..a710283388 100644
--- a/src/packages/frontend/site-licenses/site-license-public-info-component.tsx
+++ b/src/packages/frontend/site-licenses/site-license-public-info-component.tsx
@@ -238,7 +238,7 @@ export const SiteLicensePublicInfo: React.FC = (
// 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 ? (
diff --git a/src/packages/frontend/site-licenses/site-license-public-info.tsx b/src/packages/frontend/site-licenses/site-license-public-info.tsx
index 35fbba1062..e4ef896cb7 100644
--- a/src/packages/frontend/site-licenses/site-license-public-info.tsx
+++ b/src/packages/frontend/site-licenses/site-license-public-info.tsx
@@ -353,7 +353,7 @@ export const SiteLicensePublicInfoTable: React.FC = (
return (
<>
- {runLimitTxt} Paid through {when}.
+ {runLimitTxt} Valid through {when}.
>
);
}
diff --git a/src/packages/server/purchases/maintain-subscriptions.ts b/src/packages/server/purchases/maintain-subscriptions.ts
index 264b4bb74a..aac3d22b73 100644
--- a/src/packages/server/purchases/maintain-subscriptions.ts
+++ b/src/packages/server/purchases/maintain-subscriptions.ts
@@ -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.
@@ -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");
@@ -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(
@@ -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,
+ ]);
}
}
diff --git a/src/packages/server/purchases/stripe/create-subscription-payment.ts b/src/packages/server/purchases/stripe/create-subscription-payment.ts
index ce264bd7c6..505d3f3560 100644
--- a/src/packages/server/purchases/stripe/create-subscription-payment.ts
+++ b/src/packages/server/purchases/stripe/create-subscription-payment.ts
@@ -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;
@@ -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
@@ -167,6 +168,8 @@ 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,
@@ -174,8 +177,7 @@ export default async function createSubscriptionPayment({
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}`,
});
}