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,5 +1,28 @@
import { getRedisCache } from './redis';
export async function getCache<T>(
key: string,
expireInSec: number,
fn: () => Promise<T>,
): Promise<T> {
const hit = await getRedisCache().get(key);
if (hit) {
return JSON.parse(hit, (_, value) => {
if (
typeof value === 'string' &&
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*Z$/.test(value)
) {
return new Date(value);
}
return value;
});
}
const data = await fn();
await getRedisCache().setex(key, expireInSec, JSON.stringify(data));
return data;
}
export function cacheable<T extends (...args: any) => any>(
fn: T,
expireInSec: number,
@@ -37,7 +60,15 @@ export function cacheable<T extends (...args: any) => any>(
const cached = await getRedisCache().get(key);
if (cached) {
try {
return JSON.parse(cached);
return JSON.parse(cached, (_, value) => {
if (
typeof value === 'string' &&
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*Z$/.test(value)
) {
return new Date(value);
}
return value;
});
} catch (e) {
console.error('Failed to parse cache', e);
}

View File

@@ -1,3 +1,4 @@
export * from './redis';
export * from './cachable';
export * from './run-every';
export * from './publisher';

View File

@@ -6,9 +6,11 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@openpanel/json": "workspace:*",
"ioredis": "^5.4.1"
},
"devDependencies": {
"@openpanel/db": "workspace:*",
"@openpanel/tsconfig": "workspace:*",
"@types/node": "20.14.8",
"prisma": "^5.1.1",

View File

@@ -0,0 +1,86 @@
import { type Redis, getRedisPub, getRedisSub } from './redis';
import type { IServiceEvent, Notification } from '@openpanel/db';
import { getSuperJson, setSuperJson } from '@openpanel/json';
export type IPublishChannels = {
organization: {
subscription_updated: {
organizationId: string;
};
};
events: {
received: IServiceEvent;
saved: IServiceEvent;
};
notification: {
created: Notification;
};
};
export function getSubscribeChannel<Channel extends keyof IPublishChannels>(
channel: Channel,
type: keyof IPublishChannels[Channel],
) {
return `${channel}:${String(type)}`;
}
export function publishEvent<Channel extends keyof IPublishChannels>(
channel: Channel,
type: keyof IPublishChannels[Channel],
event: IPublishChannels[Channel][typeof type],
multi?: ReturnType<Redis['multi']>,
) {
const redis = multi ?? getRedisPub();
return redis.publish(getSubscribeChannel(channel, type), setSuperJson(event));
}
export function parsePublishedEvent<Channel extends keyof IPublishChannels>(
_channel: Channel,
_type: keyof IPublishChannels[Channel],
message: string,
): IPublishChannels[Channel][typeof _type] {
return getSuperJson<IPublishChannels[Channel][typeof _type]>(message)!;
}
export function subscribeToPublishedEvent<
Channel extends keyof IPublishChannels,
>(
channel: Channel,
type: keyof IPublishChannels[Channel],
callback: (event: IPublishChannels[Channel][typeof type]) => void,
) {
const subscribeChannel = getSubscribeChannel(channel, type);
getRedisSub().subscribe(subscribeChannel);
const message = (messageChannel: string, message: string) => {
if (subscribeChannel === messageChannel) {
const event = parsePublishedEvent(channel, type, message);
if (event) {
callback(event);
}
}
};
getRedisSub().on('message', message);
return () => {
getRedisSub().unsubscribe(subscribeChannel);
getRedisSub().off('message', message);
};
}
export function psubscribeToPublishedEvent(
pattern: string,
callback: (key: string) => void,
) {
getRedisSub().psubscribe(pattern);
const pmessage = (_: unknown, pattern: string, key: string) => callback(key);
getRedisSub().on('pmessage', pmessage);
return () => {
getRedisSub().punsubscribe(pattern);
getRedisSub().off('pmessage', pmessage);
};
}