feat: add subscription management features and database schema updates
- Introduced new database tables for managing subscriptions and user subscriptions. - Updated package.json with new database commands for generation, migration, and studio. - Refactored the user schema to remove deprecated fields and added foreign key relationships. - Enhanced Stripe payment provider logic to handle subscription creation, updates, and cancellations. - Improved error handling and logging for subscription events.
This commit is contained in:
parent
e9c9d9f451
commit
2248cfdc35
27
drizzle/0009_uneven_morgan_stark.sql
Normal file
27
drizzle/0009_uneven_morgan_stark.sql
Normal file
@ -0,0 +1,27 @@
|
||||
CREATE TABLE "subscription" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"plan_id" text NOT NULL,
|
||||
"price_id" text NOT NULL,
|
||||
"reference_id" text NOT NULL,
|
||||
"customer_id" text,
|
||||
"subscription_id" text,
|
||||
"status" text NOT NULL,
|
||||
"period_start" timestamp,
|
||||
"period_end" timestamp,
|
||||
"cancel_at_period_end" boolean,
|
||||
"trial_start" timestamp,
|
||||
"trial_end" timestamp,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "user_subscriptions" (
|
||||
"user_id" text NOT NULL,
|
||||
"subscription_id" text NOT NULL,
|
||||
CONSTRAINT "user_subscriptions_user_id_subscription_id_pk" PRIMARY KEY("user_id","subscription_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "user_subscriptions" ADD CONSTRAINT "user_subscriptions_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "user_subscriptions" ADD CONSTRAINT "user_subscriptions_subscription_id_subscription_id_fk" FOREIGN KEY ("subscription_id") REFERENCES "public"."subscription"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "user" DROP COLUMN "subscription_id";--> statement-breakpoint
|
||||
ALTER TABLE "user" DROP COLUMN "subscription_status";
|
5
drizzle/0010_cooing_millenium_guard.sql
Normal file
5
drizzle/0010_cooing_millenium_guard.sql
Normal file
@ -0,0 +1,5 @@
|
||||
ALTER TABLE "user_subscriptions" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
DROP TABLE "user_subscriptions" CASCADE;--> statement-breakpoint
|
||||
ALTER TABLE "subscription" ADD COLUMN "user_id" text NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "subscription" ADD CONSTRAINT "subscription_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "subscription" DROP COLUMN "reference_id";
|
5
drizzle/0010_futuristic_odin.sql
Normal file
5
drizzle/0010_futuristic_odin.sql
Normal file
@ -0,0 +1,5 @@
|
||||
ALTER TABLE "user_subscriptions" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
DROP TABLE "user_subscriptions" CASCADE;--> statement-breakpoint
|
||||
ALTER TABLE "subscription" ADD COLUMN "user_id" text NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "subscription" ADD CONSTRAINT "subscription_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "subscription" DROP COLUMN "reference_id";
|
533
drizzle/meta/0009_snapshot.json
Normal file
533
drizzle/meta/0009_snapshot.json
Normal file
@ -0,0 +1,533 @@
|
||||
{
|
||||
"id": "0c38a6eb-f695-430a-8d70-c60a2cbb745a",
|
||||
"prevId": "4542a435-02d6-4849-9970-c11721171a00",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"impersonated_by": {
|
||||
"name": "impersonated_by",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.subscription": {
|
||||
"name": "subscription",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"plan_id": {
|
||||
"name": "plan_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"price_id": {
|
||||
"name": "price_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"reference_id": {
|
||||
"name": "reference_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"subscription_id": {
|
||||
"name": "subscription_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"period_start": {
|
||||
"name": "period_start",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"period_end": {
|
||||
"name": "period_end",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cancel_at_period_end": {
|
||||
"name": "cancel_at_period_end",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"trial_start": {
|
||||
"name": "trial_start",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"trial_end": {
|
||||
"name": "trial_end",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"banned": {
|
||||
"name": "banned",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"ban_reason": {
|
||||
"name": "ban_reason",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"ban_expires": {
|
||||
"name": "ban_expires",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"lifetime_member": {
|
||||
"name": "lifetime_member",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"user_username_unique": {
|
||||
"name": "user_username_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"username"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user_subscriptions": {
|
||||
"name": "user_subscriptions",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"subscription_id": {
|
||||
"name": "subscription_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"user_subscriptions_user_id_user_id_fk": {
|
||||
"name": "user_subscriptions_user_id_user_id_fk",
|
||||
"tableFrom": "user_subscriptions",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"user_subscriptions_subscription_id_subscription_id_fk": {
|
||||
"name": "user_subscriptions_subscription_id_subscription_id_fk",
|
||||
"tableFrom": "user_subscriptions",
|
||||
"tableTo": "subscription",
|
||||
"columnsFrom": [
|
||||
"subscription_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"user_subscriptions_user_id_subscription_id_pk": {
|
||||
"name": "user_subscriptions_user_id_subscription_id_pk",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"subscription_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.verification": {
|
||||
"name": "verification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
487
drizzle/meta/0010_snapshot.json
Normal file
487
drizzle/meta/0010_snapshot.json
Normal file
@ -0,0 +1,487 @@
|
||||
{
|
||||
"id": "92ae96f1-74f5-4253-b5c4-d287b7de7d9e",
|
||||
"prevId": "0c38a6eb-f695-430a-8d70-c60a2cbb745a",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"impersonated_by": {
|
||||
"name": "impersonated_by",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.subscription": {
|
||||
"name": "subscription",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"plan_id": {
|
||||
"name": "plan_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"price_id": {
|
||||
"name": "price_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"subscription_id": {
|
||||
"name": "subscription_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"period_start": {
|
||||
"name": "period_start",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"period_end": {
|
||||
"name": "period_end",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cancel_at_period_end": {
|
||||
"name": "cancel_at_period_end",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"trial_start": {
|
||||
"name": "trial_start",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"trial_end": {
|
||||
"name": "trial_end",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"subscription_user_id_user_id_fk": {
|
||||
"name": "subscription_user_id_user_id_fk",
|
||||
"tableFrom": "subscription",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"banned": {
|
||||
"name": "banned",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"ban_reason": {
|
||||
"name": "ban_reason",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"ban_expires": {
|
||||
"name": "ban_expires",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"lifetime_member": {
|
||||
"name": "lifetime_member",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"user_username_unique": {
|
||||
"name": "user_username_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"username"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.verification": {
|
||||
"name": "verification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
@ -64,6 +64,20 @@
|
||||
"when": 1743743427477,
|
||||
"tag": "0008_curious_pepper_potts",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "7",
|
||||
"when": 1743905967329,
|
||||
"tag": "0009_uneven_morgan_stark",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 10,
|
||||
"version": "7",
|
||||
"when": 1743906787012,
|
||||
"tag": "0010_cooing_millenium_guard",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
@ -7,6 +7,9 @@
|
||||
"build": "content-collections build && next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"docs": "content-collections build",
|
||||
"email": "email dev --dir src/mail/emails --port 3333"
|
||||
},
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { pgTable, text, integer, timestamp, boolean } from "drizzle-orm/pg-core";
|
||||
import { pgTable, text, integer, timestamp, boolean, primaryKey, foreignKey } from "drizzle-orm/pg-core";
|
||||
|
||||
export const user = pgTable("user", {
|
||||
id: text("id").primaryKey(),
|
||||
@ -15,8 +15,6 @@ export const user = pgTable("user", {
|
||||
banExpires: timestamp('ban_expires'),
|
||||
customerId: text('customer_id'),
|
||||
lifetimeMember: boolean('lifetime_member'),
|
||||
subscriptionId: text('subscription_id'),
|
||||
subscriptionStatus: text('subscription_status'),
|
||||
});
|
||||
|
||||
export const session = pgTable("session", {
|
||||
@ -55,3 +53,20 @@ export const verification = pgTable("verification", {
|
||||
createdAt: timestamp('created_at'),
|
||||
updatedAt: timestamp('updated_at')
|
||||
});
|
||||
|
||||
export const subscription = pgTable("subscription", {
|
||||
id: text("id").primaryKey(),
|
||||
planId: text('plan_id').notNull(),
|
||||
priceId: text('price_id').notNull(),
|
||||
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
|
||||
customerId: text('customer_id'),
|
||||
subscriptionId: text('subscription_id'),
|
||||
status: text('status').notNull(),
|
||||
periodStart: timestamp('period_start'),
|
||||
periodEnd: timestamp('period_end'),
|
||||
cancelAtPeriodEnd: boolean('cancel_at_period_end'),
|
||||
trialStart: timestamp('trial_start'),
|
||||
trialEnd: timestamp('trial_end'),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
});
|
||||
|
@ -123,18 +123,6 @@ export const auth = betterAuth({
|
||||
defaultValue: false,
|
||||
input: false, // don't allow user to set lifetimeMember
|
||||
},
|
||||
subscriptionId: {
|
||||
type: "string",
|
||||
required: false,
|
||||
defaultValue: "",
|
||||
input: false, // don't allow user to set subscriptionId
|
||||
},
|
||||
subscriptionStatus: {
|
||||
type: "string",
|
||||
required: false,
|
||||
defaultValue: "",
|
||||
input: false, // don't allow user to set subscriptionStatus
|
||||
},
|
||||
},
|
||||
// https://www.better-auth.com/docs/concepts/users-accounts#delete-user
|
||||
deleteUser: {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import db from '@/db/index';
|
||||
import { user } from '@/db/schema';
|
||||
import { subscription as subscriptionTable, user } from '@/db/schema';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import Stripe from 'stripe';
|
||||
import { findPriceInPlan, getPlanById } from '../index';
|
||||
@ -176,7 +177,7 @@ export class StripeProvider implements PaymentProvider {
|
||||
...metadata,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
// Add trial period if applicable
|
||||
if (price.trialPeriodDays && price.trialPeriodDays > 0) {
|
||||
checkoutParams.subscription_data.trial_period_days = price.trialPeriodDays;
|
||||
@ -341,7 +342,6 @@ export class StripeProvider implements PaymentProvider {
|
||||
? new Date(subscription.trial_end * 1000)
|
||||
: undefined,
|
||||
createdAt: new Date(subscription.created * 1000),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
@ -382,64 +382,88 @@ export class StripeProvider implements PaymentProvider {
|
||||
|
||||
// Handle subscription events
|
||||
if (eventType.startsWith('customer.subscription.')) {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
console.log(`Processing subscription ${subscription.id}, status: ${subscription.status}`);
|
||||
|
||||
const stripeSubscription = event.data.object as Stripe.Subscription;
|
||||
// Get customerId from subscription
|
||||
const customerId = subscription.customer as string;
|
||||
const customerId = stripeSubscription.customer as string;
|
||||
console.log(`Processing subscription ${stripeSubscription.id} for user ${customerId}, status: ${stripeSubscription.status}`);
|
||||
|
||||
// Extract information from metadata, then we do not need to query the database for userId
|
||||
const userId = stripeSubscription.metadata.userId || 'unknown';
|
||||
const planId = stripeSubscription.metadata.planId || 'unknown';
|
||||
const priceId = stripeSubscription.metadata.priceId || stripeSubscription.items.data[0]?.price.id || 'unknown';
|
||||
|
||||
// Process based on subscription status and event type
|
||||
switch (eventType) {
|
||||
case 'customer.subscription.created': {
|
||||
// New subscription created - update user record with subscription ID and status
|
||||
const result = await db
|
||||
.update(user)
|
||||
.set({
|
||||
subscriptionId: subscription.id,
|
||||
subscriptionStatus: subscription.status,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(user.customerId, customerId))
|
||||
.returning({ id: user.id });
|
||||
// Create new subscription record with a unique ID
|
||||
const newSubscriptionId = randomUUID();
|
||||
const result = await db.insert(subscriptionTable).values({
|
||||
id: newSubscriptionId,
|
||||
planId: planId,
|
||||
priceId: priceId,
|
||||
userId: userId,
|
||||
customerId: customerId,
|
||||
subscriptionId: stripeSubscription.id,
|
||||
status: this.mapSubscriptionStatus(stripeSubscription.status),
|
||||
periodStart: stripeSubscription.current_period_start ? new Date(stripeSubscription.current_period_start * 1000) : null,
|
||||
periodEnd: stripeSubscription.current_period_end ? new Date(stripeSubscription.current_period_end * 1000) : null,
|
||||
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
|
||||
trialStart: stripeSubscription.trial_start ? new Date(stripeSubscription.trial_start * 1000) : null,
|
||||
trialEnd: stripeSubscription.trial_end ? new Date(stripeSubscription.trial_end * 1000) : null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}).returning({ id: subscriptionTable.id });
|
||||
|
||||
if (result.length > 0) {
|
||||
console.log(`Updated user ${customerId} with subscription ${subscription.id}`);
|
||||
console.log(`Created new subscription ${newSubscriptionId} for Stripe subscription ${stripeSubscription.id}`);
|
||||
} else {
|
||||
console.warn(`Update operation performed but no rows were updated for customerId ${customerId}`);
|
||||
console.warn(`No subscription created for Stripe subscription ${stripeSubscription.id}`);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'customer.subscription.updated': {
|
||||
// Subscription was updated - update status
|
||||
await db
|
||||
.update(user)
|
||||
// Update subscription record
|
||||
const result = await db
|
||||
.update(subscriptionTable)
|
||||
.set({
|
||||
subscriptionStatus: subscription.status,
|
||||
status: this.mapSubscriptionStatus(stripeSubscription.status),
|
||||
periodStart: stripeSubscription.current_period_start ? new Date(stripeSubscription.current_period_start * 1000) : undefined,
|
||||
periodEnd: stripeSubscription.current_period_end ? new Date(stripeSubscription.current_period_end * 1000) : undefined,
|
||||
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
|
||||
trialEnd: stripeSubscription.trial_end ? new Date(stripeSubscription.trial_end * 1000) : undefined,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(user.customerId, customerId) && eq(user.subscriptionId, subscription.id));
|
||||
.where(eq(subscriptionTable.subscriptionId, stripeSubscription.id))
|
||||
.returning({ id: subscriptionTable.id });
|
||||
|
||||
console.log(`Updated subscription status for user ${customerId} to ${subscription.status}`);
|
||||
if (result.length > 0) {
|
||||
console.log(`Updated subscription ${result[0].id} for Stripe subscription ${stripeSubscription.id}`);
|
||||
} else {
|
||||
console.warn(`No subscription found for Stripe subscription ${stripeSubscription.id}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'customer.subscription.deleted': {
|
||||
// Subscription was cancelled/deleted - remove from user
|
||||
await db
|
||||
.update(user)
|
||||
// Update subscription record, set status to canceled
|
||||
const result = await db
|
||||
.update(subscriptionTable)
|
||||
.set({
|
||||
subscriptionId: null,
|
||||
subscriptionStatus: 'canceled',
|
||||
status: this.mapSubscriptionStatus(stripeSubscription.status),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(user.customerId, customerId) && eq(user.subscriptionId, subscription.id));
|
||||
.where(eq(subscriptionTable.subscriptionId, stripeSubscription.id))
|
||||
.returning({ id: subscriptionTable.id });
|
||||
|
||||
console.log(`Removed subscription from user ${customerId}`);
|
||||
if (result.length > 0) {
|
||||
console.log(`Marked subscription ${stripeSubscription.id} as canceled`);
|
||||
} else {
|
||||
console.warn(`No subscription found to cancel for Stripe subscription ${stripeSubscription.id}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'customer.subscription.trial_will_end': {
|
||||
// Trial ending soon - we could trigger an email notification here
|
||||
console.log(`Trial ending soon for subscription ${subscription.id}, customerId ${customerId}`);
|
||||
// Just log for now, could send notifications or update the subscription record
|
||||
console.log(`Trial ending soon for subscription ${stripeSubscription.id}, customerId ${customerId}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -460,22 +484,28 @@ export class StripeProvider implements PaymentProvider {
|
||||
|
||||
// Safely handle the case where planId might not exist
|
||||
if (!planId) {
|
||||
console.log(`No planId found in metadata for checkout session ${session.id}, customerId: ${customerId}`);
|
||||
console.log(`No planId found for checkout session ${session.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const plan = getPlanById(planId);
|
||||
if (plan && plan.isLifetime) {
|
||||
// Mark user as lifetime member
|
||||
await db
|
||||
// Mark user as lifetime member by customerId
|
||||
const result = await db
|
||||
.update(user)
|
||||
.set({
|
||||
lifetimeMember: true,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(user.customerId, customerId));
|
||||
.where(eq(user.customerId, customerId))
|
||||
.returning({ id: user.id });
|
||||
|
||||
console.log(`Marked user ${customerId} as lifetime member`);
|
||||
if (result.length === 0) {
|
||||
console.warn(`No user ${customerId} marked as lifetime member`);
|
||||
return;
|
||||
} else {
|
||||
console.log(`Marked user ${customerId} as lifetime member`);
|
||||
}
|
||||
} else {
|
||||
// Handle other one-time payments if needed, like increase user credits
|
||||
console.log(`One-time payment for non-lifetime plan: ${planId}, customerId: ${customerId}`);
|
||||
@ -521,6 +551,7 @@ export class StripeProvider implements PaymentProvider {
|
||||
|
||||
/**
|
||||
* Convert Stripe subscription status to PaymentStatus
|
||||
* We narrow down the status to our own status types
|
||||
* @param status Stripe subscription status
|
||||
* @returns PaymentStatus
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user