(breaking change) use stripe customer id instead of subscription id

main
TZGyn 1 year ago
parent 2541d02f77
commit 68ba0258a6
Signed by: TZGyn
GPG Key ID: 122EAF77AE81FD4A

@ -58,6 +58,12 @@ go build
## Breaking Changes (For builds before this date)
### 29 August 2024
Using stripe customer id instead of stripe subscription id
Existing customer subscription id will be removed from database, so have to manually find every customer with the same email and fill in their ids
### 23 July 2024
Transition from using ipbase to using geoipupdate for geolocation.

@ -0,0 +1,2 @@
ALTER TABLE "user" ADD COLUMN "stripe_customer_id" varchar(255);--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN IF EXISTS "stripe_subscription";

@ -0,0 +1 @@
DROP TABLE "stripe_session";

@ -0,0 +1,461 @@
{
"id": "227300d6-3473-4b34-90c7-15aa5e594784",
"prevId": "5014f286-7c9c-411b-a03e-16a05b37caa0",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.email_verification_token": {
"name": "email_verification_token",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(255)",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.project": {
"name": "project",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"uuid": {
"name": "uuid",
"type": "uuid",
"primaryKey": false,
"notNull": false,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"qr_background": {
"name": "qr_background",
"type": "varchar(7)",
"primaryKey": false,
"notNull": true,
"default": "'#ffffff'"
},
"qr_foreground": {
"name": "qr_foreground",
"type": "varchar(7)",
"primaryKey": false,
"notNull": true,
"default": "'#000000'"
},
"domain_status": {
"name": "domain_status",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "'verified'"
},
"enable_custom_domain": {
"name": "enable_custom_domain",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"custom_ip": {
"name": "custom_ip",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"custom_domain_id": {
"name": "custom_domain_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"custom_domain": {
"name": "custom_domain",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(255)",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.setting": {
"name": "setting",
"schema": "",
"columns": {
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"qr_background": {
"name": "qr_background",
"type": "varchar(7)",
"primaryKey": false,
"notNull": false
},
"qr_foreground": {
"name": "qr_foreground",
"type": "varchar(7)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.shortener": {
"name": "shortener",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"link": {
"name": "link",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"ios": {
"name": "ios",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"ios_link": {
"name": "ios_link",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"android": {
"name": "android",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"android_link": {
"name": "android_link",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"code": {
"name": "code",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"active": {
"name": "active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"project_id": {
"name": "project_id",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"shortener_code_unique": {
"name": "shortener_code_unique",
"nullsNotDistinct": false,
"columns": [
"code"
]
}
}
},
"public.stripe_session": {
"name": "stripe_session",
"schema": "",
"columns": {
"session_id": {
"name": "session_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"expired": {
"name": "expired",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"uuid": {
"name": "uuid",
"type": "uuid",
"primaryKey": false,
"notNull": false,
"default": "gen_random_uuid()"
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"google_id": {
"name": "google_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"plan": {
"name": "plan",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "'free'"
},
"stripe_customer_id": {
"name": "stripe_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
}
},
"public.visitor": {
"name": "visitor",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"shortener_id": {
"name": "shortener_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"country_code": {
"name": "country_code",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country": {
"name": "country",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"device_type": {
"name": "device_type",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"device_vendor": {
"name": "device_vendor",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"os": {
"name": "os",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"browser": {
"name": "browser",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

@ -0,0 +1,432 @@
{
"id": "b2f02cb7-53e4-4077-9cea-ad5e3b062f51",
"prevId": "227300d6-3473-4b34-90c7-15aa5e594784",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.email_verification_token": {
"name": "email_verification_token",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(255)",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.project": {
"name": "project",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"uuid": {
"name": "uuid",
"type": "uuid",
"primaryKey": false,
"notNull": false,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"qr_background": {
"name": "qr_background",
"type": "varchar(7)",
"primaryKey": false,
"notNull": true,
"default": "'#ffffff'"
},
"qr_foreground": {
"name": "qr_foreground",
"type": "varchar(7)",
"primaryKey": false,
"notNull": true,
"default": "'#000000'"
},
"domain_status": {
"name": "domain_status",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "'verified'"
},
"enable_custom_domain": {
"name": "enable_custom_domain",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"custom_ip": {
"name": "custom_ip",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"custom_domain_id": {
"name": "custom_domain_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"custom_domain": {
"name": "custom_domain",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(255)",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.setting": {
"name": "setting",
"schema": "",
"columns": {
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"qr_background": {
"name": "qr_background",
"type": "varchar(7)",
"primaryKey": false,
"notNull": false
},
"qr_foreground": {
"name": "qr_foreground",
"type": "varchar(7)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.shortener": {
"name": "shortener",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"link": {
"name": "link",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"ios": {
"name": "ios",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"ios_link": {
"name": "ios_link",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"android": {
"name": "android",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"android_link": {
"name": "android_link",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"code": {
"name": "code",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"active": {
"name": "active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"project_id": {
"name": "project_id",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"shortener_code_unique": {
"name": "shortener_code_unique",
"nullsNotDistinct": false,
"columns": [
"code"
]
}
}
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"uuid": {
"name": "uuid",
"type": "uuid",
"primaryKey": false,
"notNull": false,
"default": "gen_random_uuid()"
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"google_id": {
"name": "google_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"plan": {
"name": "plan",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "'free'"
},
"stripe_customer_id": {
"name": "stripe_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
}
},
"public.visitor": {
"name": "visitor",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"shortener_id": {
"name": "shortener_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"country_code": {
"name": "country_code",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country": {
"name": "country",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"device_type": {
"name": "device_type",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"device_vendor": {
"name": "device_vendor",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"os": {
"name": "os",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"browser": {
"name": "browser",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

@ -127,6 +127,20 @@
"when": 1724731642381,
"tag": "0017_mysterious_marten_broadcloak",
"breakpoints": true
},
{
"idx": 18,
"version": "7",
"when": 1724920767007,
"tag": "0018_living_taskmaster",
"breakpoints": true
},
{
"idx": 19,
"version": "7",
"when": 1724923406366,
"tag": "0019_chief_mephisto",
"breakpoints": true
}
]
}

@ -24,7 +24,7 @@ export const user = pgTable('user', {
.notNull()
.$type<'free' | 'pro' | 'owner'>()
.default('free'),
stripeSubscription: varchar('stripe_subscription', { length: 255 }),
stripeCustomerId: varchar('stripe_customer_id', { length: 255 }),
})
export const shortener = pgTable('shortener', {
@ -117,12 +117,6 @@ export const emailVerificationToken = pgTable(
},
)
export const stripeSession = pgTable('stripe_session', {
session_id: varchar('session_id', { length: 255 }).notNull(),
userId: integer('user_id').notNull(),
expired: boolean('expired').notNull().default(false),
})
// relations
export const userRelations = relations(user, ({ one, many }) => ({
shortener: many(shortener),
@ -175,13 +169,3 @@ export const settingRelations = relations(setting, ({ one }) => ({
references: [user.id],
}),
}))
export const stripeSessionRelations = relations(
stripeSession,
({ one }) => ({
user: one(user, {
fields: [stripeSession.userId],
references: [user.id],
}),
}),
)

@ -28,10 +28,16 @@ export const actions = {
const user = event.locals.user
if (!user.stripeSubscription) return { form }
if (!user.stripeCustomerId) return { form }
const subscription = await stripe.subscriptions.update(
user.stripeSubscription,
const subscription = await stripe.subscriptions.list({
customer: user.stripeCustomerId,
price: env.PRIVATE_PRO_PLAN_PRICE_ID,
limit: 1,
})
const cancelSubscription = await stripe.subscriptions.update(
subscription.data[0].id,
{ cancel_at_period_end: true },
)

@ -2,19 +2,39 @@ import { redirect } from '@sveltejs/kit'
import type { PageServerLoad } from './$types'
import Stripe from 'stripe'
import { db } from '$lib/db'
import { stripeSession } from '$lib/db/schema'
import { env } from '$env/dynamic/private'
import { user as userTable } from '$lib/db/schema'
import { eq } from 'drizzle-orm'
export const load = (async (events) => {
if (events.locals.user.stripeSubscription) {
if (events.locals.user.plan !== 'free') {
redirect(301, '/dashboard/billing')
}
if (!events.locals.user.email_verified) {
redirect(301, '/dashboard/billing')
}
const user = events.locals.user
const stripe = new Stripe(env.PRIVATE_STRIPE_SECRET_KEY)
let stripeCustomerId = events.locals.user.stripeCustomerId
if (!stripeCustomerId) {
const customer = await stripe.customers.create({
email: user.email,
})
stripeCustomerId = customer.id
await db
.update(userTable)
.set({
stripeCustomerId: customer.id,
})
.where(eq(userTable.id, user.id))
}
const session = await stripe.checkout.sessions.create({
customer_email: events.locals.user.email,
customer: stripeCustomerId,
line_items: [
{ price: env.PRIVATE_PRO_PLAN_PRICE_ID, quantity: 1 },
],
@ -30,11 +50,5 @@ export const load = (async (events) => {
redirect(301, '/dashboard/billing')
}
await db.insert(stripeSession).values({
session_id: session.id,
userId: events.locals.user.id,
expired: false,
})
redirect(301, session.url)
}) satisfies PageServerLoad

@ -2,7 +2,7 @@ import { redirect } from '@sveltejs/kit'
import type { PageServerLoad } from './$types'
import Stripe from 'stripe'
import { db } from '$lib/db'
import { user, stripeSession } from '$lib/db/schema'
import { user } from '$lib/db/schema'
import { eq } from 'drizzle-orm'
import { env } from '$env/dynamic/private'
@ -17,27 +17,14 @@ export const load = (async ({ url }) => {
session.status === 'complete' &&
session.payment_status === 'paid'
) {
const stripe_session = await db.query.stripeSession.findFirst({
where: (stripeSession, { eq }) =>
eq(stripeSession.session_id, session_id),
with: {
user: true,
},
})
if (!stripe_session) redirect(301, '/dashboard/billing')
if (!session.customer) redirect(301, '/dashboard/billing')
await db
.update(user)
.set({
plan: 'pro',
stripeSubscription: session.subscription?.toString(),
})
.where(eq(user.id, stripe_session.user.id))
await db
.update(stripeSession)
.set({ expired: true })
.where(eq(stripeSession.session_id, session_id))
.where(eq(user.stripeCustomerId, session.customer?.toString()))
redirect(301, '/dashboard/billing')
}

@ -0,0 +1,23 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button'
import { CheckCircleIcon } from 'lucide-svelte'
import * as Dialog from '$lib/components/ui/dialog'
export let data
</script>
<Dialog.Root open>
<Dialog.Content>
<Dialog.Header class="items-center justify-center space-y-4">
<CheckCircleIcon class="text-success h-16 w-16" />
<Dialog.Title>Payment Success</Dialog.Title>
<Dialog.Description>
You have subscribed to the pro plan, thank you for the
support.
</Dialog.Description>
</Dialog.Header>
<div class="pt-12">
<Button href="/dashboard/billing">Return Billing</Button>
</div>
</Dialog.Content>
</Dialog.Root>

@ -20,6 +20,7 @@
if (result.status === 200) {
toast.success(
'Welcome to kon.sh, a verification email has been sent to your mailbox',
{ duration: 10000 },
)
}
},

@ -27,7 +27,10 @@ export const POST: RequestHandler = async (event) => {
.update(user)
.set({ plan: 'free' })
.where(
eq(user.stripeSubscription, stripeEvent.data.object.id),
eq(
user.stripeCustomerId,
stripeEvent.data.object.customer.toString(),
),
)
}
return new Response('Success', { status: 200 })

Loading…
Cancel
Save