Add subscription foundation
This commit is contained in:
parent
9012dde138
commit
13e91cb764
@ -0,0 +1,55 @@
|
|||||||
|
CREATE TYPE "SubscriptionStatus" AS ENUM ('TRIAL', 'ACTIVE', 'PAST_DUE', 'CANCELLED', 'EXPIRED');
|
||||||
|
CREATE TYPE "SubscriptionPlan" AS ENUM ('FREE', 'PRO');
|
||||||
|
CREATE TYPE "BillingProvider" AS ENUM ('PADDLE');
|
||||||
|
|
||||||
|
CREATE TABLE "user_subscriptions" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"user_id" UUID NOT NULL,
|
||||||
|
"provider" "BillingProvider" NOT NULL,
|
||||||
|
"provider_customer_id" TEXT,
|
||||||
|
"provider_subscription_id" TEXT,
|
||||||
|
"plan" "SubscriptionPlan" NOT NULL,
|
||||||
|
"status" "SubscriptionStatus" NOT NULL,
|
||||||
|
"current_period_start" TIMESTAMP(3) NOT NULL,
|
||||||
|
"current_period_end" TIMESTAMP(3) NOT NULL,
|
||||||
|
"cancel_at_period_end" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"cancelled_at" TIMESTAMP(3),
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
CONSTRAINT "user_subscriptions_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "user_subscriptions_user_id_key"
|
||||||
|
ON "user_subscriptions"("user_id");
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "user_subscriptions_provider_customer_id_key"
|
||||||
|
ON "user_subscriptions"("provider_customer_id");
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "user_subscriptions_provider_subscription_id_key"
|
||||||
|
ON "user_subscriptions"("provider_subscription_id");
|
||||||
|
|
||||||
|
CREATE TABLE "user_entitlements" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"user_id" UUID NOT NULL,
|
||||||
|
"entitlement_key" TEXT NOT NULL,
|
||||||
|
"granted" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"source" TEXT NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
CONSTRAINT "user_entitlements_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "user_entitlements_user_id_entitlement_key_key"
|
||||||
|
ON "user_entitlements"("user_id", "entitlement_key");
|
||||||
|
|
||||||
|
ALTER TABLE "user_subscriptions"
|
||||||
|
ADD CONSTRAINT "user_subscriptions_user_id_fkey"
|
||||||
|
FOREIGN KEY ("user_id") REFERENCES "users"("id")
|
||||||
|
ON DELETE CASCADE
|
||||||
|
ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE "user_entitlements"
|
||||||
|
ADD CONSTRAINT "user_entitlements_user_id_fkey"
|
||||||
|
FOREIGN KEY ("user_id") REFERENCES "users"("id")
|
||||||
|
ON DELETE CASCADE
|
||||||
|
ON UPDATE CASCADE;
|
||||||
@ -34,6 +34,8 @@ model User {
|
|||||||
incomingTransfers OwnershipTransfer[] @relation("OwnershipTransferToUser")
|
incomingTransfers OwnershipTransfer[] @relation("OwnershipTransferToUser")
|
||||||
initiatedTransfers OwnershipTransfer[] @relation("OwnershipTransferRequestedByUser")
|
initiatedTransfers OwnershipTransfer[] @relation("OwnershipTransferRequestedByUser")
|
||||||
accountSessions AccountSession[]
|
accountSessions AccountSession[]
|
||||||
|
subscription UserSubscription?
|
||||||
|
entitlements UserEntitlement[]
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
@ -302,6 +304,56 @@ model AccountSession {
|
|||||||
@@map("account_sessions")
|
@@map("account_sessions")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model UserSubscription {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
userId String @unique @map("user_id") @db.Uuid
|
||||||
|
provider BillingProvider
|
||||||
|
providerCustomerId String? @unique @map("provider_customer_id")
|
||||||
|
providerSubscriptionId String? @unique @map("provider_subscription_id")
|
||||||
|
plan SubscriptionPlan
|
||||||
|
status SubscriptionStatus
|
||||||
|
currentPeriodStart DateTime @map("current_period_start")
|
||||||
|
currentPeriodEnd DateTime @map("current_period_end")
|
||||||
|
cancelAtPeriodEnd Boolean @default(false) @map("cancel_at_period_end")
|
||||||
|
cancelledAt DateTime? @map("cancelled_at")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
|
@@map("user_subscriptions")
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserEntitlement {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
userId String @map("user_id") @db.Uuid
|
||||||
|
entitlementKey String @map("entitlement_key")
|
||||||
|
granted Boolean @default(false)
|
||||||
|
source String
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
|
||||||
|
@@unique([userId, entitlementKey])
|
||||||
|
@@map("user_entitlements")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SubscriptionStatus {
|
||||||
|
TRIAL
|
||||||
|
ACTIVE
|
||||||
|
PAST_DUE
|
||||||
|
CANCELLED
|
||||||
|
EXPIRED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SubscriptionPlan {
|
||||||
|
FREE
|
||||||
|
PRO
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BillingProvider {
|
||||||
|
PADDLE
|
||||||
|
}
|
||||||
|
|
||||||
enum UserAccountKind {
|
enum UserAccountKind {
|
||||||
LEGACY_DEFAULT
|
LEGACY_DEFAULT
|
||||||
GUEST
|
GUEST
|
||||||
|
|||||||
@ -0,0 +1,76 @@
|
|||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
describe('subscription foundation migration', () => {
|
||||||
|
let migrationSql: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
migrationSql = await readFile(
|
||||||
|
join(
|
||||||
|
process.cwd(),
|
||||||
|
'prisma/migrations/20260628160000_milestone121_subscription_foundation/migration.sql',
|
||||||
|
),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates subscription and entitlement tables', () => {
|
||||||
|
expect(migrationSql).toContain(`CREATE TABLE "user_subscriptions"`);
|
||||||
|
expect(migrationSql).toContain(`CREATE TABLE "user_entitlements"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates the subscription status, plan, and billing provider enums', () => {
|
||||||
|
expect(migrationSql).toContain(
|
||||||
|
`CREATE TYPE "SubscriptionStatus" AS ENUM ('TRIAL', 'ACTIVE', 'PAST_DUE', 'CANCELLED', 'EXPIRED')`,
|
||||||
|
);
|
||||||
|
expect(migrationSql).toContain(
|
||||||
|
`CREATE TYPE "SubscriptionPlan" AS ENUM ('FREE', 'PRO')`,
|
||||||
|
);
|
||||||
|
expect(migrationSql).toContain(
|
||||||
|
`CREATE TYPE "BillingProvider" AS ENUM ('PADDLE')`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('enforces one subscription per user and unique provider identifiers', () => {
|
||||||
|
expect(migrationSql).toContain(
|
||||||
|
`CREATE UNIQUE INDEX "user_subscriptions_user_id_key"`,
|
||||||
|
);
|
||||||
|
expect(migrationSql).toContain(
|
||||||
|
`CREATE UNIQUE INDEX "user_subscriptions_provider_customer_id_key"`,
|
||||||
|
);
|
||||||
|
expect(migrationSql).toContain(
|
||||||
|
`CREATE UNIQUE INDEX "user_subscriptions_provider_subscription_id_key"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps provider identifiers nullable', () => {
|
||||||
|
expect(migrationSql).toContain(`"provider_customer_id" TEXT,`);
|
||||||
|
expect(migrationSql).toContain(`"provider_subscription_id" TEXT,`);
|
||||||
|
expect(migrationSql).not.toContain(
|
||||||
|
`"provider_customer_id" TEXT NOT NULL`,
|
||||||
|
);
|
||||||
|
expect(migrationSql).not.toContain(
|
||||||
|
`"provider_subscription_id" TEXT NOT NULL`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('enforces entitlement uniqueness per user and key', () => {
|
||||||
|
expect(migrationSql).toContain(
|
||||||
|
`CREATE UNIQUE INDEX "user_entitlements_user_id_entitlement_key_key"`,
|
||||||
|
);
|
||||||
|
expect(migrationSql).toContain(
|
||||||
|
`ON "user_entitlements"("user_id", "entitlement_key")`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('relates subscriptions and entitlements to users with cascading deletes', () => {
|
||||||
|
expect(migrationSql).toContain(
|
||||||
|
`ADD CONSTRAINT "user_subscriptions_user_id_fkey"`,
|
||||||
|
);
|
||||||
|
expect(migrationSql).toContain(
|
||||||
|
`ADD CONSTRAINT "user_entitlements_user_id_fkey"`,
|
||||||
|
);
|
||||||
|
expect(migrationSql.match(/REFERENCES "users"\("id"\)/g)).toHaveLength(2);
|
||||||
|
expect(migrationSql.match(/ON DELETE CASCADE/g)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user