Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions apps/dashboard/src/lib/server/billing/autumn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,13 @@ function isActiveSubscription(subscription: {
planId?: string | null;
status?: string | null;
pastDue?: boolean | null;
canceledAt?: number | null;
expiresAt?: number | null;
}) {
const now = Date.now();

return (
subscription.status === 'active' &&
!subscription.pastDue &&
!subscription.canceledAt &&
(!subscription.expiresAt || subscription.expiresAt > now)
);
}
Expand Down
28 changes: 21 additions & 7 deletions apps/dashboard/src/lib/server/billing/enforcement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { and, asc, eq, isNull } from 'drizzle-orm';
import { initDrizzle } from '$lib/server/db';
import { projectBillingCustomers, vms } from '$lib/server/db/schema';
import { getBackend } from '$lib/server/backends';
import { getProjectBillingState } from './autumn';
import { getProjectBillingState, isBillingConfigured } from './autumn';
import { sendProjectPastDueEmail, sendProjectSuspendedEmail } from '$lib/server/email-notifications';

const GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1000;
Expand Down Expand Up @@ -30,6 +30,8 @@ async function suspendProjectVms(projectId: string) {
}

export async function enforceProjectBillingGrace(now = Date.now()) {
if (!isBillingConfigured()) return { checked: 0, suspended: 0 };

const db = initDrizzle();
const projects = await db
.selectDistinct({ projectId: vms.ownerProjectId })
Expand Down Expand Up @@ -59,9 +61,15 @@ export async function enforceProjectBillingGrace(now = Date.now()) {
)
.returning({ projectId: projectBillingCustomers.projectId });
if (claimed.length > 0) {
await sendProjectPastDueEmail(projectId, GRACE_PERIOD_DAYS).catch((err) => {
console.warn(`Failed to send past-due email for project ${projectId}`, err);
});
await sendProjectPastDueEmail(projectId, GRACE_PERIOD_DAYS)
.then((email) => {
if (email) {
console.info(`Sent past-due email to ${email} for project ${projectId}`);
}
})
.catch((err) => {
console.warn(`Failed to send past-due email for project ${projectId}`, err);
});
}
continue;
}
Expand All @@ -79,9 +87,15 @@ export async function enforceProjectBillingGrace(now = Date.now()) {
)
.returning({ projectId: projectBillingCustomers.projectId });
if (claimed.length > 0) {
await sendProjectSuspendedEmail(projectId).catch((err) => {
console.warn(`Failed to send suspension email for project ${projectId}`, err);
});
await sendProjectSuspendedEmail(projectId)
.then((email) => {
if (email) {
console.info(`Sent suspension email to ${email} for project ${projectId}`);
}
})
.catch((err) => {
console.warn(`Failed to send suspension email for project ${projectId}`, err);
});
}
}
continue;
Expand Down
12 changes: 8 additions & 4 deletions apps/dashboard/src/lib/server/email-notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ function projectBillingUrl(projectId: string) {

export async function sendProjectPastDueEmail(projectId: string, graceDays: number) {
const owner = await getProjectOwnerRecipient(projectId);
if (!owner) return;
if (!owner) return null;

return sendRenderedEmail({
await sendRenderedEmail({
component: BillingNoticeEmail,
props: {
userName: owner.name,
Expand All @@ -135,13 +135,15 @@ export async function sendProjectPastDueEmail(projectId: string, graceDays: numb
subject: 'Action required: overdue balance on your Stack project',
to: owner.email
});

return owner.email;
}

export async function sendProjectSuspendedEmail(projectId: string) {
const owner = await getProjectOwnerRecipient(projectId);
if (!owner) return;
if (!owner) return null;

return sendRenderedEmail({
await sendRenderedEmail({
component: BillingNoticeEmail,
props: {
userName: owner.name,
Expand All @@ -154,4 +156,6 @@ export async function sendProjectSuspendedEmail(projectId: string) {
subject: 'Your Stack servers have been suspended',
to: owner.email
});

return owner.email;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { error, json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { meterActiveResources, syncPendingUsage } from '$lib/server/billing/metering';
import { retryOrphanedProjectBillingCancellations } from '$lib/server/billing/autumn';
import { isBillingConfigured, retryOrphanedProjectBillingCancellations } from '$lib/server/billing/autumn';
import { enforceProjectBillingGrace } from '$lib/server/billing/enforcement';
import { getRuntimeEnv } from '$lib/server/env';

Expand All @@ -12,6 +12,8 @@ export const POST: RequestHandler = async ({ request }) => {
const authorization = request.headers.get('authorization');
if (authorization !== `Bearer ${secret}`) error(401, 'Unauthorized');

if (!isBillingConfigured()) error(503, 'Billing is not configured');

const metered = await meterActiveResources();
const synced = await syncPendingUsage();
const cancellations = await retryOrphanedProjectBillingCancellations();
Expand Down