feat(subscriptions): added polar as payment provider for subscriptions

* feature(dashboard): add polar / subscription

* wip(payments): manage subscription

* wip(payments): add free product, faq and some other improvements

* fix(root): change node to bundler in tsconfig

* wip(payments): display current subscription

* feat(dashboard): schedule project for deletion

* wip(payments): support custom products/subscriptions

* wip(payments): fix polar scripts

* wip(payments): add json package to dockerfiles
This commit is contained in:
Carl-Gerhard Lindesvärd
2025-02-26 11:24:00 +01:00
committed by GitHub
parent 86bf9dd064
commit 168ebc3430
105 changed files with 3395 additions and 463 deletions

View File

@@ -1,11 +1,31 @@
import { createLogger } from '@openpanel/logger';
import { PrismaClient } from '@prisma/client';
import { type Organization, PrismaClient } from '@prisma/client';
import { readReplicas } from '@prisma/extension-read-replicas';
export * from '@prisma/client';
const logger = createLogger({ name: 'db' });
const isWillBeCanceled = (
organization: Pick<
Organization,
'subscriptionStatus' | 'subscriptionCanceledAt' | 'subscriptionEndsAt'
>,
) =>
organization.subscriptionStatus === 'active' &&
organization.subscriptionCanceledAt &&
organization.subscriptionEndsAt;
const isCanceled = (
organization: Pick<
Organization,
'subscriptionStatus' | 'subscriptionCanceledAt'
>,
) =>
organization.subscriptionStatus === 'canceled' &&
organization.subscriptionCanceledAt &&
organization.subscriptionCanceledAt < new Date();
const getPrismaClient = () => {
const prisma = new PrismaClient({
log: ['error'],
@@ -32,15 +52,182 @@ const getPrismaClient = () => {
return query(args);
},
},
})
.$extends({
result: {
organization: {
subscriptionStatus: {
needs: { subscriptionStatus: true, subscriptionCanceledAt: true },
compute(org) {
return org.subscriptionStatus || 'trialing';
},
},
hasSubscription: {
needs: { subscriptionStatus: true, subscriptionEndsAt: true },
compute(org) {
if (
[null, 'canceled', 'trialing'].includes(org.subscriptionStatus)
) {
return false;
}
return true;
},
},
slug: {
needs: { id: true },
compute(org) {
return org.id;
},
},
subscriptionChartEndDate: {
needs: {
subscriptionEndsAt: true,
subscriptionPeriodEventsCountExceededAt: true,
},
compute(org) {
if (
org.subscriptionEndsAt &&
org.subscriptionPeriodEventsCountExceededAt
) {
return org.subscriptionEndsAt >
org.subscriptionPeriodEventsCountExceededAt
? org.subscriptionPeriodEventsCountExceededAt
: org.subscriptionEndsAt;
}
if (org.subscriptionEndsAt) {
return org.subscriptionEndsAt;
}
return new Date();
},
},
isTrial: {
needs: { subscriptionStatus: true, subscriptionEndsAt: true },
compute(org) {
const isSubscriptionInFuture =
org.subscriptionEndsAt && org.subscriptionEndsAt > new Date();
return (
(org.subscriptionStatus === 'trialing' ||
org.subscriptionStatus === null) &&
isSubscriptionInFuture
);
},
},
isCanceled: {
needs: { subscriptionStatus: true, subscriptionCanceledAt: true },
compute(org) {
return isCanceled(org);
},
},
isWillBeCanceled: {
needs: {
subscriptionStatus: true,
subscriptionCanceledAt: true,
subscriptionEndsAt: true,
},
compute(org) {
return isWillBeCanceled(org);
},
},
isExpired: {
needs: {
subscriptionEndsAt: true,
subscriptionStatus: true,
subscriptionCanceledAt: true,
},
compute(org) {
if (isCanceled(org)) {
return false;
}
if (isWillBeCanceled(org)) {
return false;
}
return (
org.subscriptionEndsAt && org.subscriptionEndsAt < new Date()
);
},
},
isExceeded: {
needs: {
subscriptionPeriodEventsCount: true,
subscriptionPeriodEventsLimit: true,
},
compute(org) {
return (
org.subscriptionPeriodEventsCount >
org.subscriptionPeriodEventsLimit
);
},
},
subscriptionCurrentPeriodStart: {
needs: { subscriptionStartsAt: true, subscriptionInterval: true },
compute(org) {
if (!org.subscriptionStartsAt) return org.subscriptionStartsAt;
if (org.subscriptionInterval === 'year') {
const startDay = org.subscriptionStartsAt.getUTCDate();
const now = new Date();
return new Date(
Date.UTC(
now.getUTCFullYear(),
now.getUTCMonth(),
startDay,
0,
0,
0,
0,
),
);
}
return org.subscriptionStartsAt;
},
},
subscriptionCurrentPeriodEnd: {
needs: {
subscriptionStartsAt: true,
subscriptionEndsAt: true,
subscriptionInterval: true,
},
compute(org) {
if (!org.subscriptionStartsAt) return org.subscriptionEndsAt;
if (org.subscriptionInterval === 'year') {
const startDay = org.subscriptionStartsAt.getUTCDate();
const now = new Date();
return new Date(
Date.UTC(
now.getUTCFullYear(),
now.getUTCMonth() + 1,
startDay - 1,
0,
0,
0,
0,
),
);
}
return org.subscriptionEndsAt;
},
},
},
},
});
return prisma;
};
const globalForPrisma = globalThis as unknown as {
prisma: ReturnType<typeof getPrismaClient> | undefined;
prisma: ReturnType<typeof getPrismaClient>;
};
export const db = globalForPrisma.prisma ?? getPrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = db;
}