init 🎉
This commit is contained in:
parent
254d2bc5d5
commit
103caf9d3d
@ -15,7 +15,13 @@
|
||||
"Bash(pnpm db:generate:*)",
|
||||
"Bash(DATABASE_URL=\"postgresql://fedjens@localhost:5432/eyrun_test\" JWT_SECRET=\"test-secret-that-is-at-least-32-characters-long\" pnpm db:migrate:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(curl:*)"
|
||||
"Bash(curl:*)",
|
||||
"WebFetch(domain:polar.sh)",
|
||||
"Bash(printf:*)",
|
||||
"Bash(kill:*)",
|
||||
"Bash(node -e:*)",
|
||||
"Bash(timeout 5 pnpm dev:*)",
|
||||
"Bash(gtimeout:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,4 +2,6 @@ DATABASE_URL=postgresql://user:password@localhost:5432/eyrun
|
||||
JWT_SECRET=change-me-to-a-random-string-at-least-32-chars
|
||||
PORT=3000
|
||||
HOST=0.0.0.0
|
||||
RESEND_API_KEY=your-resend-api-key-here
|
||||
RESEND_API_KEY=your-resend-api-key-here
|
||||
POLAR_ACCESS_TOKEN=
|
||||
POLAR_WEBHOOK_SECRET=
|
||||
|
||||
39
drizzle/0007_married_invaders.sql
Normal file
39
drizzle/0007_married_invaders.sql
Normal file
@ -0,0 +1,39 @@
|
||||
CREATE TYPE "public"."checkout_status" AS ENUM('pending', 'completed', 'expired');--> statement-breakpoint
|
||||
CREATE TYPE "public"."subscription_status" AS ENUM('active', 'canceled', 'revoked', 'past_due');--> statement-breakpoint
|
||||
CREATE TABLE "checkouts" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"account_id" uuid NOT NULL,
|
||||
"project_id" uuid NOT NULL,
|
||||
"polar_checkout_id" text NOT NULL,
|
||||
"wm_name" text NOT NULL,
|
||||
"package_id" uuid NOT NULL,
|
||||
"vcpu" real NOT NULL,
|
||||
"ram" integer NOT NULL,
|
||||
"disk" integer NOT NULL,
|
||||
"status" "checkout_status" DEFAULT 'pending' NOT NULL,
|
||||
"wm_id" uuid,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "checkouts_polar_checkout_id_unique" UNIQUE("polar_checkout_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "subscriptions" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"account_id" uuid NOT NULL,
|
||||
"wm_id" uuid,
|
||||
"polar_subscription_id" text NOT NULL,
|
||||
"status" "subscription_status" DEFAULT 'active' NOT NULL,
|
||||
"current_period_start" timestamp with time zone,
|
||||
"current_period_end" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "subscriptions_polar_subscription_id_unique" UNIQUE("polar_subscription_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "accounts" ADD COLUMN "polar_customer_id" text;--> statement-breakpoint
|
||||
ALTER TABLE "wms" ADD COLUMN "polar_subscription_id" text;--> statement-breakpoint
|
||||
ALTER TABLE "checkouts" ADD CONSTRAINT "checkouts_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "checkouts" ADD CONSTRAINT "checkouts_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "checkouts" ADD CONSTRAINT "checkouts_package_id_packages_id_fk" FOREIGN KEY ("package_id") REFERENCES "public"."packages"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "checkouts" ADD CONSTRAINT "checkouts_wm_id_wms_id_fk" FOREIGN KEY ("wm_id") REFERENCES "public"."wms"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_wm_id_wms_id_fk" FOREIGN KEY ("wm_id") REFERENCES "public"."wms"("id") ON DELETE no action ON UPDATE no action;
|
||||
27
drizzle/0008_seat_based_billing.sql
Normal file
27
drizzle/0008_seat_based_billing.sql
Normal file
@ -0,0 +1,27 @@
|
||||
-- Subscriptions: add package_id (nullable first for backfill), add seats, drop wm_id
|
||||
ALTER TABLE "subscriptions" ADD COLUMN "package_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "subscriptions" ADD COLUMN "seats" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
|
||||
|
||||
-- Backfill package_id from wms via wm_id join
|
||||
UPDATE "subscriptions" s
|
||||
SET "package_id" = w."package_id"
|
||||
FROM "wms" w
|
||||
WHERE s."wm_id" = w."id" AND w."package_id" IS NOT NULL;--> statement-breakpoint
|
||||
|
||||
-- Make package_id NOT NULL and add FK
|
||||
ALTER TABLE "subscriptions" ALTER COLUMN "package_id" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_package_id_packages_id_fk" FOREIGN KEY ("package_id") REFERENCES "public"."packages"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
|
||||
-- Drop wm_id FK and column from subscriptions
|
||||
ALTER TABLE "subscriptions" DROP CONSTRAINT IF EXISTS "subscriptions_wm_id_wms_id_fk";--> statement-breakpoint
|
||||
ALTER TABLE "subscriptions" DROP COLUMN IF EXISTS "wm_id";--> statement-breakpoint
|
||||
|
||||
-- Checkouts: drop columns no longer needed
|
||||
ALTER TABLE "checkouts" DROP CONSTRAINT IF EXISTS "checkouts_project_id_projects_id_fk";--> statement-breakpoint
|
||||
ALTER TABLE "checkouts" DROP CONSTRAINT IF EXISTS "checkouts_wm_id_wms_id_fk";--> statement-breakpoint
|
||||
ALTER TABLE "checkouts" DROP COLUMN IF EXISTS "project_id";--> statement-breakpoint
|
||||
ALTER TABLE "checkouts" DROP COLUMN IF EXISTS "wm_name";--> statement-breakpoint
|
||||
ALTER TABLE "checkouts" DROP COLUMN IF EXISTS "vcpu";--> statement-breakpoint
|
||||
ALTER TABLE "checkouts" DROP COLUMN IF EXISTS "ram";--> statement-breakpoint
|
||||
ALTER TABLE "checkouts" DROP COLUMN IF EXISTS "disk";--> statement-breakpoint
|
||||
ALTER TABLE "checkouts" DROP COLUMN IF EXISTS "wm_id";
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "08d293e6-8a0d-4abf-bbe5-600133a8d287",
|
||||
"prevId": "3fe6ec31-b399-41bd-88d8-b1b855d5b3ad",
|
||||
"id": "f51ba4f1-5d34-487c-9eea-2af8588007ed",
|
||||
"prevId": "08d293e6-8a0d-4abf-bbe5-600133a8d287",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
|
||||
860
drizzle/meta/0007_snapshot.json
Normal file
860
drizzle/meta/0007_snapshot.json
Normal file
@ -0,0 +1,860 @@
|
||||
{
|
||||
"id": "0370af60-4753-4a03-891e-4018d0a36fda",
|
||||
"prevId": "f51ba4f1-5d34-487c-9eea-2af8588007ed",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.accounts": {
|
||||
"name": "accounts",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"polar_customer_id": {
|
||||
"name": "polar_customer_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.checkouts": {
|
||||
"name": "checkouts",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"project_id": {
|
||||
"name": "project_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"polar_checkout_id": {
|
||||
"name": "polar_checkout_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"wm_name": {
|
||||
"name": "wm_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"package_id": {
|
||||
"name": "package_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"vcpu": {
|
||||
"name": "vcpu",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ram": {
|
||||
"name": "ram",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"disk": {
|
||||
"name": "disk",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "checkout_status",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"wm_id": {
|
||||
"name": "wm_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"checkouts_account_id_accounts_id_fk": {
|
||||
"name": "checkouts_account_id_accounts_id_fk",
|
||||
"tableFrom": "checkouts",
|
||||
"tableTo": "accounts",
|
||||
"columnsFrom": [
|
||||
"account_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"checkouts_project_id_projects_id_fk": {
|
||||
"name": "checkouts_project_id_projects_id_fk",
|
||||
"tableFrom": "checkouts",
|
||||
"tableTo": "projects",
|
||||
"columnsFrom": [
|
||||
"project_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"checkouts_package_id_packages_id_fk": {
|
||||
"name": "checkouts_package_id_packages_id_fk",
|
||||
"tableFrom": "checkouts",
|
||||
"tableTo": "packages",
|
||||
"columnsFrom": [
|
||||
"package_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"checkouts_wm_id_wms_id_fk": {
|
||||
"name": "checkouts_wm_id_wms_id_fk",
|
||||
"tableFrom": "checkouts",
|
||||
"tableTo": "wms",
|
||||
"columnsFrom": [
|
||||
"wm_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"checkouts_polar_checkout_id_unique": {
|
||||
"name": "checkouts_polar_checkout_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"polar_checkout_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.memberships": {
|
||||
"name": "memberships",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "membership_role",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"memberships_user_id_users_id_fk": {
|
||||
"name": "memberships_user_id_users_id_fk",
|
||||
"tableFrom": "memberships",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"memberships_account_id_accounts_id_fk": {
|
||||
"name": "memberships_account_id_accounts_id_fk",
|
||||
"tableFrom": "memberships",
|
||||
"tableTo": "accounts",
|
||||
"columnsFrom": [
|
||||
"account_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"memberships_user_id_account_id_unique": {
|
||||
"name": "memberships_user_id_account_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"user_id",
|
||||
"account_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.otp_codes": {
|
||||
"name": "otp_codes",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"code": {
|
||||
"name": "code",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"used": {
|
||||
"name": "used",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"attempts": {
|
||||
"name": "attempts",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.packages": {
|
||||
"name": "packages",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"vcpu": {
|
||||
"name": "vcpu",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ram": {
|
||||
"name": "ram",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"disk": {
|
||||
"name": "disk",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"price_monthly": {
|
||||
"name": "price_monthly",
|
||||
"type": "numeric(10, 2)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"packages_slug_unique": {
|
||||
"name": "packages_slug_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"slug"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.projects": {
|
||||
"name": "projects",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"projects_account_id_accounts_id_fk": {
|
||||
"name": "projects_account_id_accounts_id_fk",
|
||||
"tableFrom": "projects",
|
||||
"tableTo": "accounts",
|
||||
"columnsFrom": [
|
||||
"account_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.sessions": {
|
||||
"name": "sessions",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"revoked_at": {
|
||||
"name": "revoked_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sessions_user_id_users_id_fk": {
|
||||
"name": "sessions_user_id_users_id_fk",
|
||||
"tableFrom": "sessions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"sessions_refresh_token_unique": {
|
||||
"name": "sessions_refresh_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"refresh_token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.subscriptions": {
|
||||
"name": "subscriptions",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"wm_id": {
|
||||
"name": "wm_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"polar_subscription_id": {
|
||||
"name": "polar_subscription_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "subscription_status",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'active'"
|
||||
},
|
||||
"current_period_start": {
|
||||
"name": "current_period_start",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"current_period_end": {
|
||||
"name": "current_period_end",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"subscriptions_account_id_accounts_id_fk": {
|
||||
"name": "subscriptions_account_id_accounts_id_fk",
|
||||
"tableFrom": "subscriptions",
|
||||
"tableTo": "accounts",
|
||||
"columnsFrom": [
|
||||
"account_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"subscriptions_wm_id_wms_id_fk": {
|
||||
"name": "subscriptions_wm_id_wms_id_fk",
|
||||
"tableFrom": "subscriptions",
|
||||
"tableTo": "wms",
|
||||
"columnsFrom": [
|
||||
"wm_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"subscriptions_polar_subscription_id_unique": {
|
||||
"name": "subscriptions_polar_subscription_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"polar_subscription_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.wms": {
|
||||
"name": "wms",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"project_id": {
|
||||
"name": "project_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"package_id": {
|
||||
"name": "package_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"vcpu": {
|
||||
"name": "vcpu",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ram": {
|
||||
"name": "ram",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"disk": {
|
||||
"name": "disk",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"polar_subscription_id": {
|
||||
"name": "polar_subscription_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"wms_project_id_projects_id_fk": {
|
||||
"name": "wms_project_id_projects_id_fk",
|
||||
"tableFrom": "wms",
|
||||
"tableTo": "projects",
|
||||
"columnsFrom": [
|
||||
"project_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"wms_package_id_packages_id_fk": {
|
||||
"name": "wms_package_id_packages_id_fk",
|
||||
"tableFrom": "wms",
|
||||
"tableTo": "packages",
|
||||
"columnsFrom": [
|
||||
"package_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"public.checkout_status": {
|
||||
"name": "checkout_status",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"pending",
|
||||
"completed",
|
||||
"expired"
|
||||
]
|
||||
},
|
||||
"public.membership_role": {
|
||||
"name": "membership_role",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"owner",
|
||||
"admin",
|
||||
"member"
|
||||
]
|
||||
},
|
||||
"public.subscription_status": {
|
||||
"name": "subscription_status",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"active",
|
||||
"canceled",
|
||||
"revoked",
|
||||
"past_due"
|
||||
]
|
||||
}
|
||||
},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
805
drizzle/meta/0008_snapshot.json
Normal file
805
drizzle/meta/0008_snapshot.json
Normal file
@ -0,0 +1,805 @@
|
||||
{
|
||||
"id": "5fae9fd8-74d1-4bd4-b2de-01c2912c3dad",
|
||||
"prevId": "0370af60-4753-4a03-891e-4018d0a36fda",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.accounts": {
|
||||
"name": "accounts",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"polar_customer_id": {
|
||||
"name": "polar_customer_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.checkouts": {
|
||||
"name": "checkouts",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"polar_checkout_id": {
|
||||
"name": "polar_checkout_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"package_id": {
|
||||
"name": "package_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "checkout_status",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"checkouts_account_id_accounts_id_fk": {
|
||||
"name": "checkouts_account_id_accounts_id_fk",
|
||||
"tableFrom": "checkouts",
|
||||
"tableTo": "accounts",
|
||||
"columnsFrom": [
|
||||
"account_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"checkouts_package_id_packages_id_fk": {
|
||||
"name": "checkouts_package_id_packages_id_fk",
|
||||
"tableFrom": "checkouts",
|
||||
"tableTo": "packages",
|
||||
"columnsFrom": [
|
||||
"package_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"checkouts_polar_checkout_id_unique": {
|
||||
"name": "checkouts_polar_checkout_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"polar_checkout_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.memberships": {
|
||||
"name": "memberships",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "membership_role",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"memberships_user_id_users_id_fk": {
|
||||
"name": "memberships_user_id_users_id_fk",
|
||||
"tableFrom": "memberships",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"memberships_account_id_accounts_id_fk": {
|
||||
"name": "memberships_account_id_accounts_id_fk",
|
||||
"tableFrom": "memberships",
|
||||
"tableTo": "accounts",
|
||||
"columnsFrom": [
|
||||
"account_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"memberships_user_id_account_id_unique": {
|
||||
"name": "memberships_user_id_account_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"user_id",
|
||||
"account_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.otp_codes": {
|
||||
"name": "otp_codes",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"code": {
|
||||
"name": "code",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"used": {
|
||||
"name": "used",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"attempts": {
|
||||
"name": "attempts",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.packages": {
|
||||
"name": "packages",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"vcpu": {
|
||||
"name": "vcpu",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ram": {
|
||||
"name": "ram",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"disk": {
|
||||
"name": "disk",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"price_monthly": {
|
||||
"name": "price_monthly",
|
||||
"type": "numeric(10, 2)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"packages_slug_unique": {
|
||||
"name": "packages_slug_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"slug"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.projects": {
|
||||
"name": "projects",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"projects_account_id_accounts_id_fk": {
|
||||
"name": "projects_account_id_accounts_id_fk",
|
||||
"tableFrom": "projects",
|
||||
"tableTo": "accounts",
|
||||
"columnsFrom": [
|
||||
"account_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.sessions": {
|
||||
"name": "sessions",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"revoked_at": {
|
||||
"name": "revoked_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sessions_user_id_users_id_fk": {
|
||||
"name": "sessions_user_id_users_id_fk",
|
||||
"tableFrom": "sessions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"sessions_refresh_token_unique": {
|
||||
"name": "sessions_refresh_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"refresh_token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.subscriptions": {
|
||||
"name": "subscriptions",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"polar_subscription_id": {
|
||||
"name": "polar_subscription_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "subscription_status",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'active'"
|
||||
},
|
||||
"current_period_start": {
|
||||
"name": "current_period_start",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"current_period_end": {
|
||||
"name": "current_period_end",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"package_id": {
|
||||
"name": "package_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"seats": {
|
||||
"name": "seats",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"subscriptions_account_id_accounts_id_fk": {
|
||||
"name": "subscriptions_account_id_accounts_id_fk",
|
||||
"tableFrom": "subscriptions",
|
||||
"tableTo": "accounts",
|
||||
"columnsFrom": [
|
||||
"account_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"subscriptions_package_id_packages_id_fk": {
|
||||
"name": "subscriptions_package_id_packages_id_fk",
|
||||
"tableFrom": "subscriptions",
|
||||
"tableTo": "packages",
|
||||
"columnsFrom": [
|
||||
"package_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"subscriptions_polar_subscription_id_unique": {
|
||||
"name": "subscriptions_polar_subscription_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"polar_subscription_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.wms": {
|
||||
"name": "wms",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"project_id": {
|
||||
"name": "project_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"package_id": {
|
||||
"name": "package_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"vcpu": {
|
||||
"name": "vcpu",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ram": {
|
||||
"name": "ram",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"disk": {
|
||||
"name": "disk",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"polar_subscription_id": {
|
||||
"name": "polar_subscription_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"wms_project_id_projects_id_fk": {
|
||||
"name": "wms_project_id_projects_id_fk",
|
||||
"tableFrom": "wms",
|
||||
"tableTo": "projects",
|
||||
"columnsFrom": [
|
||||
"project_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"wms_package_id_packages_id_fk": {
|
||||
"name": "wms_package_id_packages_id_fk",
|
||||
"tableFrom": "wms",
|
||||
"tableTo": "packages",
|
||||
"columnsFrom": [
|
||||
"package_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"public.checkout_status": {
|
||||
"name": "checkout_status",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"pending",
|
||||
"completed",
|
||||
"expired"
|
||||
]
|
||||
},
|
||||
"public.membership_role": {
|
||||
"name": "membership_role",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"owner",
|
||||
"admin",
|
||||
"member"
|
||||
]
|
||||
},
|
||||
"public.subscription_status": {
|
||||
"name": "subscription_status",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"active",
|
||||
"canceled",
|
||||
"revoked",
|
||||
"past_due"
|
||||
]
|
||||
}
|
||||
},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@ -50,6 +50,20 @@
|
||||
"when": 1770547300000,
|
||||
"tag": "0006_seed_packages",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "7",
|
||||
"when": 1770565316469,
|
||||
"tag": "0007_married_invaders",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "7",
|
||||
"when": 1770652800000,
|
||||
"tag": "0008_seat_based_billing",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -19,6 +19,7 @@
|
||||
"packageManager": "pnpm@10.18.3",
|
||||
"dependencies": {
|
||||
"@fastify/swagger": "^9.7.0",
|
||||
"@polar-sh/sdk": "^0.42.5",
|
||||
"dotenv": "^17.2.4",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"fastify": "^5.7.4",
|
||||
|
||||
11
pnpm-lock.yaml
generated
11
pnpm-lock.yaml
generated
@ -11,6 +11,9 @@ importers:
|
||||
'@fastify/swagger':
|
||||
specifier: ^9.7.0
|
||||
version: 9.7.0
|
||||
'@polar-sh/sdk':
|
||||
specifier: ^0.42.5
|
||||
version: 0.42.5
|
||||
dotenv:
|
||||
specifier: ^17.2.4
|
||||
version: 17.2.4
|
||||
@ -573,6 +576,9 @@ packages:
|
||||
'@pinojs/redact@0.4.0':
|
||||
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
||||
|
||||
'@polar-sh/sdk@0.42.5':
|
||||
resolution: {integrity: sha512-GzC3/ElCtMO55+KeXwFTANlydZzw5qI3DU/F9vAFIsUKuegSmh+Xu03KCL+ct9/imJOvLUQucYhUSsNKqo2j2Q==}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.57.1':
|
||||
resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==}
|
||||
cpu: [arm]
|
||||
@ -1784,6 +1790,11 @@ snapshots:
|
||||
|
||||
'@pinojs/redact@0.4.0': {}
|
||||
|
||||
'@polar-sh/sdk@0.42.5':
|
||||
dependencies:
|
||||
standardwebhooks: 1.0.0
|
||||
zod: 4.3.6
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.57.1':
|
||||
optional: true
|
||||
|
||||
|
||||
@ -8,6 +8,9 @@ const envSchema = z.object({
|
||||
HOST: z.string().default("0.0.0.0"),
|
||||
API_URL: z.url().optional(),
|
||||
RESEND_API_KEY: z.string().min(1),
|
||||
POLAR_ACCESS_TOKEN: z.string().min(1).optional(),
|
||||
POLAR_WEBHOOK_SECRET: z.string().min(1).optional(),
|
||||
POLAR_SERVER: z.enum(["sandbox", "production"]).default("sandbox"),
|
||||
});
|
||||
|
||||
const parsed = envSchema.safeParse(process.env);
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import { boolean, integer, numeric, pgEnum, pgTable, real, text, timestamp, unique, uuid } from "drizzle-orm/pg-core";
|
||||
import { boolean, integer, pgEnum, pgTable, real, text, timestamp, unique, uuid } from "drizzle-orm/pg-core";
|
||||
|
||||
export const checkoutStatusEnum = pgEnum("checkout_status", ["pending", "completed", "expired"]);
|
||||
export const subscriptionStatusEnum = pgEnum("subscription_status", ["active", "canceled", "revoked", "past_due"]);
|
||||
|
||||
export const membershipRoleEnum = pgEnum("membership_role", ["owner", "admin", "member"]);
|
||||
|
||||
@ -23,6 +26,7 @@ export const otpCodes = pgTable("otp_codes", {
|
||||
export const accounts = pgTable("accounts", {
|
||||
id: uuid().defaultRandom().primaryKey(),
|
||||
name: text().notNull(),
|
||||
polarCustomerId: text("polar_customer_id"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
@ -53,28 +57,43 @@ export const projects = pgTable("projects", {
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const packages = pgTable("packages", {
|
||||
id: uuid().defaultRandom().primaryKey(),
|
||||
slug: text().notNull().unique(),
|
||||
name: text().notNull(),
|
||||
vcpu: real().notNull(),
|
||||
ram: integer().notNull(),
|
||||
disk: integer().notNull(),
|
||||
priceMonthly: numeric("price_monthly", { precision: 10, scale: 2 }).notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const wms = pgTable("wms", {
|
||||
id: uuid().defaultRandom().primaryKey(),
|
||||
projectId: uuid("project_id")
|
||||
.notNull()
|
||||
.references(() => projects.id),
|
||||
name: text().notNull(),
|
||||
packageId: uuid("package_id").references(() => packages.id),
|
||||
polarProductId: text("polar_product_id"),
|
||||
vcpu: real().notNull(),
|
||||
ram: integer().notNull(),
|
||||
disk: integer().notNull(),
|
||||
polarSubscriptionId: text("polar_subscription_id"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const checkouts = pgTable("checkouts", {
|
||||
id: uuid().defaultRandom().primaryKey(),
|
||||
accountId: uuid("account_id")
|
||||
.notNull()
|
||||
.references(() => accounts.id),
|
||||
polarCheckoutId: text("polar_checkout_id").notNull().unique(),
|
||||
polarProductId: text("polar_product_id").notNull(),
|
||||
status: checkoutStatusEnum().notNull().default("pending"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const subscriptions = pgTable("subscriptions", {
|
||||
id: uuid().defaultRandom().primaryKey(),
|
||||
accountId: uuid("account_id")
|
||||
.notNull()
|
||||
.references(() => accounts.id),
|
||||
polarProductId: text("polar_product_id").notNull(),
|
||||
seats: integer().notNull().default(1),
|
||||
polarSubscriptionId: text("polar_subscription_id").notNull().unique(),
|
||||
status: subscriptionStatusEnum().notNull().default("active"),
|
||||
currentPeriodStart: timestamp("current_period_start", { withTimezone: true }),
|
||||
currentPeriodEnd: timestamp("current_period_end", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
141
src/lib/polar.ts
Normal file
141
src/lib/polar.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import { Polar } from "@polar-sh/sdk";
|
||||
import { validateEvent, WebhookVerificationError } from "@polar-sh/sdk/webhooks";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { config } from "../config.js";
|
||||
import { db } from "../db/index.js";
|
||||
import { accounts, checkouts, subscriptions } from "../db/schema.js";
|
||||
|
||||
export const polar = new Polar({
|
||||
accessToken: config.POLAR_ACCESS_TOKEN,
|
||||
server: config.POLAR_SERVER,
|
||||
});
|
||||
|
||||
export { WebhookVerificationError };
|
||||
|
||||
export function parseWebhookEvent(body: string | Buffer, headers: Record<string, string>) {
|
||||
if (!config.POLAR_WEBHOOK_SECRET) {
|
||||
throw new Error("POLAR_WEBHOOK_SECRET is not configured");
|
||||
}
|
||||
return validateEvent(body, headers, config.POLAR_WEBHOOK_SECRET);
|
||||
}
|
||||
|
||||
export async function handleWebhookEvent(event: { type: string; data: Record<string, unknown> }) {
|
||||
switch (event.type) {
|
||||
case "checkout.updated":
|
||||
await handleCheckoutUpdated(event.data);
|
||||
break;
|
||||
case "subscription.updated":
|
||||
await handleSubscriptionUpdated(event.data);
|
||||
break;
|
||||
case "subscription.canceled":
|
||||
await handleSubscriptionCanceled(event.data);
|
||||
break;
|
||||
case "subscription.revoked":
|
||||
await handleSubscriptionRevoked(event.data);
|
||||
break;
|
||||
case "customer.created":
|
||||
await handleCustomerCreated(event.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCheckoutUpdated(data: Record<string, unknown>) {
|
||||
if (data.status !== "succeeded") return;
|
||||
|
||||
const polarCheckoutId = data.id as string;
|
||||
|
||||
const [checkout] = await db
|
||||
.select()
|
||||
.from(checkouts)
|
||||
.where(eq(checkouts.polarCheckoutId, polarCheckoutId));
|
||||
|
||||
if (!checkout || checkout.status !== "pending") return;
|
||||
|
||||
// Create local subscription record (no WM creation — that happens via POST /wms)
|
||||
if (data.subscriptionId) {
|
||||
await db.insert(subscriptions).values({
|
||||
accountId: checkout.accountId,
|
||||
packageId: checkout.packageId,
|
||||
polarSubscriptionId: data.subscriptionId as string,
|
||||
seats: 1,
|
||||
status: "active",
|
||||
});
|
||||
}
|
||||
|
||||
// Mark checkout as completed
|
||||
await db
|
||||
.update(checkouts)
|
||||
.set({ status: "completed" })
|
||||
.where(eq(checkouts.id, checkout.id));
|
||||
}
|
||||
|
||||
async function handleSubscriptionUpdated(data: Record<string, unknown>) {
|
||||
const polarSubId = data.id as string;
|
||||
const status = data.status as string;
|
||||
|
||||
const mappedStatus = mapSubscriptionStatus(status);
|
||||
if (!mappedStatus) return;
|
||||
|
||||
const seats = typeof data.seats === "number" ? data.seats : undefined;
|
||||
|
||||
await db
|
||||
.update(subscriptions)
|
||||
.set({
|
||||
status: mappedStatus,
|
||||
...(seats !== undefined ? { seats } : {}),
|
||||
currentPeriodStart: data.currentPeriodStart
|
||||
? new Date(data.currentPeriodStart as string)
|
||||
: undefined,
|
||||
currentPeriodEnd: data.currentPeriodEnd
|
||||
? new Date(data.currentPeriodEnd as string)
|
||||
: undefined,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(subscriptions.polarSubscriptionId, polarSubId));
|
||||
}
|
||||
|
||||
async function handleSubscriptionCanceled(data: Record<string, unknown>) {
|
||||
const polarSubId = data.id as string;
|
||||
await db
|
||||
.update(subscriptions)
|
||||
.set({ status: "canceled", updatedAt: new Date() })
|
||||
.where(eq(subscriptions.polarSubscriptionId, polarSubId));
|
||||
}
|
||||
|
||||
async function handleSubscriptionRevoked(data: Record<string, unknown>) {
|
||||
const polarSubId = data.id as string;
|
||||
await db
|
||||
.update(subscriptions)
|
||||
.set({ status: "revoked", updatedAt: new Date() })
|
||||
.where(eq(subscriptions.polarSubscriptionId, polarSubId));
|
||||
}
|
||||
|
||||
async function handleCustomerCreated(data: Record<string, unknown>) {
|
||||
const polarCustomerId = data.id as string;
|
||||
const metadata = data.metadata as Record<string, unknown> | undefined;
|
||||
const accountId = metadata?.accountId as string | undefined;
|
||||
|
||||
if (!accountId) return;
|
||||
|
||||
await db
|
||||
.update(accounts)
|
||||
.set({ polarCustomerId, updatedAt: new Date() })
|
||||
.where(eq(accounts.id, accountId));
|
||||
}
|
||||
|
||||
function mapSubscriptionStatus(
|
||||
status: string,
|
||||
): "active" | "canceled" | "revoked" | "past_due" | null {
|
||||
switch (status) {
|
||||
case "active":
|
||||
return "active";
|
||||
case "canceled":
|
||||
return "canceled";
|
||||
case "revoked":
|
||||
return "revoked";
|
||||
case "past_due":
|
||||
return "past_due";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
256
src/routes/billing.ts
Normal file
256
src/routes/billing.ts
Normal file
@ -0,0 +1,256 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { db } from "../db/index.js";
|
||||
import { accounts, checkouts, subscriptions } from "../db/schema.js";
|
||||
import { polar } from "../lib/polar.js";
|
||||
|
||||
export async function billingRoutes(app: FastifyInstance) {
|
||||
// POST /billing/checkout — create a checkout for a package subscription
|
||||
app.post<{
|
||||
Params: { accountId: string };
|
||||
Body: { productId: string };
|
||||
}>(
|
||||
"/billing/checkout",
|
||||
{
|
||||
preHandler: [app.authenticate, app.requireAccount],
|
||||
schema: {
|
||||
description: "Create a Polar checkout for a package subscription",
|
||||
tags: ["Billing"],
|
||||
security: [{ bearerAuth: [] }],
|
||||
params: {
|
||||
type: "object",
|
||||
properties: {
|
||||
accountId: { type: "string", format: "uuid" },
|
||||
},
|
||||
},
|
||||
body: {
|
||||
type: "object",
|
||||
required: ["productId"],
|
||||
properties: {
|
||||
productId: { type: "string", minLength: 1 },
|
||||
},
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
description: "Checkout created",
|
||||
type: "object",
|
||||
properties: {
|
||||
checkoutId: { type: "string" },
|
||||
checkoutUrl: { type: "string" },
|
||||
},
|
||||
},
|
||||
400: {
|
||||
type: "object",
|
||||
properties: { error: { type: "string" } },
|
||||
},
|
||||
409: {
|
||||
type: "object",
|
||||
properties: { error: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { productId } = request.body;
|
||||
|
||||
// Check for existing active subscription for this product
|
||||
const [existingSub] = await db
|
||||
.select()
|
||||
.from(subscriptions)
|
||||
.where(
|
||||
and(
|
||||
eq(subscriptions.accountId, request.accountId),
|
||||
eq(subscriptions.polarProductId, productId),
|
||||
eq(subscriptions.status, "active"),
|
||||
),
|
||||
);
|
||||
|
||||
if (existingSub) {
|
||||
return reply
|
||||
.status(409)
|
||||
.send({ error: "Active subscription already exists for this product" });
|
||||
}
|
||||
|
||||
// Look up account for polarCustomerId
|
||||
const [account] = await db
|
||||
.select()
|
||||
.from(accounts)
|
||||
.where(eq(accounts.id, request.accountId));
|
||||
|
||||
const checkout = await polar.checkouts.create({
|
||||
products: [productId],
|
||||
metadata: {
|
||||
accountId: request.accountId,
|
||||
productId,
|
||||
},
|
||||
customerMetadata: {
|
||||
accountId: request.accountId,
|
||||
},
|
||||
...(account?.polarCustomerId ? { customerId: account.polarCustomerId } : {}),
|
||||
});
|
||||
|
||||
// Store checkout record
|
||||
const [localCheckout] = await db
|
||||
.insert(checkouts)
|
||||
.values({
|
||||
accountId: request.accountId,
|
||||
polarCheckoutId: checkout.id,
|
||||
polarProductId: productId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return reply.status(201).send({
|
||||
checkoutId: localCheckout.id,
|
||||
checkoutUrl: checkout.url,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// GET /billing/checkout/:checkoutId — poll checkout status
|
||||
app.get<{
|
||||
Params: { accountId: string; checkoutId: string };
|
||||
}>(
|
||||
"/billing/checkout/:checkoutId",
|
||||
{
|
||||
preHandler: [app.authenticate, app.requireAccount],
|
||||
schema: {
|
||||
description: "Poll checkout status (for CLI)",
|
||||
tags: ["Billing"],
|
||||
security: [{ bearerAuth: [] }],
|
||||
params: {
|
||||
type: "object",
|
||||
properties: {
|
||||
accountId: { type: "string", format: "uuid" },
|
||||
checkoutId: { type: "string", format: "uuid" },
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: "object",
|
||||
properties: {
|
||||
status: { type: "string", enum: ["pending", "completed", "expired"] },
|
||||
},
|
||||
},
|
||||
404: {
|
||||
type: "object",
|
||||
properties: { error: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const [checkout] = await db
|
||||
.select()
|
||||
.from(checkouts)
|
||||
.where(
|
||||
and(
|
||||
eq(checkouts.id, request.params.checkoutId),
|
||||
eq(checkouts.accountId, request.accountId),
|
||||
),
|
||||
);
|
||||
|
||||
if (!checkout) {
|
||||
return reply.status(404).send({ error: "Checkout not found" });
|
||||
}
|
||||
|
||||
return { status: checkout.status };
|
||||
},
|
||||
);
|
||||
|
||||
// GET /billing/portal — get customer portal URL
|
||||
app.get<{
|
||||
Params: { accountId: string };
|
||||
}>(
|
||||
"/billing/portal",
|
||||
{
|
||||
preHandler: [app.authenticate, app.requireAccount],
|
||||
schema: {
|
||||
description: "Get Polar customer portal URL",
|
||||
tags: ["Billing"],
|
||||
security: [{ bearerAuth: [] }],
|
||||
params: {
|
||||
type: "object",
|
||||
properties: {
|
||||
accountId: { type: "string", format: "uuid" },
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: "object",
|
||||
properties: {
|
||||
portalUrl: { type: "string" },
|
||||
},
|
||||
},
|
||||
404: {
|
||||
type: "object",
|
||||
properties: { error: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const [account] = await db
|
||||
.select()
|
||||
.from(accounts)
|
||||
.where(eq(accounts.id, request.accountId));
|
||||
|
||||
if (!account?.polarCustomerId) {
|
||||
return reply
|
||||
.status(404)
|
||||
.send({ error: "No billing account found. Complete a checkout first." });
|
||||
}
|
||||
|
||||
const session = await polar.customerSessions.create({
|
||||
customerId: account.polarCustomerId,
|
||||
});
|
||||
|
||||
return { portalUrl: session.customerPortalUrl };
|
||||
},
|
||||
);
|
||||
|
||||
// GET /billing/subscriptions — list active subscriptions
|
||||
app.get<{
|
||||
Params: { accountId: string };
|
||||
}>(
|
||||
"/billing/subscriptions",
|
||||
{
|
||||
preHandler: [app.authenticate, app.requireAccount],
|
||||
schema: {
|
||||
description: "List subscriptions for the account",
|
||||
tags: ["Billing"],
|
||||
security: [{ bearerAuth: [] }],
|
||||
params: {
|
||||
type: "object",
|
||||
properties: {
|
||||
accountId: { type: "string", format: "uuid" },
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
polarProductId: { type: "string" },
|
||||
seats: { type: "integer" },
|
||||
polarSubscriptionId: { type: "string" },
|
||||
status: { type: "string" },
|
||||
currentPeriodStart: { type: "string", format: "date-time", nullable: true },
|
||||
currentPeriodEnd: { type: "string", format: "date-time", nullable: true },
|
||||
createdAt: { type: "string", format: "date-time" },
|
||||
updatedAt: { type: "string", format: "date-time" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request) => {
|
||||
return db
|
||||
.select()
|
||||
.from(subscriptions)
|
||||
.where(eq(subscriptions.accountId, request.accountId));
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -8,6 +8,8 @@ import { accountRoutes } from "./accounts.js";
|
||||
import { projectRoutes } from "./projects.js";
|
||||
import { packageRoutes } from "./packages.js";
|
||||
import { wmRoutes } from "./wms.js";
|
||||
import { billingRoutes } from "./billing.js";
|
||||
import { webhookRoutes } from "./webhooks.js";
|
||||
|
||||
export async function registerRoutes(app: FastifyInstance) {
|
||||
app.register(healthRoutes);
|
||||
@ -19,4 +21,6 @@ export async function registerRoutes(app: FastifyInstance) {
|
||||
app.register(projectRoutes, { prefix: "/accounts/:accountId" });
|
||||
app.register(packageRoutes);
|
||||
app.register(wmRoutes, { prefix: "/accounts/:accountId" });
|
||||
app.register(billingRoutes, { prefix: "/accounts/:accountId" });
|
||||
app.register(webhookRoutes);
|
||||
}
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { db } from "../db/index.js";
|
||||
import { packages } from "../db/schema.js";
|
||||
import { polar } from "../lib/polar.js";
|
||||
|
||||
export async function packageRoutes(app: FastifyInstance) {
|
||||
// GET /packages — list available packages
|
||||
// GET /packages — list available packages from Polar
|
||||
app.get(
|
||||
"/packages",
|
||||
{
|
||||
@ -18,12 +17,12 @@ export async function packageRoutes(app: FastifyInstance) {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string" },
|
||||
slug: { type: "string" },
|
||||
name: { type: "string" },
|
||||
vcpu: { type: "number" },
|
||||
ram: { type: "integer" },
|
||||
disk: { type: "integer" },
|
||||
priceMonthly: { type: "string" },
|
||||
ram: { type: "number" },
|
||||
disk: { type: "number" },
|
||||
priceMonthly: { type: "integer", description: "Price in cents per seat per month" },
|
||||
isFree: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -31,7 +30,25 @@ export async function packageRoutes(app: FastifyInstance) {
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
return db.select().from(packages);
|
||||
const result = await polar.products.list({ isArchived: false });
|
||||
|
||||
console.log("Fetched products from Polar:", result.result.items);
|
||||
|
||||
return result.result.items.map((product) => {
|
||||
const meta = product.metadata ?? {};
|
||||
const price = product.prices[0];
|
||||
const isFree = price && "amountType" in price && price.amountType === "free";
|
||||
|
||||
return {
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
vcpu: Number(meta.vcpu) || 0,
|
||||
ram: Number(meta.ram) || 0,
|
||||
disk: Number(meta.disk) || 0,
|
||||
priceMonthly: isFree ? 0 : (price && "priceAmount" in price ? price.priceAmount : 0),
|
||||
isFree,
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
31
src/routes/webhooks.ts
Normal file
31
src/routes/webhooks.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { handleWebhookEvent, parseWebhookEvent, WebhookVerificationError } from "../lib/polar.js";
|
||||
|
||||
export async function webhookRoutes(app: FastifyInstance) {
|
||||
// Capture raw body for signature verification — scoped to this plugin only
|
||||
app.addContentTypeParser(
|
||||
"application/json",
|
||||
{ parseAs: "buffer" },
|
||||
(_req, body, done) => {
|
||||
done(null, body);
|
||||
},
|
||||
);
|
||||
|
||||
app.post("/webhooks/polar", async (request, reply) => {
|
||||
const rawBody = request.body as Buffer;
|
||||
|
||||
let event;
|
||||
try {
|
||||
event = parseWebhookEvent(rawBody, request.headers as Record<string, string>);
|
||||
} catch (err) {
|
||||
if (err instanceof WebhookVerificationError) {
|
||||
return reply.status(400).send({ error: "Invalid webhook signature" });
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
await handleWebhookEvent(event as { type: string; data: Record<string, unknown> });
|
||||
|
||||
return reply.status(200).send({ received: true });
|
||||
});
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { eq, and, count } from "drizzle-orm";
|
||||
import { db } from "../db/index.js";
|
||||
import { wms, packages, projects } from "../db/schema.js";
|
||||
import { wms, projects, subscriptions } from "../db/schema.js";
|
||||
import { polar } from "../lib/polar.js";
|
||||
|
||||
export async function wmRoutes(app: FastifyInstance) {
|
||||
// GET /projects/:projectId/wms — list WMs for a project
|
||||
@ -30,7 +31,7 @@ export async function wmRoutes(app: FastifyInstance) {
|
||||
id: { type: "string" },
|
||||
projectId: { type: "string" },
|
||||
name: { type: "string" },
|
||||
packageId: { type: "string", nullable: true },
|
||||
polarProductId: { type: "string", nullable: true },
|
||||
vcpu: { type: "number" },
|
||||
ram: { type: "integer" },
|
||||
disk: { type: "integer" },
|
||||
@ -67,14 +68,14 @@ export async function wmRoutes(app: FastifyInstance) {
|
||||
// POST /projects/:projectId/wms — create a WM
|
||||
app.post<{
|
||||
Params: { projectId: string };
|
||||
Body: { name: string; packageId?: string; vcpu?: number; ram?: number; disk?: number };
|
||||
Body: { name: string; productId: string };
|
||||
}>(
|
||||
"/projects/:projectId/wms",
|
||||
{
|
||||
preHandler: [app.authenticate, app.requireAccount],
|
||||
schema: {
|
||||
description:
|
||||
"Create a WM from a package or with custom settings. Provide packageId to use a package, or vcpu/ram/disk for custom settings.",
|
||||
"Create a WM from a Polar product. Free products create directly; paid products require an active subscription.",
|
||||
tags: ["WMs"],
|
||||
security: [{ bearerAuth: [] }],
|
||||
params: {
|
||||
@ -86,13 +87,10 @@ export async function wmRoutes(app: FastifyInstance) {
|
||||
},
|
||||
body: {
|
||||
type: "object",
|
||||
required: ["name"],
|
||||
required: ["name", "productId"],
|
||||
properties: {
|
||||
name: { type: "string", minLength: 1 },
|
||||
packageId: { type: "string", format: "uuid" },
|
||||
vcpu: { type: "number", minimum: 0.25 },
|
||||
ram: { type: "integer", minimum: 1 },
|
||||
disk: { type: "integer", minimum: 1 },
|
||||
productId: { type: "string", minLength: 1 },
|
||||
},
|
||||
},
|
||||
response: {
|
||||
@ -103,7 +101,7 @@ export async function wmRoutes(app: FastifyInstance) {
|
||||
id: { type: "string" },
|
||||
projectId: { type: "string" },
|
||||
name: { type: "string" },
|
||||
packageId: { type: "string", nullable: true },
|
||||
polarProductId: { type: "string", nullable: true },
|
||||
vcpu: { type: "number" },
|
||||
ram: { type: "integer" },
|
||||
disk: { type: "integer" },
|
||||
@ -116,8 +114,13 @@ export async function wmRoutes(app: FastifyInstance) {
|
||||
type: "object",
|
||||
properties: { error: { type: "string" } },
|
||||
},
|
||||
402: {
|
||||
description: "Subscription required or payment failed",
|
||||
type: "object",
|
||||
properties: { error: { type: "string" } },
|
||||
},
|
||||
404: {
|
||||
description: "Project or package not found",
|
||||
description: "Project or product not found",
|
||||
type: "object",
|
||||
properties: { error: { type: "string" } },
|
||||
},
|
||||
@ -125,7 +128,7 @@ export async function wmRoutes(app: FastifyInstance) {
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { name, packageId, vcpu, ram, disk } = request.body;
|
||||
const { name, productId } = request.body;
|
||||
|
||||
// Verify project belongs to the account
|
||||
const [project] = await db
|
||||
@ -139,28 +142,100 @@ export async function wmRoutes(app: FastifyInstance) {
|
||||
return reply.status(404).send({ error: "Project not found" });
|
||||
}
|
||||
|
||||
let settings: { vcpu: number; ram: number; disk: number; packageId: string | null };
|
||||
|
||||
if (packageId) {
|
||||
const [pkg] = await db.select().from(packages).where(eq(packages.id, packageId));
|
||||
if (!pkg) {
|
||||
return reply.status(404).send({ error: "Package not found" });
|
||||
}
|
||||
settings = { vcpu: pkg.vcpu, ram: pkg.ram, disk: pkg.disk, packageId: pkg.id };
|
||||
} else if (vcpu !== undefined && ram !== undefined && disk !== undefined) {
|
||||
settings = { vcpu, ram, disk, packageId: null };
|
||||
} else {
|
||||
return reply
|
||||
.status(400)
|
||||
.send({ error: "Provide either packageId or all of vcpu, ram, and disk" });
|
||||
// Fetch product from Polar for specs
|
||||
let product;
|
||||
try {
|
||||
product = await polar.products.get({ id: productId });
|
||||
} catch {
|
||||
return reply.status(404).send({ error: "Product not found" });
|
||||
}
|
||||
|
||||
const meta = product.metadata ?? {};
|
||||
const vcpu = Number(meta.vcpu) || 0;
|
||||
const ram = Number(meta.ram) || 0;
|
||||
const disk = Number(meta.disk) || 0;
|
||||
|
||||
if (!vcpu || !ram || !disk) {
|
||||
return reply.status(400).send({ error: "Product is missing required metadata (vcpu, ram, disk)" });
|
||||
}
|
||||
|
||||
// Determine if product is free
|
||||
const price = product.prices[0];
|
||||
const isFree = price && "amountType" in price && price.amountType === "free";
|
||||
|
||||
if (isFree) {
|
||||
// Free product — create directly without subscription
|
||||
const [wm] = await db
|
||||
.insert(wms)
|
||||
.values({
|
||||
projectId: request.params.projectId,
|
||||
name,
|
||||
polarProductId: productId,
|
||||
vcpu,
|
||||
ram,
|
||||
disk,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return reply.status(201).send(wm);
|
||||
}
|
||||
|
||||
// Paid product — find active subscription
|
||||
const [sub] = await db
|
||||
.select()
|
||||
.from(subscriptions)
|
||||
.where(
|
||||
and(
|
||||
eq(subscriptions.accountId, request.accountId),
|
||||
eq(subscriptions.polarProductId, productId),
|
||||
eq(subscriptions.status, "active"),
|
||||
),
|
||||
);
|
||||
|
||||
if (!sub) {
|
||||
return reply
|
||||
.status(402)
|
||||
.send({ error: "Subscribe first via POST /accounts/:accountId/billing/checkout" });
|
||||
}
|
||||
|
||||
// Count WMs currently using this subscription
|
||||
const [{ wmCount }] = await db
|
||||
.select({ wmCount: count() })
|
||||
.from(wms)
|
||||
.where(eq(wms.polarSubscriptionId, sub.polarSubscriptionId));
|
||||
|
||||
if (wmCount >= sub.seats) {
|
||||
// Need to increment seats
|
||||
try {
|
||||
await polar.subscriptions.update({
|
||||
id: sub.polarSubscriptionId,
|
||||
subscriptionUpdate: { seats: sub.seats + 1 },
|
||||
});
|
||||
|
||||
// Update local seat count
|
||||
await db
|
||||
.update(subscriptions)
|
||||
.set({ seats: sub.seats + 1, updatedAt: new Date() })
|
||||
.where(eq(subscriptions.id, sub.id));
|
||||
} catch (err) {
|
||||
request.log.warn({ err }, "Failed to increment seats on Polar subscription");
|
||||
return reply
|
||||
.status(402)
|
||||
.send({ error: "Failed to add seat — payment may have been declined" });
|
||||
}
|
||||
}
|
||||
|
||||
// Create the WM with the subscription link
|
||||
const [wm] = await db
|
||||
.insert(wms)
|
||||
.values({
|
||||
projectId: request.params.projectId,
|
||||
name,
|
||||
...settings,
|
||||
polarProductId: productId,
|
||||
vcpu,
|
||||
ram,
|
||||
disk,
|
||||
polarSubscriptionId: sub.polarSubscriptionId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
@ -177,7 +252,7 @@ export async function wmRoutes(app: FastifyInstance) {
|
||||
{
|
||||
preHandler: [app.authenticate, app.requireAccount],
|
||||
schema: {
|
||||
description: "Update a WM's name or settings",
|
||||
description: "Update a WM's name or settings (paid WMs can only change name)",
|
||||
tags: ["WMs"],
|
||||
security: [{ bearerAuth: [] }],
|
||||
params: {
|
||||
@ -205,7 +280,7 @@ export async function wmRoutes(app: FastifyInstance) {
|
||||
id: { type: "string" },
|
||||
projectId: { type: "string" },
|
||||
name: { type: "string" },
|
||||
packageId: { type: "string", nullable: true },
|
||||
polarProductId: { type: "string", nullable: true },
|
||||
vcpu: { type: "number" },
|
||||
ram: { type: "integer" },
|
||||
disk: { type: "integer" },
|
||||
@ -213,6 +288,10 @@ export async function wmRoutes(app: FastifyInstance) {
|
||||
updatedAt: { type: "string", format: "date-time" },
|
||||
},
|
||||
},
|
||||
400: {
|
||||
type: "object",
|
||||
properties: { error: { type: "string" } },
|
||||
},
|
||||
404: {
|
||||
description: "WM not found",
|
||||
type: "object",
|
||||
@ -235,6 +314,21 @@ export async function wmRoutes(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
const { name, vcpu, ram, disk } = request.body;
|
||||
|
||||
// If trying to change specs, check if it's a paid WM
|
||||
if (vcpu !== undefined || ram !== undefined || disk !== undefined) {
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(wms)
|
||||
.where(and(eq(wms.id, request.params.id), eq(wms.projectId, request.params.projectId)));
|
||||
|
||||
if (existing?.polarSubscriptionId) {
|
||||
return reply
|
||||
.status(400)
|
||||
.send({ error: "Cannot change specs on a paid WM — delete and recreate with a different product" });
|
||||
}
|
||||
}
|
||||
|
||||
const updates: Record<string, unknown> = { updatedAt: new Date() };
|
||||
if (name !== undefined) updates.name = name;
|
||||
if (vcpu !== undefined) updates.vcpu = vcpu;
|
||||
@ -261,7 +355,7 @@ export async function wmRoutes(app: FastifyInstance) {
|
||||
{
|
||||
preHandler: [app.authenticate, app.requireAccount],
|
||||
schema: {
|
||||
description: "Delete a WM",
|
||||
description: "Delete a WM (adjusts seat count or revokes subscription)",
|
||||
tags: ["WMs"],
|
||||
security: [{ bearerAuth: [] }],
|
||||
params: {
|
||||
@ -297,15 +391,58 @@ export async function wmRoutes(app: FastifyInstance) {
|
||||
return reply.status(404).send({ error: "Project not found" });
|
||||
}
|
||||
|
||||
const [deleted] = await db
|
||||
.delete(wms)
|
||||
.where(and(eq(wms.id, request.params.id), eq(wms.projectId, request.params.projectId)))
|
||||
.returning();
|
||||
// Find the WM
|
||||
const [wm] = await db
|
||||
.select()
|
||||
.from(wms)
|
||||
.where(and(eq(wms.id, request.params.id), eq(wms.projectId, request.params.projectId)));
|
||||
|
||||
if (!deleted) {
|
||||
if (!wm) {
|
||||
return reply.status(404).send({ error: "WM not found" });
|
||||
}
|
||||
|
||||
// Handle subscription seat management
|
||||
if (wm.polarSubscriptionId) {
|
||||
// Count WMs on this subscription (including the one being deleted)
|
||||
const [{ remaining }] = await db
|
||||
.select({ remaining: count() })
|
||||
.from(wms)
|
||||
.where(eq(wms.polarSubscriptionId, wm.polarSubscriptionId));
|
||||
|
||||
const remainingAfterDelete = remaining - 1;
|
||||
|
||||
if (remainingAfterDelete <= 0) {
|
||||
// No more WMs — revoke the subscription
|
||||
try {
|
||||
await polar.subscriptions.revoke({ id: wm.polarSubscriptionId });
|
||||
} catch (err) {
|
||||
request.log.warn({ err, subscriptionId: wm.polarSubscriptionId }, "Failed to revoke Polar subscription");
|
||||
}
|
||||
|
||||
await db
|
||||
.update(subscriptions)
|
||||
.set({ status: "revoked", updatedAt: new Date() })
|
||||
.where(eq(subscriptions.polarSubscriptionId, wm.polarSubscriptionId));
|
||||
} else {
|
||||
// Decrement seats
|
||||
try {
|
||||
await polar.subscriptions.update({
|
||||
id: wm.polarSubscriptionId,
|
||||
subscriptionUpdate: { seats: remainingAfterDelete },
|
||||
});
|
||||
|
||||
await db
|
||||
.update(subscriptions)
|
||||
.set({ seats: remainingAfterDelete, updatedAt: new Date() })
|
||||
.where(eq(subscriptions.polarSubscriptionId, wm.polarSubscriptionId));
|
||||
} catch (err) {
|
||||
request.log.warn({ err, subscriptionId: wm.polarSubscriptionId }, "Failed to decrement seats on Polar subscription");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await db.delete(wms).where(eq(wms.id, wm.id));
|
||||
|
||||
return reply.status(204).send();
|
||||
},
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user