init 🎉
This commit is contained in:
parent
254d2bc5d5
commit
103caf9d3d
@ -15,7 +15,13 @@
|
|||||||
"Bash(pnpm db:generate:*)",
|
"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(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(ls:*)",
|
||||||
"Bash(curl:*)"
|
"Bash(curl:*)",
|
||||||
|
"WebFetch(domain:polar.sh)",
|
||||||
|
"Bash(printf:*)",
|
||||||
|
"Bash(kill:*)",
|
||||||
|
"Bash(node -e:*)",
|
||||||
|
"Bash(timeout 5 pnpm dev:*)",
|
||||||
|
"Bash(gtimeout:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,3 +3,5 @@ JWT_SECRET=change-me-to-a-random-string-at-least-32-chars
|
|||||||
PORT=3000
|
PORT=3000
|
||||||
HOST=0.0.0.0
|
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",
|
"id": "f51ba4f1-5d34-487c-9eea-2af8588007ed",
|
||||||
"prevId": "3fe6ec31-b399-41bd-88d8-b1b855d5b3ad",
|
"prevId": "08d293e6-8a0d-4abf-bbe5-600133a8d287",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
"tables": {
|
"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,
|
"when": 1770547300000,
|
||||||
"tag": "0006_seed_packages",
|
"tag": "0006_seed_packages",
|
||||||
"breakpoints": true
|
"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",
|
"packageManager": "pnpm@10.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/swagger": "^9.7.0",
|
"@fastify/swagger": "^9.7.0",
|
||||||
|
"@polar-sh/sdk": "^0.42.5",
|
||||||
"dotenv": "^17.2.4",
|
"dotenv": "^17.2.4",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
"fastify": "^5.7.4",
|
"fastify": "^5.7.4",
|
||||||
|
|||||||
11
pnpm-lock.yaml
generated
11
pnpm-lock.yaml
generated
@ -11,6 +11,9 @@ importers:
|
|||||||
'@fastify/swagger':
|
'@fastify/swagger':
|
||||||
specifier: ^9.7.0
|
specifier: ^9.7.0
|
||||||
version: 9.7.0
|
version: 9.7.0
|
||||||
|
'@polar-sh/sdk':
|
||||||
|
specifier: ^0.42.5
|
||||||
|
version: 0.42.5
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^17.2.4
|
specifier: ^17.2.4
|
||||||
version: 17.2.4
|
version: 17.2.4
|
||||||
@ -573,6 +576,9 @@ packages:
|
|||||||
'@pinojs/redact@0.4.0':
|
'@pinojs/redact@0.4.0':
|
||||||
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
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':
|
'@rollup/rollup-android-arm-eabi@4.57.1':
|
||||||
resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==}
|
resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
@ -1784,6 +1790,11 @@ snapshots:
|
|||||||
|
|
||||||
'@pinojs/redact@0.4.0': {}
|
'@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':
|
'@rollup/rollup-android-arm-eabi@4.57.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,9 @@ const envSchema = z.object({
|
|||||||
HOST: z.string().default("0.0.0.0"),
|
HOST: z.string().default("0.0.0.0"),
|
||||||
API_URL: z.url().optional(),
|
API_URL: z.url().optional(),
|
||||||
RESEND_API_KEY: z.string().min(1),
|
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);
|
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"]);
|
export const membershipRoleEnum = pgEnum("membership_role", ["owner", "admin", "member"]);
|
||||||
|
|
||||||
@ -23,6 +26,7 @@ export const otpCodes = pgTable("otp_codes", {
|
|||||||
export const accounts = pgTable("accounts", {
|
export const accounts = pgTable("accounts", {
|
||||||
id: uuid().defaultRandom().primaryKey(),
|
id: uuid().defaultRandom().primaryKey(),
|
||||||
name: text().notNull(),
|
name: text().notNull(),
|
||||||
|
polarCustomerId: text("polar_customer_id"),
|
||||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||||
updatedAt: timestamp("updated_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(),
|
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", {
|
export const wms = pgTable("wms", {
|
||||||
id: uuid().defaultRandom().primaryKey(),
|
id: uuid().defaultRandom().primaryKey(),
|
||||||
projectId: uuid("project_id")
|
projectId: uuid("project_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => projects.id),
|
.references(() => projects.id),
|
||||||
name: text().notNull(),
|
name: text().notNull(),
|
||||||
packageId: uuid("package_id").references(() => packages.id),
|
polarProductId: text("polar_product_id"),
|
||||||
vcpu: real().notNull(),
|
vcpu: real().notNull(),
|
||||||
ram: integer().notNull(),
|
ram: integer().notNull(),
|
||||||
disk: 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(),
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||||
updatedAt: timestamp("updated_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 { projectRoutes } from "./projects.js";
|
||||||
import { packageRoutes } from "./packages.js";
|
import { packageRoutes } from "./packages.js";
|
||||||
import { wmRoutes } from "./wms.js";
|
import { wmRoutes } from "./wms.js";
|
||||||
|
import { billingRoutes } from "./billing.js";
|
||||||
|
import { webhookRoutes } from "./webhooks.js";
|
||||||
|
|
||||||
export async function registerRoutes(app: FastifyInstance) {
|
export async function registerRoutes(app: FastifyInstance) {
|
||||||
app.register(healthRoutes);
|
app.register(healthRoutes);
|
||||||
@ -19,4 +21,6 @@ export async function registerRoutes(app: FastifyInstance) {
|
|||||||
app.register(projectRoutes, { prefix: "/accounts/:accountId" });
|
app.register(projectRoutes, { prefix: "/accounts/:accountId" });
|
||||||
app.register(packageRoutes);
|
app.register(packageRoutes);
|
||||||
app.register(wmRoutes, { prefix: "/accounts/:accountId" });
|
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 type { FastifyInstance } from "fastify";
|
||||||
import { db } from "../db/index.js";
|
import { polar } from "../lib/polar.js";
|
||||||
import { packages } from "../db/schema.js";
|
|
||||||
|
|
||||||
export async function packageRoutes(app: FastifyInstance) {
|
export async function packageRoutes(app: FastifyInstance) {
|
||||||
// GET /packages — list available packages
|
// GET /packages — list available packages from Polar
|
||||||
app.get(
|
app.get(
|
||||||
"/packages",
|
"/packages",
|
||||||
{
|
{
|
||||||
@ -18,12 +17,12 @@ export async function packageRoutes(app: FastifyInstance) {
|
|||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
id: { type: "string" },
|
id: { type: "string" },
|
||||||
slug: { type: "string" },
|
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
vcpu: { type: "number" },
|
vcpu: { type: "number" },
|
||||||
ram: { type: "integer" },
|
ram: { type: "number" },
|
||||||
disk: { type: "integer" },
|
disk: { type: "number" },
|
||||||
priceMonthly: { type: "string" },
|
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 () => {
|
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 type { FastifyInstance } from "fastify";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and, count } from "drizzle-orm";
|
||||||
import { db } from "../db/index.js";
|
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) {
|
export async function wmRoutes(app: FastifyInstance) {
|
||||||
// GET /projects/:projectId/wms — list WMs for a project
|
// GET /projects/:projectId/wms — list WMs for a project
|
||||||
@ -30,7 +31,7 @@ export async function wmRoutes(app: FastifyInstance) {
|
|||||||
id: { type: "string" },
|
id: { type: "string" },
|
||||||
projectId: { type: "string" },
|
projectId: { type: "string" },
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
packageId: { type: "string", nullable: true },
|
polarProductId: { type: "string", nullable: true },
|
||||||
vcpu: { type: "number" },
|
vcpu: { type: "number" },
|
||||||
ram: { type: "integer" },
|
ram: { type: "integer" },
|
||||||
disk: { type: "integer" },
|
disk: { type: "integer" },
|
||||||
@ -67,14 +68,14 @@ export async function wmRoutes(app: FastifyInstance) {
|
|||||||
// POST /projects/:projectId/wms — create a WM
|
// POST /projects/:projectId/wms — create a WM
|
||||||
app.post<{
|
app.post<{
|
||||||
Params: { projectId: string };
|
Params: { projectId: string };
|
||||||
Body: { name: string; packageId?: string; vcpu?: number; ram?: number; disk?: number };
|
Body: { name: string; productId: string };
|
||||||
}>(
|
}>(
|
||||||
"/projects/:projectId/wms",
|
"/projects/:projectId/wms",
|
||||||
{
|
{
|
||||||
preHandler: [app.authenticate, app.requireAccount],
|
preHandler: [app.authenticate, app.requireAccount],
|
||||||
schema: {
|
schema: {
|
||||||
description:
|
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"],
|
tags: ["WMs"],
|
||||||
security: [{ bearerAuth: [] }],
|
security: [{ bearerAuth: [] }],
|
||||||
params: {
|
params: {
|
||||||
@ -86,13 +87,10 @@ export async function wmRoutes(app: FastifyInstance) {
|
|||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["name"],
|
required: ["name", "productId"],
|
||||||
properties: {
|
properties: {
|
||||||
name: { type: "string", minLength: 1 },
|
name: { type: "string", minLength: 1 },
|
||||||
packageId: { type: "string", format: "uuid" },
|
productId: { type: "string", minLength: 1 },
|
||||||
vcpu: { type: "number", minimum: 0.25 },
|
|
||||||
ram: { type: "integer", minimum: 1 },
|
|
||||||
disk: { type: "integer", minimum: 1 },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
response: {
|
response: {
|
||||||
@ -103,7 +101,7 @@ export async function wmRoutes(app: FastifyInstance) {
|
|||||||
id: { type: "string" },
|
id: { type: "string" },
|
||||||
projectId: { type: "string" },
|
projectId: { type: "string" },
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
packageId: { type: "string", nullable: true },
|
polarProductId: { type: "string", nullable: true },
|
||||||
vcpu: { type: "number" },
|
vcpu: { type: "number" },
|
||||||
ram: { type: "integer" },
|
ram: { type: "integer" },
|
||||||
disk: { type: "integer" },
|
disk: { type: "integer" },
|
||||||
@ -116,8 +114,13 @@ export async function wmRoutes(app: FastifyInstance) {
|
|||||||
type: "object",
|
type: "object",
|
||||||
properties: { error: { type: "string" } },
|
properties: { error: { type: "string" } },
|
||||||
},
|
},
|
||||||
|
402: {
|
||||||
|
description: "Subscription required or payment failed",
|
||||||
|
type: "object",
|
||||||
|
properties: { error: { type: "string" } },
|
||||||
|
},
|
||||||
404: {
|
404: {
|
||||||
description: "Project or package not found",
|
description: "Project or product not found",
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: { error: { type: "string" } },
|
properties: { error: { type: "string" } },
|
||||||
},
|
},
|
||||||
@ -125,7 +128,7 @@ export async function wmRoutes(app: FastifyInstance) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
const { name, packageId, vcpu, ram, disk } = request.body;
|
const { name, productId } = request.body;
|
||||||
|
|
||||||
// Verify project belongs to the account
|
// Verify project belongs to the account
|
||||||
const [project] = await db
|
const [project] = await db
|
||||||
@ -139,28 +142,100 @@ export async function wmRoutes(app: FastifyInstance) {
|
|||||||
return reply.status(404).send({ error: "Project not found" });
|
return reply.status(404).send({ error: "Project not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
let settings: { vcpu: number; ram: number; disk: number; packageId: string | null };
|
// Fetch product from Polar for specs
|
||||||
|
let product;
|
||||||
if (packageId) {
|
try {
|
||||||
const [pkg] = await db.select().from(packages).where(eq(packages.id, packageId));
|
product = await polar.products.get({ id: productId });
|
||||||
if (!pkg) {
|
} catch {
|
||||||
return reply.status(404).send({ error: "Package not found" });
|
return reply.status(404).send({ error: "Product 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" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
const [wm] = await db
|
||||||
.insert(wms)
|
.insert(wms)
|
||||||
.values({
|
.values({
|
||||||
projectId: request.params.projectId,
|
projectId: request.params.projectId,
|
||||||
name,
|
name,
|
||||||
...settings,
|
polarProductId: productId,
|
||||||
|
vcpu,
|
||||||
|
ram,
|
||||||
|
disk,
|
||||||
|
polarSubscriptionId: sub.polarSubscriptionId,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@ -177,7 +252,7 @@ export async function wmRoutes(app: FastifyInstance) {
|
|||||||
{
|
{
|
||||||
preHandler: [app.authenticate, app.requireAccount],
|
preHandler: [app.authenticate, app.requireAccount],
|
||||||
schema: {
|
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"],
|
tags: ["WMs"],
|
||||||
security: [{ bearerAuth: [] }],
|
security: [{ bearerAuth: [] }],
|
||||||
params: {
|
params: {
|
||||||
@ -205,7 +280,7 @@ export async function wmRoutes(app: FastifyInstance) {
|
|||||||
id: { type: "string" },
|
id: { type: "string" },
|
||||||
projectId: { type: "string" },
|
projectId: { type: "string" },
|
||||||
name: { type: "string" },
|
name: { type: "string" },
|
||||||
packageId: { type: "string", nullable: true },
|
polarProductId: { type: "string", nullable: true },
|
||||||
vcpu: { type: "number" },
|
vcpu: { type: "number" },
|
||||||
ram: { type: "integer" },
|
ram: { type: "integer" },
|
||||||
disk: { type: "integer" },
|
disk: { type: "integer" },
|
||||||
@ -213,6 +288,10 @@ export async function wmRoutes(app: FastifyInstance) {
|
|||||||
updatedAt: { type: "string", format: "date-time" },
|
updatedAt: { type: "string", format: "date-time" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
400: {
|
||||||
|
type: "object",
|
||||||
|
properties: { error: { type: "string" } },
|
||||||
|
},
|
||||||
404: {
|
404: {
|
||||||
description: "WM not found",
|
description: "WM not found",
|
||||||
type: "object",
|
type: "object",
|
||||||
@ -235,6 +314,21 @@ export async function wmRoutes(app: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { name, vcpu, ram, disk } = request.body;
|
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() };
|
const updates: Record<string, unknown> = { updatedAt: new Date() };
|
||||||
if (name !== undefined) updates.name = name;
|
if (name !== undefined) updates.name = name;
|
||||||
if (vcpu !== undefined) updates.vcpu = vcpu;
|
if (vcpu !== undefined) updates.vcpu = vcpu;
|
||||||
@ -261,7 +355,7 @@ export async function wmRoutes(app: FastifyInstance) {
|
|||||||
{
|
{
|
||||||
preHandler: [app.authenticate, app.requireAccount],
|
preHandler: [app.authenticate, app.requireAccount],
|
||||||
schema: {
|
schema: {
|
||||||
description: "Delete a WM",
|
description: "Delete a WM (adjusts seat count or revokes subscription)",
|
||||||
tags: ["WMs"],
|
tags: ["WMs"],
|
||||||
security: [{ bearerAuth: [] }],
|
security: [{ bearerAuth: [] }],
|
||||||
params: {
|
params: {
|
||||||
@ -297,15 +391,58 @@ export async function wmRoutes(app: FastifyInstance) {
|
|||||||
return reply.status(404).send({ error: "Project not found" });
|
return reply.status(404).send({ error: "Project not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const [deleted] = await db
|
// Find the WM
|
||||||
.delete(wms)
|
const [wm] = await db
|
||||||
.where(and(eq(wms.id, request.params.id), eq(wms.projectId, request.params.projectId)))
|
.select()
|
||||||
.returning();
|
.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" });
|
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();
|
return reply.status(204).send();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user