This commit is contained in:
AquaWolf
2025-03-15 21:06:30 +01:00
parent b734892c55
commit 5189bf768a
23 changed files with 890 additions and 102 deletions

View File

@@ -31,6 +31,7 @@
"@aws-sdk/client-sesv2": "^3.753.0",
"@instantdb/admin": "^0.17.7",
"@neondatabase/serverless": "^0.10.4",
"@openauthjs/openauth": "0.4.3",
"@openauthjs/openevent": "^0.0.27",
"@polar-sh/sdk": "^0.26.1",
"drizzle-orm": "^0.39.3",

View File

@@ -1,4 +1,3 @@
import { teamID } from "./drizzle/types";
import { prefixes } from "./utils";
export module Examples {
export const Id = (prefix: keyof typeof prefixes) =>
@@ -18,7 +17,7 @@ export module Examples {
name: "John Does' Team",
slug: "john_doe",
}
export const Member = {
id: Id("member"),
email: "john@example.com",
@@ -26,4 +25,9 @@ export module Examples {
timeSeen: new Date("2025-02-23T13:39:52.249Z"),
}
export const Polar = {
teamID: Id("team"),
timeSeen: new Date("2025-02-23T13:39:52.249Z"),
}
}

View File

@@ -1,8 +0,0 @@
import { Resource } from "sst";
import { Polar as PolarSdk } from "@polar-sh/sdk";
const polar = new PolarSdk({ accessToken: Resource.PolarSecret.value, server: Resource.App.stage !== "production" ? "sandbox" : "production" });
export module Polar {
export const client = polar;
}

View File

@@ -0,0 +1,169 @@
import { z } from "zod";
import { fn } from "../utils";
import { Resource } from "sst";
import { eq, and } from "../drizzle";
import { useTeam } from "../actor";
import { createEvent } from "../event";
import { polarTable, Standing } from "./polar.sql";
import { Polar as PolarSdk } from "@polar-sh/sdk";
import { useTransaction } from "../drizzle/transaction";
const polar = new PolarSdk({ accessToken: Resource.PolarSecret.value, server: Resource.App.stage !== "production" ? "sandbox" : "production" });
export module Polar {
export const client = polar;
export const Info = z.object({
teamID: z.string(),
customerID: z.string(),
subscriptionID: z.string().nullable(),
subscriptionItemID: z.string().nullable(),
standing: z.enum(Standing),
});
export type Info = z.infer<typeof Info>;
export const Checkout = z.object({
annual: z.boolean().optional(),
successUrl: z.string(),
cancelUrl: z.string(),
});
export const CheckoutSession = z.object({
url: z.string().nullable(),
});
export const CustomerSubscriptionEventType = [
"created",
"updated",
"deleted",
] as const;
export const Events = {
CustomerSubscriptionEvent: createEvent(
"polar.customer-subscription-event",
z.object({
type: z.enum(CustomerSubscriptionEventType),
status: z.string(),
teamID: z.string().min(1),
customerID: z.string().min(1),
subscriptionID: z.string().min(1),
subscriptionItemID: z.string().min(1),
}),
),
};
export function get() {
return useTransaction(async (tx) =>
tx
.select()
.from(polarTable)
.where(eq(polarTable.teamID, useTeam()))
.execute()
.then((rows) => rows.map(serialize).at(0)),
);
}
export const fromUserEmail = fn(z.string().min(1), async (email) => {
try {
const customers = await client.customers.list({ email })
if (customers.result.items.length === 0) {
return await client.customers.create({ email })
} else {
return customers.result.items[0]
}
} catch (err) {
//FIXME: This is the issue [Polar.sh/#5147](https://github.com/polarsource/polar/issues/5147)
// console.log("error", err)
return undefined
}
})
export const setCustomerID = fn(Info.shape.customerID, async (customerID) =>
useTransaction(async (tx) =>
tx
.insert(polarTable)
.values({
teamID: useTeam(),
customerID,
standing: "new",
})
.execute(),
),
);
export const setSubscription = fn(
Info.pick({
subscriptionID: true,
subscriptionItemID: true,
}),
(input) =>
useTransaction(async (tx) =>
tx
.update(polarTable)
.set({
subscriptionID: input.subscriptionID,
subscriptionItemID: input.subscriptionItemID,
})
.where(eq(polarTable.teamID, useTeam()))
.returning()
.execute()
.then((rows) => rows.map(serialize).at(0)),
),
);
export const removeSubscription = fn(
z.string().min(1),
(stripeSubscriptionID) =>
useTransaction((tx) =>
tx
.update(polarTable)
.set({
subscriptionItemID: null,
subscriptionID: null,
})
.where(and(eq(polarTable.subscriptionID, stripeSubscriptionID)))
.execute(),
),
);
export const setStanding = fn(
Info.pick({
subscriptionID: true,
standing: true,
}),
(input) =>
useTransaction((tx) =>
tx
.update(polarTable)
.set({ standing: input.standing })
.where(and(eq(polarTable.subscriptionID, input.subscriptionID!)))
.execute(),
),
);
export const fromCustomerID = fn(Info.shape.customerID, (customerID) =>
useTransaction((tx) =>
tx
.select()
.from(polarTable)
.where(and(eq(polarTable.customerID, customerID)))
.execute()
.then((rows) => rows.map(serialize).at(0)),
),
);
function serialize(
input: typeof polarTable.$inferSelect,
): z.infer<typeof Info> {
return {
teamID: input.teamID,
customerID: input.customerID,
subscriptionID: input.subscriptionID,
subscriptionItemID: input.subscriptionItemID,
standing: input.standing,
};
}
}

View File

@@ -0,0 +1,22 @@
import { timestamps, teamID } from "../drizzle/types";
import { teamIndexes, teamTable } from "../team/team.sql";
import { pgTable, text, varchar } from "drizzle-orm/pg-core";
export const Standing = ["new", "good", "overdue"] as const;
export const polarTable = pgTable(
"polar",
{
teamID: teamID.teamID.primaryKey().references(() => teamTable.id),
...timestamps,
customerID: varchar("customer_id", { length: 255 }).notNull(),
subscriptionID: varchar("subscription_id", { length: 255 }),
subscriptionItemID: varchar("subscription_item_id", {
length: 255,
}),
standing: text("standing", { enum: Standing }).notNull(),
},
(table) => ({
...teamIndexes(table),
})
)

View File

@@ -3,13 +3,13 @@ import { Resource } from "sst";
import { bus } from "sst/aws/bus";
import { Common } from "../common";
import { createID, fn } from "../utils";
import { VisibleError } from "../error";
import { Examples } from "../examples";
import { teamTable } from "./team.sql";
import { createEvent } from "../event";
import { assertActor } from "../actor";
import { assertActor, withActor } from "../actor";
import { and, eq, sql } from "../drizzle";
import { memberTable } from "../member/member.sql";
import { HTTPException } from 'hono/http-exception';
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
export module Team {
@@ -45,11 +45,11 @@ export module Team {
),
};
export class WorkspaceExistsError extends VisibleError {
export class TeamExistsError extends HTTPException {
constructor(slug: string) {
super(
"team.slug_exists",
`there is already a workspace named "${slug}"`,
400,
{ message: `There is already a team named "${slug}"`, }
);
}
}
@@ -65,15 +65,16 @@ export module Team {
slug: input.slug,
name: input.name
})
.onConflictDoNothing()
.returning({ insertedID: teamTable.id })
.onConflictDoNothing({ target: teamTable.slug })
if (result.length === 0) throw new WorkspaceExistsError(input.slug);
if (!result.rowCount) throw new TeamExistsError(input.slug);
await afterTx(() =>
bus.publish(Resource.Bus, Events.Created, {
teamID: id,
}),
withActor({ type: "system", properties: { teamID: id } }, () =>
bus.publish(Resource.Bus, Events.Created, {
teamID: id,
})
),
);
return id;
})

View File

@@ -1,4 +1,6 @@
import { z } from "zod";
import { Polar } from "../polar";
import { Team } from "../team";
import { bus } from "sst/aws/bus";
import { Common } from "../common";
import { createID, fn } from "../utils";
@@ -11,7 +13,6 @@ import { assertActor, withActor } from "../actor";
import { memberTable } from "../member/member.sql";
import { and, eq, isNull, asc, getTableColumns, sql } from "../drizzle";
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
import { Team } from "../team";
export module User {
@@ -106,12 +107,8 @@ export module User {
//FIXME: Do this much later, as Polar.sh has so many inconsistencies for fuck's sake
// const customer = await Polar.client.customers.create({
// email: input.email,
// metadata: {
// userID,
// },
// });
const customer = await Polar.fromUserEmail(input.email)
console.log("customer", customer)
const name = sanitizeUsername(input.name);
@@ -131,6 +128,7 @@ export module User {
avatarUrl: input.avatarUrl,
email: input.email,
discriminator: Number(discriminator),
polarCustomerID: customer?.id
})
await afterTx(() =>
withActor({