feat: remove planId from payment and subscription schema

- Created new SQL tables for account, payment, session, user, and verification to support user management and payment processing.
- Added foreign key constraints to link account, payment, and session tables to the user table for data integrity.
- Updated journal and snapshot metadata to reflect the new schema changes.
This commit is contained in:
javayhu 2025-04-11 01:23:13 +08:00
parent f982e1b01a
commit 313625577c
8 changed files with 20 additions and 83 deletions

View File

@ -16,7 +16,6 @@ CREATE TABLE "account" (
--> statement-breakpoint
CREATE TABLE "payment" (
"id" text PRIMARY KEY NOT NULL,
"plan_id" text NOT NULL,
"price_id" text NOT NULL,
"type" text NOT NULL,
"interval" text,

View File

@ -1,5 +1,5 @@
{
"id": "4e932ffc-faf7-4223-b04c-382bf773e626",
"id": "7ecbd97a-94eb-4a46-996e-dbff727fc0c7",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
@ -119,12 +119,6 @@
"primaryKey": true,
"notNull": true
},
"plan_id": {
"name": "plan_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"price_id": {
"name": "price_id",
"type": "text",

View File

@ -5,8 +5,8 @@
{
"idx": 0,
"version": "7",
"when": 1744167230793,
"tag": "0000_public_mongoose",
"when": 1744304844165,
"tag": "0000_fine_sir_ram",
"breakpoints": true
}
]

View File

@ -3,7 +3,7 @@
import db from "@/db";
import { payment } from "@/db/schema";
import { getSession } from "@/lib/server";
import { getAllPricePlans } from "@/lib/price-plan";
import { getAllPricePlans, findPlanByPriceId } from "@/lib/price-plan";
import { PaymentTypes } from "@/payment/types";
import { and, eq } from "drizzle-orm";
import { createSafeActionClient } from 'next-safe-action';
@ -66,7 +66,7 @@ export const getLifetimeStatusAction = actionClient
// Query the database for one-time payments with lifetime plans
const result = await db
.select({ id: payment.id, planId: payment.planId, type: payment.type })
.select({ id: payment.id, priceId: payment.priceId, type: payment.type })
.from(payment)
.where(
and(
@ -77,9 +77,10 @@ export const getLifetimeStatusAction = actionClient
);
// Check if any payment has a lifetime plan
const hasLifetimePayment = result.some(paymentRecord =>
lifetimePlanIds.includes(paymentRecord.planId)
);
const hasLifetimePayment = result.some(paymentRecord => {
const plan = findPlanByPriceId(paymentRecord.priceId);
return plan && lifetimePlanIds.includes(plan.id);
});
return {
success: true,

View File

@ -54,7 +54,6 @@ export const verification = pgTable("verification", {
export const payment = pgTable("payment", {
id: text("id").primaryKey(),
planId: text('plan_id').notNull(),
priceId: text('price_id').notNull(),
type: text('type').notNull(),
interval: text('interval'),

View File

@ -1,5 +1,5 @@
import db from '@/db';
import { payment, user } from '@/db/schema';
import { payment, session, user } from '@/db/schema';
import { findPlanByPriceId, findPriceInPlan, findPlanByPlanId } from '@/lib/price-plan';
import { randomUUID } from 'crypto';
import { desc, eq } from 'drizzle-orm';
@ -145,11 +145,6 @@ export class StripeProvider implements PaymentProvider {
throw new Error(`Plan with ID ${planId} not found`);
}
// Free plan doesn't need a checkout session
if (plan.isFree) {
throw new Error('Cannot create checkout session for free plan');
}
// Find price in plan
const price = findPriceInPlan(planId, priceId);
if (!price) {
@ -270,9 +265,8 @@ export class StripeProvider implements PaymentProvider {
return subscriptions.map(subscription => ({
id: subscription.subscriptionId || '',
customerId: subscription.customerId,
status: subscription.status as PaymentStatus,
planId: subscription.planId,
priceId: subscription.priceId,
status: subscription.status as PaymentStatus,
type: subscription.type as PaymentTypes,
interval: subscription.interval as PlanInterval,
currentPeriodStart: subscription.periodStart || undefined,
@ -355,38 +349,16 @@ export class StripeProvider implements PaymentProvider {
return;
}
// get userId from metadata or find it by customerId from database
let userId = stripeSubscription.metadata.userId;
// get userId from metadata, we add it in the createCheckout session
const userId = stripeSubscription.metadata.userId;
if (!userId) {
const foundUserId = await this.findUserIdByCustomerId(customerId);
if (!foundUserId) {
console.warn(`<< No user found for customer ${customerId}, skipping payment record creation`);
return;
}
userId = foundUserId;
console.log(`Found userId ${userId} for customer ${customerId} from database`);
} else {
console.log(`Using userId ${userId} from subscription metadata`);
console.warn(`<< No userId found for subscription ${stripeSubscription.id}`);
return;
}
// get planId from metadata or find it by priceId from payment config
let planId = stripeSubscription.metadata.planId;
if (!planId) {
const foundPlan = findPlanByPriceId(priceId);
if (!foundPlan) {
console.warn(`<< No plan found for price ${priceId}, skipping payment record creation`);
return;
}
planId = foundPlan.id;
console.log(`Found planId ${planId} for price ${priceId} from config`);
} else {
console.log(`Using planId ${planId} from subscription metadata`);
}
// prepare create fields
// create fields
const createFields: any = {
id: randomUUID(),
planId: planId,
priceId: priceId,
type: PaymentTypes.SUBSCRIPTION,
userId: userId,
@ -432,21 +404,7 @@ export class StripeProvider implements PaymentProvider {
return;
}
// we can not trust the planId from metadata when updating subscription, so get it from config
// why? because user may update subscription in Stripe Customer Portal, and the planId is not in metadata
let planId;
let shouldUpdatePlanId = false;
const foundPlan = findPlanByPriceId(priceId);
if (!foundPlan) {
shouldUpdatePlanId = false;
console.warn(`No plan found for price ${priceId}, did you update the plans and prices in payment config?`);
} else {
planId = foundPlan.id;
shouldUpdatePlanId = true;
console.log(`Found planId ${planId} for price ${priceId} from config`);
}
// prepare update fields
// update fields
const updateFields: any = {
priceId: priceId,
interval: this.mapStripeIntervalToPlanInterval(stripeSubscription),
@ -463,12 +421,6 @@ export class StripeProvider implements PaymentProvider {
updatedAt: new Date()
};
// Only include planId if it should be updated
if (shouldUpdatePlanId && planId) {
updateFields.planId = planId;
}
console.log('updateFields', updateFields);
const result = await db
.update(payment)
.set(updateFields)
@ -527,20 +479,12 @@ export class StripeProvider implements PaymentProvider {
return;
}
// get planId from session metadata, we add it in the createCheckout session
const planId = session.metadata?.planId;
if (!planId) {
console.warn(`<< No planId found for checkout session ${session.id}`);
return;
}
// Create a one-time payment record
const now = new Date();
const result = await db
.insert(payment)
.values({
id: randomUUID(),
planId: planId,
priceId: priceId,
type: PaymentTypes.ONE_TIME,
userId: userId,
@ -556,7 +500,7 @@ export class StripeProvider implements PaymentProvider {
console.warn(`<< Failed to create one-time payment record for user ${userId}`);
return;
} else {
console.log(`<< Created one-time payment record for user ${userId}, plan: ${planId}`);
console.log(`<< Created one-time payment record for user ${userId}, price: ${priceId}`);
}
}

View File

@ -96,7 +96,6 @@ export interface Subscription {
id: string;
customerId: string;
status: PaymentStatus;
planId: string;
priceId: string;
type: PaymentType;
interval?: PlanInterval;

View File

@ -102,7 +102,8 @@ export const usePaymentStore = create<PaymentState>((set, get) => ({
// Set subscription state
if (activeSubscription) {
const plan = plans.find(p => p.id === activeSubscription.planId) || null;
const plan = plans.find(p => p.prices.find(price =>
price.priceId === activeSubscription.priceId)) || null;
console.log('subscription found, setting plan for user', user.id, plan?.id);
set({
currentPlan: plan,