Files
stats/packages/trpc/src/routers/subscription.ts
Carl-Gerhard Lindesvärd 680727355b feature(dashboard,api): add timezone support
* feat(dashboard): add support for today, yesterday etc (timezones)

* fix(db): escape js dates

* fix(dashboard): ensure we support default timezone

* final fixes

* remove complete series and add sql with fill instead
2025-05-23 11:26:44 +02:00

180 lines
5.0 KiB
TypeScript

import {
db,
getOrganizationBillingEventsCountSerieCached,
getOrganizationById,
} from '@openpanel/db';
import {
cancelSubscription,
changeSubscription,
createCheckout,
createPortal,
getProduct,
getProducts,
reactivateSubscription,
} from '@openpanel/payments';
import { zCheckout } from '@openpanel/validation';
import { getCache } from '@openpanel/redis';
import { subDays } from 'date-fns';
import { z } from 'zod';
import { TRPCBadRequestError } from '../errors';
import { createTRPCRouter, protectedProcedure } from '../trpc';
export const subscriptionRouter = createTRPCRouter({
getCurrent: protectedProcedure
.input(z.object({ organizationId: z.string() }))
.query(async ({ input }) => {
const organization = await getOrganizationById(input.organizationId);
if (!organization.subscriptionProductId) {
return null;
}
return getProduct(organization.subscriptionProductId);
}),
checkout: protectedProcedure
.input(zCheckout)
.mutation(async ({ input, ctx }) => {
const [user, organization] = await Promise.all([
db.user.findFirstOrThrow({
where: {
id: ctx.session.user.id,
},
}),
db.organization.findFirstOrThrow({
where: {
id: input.organizationId,
},
}),
]);
if (
organization.subscriptionId &&
organization.subscriptionStatus === 'active'
) {
const existingProduct = organization.subscriptionProductId
? await getProduct(organization.subscriptionProductId)
: null;
// If the existing product is free, cancel the subscription and then jump to the checkout
if (existingProduct?.prices.some((p) => p.amountType === 'free')) {
await cancelSubscription(organization.subscriptionId);
} else {
if (organization.subscriptionCanceledAt) {
await reactivateSubscription(organization.subscriptionId);
return null;
}
await changeSubscription(
organization.subscriptionId,
input.productId,
);
return null;
}
}
const checkout = await createCheckout({
priceId: input.productPriceId,
organizationId: input.organizationId,
projectId: input.projectId ?? undefined,
user,
ipAddress: ctx.req.ip,
});
return {
url: checkout.url,
};
}),
products: protectedProcedure
.input(z.object({ organizationId: z.string() }))
.query(async ({ input }) => {
const organization = await db.organization.findUniqueOrThrow({
where: {
id: input.organizationId,
},
select: {
subscriptionPeriodEventsCount: true,
},
});
return (
await getCache('polar:products', 60 * 60 * 24, () => getProducts())
).map((product) => {
const eventsLimit = product.metadata.eventsLimit;
return {
...product,
disabled:
typeof eventsLimit === 'number' &&
organization.subscriptionPeriodEventsCount >= eventsLimit
? 'This product is not applicable since you have exceeded the limits for this subscription.'
: null,
};
});
}),
usage: protectedProcedure
.input(
z.object({
organizationId: z.string(),
}),
)
.query(async ({ input }) => {
const organization = await db.organization.findUniqueOrThrow({
where: {
id: input.organizationId,
},
include: {
projects: { select: { id: true } },
},
});
if (
organization.hasSubscription &&
organization.subscriptionStartsAt &&
organization.subscriptionEndsAt
) {
return getOrganizationBillingEventsCountSerieCached(organization, {
startDate: organization.subscriptionStartsAt,
endDate: organization.subscriptionEndsAt,
});
}
return getOrganizationBillingEventsCountSerieCached(organization, {
startDate: subDays(new Date(), 30),
endDate: new Date(),
});
}),
cancelSubscription: protectedProcedure
.input(z.object({ organizationId: z.string() }))
.mutation(async ({ input }) => {
const organization = await getOrganizationById(input.organizationId);
if (!organization.subscriptionId) {
throw TRPCBadRequestError('Organization has no subscription');
}
const res = await cancelSubscription(organization.subscriptionId);
return res;
}),
portal: protectedProcedure
.input(z.object({ organizationId: z.string() }))
.mutation(async ({ input }) => {
const organization = await getOrganizationById(input.organizationId);
if (!organization.subscriptionCustomerId) {
throw TRPCBadRequestError('Organization has no subscription');
}
const portal = await createPortal({
customerId: organization.subscriptionCustomerId,
});
return {
url: portal.customerPortalUrl,
};
}),
});