feat(server): stripe service, more stripe table

This commit is contained in:
RainbowBird
2026-03-28 02:25:44 +08:00
committed by RainbowBird
parent 889a1e0800
commit 4e510bf5c2
8 changed files with 2605 additions and 17 deletions
+75
View File
@@ -0,0 +1,75 @@
CREATE TABLE "stripe_checkout_session" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"stripe_session_id" text NOT NULL,
"stripe_customer_id" text,
"mode" text NOT NULL,
"status" text,
"payment_status" text,
"amount_total" integer,
"currency" text,
"success_url" text,
"cancel_url" text,
"stripe_payment_intent_id" text,
"stripe_subscription_id" text,
"metadata" text,
"expires_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "stripe_checkout_session_stripe_session_id_unique" UNIQUE("stripe_session_id")
);
--> statement-breakpoint
CREATE TABLE "stripe_customer" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"stripe_customer_id" text NOT NULL,
"email" text,
"name" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "stripe_customer_stripe_customer_id_unique" UNIQUE("stripe_customer_id")
);
--> statement-breakpoint
CREATE TABLE "stripe_invoice" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"stripe_invoice_id" text NOT NULL,
"stripe_customer_id" text,
"stripe_subscription_id" text,
"status" text,
"amount_due" integer,
"amount_paid" integer,
"currency" text,
"invoice_url" text,
"invoice_pdf" text,
"period_start" timestamp,
"period_end" timestamp,
"paid_at" timestamp,
"metadata" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "stripe_invoice_stripe_invoice_id_unique" UNIQUE("stripe_invoice_id")
);
--> statement-breakpoint
CREATE TABLE "stripe_subscription" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"stripe_subscription_id" text NOT NULL,
"stripe_customer_id" text NOT NULL,
"stripe_price_id" text,
"status" text NOT NULL,
"current_period_start" timestamp,
"current_period_end" timestamp,
"cancel_at_period_end" text,
"canceled_at" timestamp,
"ended_at" timestamp,
"metadata" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "stripe_subscription_stripe_subscription_id_unique" UNIQUE("stripe_subscription_id")
);
--> statement-breakpoint
ALTER TABLE "stripe_checkout_session" ADD CONSTRAINT "stripe_checkout_session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "stripe_customer" ADD CONSTRAINT "stripe_customer_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "stripe_invoice" ADD CONSTRAINT "stripe_invoice_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "stripe_subscription" ADD CONSTRAINT "stripe_subscription_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -15,6 +15,13 @@
"when": 1772532802115,
"tag": "0001_magenta_skrulls",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1772634952890,
"tag": "0002_mean_tigra",
"breakpoints": true
}
]
}
+12 -3
View File
@@ -28,6 +28,7 @@ import { createChatService } from './services/chats'
import { createConfigKVService } from './services/config-kv'
import { createFluxService } from './services/flux'
import { createProviderService } from './services/providers'
import { createStripeService } from './services/stripe'
import { ApiError, createInternalError } from './utils/error'
import { getTrustedOrigin } from './utils/origin'
@@ -37,6 +38,7 @@ type ChatService = ReturnType<typeof createChatService>
type ProviderService = ReturnType<typeof createProviderService>
type FluxService = ReturnType<typeof createFluxService>
type ConfigKVService = ReturnType<typeof createConfigKVService>
type StripeDBService = ReturnType<typeof createStripeService>
type OtelMetrics = ReturnType<typeof initOtel>
@@ -46,11 +48,12 @@ interface AppDeps {
chatService: ChatService
providerService: ProviderService
fluxService: FluxService
stripeService: StripeDBService
configKV: ConfigKVService
env: Env
}
function buildApp({ auth, characterService, chatService, providerService, fluxService, configKV, env }: AppDeps) {
function buildApp({ auth, characterService, chatService, providerService, fluxService, stripeService, configKV, env }: AppDeps) {
const logger = useLogger('app').useGlobalConfig()
const app = new Hono<HonoEnv>()
@@ -128,7 +131,7 @@ function buildApp({ auth, characterService, chatService, providerService, fluxSe
/**
* Stripe routes.
*/
.route('/api/stripe', createStripeRoutes(fluxService, configKV, env))
.route('/api/stripe', createStripeRoutes(fluxService, stripeService, configKV, env))
}
export type AppType = ReturnType<typeof buildApp>
@@ -199,19 +202,25 @@ async function createApp() {
build: ({ dependsOn }) => createConfigKVService(dependsOn.redis),
})
const stripeService = injeca.provide('services:stripe', {
dependsOn: { db },
build: ({ dependsOn }) => createStripeService(dependsOn.db),
})
const fluxService = injeca.provide('services:flux', {
dependsOn: { db, configKV },
build: ({ dependsOn }) => createFluxService(dependsOn.db, dependsOn.configKV),
})
await injeca.start()
const resolved = await injeca.resolve({ auth, characterService, chatService, providerService, fluxService, configKV, env: parsedEnv })
const resolved = await injeca.resolve({ auth, characterService, chatService, providerService, fluxService, stripeService, configKV, env: parsedEnv })
const app = buildApp({
auth: resolved.auth,
characterService: resolved.characterService,
chatService: resolved.chatService,
providerService: resolved.providerService,
fluxService: resolved.fluxService,
stripeService: resolved.stripeService,
configKV: resolved.configKV,
env: resolved.env,
})
+216 -14
View File
@@ -1,5 +1,7 @@
import type { Env } from '../libs/env'
import type { ConfigKVService } from '../services/config-kv'
import type { FluxService } from '../services/flux'
import type { StripeService } from '../services/stripe'
import type { HonoEnv } from '../types/hono'
import Stripe from 'stripe'
@@ -14,7 +16,7 @@ const CheckoutBodySchema = object({
amount: pipe(number(), integer(), minValue(1)),
})
export function createStripeRoutes(fluxService: FluxService, env: Env) {
export function createStripeRoutes(fluxService: FluxService, stripeService: StripeService, configKV: ConfigKVService, env: Env) {
const stripe = env.STRIPE_SECRET_KEY ? new Stripe(env.STRIPE_SECRET_KEY) : null
return new Hono<HonoEnv>()
@@ -31,6 +33,10 @@ export function createStripeRoutes(fluxService: FluxService, env: Env) {
const { amount } = result.output
// Reuse existing stripe customer if available
const customer = await stripeService.getCustomerByUserId(user.id)
const stripeCustomerId = customer?.stripeCustomerId
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
@@ -48,14 +54,67 @@ export function createStripeRoutes(fluxService: FluxService, env: Env) {
mode: 'payment',
success_url: `${env.CLIENT_URL}/settings/flux?success=true`,
cancel_url: `${env.CLIENT_URL}/settings/flux?canceled=true`,
customer_email: user.email,
customer: stripeCustomerId,
customer_email: stripeCustomerId ? undefined : user.email,
metadata: {
userId: user.id,
},
})
// Persist the checkout session
await stripeService.upsertCheckoutSession({
userId: user.id,
stripeSessionId: session.id,
stripeCustomerId: typeof session.customer === 'string' ? session.customer : session.customer?.id,
mode: session.mode ?? 'payment',
status: session.status,
paymentStatus: session.payment_status,
amountTotal: session.amount_total,
currency: session.currency,
successUrl: session.success_url,
cancelUrl: session.cancel_url,
stripePaymentIntentId: typeof session.payment_intent === 'string' ? session.payment_intent : session.payment_intent?.id,
stripeSubscriptionId: typeof session.subscription === 'string' ? session.subscription : session.subscription?.id,
metadata: session.metadata ? JSON.stringify(session.metadata) : null,
expiresAt: session.expires_at ? new Date(session.expires_at * 1000) : null,
})
return c.json({ url: session.url })
})
// ---- Orders / checkout sessions history ----
.get('/orders', authGuard, async (c) => {
const user = c.get('user')!
const sessions = await stripeService.getCheckoutSessionsByUserId(user.id)
return c.json(sessions)
})
// ---- Invoices history ----
.get('/invoices', authGuard, async (c) => {
const user = c.get('user')!
const invoices = await stripeService.getInvoicesByUserId(user.id)
return c.json(invoices)
})
// ---- Customer portal ----
.post('/portal', authGuard, async (c) => {
if (!stripe)
throw createServiceUnavailableError('Stripe is not configured', 'STRIPE_NOT_CONFIGURED')
const user = c.get('user')!
const customer = await stripeService.getCustomerByUserId(user.id)
if (!customer)
throw createBadRequestError('No billing account found', 'NO_CUSTOMER')
const portalSession = await stripe.billingPortal.sessions.create({
customer: customer.stripeCustomerId,
return_url: `${env.CLIENT_URL}/settings/flux`,
})
return c.json({ url: portalSession.url })
})
// ---- Webhook ----
.post('/webhook', async (c) => {
if (!stripe || !env.STRIPE_WEBHOOK_SECRET)
throw createServiceUnavailableError('Stripe is not configured', 'STRIPE_NOT_CONFIGURED')
@@ -74,21 +133,164 @@ export function createStripeRoutes(fluxService: FluxService, env: Env) {
throw createBadRequestError(`Webhook Error: ${message}`, 'WEBHOOK_ERROR')
}
if (event.type === 'checkout.session.completed') {
const session = event.data.object
const userId = session.metadata?.userId
const amount = session.amount_total
if (userId && amount) {
await fluxService.addFlux(userId, amount * env.FLUX_PER_CENT)
if (session.customer) {
const customerId = typeof session.customer === 'string' ? session.customer : session.customer?.id
await fluxService.updateStripeCustomerId(userId, customerId)
}
switch (event.type) {
case 'checkout.session.completed': {
await handleCheckoutSessionCompleted(event.data.object, fluxService, stripeService, configKV)
break
}
case 'customer.created':
case 'customer.updated': {
await handleCustomerEvent(event.data.object, stripeService)
break
}
case 'customer.subscription.created':
case 'customer.subscription.updated':
case 'customer.subscription.deleted': {
await handleSubscriptionEvent(event.data.object, stripeService)
break
}
case 'invoice.created':
case 'invoice.updated':
case 'invoice.paid':
case 'invoice.payment_failed': {
await handleInvoiceEvent(event.data.object, fluxService, stripeService, configKV)
break
}
}
return c.json({ received: true })
})
}
// ---- Webhook handlers ----
async function handleCheckoutSessionCompleted(
session: Stripe.Checkout.Session,
fluxService: FluxService,
stripeService: StripeService,
configKV: ConfigKVService,
) {
const userId = session.metadata?.userId
if (!userId)
return
// Upsert customer record if we got a customer back
if (session.customer) {
const stripeCustomerId = typeof session.customer === 'string' ? session.customer : session.customer.id
await stripeService.upsertCustomer({
userId,
stripeCustomerId,
email: session.customer_email ?? undefined,
})
// Keep the legacy field in sync
await fluxService.updateStripeCustomerId(userId, stripeCustomerId)
}
// Update the checkout session record
await stripeService.upsertCheckoutSession({
userId,
stripeSessionId: session.id,
stripeCustomerId: typeof session.customer === 'string' ? session.customer : session.customer?.id,
mode: session.mode ?? 'payment',
status: session.status,
paymentStatus: session.payment_status,
amountTotal: session.amount_total,
currency: session.currency,
successUrl: session.success_url,
cancelUrl: session.cancel_url,
stripePaymentIntentId: typeof session.payment_intent === 'string' ? session.payment_intent : session.payment_intent?.id,
stripeSubscriptionId: typeof session.subscription === 'string' ? session.subscription : session.subscription?.id,
metadata: session.metadata ? JSON.stringify(session.metadata) : null,
expiresAt: session.expires_at ? new Date(session.expires_at * 1000) : null,
})
// Add flux for one-time payments
if (session.mode === 'payment' && session.amount_total) {
const fluxPerCent = await configKV.get('FLUX_PER_CENT')
await fluxService.addFlux(userId, session.amount_total * fluxPerCent)
}
}
async function handleCustomerEvent(
customer: Stripe.Customer | Stripe.DeletedCustomer,
stripeService: StripeService,
) {
if (customer.deleted)
return
// Try to find existing customer to get userId
const existing = await stripeService.getCustomerByStripeId(customer.id)
if (!existing)
return // We don't know the userId yet; will be linked on checkout
await stripeService.upsertCustomer({
userId: existing.userId,
stripeCustomerId: customer.id,
email: customer.email ?? undefined,
name: customer.name ?? undefined,
})
}
async function handleSubscriptionEvent(
subscription: Stripe.Subscription,
stripeService: StripeService,
) {
const stripeCustomerId = typeof subscription.customer === 'string' ? subscription.customer : subscription.customer.id
const customer = await stripeService.getCustomerByStripeId(stripeCustomerId)
if (!customer)
return
await stripeService.upsertSubscription({
userId: customer.userId,
stripeSubscriptionId: subscription.id,
stripeCustomerId,
stripePriceId: subscription.items.data[0]?.price?.id,
status: subscription.status,
currentPeriodStart: new Date(subscription.current_period_start * 1000),
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
cancelAtPeriodEnd: String(subscription.cancel_at_period_end),
canceledAt: subscription.canceled_at ? new Date(subscription.canceled_at * 1000) : null,
endedAt: subscription.ended_at ? new Date(subscription.ended_at * 1000) : null,
metadata: subscription.metadata ? JSON.stringify(subscription.metadata) : null,
})
}
async function handleInvoiceEvent(
invoice: Stripe.Invoice,
fluxService: FluxService,
stripeService: StripeService,
configKV: ConfigKVService,
) {
const stripeCustomerId = typeof invoice.customer === 'string' ? invoice.customer : invoice.customer?.id
if (!stripeCustomerId)
return
const customer = await stripeService.getCustomerByStripeId(stripeCustomerId)
if (!customer)
return
const subscriptionId = typeof invoice.subscription === 'string' ? invoice.subscription : invoice.subscription?.id
await stripeService.upsertInvoice({
userId: customer.userId,
stripeInvoiceId: invoice.id,
stripeCustomerId,
stripeSubscriptionId: subscriptionId,
status: invoice.status,
amountDue: invoice.amount_due,
amountPaid: invoice.amount_paid,
currency: invoice.currency,
invoiceUrl: invoice.hosted_invoice_url,
invoicePdf: invoice.invoice_pdf,
periodStart: new Date(invoice.period_start * 1000),
periodEnd: new Date(invoice.period_end * 1000),
paidAt: invoice.status_transitions?.paid_at ? new Date(invoice.status_transitions.paid_at * 1000) : null,
metadata: invoice.metadata ? JSON.stringify(invoice.metadata) : null,
})
// Add flux when a subscription invoice is paid
if (invoice.status === 'paid' && invoice.amount_paid && subscriptionId) {
const fluxPerCent = await configKV.get('FLUX_PER_CENT')
await fluxService.addFlux(customer.userId, invoice.amount_paid * fluxPerCent)
}
}
+1
View File
@@ -3,4 +3,5 @@ export * from './characters'
export * from './chats'
export * from './flux'
export * from './providers'
export * from './stripe'
export * from './user-character'
+124
View File
@@ -0,0 +1,124 @@
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'
import { relations } from 'drizzle-orm'
import { integer, pgTable, text, timestamp } from 'drizzle-orm/pg-core'
import { nanoid } from '../utils/id'
import { user } from './accounts'
/**
* Stripe customers linked to our users.
*/
export const stripeCustomer = pgTable('stripe_customer', {
id: text('id').primaryKey().$defaultFn(() => nanoid()),
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
stripeCustomerId: text('stripe_customer_id').notNull().unique(),
email: text('email'),
name: text('name'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
})
/**
* Stripe checkout sessions every checkout attempt is recorded.
*/
export const stripeCheckoutSession = pgTable('stripe_checkout_session', {
id: text('id').primaryKey().$defaultFn(() => nanoid()),
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
stripeSessionId: text('stripe_session_id').notNull().unique(),
stripeCustomerId: text('stripe_customer_id'),
mode: text('mode').notNull(), // 'payment' | 'subscription' | 'setup'
status: text('status'), // 'open' | 'complete' | 'expired'
paymentStatus: text('payment_status'), // 'paid' | 'unpaid' | 'no_payment_required'
amountTotal: integer('amount_total'), // in cents
currency: text('currency'),
successUrl: text('success_url'),
cancelUrl: text('cancel_url'),
stripePaymentIntentId: text('stripe_payment_intent_id'),
stripeSubscriptionId: text('stripe_subscription_id'),
metadata: text('metadata'), // JSON stringified
expiresAt: timestamp('expires_at'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
})
/**
* Stripe subscriptions.
*/
export const stripeSubscription = pgTable('stripe_subscription', {
id: text('id').primaryKey().$defaultFn(() => nanoid()),
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
stripeSubscriptionId: text('stripe_subscription_id').notNull().unique(),
stripeCustomerId: text('stripe_customer_id').notNull(),
stripePriceId: text('stripe_price_id'),
status: text('status').notNull(), // 'active' | 'past_due' | 'canceled' | 'incomplete' | etc
currentPeriodStart: timestamp('current_period_start'),
currentPeriodEnd: timestamp('current_period_end'),
cancelAtPeriodEnd: text('cancel_at_period_end'), // 'true' | 'false'
canceledAt: timestamp('canceled_at'),
endedAt: timestamp('ended_at'),
metadata: text('metadata'), // JSON stringified
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
})
/**
* Stripe invoices both one-time and subscription invoices.
*/
export const stripeInvoice = pgTable('stripe_invoice', {
id: text('id').primaryKey().$defaultFn(() => nanoid()),
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
stripeInvoiceId: text('stripe_invoice_id').notNull().unique(),
stripeCustomerId: text('stripe_customer_id'),
stripeSubscriptionId: text('stripe_subscription_id'),
status: text('status'), // 'draft' | 'open' | 'paid' | 'uncollectible' | 'void'
amountDue: integer('amount_due'), // in cents
amountPaid: integer('amount_paid'), // in cents
currency: text('currency'),
invoiceUrl: text('invoice_url'),
invoicePdf: text('invoice_pdf'),
periodStart: timestamp('period_start'),
periodEnd: timestamp('period_end'),
paidAt: timestamp('paid_at'),
metadata: text('metadata'), // JSON stringified
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
})
// ---------- Relations ----------
export const stripeCustomerRelations = relations(stripeCustomer, ({ one, many }) => ({
user: one(user, { fields: [stripeCustomer.userId], references: [user.id] }),
checkoutSessions: many(stripeCheckoutSession),
subscriptions: many(stripeSubscription),
invoices: many(stripeInvoice),
}))
export const stripeCheckoutSessionRelations = relations(stripeCheckoutSession, ({ one }) => ({
user: one(user, { fields: [stripeCheckoutSession.userId], references: [user.id] }),
customer: one(stripeCustomer, { fields: [stripeCheckoutSession.stripeCustomerId], references: [stripeCustomer.stripeCustomerId] }),
}))
export const stripeSubscriptionRelations = relations(stripeSubscription, ({ one }) => ({
user: one(user, { fields: [stripeSubscription.userId], references: [user.id] }),
customer: one(stripeCustomer, { fields: [stripeSubscription.stripeCustomerId], references: [stripeCustomer.stripeCustomerId] }),
}))
export const stripeInvoiceRelations = relations(stripeInvoice, ({ one }) => ({
user: one(user, { fields: [stripeInvoice.userId], references: [user.id] }),
customer: one(stripeCustomer, { fields: [stripeInvoice.stripeCustomerId], references: [stripeCustomer.stripeCustomerId] }),
}))
// ---------- Types ----------
export type StripeCustomer = InferSelectModel<typeof stripeCustomer>
export type NewStripeCustomer = InferInsertModel<typeof stripeCustomer>
export type StripeCheckoutSession = InferSelectModel<typeof stripeCheckoutSession>
export type NewStripeCheckoutSession = InferInsertModel<typeof stripeCheckoutSession>
export type StripeSubscription = InferSelectModel<typeof stripeSubscription>
export type NewStripeSubscription = InferInsertModel<typeof stripeSubscription>
export type StripeInvoice = InferSelectModel<typeof stripeInvoice>
export type NewStripeInvoice = InferInsertModel<typeof stripeInvoice>
+129
View File
@@ -0,0 +1,129 @@
import type { Database } from '../libs/db'
import type { NewStripeCheckoutSession, NewStripeCustomer, NewStripeInvoice, NewStripeSubscription } from '../schemas/stripe'
import { eq } from 'drizzle-orm'
import * as schema from '../schemas/stripe'
export function createStripeService(db: Database) {
return {
// ---- Customer ----
async upsertCustomer(data: NewStripeCustomer) {
const existing = await db.query.stripeCustomer.findFirst({
where: eq(schema.stripeCustomer.stripeCustomerId, data.stripeCustomerId),
})
if (existing) {
const [updated] = await db.update(schema.stripeCustomer)
.set({ ...data, updatedAt: new Date() })
.where(eq(schema.stripeCustomer.stripeCustomerId, data.stripeCustomerId))
.returning()
return updated
}
const [created] = await db.insert(schema.stripeCustomer)
.values(data)
.returning()
return created
},
async getCustomerByUserId(userId: string) {
return db.query.stripeCustomer.findFirst({
where: eq(schema.stripeCustomer.userId, userId),
})
},
async getCustomerByStripeId(stripeCustomerId: string) {
return db.query.stripeCustomer.findFirst({
where: eq(schema.stripeCustomer.stripeCustomerId, stripeCustomerId),
})
},
// ---- Checkout Session ----
async upsertCheckoutSession(data: NewStripeCheckoutSession) {
const existing = await db.query.stripeCheckoutSession.findFirst({
where: eq(schema.stripeCheckoutSession.stripeSessionId, data.stripeSessionId),
})
if (existing) {
const [updated] = await db.update(schema.stripeCheckoutSession)
.set({ ...data, updatedAt: new Date() })
.where(eq(schema.stripeCheckoutSession.stripeSessionId, data.stripeSessionId))
.returning()
return updated
}
const [created] = await db.insert(schema.stripeCheckoutSession)
.values(data)
.returning()
return created
},
async getCheckoutSessionsByUserId(userId: string) {
return db.query.stripeCheckoutSession.findMany({
where: eq(schema.stripeCheckoutSession.userId, userId),
orderBy: (t, { desc }) => [desc(t.createdAt)],
})
},
// ---- Subscription ----
async upsertSubscription(data: NewStripeSubscription) {
const existing = await db.query.stripeSubscription.findFirst({
where: eq(schema.stripeSubscription.stripeSubscriptionId, data.stripeSubscriptionId),
})
if (existing) {
const [updated] = await db.update(schema.stripeSubscription)
.set({ ...data, updatedAt: new Date() })
.where(eq(schema.stripeSubscription.stripeSubscriptionId, data.stripeSubscriptionId))
.returning()
return updated
}
const [created] = await db.insert(schema.stripeSubscription)
.values(data)
.returning()
return created
},
async getActiveSubscription(userId: string) {
return db.query.stripeSubscription.findFirst({
where: eq(schema.stripeSubscription.userId, userId),
orderBy: (t, { desc }) => [desc(t.createdAt)],
})
},
// ---- Invoice ----
async upsertInvoice(data: NewStripeInvoice) {
const existing = await db.query.stripeInvoice.findFirst({
where: eq(schema.stripeInvoice.stripeInvoiceId, data.stripeInvoiceId),
})
if (existing) {
const [updated] = await db.update(schema.stripeInvoice)
.set({ ...data, updatedAt: new Date() })
.where(eq(schema.stripeInvoice.stripeInvoiceId, data.stripeInvoiceId))
.returning()
return updated
}
const [created] = await db.insert(schema.stripeInvoice)
.values(data)
.returning()
return created
},
async getInvoicesByUserId(userId: string) {
return db.query.stripeInvoice.findMany({
where: eq(schema.stripeInvoice.userId, userId),
orderBy: (t, { desc }) => [desc(t.createdAt)],
})
},
}
}
export type StripeService = ReturnType<typeof createStripeService>