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:
committed by
GitHub
parent
86bf9dd064
commit
168ebc3430
@@ -14,6 +14,11 @@ export async function bootCron() {
|
||||
type: 'salt',
|
||||
pattern: '0 0 * * *',
|
||||
},
|
||||
{
|
||||
name: 'deleteProjects',
|
||||
type: 'deleteProjects',
|
||||
pattern: '0 * * * *',
|
||||
},
|
||||
{
|
||||
name: 'flush',
|
||||
type: 'flushEvents',
|
||||
|
||||
34
apps/worker/src/jobs/cron.delete-projects.ts
Normal file
34
apps/worker/src/jobs/cron.delete-projects.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { logger } from '@/utils/logger';
|
||||
import { generateSalt } from '@openpanel/common/server';
|
||||
import { TABLE_NAMES, ch, chQuery, db } from '@openpanel/db';
|
||||
import { escape } from 'sqlstring';
|
||||
|
||||
export async function deleteProjects() {
|
||||
const projects = await db.project.findMany({
|
||||
where: {
|
||||
deleteAt: {
|
||||
lte: new Date(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (projects.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const project of projects) {
|
||||
await db.project.delete({
|
||||
where: {
|
||||
id: project.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await ch.command({
|
||||
query: `DELETE FROM ${TABLE_NAMES.events} WHERE project_id IN (${projects.map((project) => escape(project.id)).join(',')});`,
|
||||
});
|
||||
|
||||
logger.info(`Deleted ${projects.length} projects`, {
|
||||
projects,
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type { Job } from 'bullmq';
|
||||
import { eventBuffer, profileBuffer } from '@openpanel/db';
|
||||
import type { CronQueuePayload } from '@openpanel/queue';
|
||||
|
||||
import { deleteProjects } from './cron.delete-projects';
|
||||
import { ping } from './cron.ping';
|
||||
import { salt } from './cron.salt';
|
||||
|
||||
@@ -20,5 +21,8 @@ export async function cronJob(job: Job<CronQueuePayload>) {
|
||||
case 'ping': {
|
||||
return await ping();
|
||||
}
|
||||
case 'deleteProjects': {
|
||||
return await deleteProjects();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +1,12 @@
|
||||
import type { Job } from 'bullmq';
|
||||
import { escape } from 'sqlstring';
|
||||
|
||||
import { TABLE_NAMES, chQuery, db } from '@openpanel/db';
|
||||
import type {
|
||||
EventsQueuePayload,
|
||||
EventsQueuePayloadCreateSessionEnd,
|
||||
EventsQueuePayloadIncomingEvent,
|
||||
} from '@openpanel/queue';
|
||||
|
||||
import { cacheable } from '@openpanel/redis';
|
||||
import { createSessionEnd } from './events.create-session-end';
|
||||
import { incomingEvent } from './events.incoming-event';
|
||||
|
||||
export async function eventsJob(job: Job<EventsQueuePayload>) {
|
||||
switch (job.data.type) {
|
||||
case 'incomingEvent': {
|
||||
return await incomingEvent(job as Job<EventsQueuePayloadIncomingEvent>);
|
||||
}
|
||||
case 'createSessionEnd': {
|
||||
try {
|
||||
await updateEventsCount(job.data.payload.projectId);
|
||||
} catch (e) {
|
||||
job.log('Failed to update count');
|
||||
}
|
||||
|
||||
return await createSessionEnd(
|
||||
job as Job<EventsQueuePayloadCreateSessionEnd>,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getProjectEventsCount = cacheable(async function getProjectEventsCount(
|
||||
projectId: string,
|
||||
) {
|
||||
const res = await chQuery<{ count: number }>(
|
||||
`SELECT count(*) as count FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)}`,
|
||||
);
|
||||
return res[0]?.count;
|
||||
}, 60 * 60);
|
||||
|
||||
async function updateEventsCount(projectId: string) {
|
||||
const count = await getProjectEventsCount(projectId);
|
||||
if (count) {
|
||||
await db.project.update({
|
||||
where: {
|
||||
id: projectId,
|
||||
},
|
||||
data: {
|
||||
eventsCount: count,
|
||||
},
|
||||
});
|
||||
}
|
||||
return await incomingEvent(job as Job<EventsQueuePayloadIncomingEvent>);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { Job } from 'bullmq';
|
||||
|
||||
import { setSuperJson } from '@openpanel/common';
|
||||
import { db } from '@openpanel/db';
|
||||
import { sendDiscordNotification } from '@openpanel/integrations/src/discord';
|
||||
import { sendSlackNotification } from '@openpanel/integrations/src/slack';
|
||||
import { setSuperJson } from '@openpanel/json';
|
||||
import type { NotificationQueuePayload } from '@openpanel/queue';
|
||||
import { getRedisPub } from '@openpanel/redis';
|
||||
import { getRedisPub, publishEvent } from '@openpanel/redis';
|
||||
|
||||
export async function notificationJob(job: Job<NotificationQueuePayload>) {
|
||||
switch (job.data.type) {
|
||||
@@ -13,7 +13,7 @@ export async function notificationJob(job: Job<NotificationQueuePayload>) {
|
||||
const { notification } = job.data.payload;
|
||||
|
||||
if (notification.sendToApp) {
|
||||
getRedisPub().publish('notification', setSuperJson(notification));
|
||||
publishEvent('notification', 'created', notification);
|
||||
// empty for now
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,79 @@ import type { Job } from 'bullmq';
|
||||
|
||||
import type { SessionsQueuePayload } from '@openpanel/queue';
|
||||
|
||||
import { logger } from '@/utils/logger';
|
||||
import {
|
||||
db,
|
||||
getOrganizationBillingEventsCount,
|
||||
getOrganizationByProjectIdCached,
|
||||
getProjectEventsCount,
|
||||
} from '@openpanel/db';
|
||||
import { cacheable } from '@openpanel/redis';
|
||||
import { createSessionEnd } from './events.create-session-end';
|
||||
|
||||
export async function sessionsJob(job: Job<SessionsQueuePayload>) {
|
||||
return await createSessionEnd(job);
|
||||
const res = await createSessionEnd(job);
|
||||
try {
|
||||
await updateEventsCount(job.data.payload.projectId);
|
||||
} catch (e) {
|
||||
logger.error('Failed to update events count', e);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
const updateEventsCount = cacheable(async function updateEventsCount(
|
||||
projectId: string,
|
||||
) {
|
||||
const organization = await db.organization.findFirst({
|
||||
where: {
|
||||
projects: {
|
||||
some: {
|
||||
id: projectId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
projects: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
return;
|
||||
}
|
||||
|
||||
const organizationEventsCount =
|
||||
await getOrganizationBillingEventsCount(organization);
|
||||
const projectEventsCount = await getProjectEventsCount(projectId);
|
||||
|
||||
if (projectEventsCount) {
|
||||
await db.project.update({
|
||||
where: {
|
||||
id: projectId,
|
||||
},
|
||||
data: {
|
||||
eventsCount: projectEventsCount,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (organizationEventsCount) {
|
||||
await db.organization.update({
|
||||
where: {
|
||||
id: organization.id,
|
||||
},
|
||||
data: {
|
||||
subscriptionPeriodEventsCount: organizationEventsCount,
|
||||
subscriptionPeriodEventsCountExceededAt:
|
||||
organizationEventsCount >
|
||||
organization.subscriptionPeriodEventsLimit &&
|
||||
!organization.subscriptionPeriodEventsCountExceededAt
|
||||
? new Date()
|
||||
: organizationEventsCount <=
|
||||
organization.subscriptionPeriodEventsLimit
|
||||
? null
|
||||
: organization.subscriptionPeriodEventsCountExceededAt,
|
||||
},
|
||||
});
|
||||
await getOrganizationByProjectIdCached.clear(projectId);
|
||||
}
|
||||
}, 60 * 60);
|
||||
|
||||
Reference in New Issue
Block a user