@saasicat/spec 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasicat/spec",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Language-neutral spec of the SaaS platform: JSON Schemas, OpenAPI contract, Prisma fragments, acceptance scenarios.",
5
5
  "type": "module",
6
6
  "main": "./index.cjs",
@@ -19,7 +19,9 @@
19
19
  },
20
20
  "./schemas/*.json": "./schemas/*.json",
21
21
  "./prisma-fragments/*.prisma": "./prisma-fragments/*.prisma",
22
- "./prisma-fragments/": "./prisma-fragments/"
22
+ "./prisma-fragments/": "./prisma-fragments/",
23
+ "./sql/*.sql": "./sql/*.sql",
24
+ "./package.json": "./package.json"
23
25
  },
24
26
  "files": [
25
27
  "index.js",
@@ -30,11 +32,13 @@
30
32
  "admin-api.openapi.yaml",
31
33
  "cli-conventions.md",
32
34
  "acceptance/",
33
- "prisma-fragments/"
35
+ "prisma-fragments/",
36
+ "sql/"
34
37
  ],
35
38
  "devDependencies": {
36
39
  "ajv": "^8.17.1",
37
- "ajv-formats": "^3.0.1"
40
+ "ajv-formats": "^3.0.1",
41
+ "prisma": "^6.19.0"
38
42
  },
39
43
  "keywords": [
40
44
  "saas",
@@ -58,6 +62,7 @@
58
62
  "access": "public"
59
63
  },
60
64
  "scripts": {
61
- "test": "node --test tests/schemas.test.js"
65
+ "gen:sql": "node scripts/generate-reference-sql.mjs",
66
+ "test": "node --test tests/*.test.js"
62
67
  }
63
68
  }
@@ -76,9 +76,8 @@ model Subscription {
76
76
  // Subscription may have either `planVersionId` or
77
77
  // `businessTypeVersionId` (or both) set. The DB CHECK
78
78
  // constraint "at least one of the two set" is added by the consumer in
79
- // the SQL migration (Prisma cannot express this directly):
80
- // ALTER TABLE subscriptions ADD CONSTRAINT subscriptions_plan_or_bt_check
81
- // CHECK (plan_version_id IS NOT NULL OR business_type_version_id IS NOT NULL);
79
+ // the SQL migration (Prisma cannot express this directly) — see
80
+ // sql/constraints.postgres.sql (`subscriptions_plan_or_bt_check`).
82
81
  planVersionId String?
83
82
  pendingPlanVersionId String?
84
83
  pendingPlanVersionEffectiveAt DateTime?
@@ -21,6 +21,10 @@ model AuditLog {
21
21
  entityId String
22
22
  action String // SCREAMING_SNAKE_CASE; e.g. "PROMO_CODE_REDEEM"
23
23
  changes Json? // { field: { old: …, new: … } }
24
+ // Origin marker 'web:<email>:<sessionId>' | 'cli:<email>:<host>' — see
25
+ // audit-event.schema.json `actorTag`. Queryable with a trailing wildcard
26
+ // ('cli:*') via AuditQueryPort.
27
+ actorTag String?
24
28
  ipAddress String?
25
29
  userAgent String?
26
30
 
@@ -34,5 +38,6 @@ model AuditLog {
34
38
  @@index([tenantId, entity, entityId])
35
39
  @@index([tenantId, createdAt])
36
40
  @@index([entity, action, createdAt])
41
+ @@index([actorTag, createdAt])
37
42
  @@map("audit_logs")
38
43
  }
@@ -167,14 +167,11 @@ model BusinessTypeVersion {
167
167
  derivedVersions BusinessTypeVersion[] @relation("BusinessTypeVersionLineage")
168
168
  bundles BusinessTypeBundle[]
169
169
 
170
- // Consumers add the Subscription relation in their own schema.prisma.
171
- // Example (commented out):
172
- //
173
- // subscriptionsCurrent Subscription[] @relation("SubscriptionBusinessTypeVersion")
174
- //
175
- // Prerequisite: in M5 Subscription gets the field
176
- // `businessTypeVersionId String?` plus a CHECK constraint
177
- // "at least one of planVersionId / businessTypeVersionId set".
170
+ // Opposite side of Subscription.businessTypeVersion (01-subscription.prisma,
171
+ // M5). Subscription carries `businessTypeVersionId String?` plus the CHECK
172
+ // constraint "at least one of planVersionId / businessTypeVersionId set"
173
+ // (sql/constraints.postgres.sql).
174
+ subscriptionsCurrent Subscription[] @relation("SubscriptionBusinessTypeVersion")
178
175
 
179
176
  @@unique([businessTypeId, version])
180
177
  @@index([businessTypeId, supersededAt])
@@ -115,6 +115,15 @@ model FeatureCatalogEntry {
115
115
  // Convention: 'CORE' < 'ADVANCED' < 'PRO' < 'ENTERPRISE'
116
116
  tier String?
117
117
 
118
+ // True = base feature, always included (not bookable per plan).
119
+ core Boolean @default(false)
120
+
121
+ // Code-discovered feature dependencies (#35) and succession (#39) —
122
+ // see FeatureCatalogEntryRow in @saasicat/types.
123
+ requires String[]
124
+ replaces String[]
125
+ successorKey String?
126
+
118
127
  // Approval lifecycle (#20): pending | approved | outdated | obsolete.
119
128
  discoveryStatus String @default("pending")
120
129
 
@@ -0,0 +1,51 @@
1
+ // =============================================================================
2
+ // SaaS-Platform Prisma fragment: SuperAdmin identity + MFA
3
+ // =============================================================================
4
+ //
5
+ // REFERENCE SNIPPET — see 01-subscription.prisma for conventions.
6
+ //
7
+ // Platform-owned SuperAdmin accounts, separate from the consumer's tenant
8
+ // user model. The `@saasicat/adapter-prisma` adapters
9
+ // (`PrismaSuperAdminBootstrapAdapter`, `PrismaMfaAdapter`) write into these
10
+ // tables; the consumer's admin auth reads them for SuperAdmin login.
11
+ //
12
+ // Apps that keep SuperAdmins inside their own user table skip this fragment
13
+ // and implement `SuperAdminProvisioningPort` / `MfaPort` themselves.
14
+ //
15
+ // Global tables (no tenantId, no RLS).
16
+
17
+ model SuperAdminUser {
18
+ id String @id @default(uuid())
19
+ email String @unique // stored lowercase; adapters normalize
20
+ passwordHash String
21
+ firstName String?
22
+ lastName String?
23
+
24
+ // Constant today ('SUPER_ADMIN'); column exists so later platform roles
25
+ // do not need a migration.
26
+ platformRole String @default("SUPER_ADMIN")
27
+ isActive Boolean @default(true)
28
+
29
+ lastLoginAt DateTime?
30
+ deletedAt DateTime?
31
+
32
+ createdAt DateTime @default(now())
33
+ updatedAt DateTime @updatedAt
34
+
35
+ @@index([isActive, deletedAt])
36
+ @@map("super_admin_users")
37
+ }
38
+
39
+ model SuperAdminMfa {
40
+ // Admin user ID — deliberately NO hard FK: `MfaPort` also serves apps
41
+ // that keep their admin identities in their own user table. Apps whose
42
+ // admins live exclusively in `super_admin_users` may add the relation:
43
+ // user SuperAdminUser @relation(fields: [userId], references: [id], onDelete: Cascade)
44
+ userId String @id
45
+ secret String? // TOTP secret; null after reset
46
+ enabledAt DateTime? // set when a secret is stored, null after reset
47
+
48
+ updatedAt DateTime @updatedAt
49
+
50
+ @@map("super_admin_mfa")
51
+ }
@@ -10,9 +10,21 @@ related:
10
10
 
11
11
  # Prisma Fragments
12
12
 
13
- Reference snippets that document the **canonical database schema** of the SaaS
14
- platform. Consumer apps copy the models into their own `schema.prisma` and add
15
- FK relations to their project-specific `Tenant`/`User` models.
13
+ Prisma-DSL rendering of the SaaSicat data model, **derived from the normative
14
+ sources**: the logical data model in [`docs/data-model.md`](../../../docs/data-model.md)
15
+ and the PostgreSQL artifacts in [`../sql/`](../sql/)
16
+ (`reference-schema.postgres.sql` is generated FROM these fragments via
17
+ `pnpm run gen:sql`; `constraints.postgres.sql` carries the invariants Prisma
18
+ cannot express and is hand-maintained). When fragments and data model
19
+ disagree, the data model + constraints win; the adapter contract tests
20
+ (`@saasicat/persistence-testing`) are the executable arbiter.
21
+
22
+ Consumer apps copy the models into their own `schema.prisma` and add FK
23
+ relations to their project-specific `Tenant`/`User` models.
24
+
25
+ Two guards keep the artifacts honest in CI: the fragments must compose into
26
+ one valid Prisma schema, and `reference-schema.postgres.sql` must be
27
+ regenerated after fragment changes (`tests/reference-sql-drift.test.js`).
16
28
 
17
29
  ## Files
18
30
 
@@ -27,6 +39,7 @@ FK relations to their project-specific `Tenant`/`User` models.
27
39
  | [`07-promotion.prisma`](07-promotion.prisma) | `Promotion` |
28
40
  | [`08-subscription-contract.prisma`](08-subscription-contract.prisma) | `SubscriptionContract`, `ContractLineItem` |
29
41
  | [`09-pending-registration.prisma`](09-pending-registration.prisma) | `PendingRegistration`, `PaymentEventLog` + `RegistrationStatus` |
42
+ | [`10-super-admin.prisma`](10-super-admin.prisma) | `SuperAdminUser`, `SuperAdminMfa` |
30
43
 
31
44
  ## How the consumer uses the fragments
32
45
 
@@ -84,23 +97,20 @@ All monetary amounts are `Decimal(10, 2)` (max ±99,999,999.99 €), promo-code
84
97
  values are `Decimal(8, 2)` (percentage or amount). Consumers should not relax
85
98
  this precision.
86
99
 
87
- ### 5. Partial unique index for drafts
100
+ ### 5. Constraints Prisma cannot express
88
101
 
89
- `PlanVersion`, `BundleVersion`
90
- and `BusinessTypeVersion` allow **exactly one** draft per
91
- identity key (`publishedAt IS NULL`). The Prisma schema cannot express this
92
- add it in the SQL migration:
102
+ `PlanVersion`, `BundleVersion` and `BusinessTypeVersion` allow **exactly
103
+ one** draft per identity key (`publishedAt IS NULL`), and a `Subscription`
104
+ must bind a PlanVersion or a BusinessTypeVersion. Both live as SQL in
105
+ [`../sql/constraints.postgres.sql`](../sql/constraints.postgres.sql)
106
+ add that file to your migration verbatim. Note that column names are
107
+ **camelCase** (the fragments `@@map` table names only), e.g.:
93
108
 
94
109
  ```sql
95
110
  CREATE UNIQUE INDEX plan_versions_draft_per_plan
96
- ON plan_versions (plan_id) WHERE published_at IS NULL;
111
+ ON plan_versions ("planId") WHERE "publishedAt" IS NULL;
97
112
  ```
98
113
 
99
- Likewise for:
100
-
101
- - `bundle_versions` (per `bundle_id`)
102
- - `business_type_versions` (per `business_type_id`)
103
-
104
114
  ## Design decisions
105
115
 
106
116
  - **No fixed quota columns** (`maxUsers`, `maxStorageGb`, …) — limits
@@ -0,0 +1,28 @@
1
+ -- =============================================================================
2
+ -- SaaSicat — normative PostgreSQL constraints the Prisma DSL cannot express.
3
+ -- =============================================================================
4
+ --
5
+ -- Consumers add these statements to their SQL migration after creating the
6
+ -- tables from the prisma-fragments. The adapter contract tests
7
+ -- (@saasicat/persistence-testing) run against a database with these
8
+ -- constraints applied — they are part of the canonical schema, not optional
9
+ -- hardening.
10
+ --
11
+ -- Column names are camelCase (the fragments map table names via @@map but
12
+ -- keep Prisma's default field→column naming), hence the quoting.
13
+
14
+ -- At most ONE draft (publishedAt IS NULL) per version lineage.
15
+ CREATE UNIQUE INDEX IF NOT EXISTS plan_versions_draft_per_plan
16
+ ON plan_versions ("planId") WHERE "publishedAt" IS NULL;
17
+
18
+ CREATE UNIQUE INDEX IF NOT EXISTS bundle_versions_draft_per_bundle
19
+ ON bundle_versions ("bundleId") WHERE "publishedAt" IS NULL;
20
+
21
+ CREATE UNIQUE INDEX IF NOT EXISTS business_type_versions_draft_per_business_type
22
+ ON business_type_versions ("businessTypeId") WHERE "publishedAt" IS NULL;
23
+
24
+ -- A subscription binds a PlanVersion, a BusinessTypeVersion, or both —
25
+ -- never neither (SPEC_V2 §11.1 M5).
26
+ ALTER TABLE subscriptions
27
+ ADD CONSTRAINT subscriptions_plan_or_bt_check
28
+ CHECK ("planVersionId" IS NOT NULL OR "businessTypeVersionId" IS NOT NULL);
@@ -0,0 +1,860 @@
1
+ -- =============================================================================
2
+ -- SaaSicat — PostgreSQL reference schema (DERIVED ARTIFACT).
3
+ -- =============================================================================
4
+ --
5
+ -- Generated via `pnpm run gen:sql` from the prisma-fragments:
6
+ -- prisma-fragments/01-subscription.prisma
7
+ -- prisma-fragments/02-promo-code.prisma
8
+ -- prisma-fragments/03-plan-versions.prisma
9
+ -- prisma-fragments/04-audit-log.prisma
10
+ -- prisma-fragments/05-bundle-business-type.prisma
11
+ -- prisma-fragments/06-catalog-entries.prisma
12
+ -- prisma-fragments/07-promotion.prisma
13
+ -- prisma-fragments/08-subscription-contract.prisma
14
+ -- prisma-fragments/09-pending-registration.prisma
15
+ -- prisma-fragments/10-super-admin.prisma
16
+ -- plus the normative constraints from sql/constraints.postgres.sql.
17
+ -- Do not edit by hand — change the fragments/constraints and regenerate.
18
+
19
+ -- CreateSchema
20
+ CREATE SCHEMA IF NOT EXISTS "public";
21
+
22
+ -- CreateEnum
23
+ CREATE TYPE "BillingCycle" AS ENUM ('MONTHLY', 'YEARLY');
24
+
25
+ -- CreateEnum
26
+ CREATE TYPE "SubscriptionStatus" AS ENUM ('TRIAL', 'ACTIVE', 'PAST_DUE', 'CANCELED', 'PENDING_SALES');
27
+
28
+ -- CreateEnum
29
+ CREATE TYPE "SubscriptionPaymentType" AS ENUM ('CARD', 'SEPA', 'PAYPAL', 'KLARNA', 'INVOICE');
30
+
31
+ -- CreateEnum
32
+ CREATE TYPE "PromoCodeValueType" AS ENUM ('PERCENT', 'ABSOLUTE');
33
+
34
+ -- CreateEnum
35
+ CREATE TYPE "PromoCodeDurationType" AS ENUM ('ONCE', 'MONTHS', 'BILLING_CYCLES');
36
+
37
+ -- CreateEnum
38
+ CREATE TYPE "PromoCodeStatus" AS ENUM ('ACTIVE', 'PAUSED', 'EXHAUSTED', 'EXPIRED');
39
+
40
+ -- CreateEnum
41
+ CREATE TYPE "PromoCodeRedemptionStatus" AS ENUM ('ACTIVE', 'REVERSED', 'EXPIRED');
42
+
43
+ -- CreateEnum
44
+ CREATE TYPE "SubscriptionContractStatus" AS ENUM ('active', 'scheduled', 'terminated', 'superseded');
45
+
46
+ -- CreateEnum
47
+ CREATE TYPE "ContractLineItemKind" AS ENUM ('plan', 'bundle', 'quota', 'feature', 'discount');
48
+
49
+ -- CreateEnum
50
+ CREATE TYPE "RegistrationStatus" AS ENUM ('PENDING_EMAIL_VERIFICATION', 'EMAIL_VERIFIED', 'PLAN_SELECTED', 'CHECKOUT_STARTED', 'EXPIRED', 'DELETED');
51
+
52
+ -- CreateTable
53
+ CREATE TABLE "subscriptions" (
54
+ "id" TEXT NOT NULL,
55
+ "tenantId" TEXT NOT NULL,
56
+ "plan" TEXT NOT NULL,
57
+ "billingCycle" "BillingCycle" NOT NULL DEFAULT 'YEARLY',
58
+ "status" "SubscriptionStatus" NOT NULL DEFAULT 'TRIAL',
59
+ "trialEndsAt" TIMESTAMP(3),
60
+ "startedAt" TIMESTAMP(3),
61
+ "canceledAt" TIMESTAMP(3),
62
+ "currentPeriodStart" TIMESTAMP(3),
63
+ "currentPeriodEnd" TIMESTAMP(3),
64
+ "planVersionId" TEXT,
65
+ "pendingPlanVersionId" TEXT,
66
+ "pendingPlanVersionEffectiveAt" TIMESTAMP(3),
67
+ "pendingPlanVersionAccepted" BOOLEAN NOT NULL DEFAULT false,
68
+ "pendingPlanVersionAcceptedAt" TIMESTAMP(3),
69
+ "pendingPlanVersionAcceptedByUserId" TEXT,
70
+ "pendingPlanVersionNotifiedAt" TIMESTAMP(3),
71
+ "pendingPlanVersionReminderSentAt" TIMESTAMP(3),
72
+ "businessTypeVersionId" TEXT,
73
+ "trialEntitlementPlan" TEXT,
74
+ "postTrialPlan" TEXT,
75
+ "pendingPlan" TEXT,
76
+ "pendingBillingCycle" "BillingCycle",
77
+ "pendingEffectiveAt" TIMESTAMP(3),
78
+ "customMonthlyNet" DECIMAL(10,2),
79
+ "customLimits" JSONB,
80
+ "customNote" TEXT,
81
+ "isPilot" BOOLEAN NOT NULL DEFAULT false,
82
+ "pilotEndsAt" TIMESTAMP(3),
83
+ "pilotNote" TEXT,
84
+ "checkoutOfferId" TEXT,
85
+ "packageSnapshot" JSONB,
86
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
87
+ "updatedAt" TIMESTAMP(3) NOT NULL,
88
+
89
+ CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("id")
90
+ );
91
+
92
+ -- CreateTable
93
+ CREATE TABLE "subscription_payment_methods" (
94
+ "id" TEXT NOT NULL,
95
+ "subscriptionId" TEXT NOT NULL,
96
+ "type" "SubscriptionPaymentType" NOT NULL,
97
+ "cardName" TEXT,
98
+ "cardBrand" TEXT,
99
+ "cardLast4" TEXT,
100
+ "cardExp" TEXT,
101
+ "ibanLast4" TEXT,
102
+ "ibanName" TEXT,
103
+ "paypalEmail" TEXT,
104
+ "klarnaPlan" TEXT,
105
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
106
+ "updatedAt" TIMESTAMP(3) NOT NULL,
107
+
108
+ CONSTRAINT "subscription_payment_methods_pkey" PRIMARY KEY ("id")
109
+ );
110
+
111
+ -- CreateTable
112
+ CREATE TABLE "checkout_offers" (
113
+ "id" TEXT NOT NULL,
114
+ "projectKey" TEXT NOT NULL,
115
+ "planKey" TEXT NOT NULL,
116
+ "planVersionId" TEXT,
117
+ "billingCycle" TEXT NOT NULL,
118
+ "promotionId" TEXT,
119
+ "promoCode" TEXT,
120
+ "bundles" JSONB NOT NULL DEFAULT '[]',
121
+ "bundleVersionIds" JSONB NOT NULL DEFAULT '[]',
122
+ "priceBreakdown" JSONB NOT NULL,
123
+ "lineItems" JSONB NOT NULL DEFAULT '[]',
124
+ "promotionSnapshots" JSONB NOT NULL DEFAULT '[]',
125
+ "promoCodeSnapshot" JSONB,
126
+ "locale" TEXT NOT NULL DEFAULT 'de',
127
+ "validUntil" TIMESTAMP(3),
128
+ "status" TEXT NOT NULL DEFAULT 'open',
129
+ "consumedAt" TIMESTAMP(3),
130
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
131
+ "updatedAt" TIMESTAMP(3) NOT NULL,
132
+
133
+ CONSTRAINT "checkout_offers_pkey" PRIMARY KEY ("id")
134
+ );
135
+
136
+ -- CreateTable
137
+ CREATE TABLE "promo_codes" (
138
+ "id" TEXT NOT NULL,
139
+ "code" TEXT NOT NULL,
140
+ "valueType" "PromoCodeValueType" NOT NULL,
141
+ "value" DECIMAL(8,2) NOT NULL,
142
+ "durationType" "PromoCodeDurationType" NOT NULL DEFAULT 'ONCE',
143
+ "durationValue" INTEGER,
144
+ "validFrom" TIMESTAMP(3),
145
+ "validUntil" TIMESTAMP(3),
146
+ "maxRedemptions" INTEGER,
147
+ "redemptionsCount" INTEGER NOT NULL DEFAULT 0,
148
+ "appliesToPlans" TEXT[],
149
+ "appliesToBilling" "BillingCycle",
150
+ "firstTimeCustomersOnly" BOOLEAN NOT NULL DEFAULT true,
151
+ "minimumPlanAmountGross" DECIMAL(10,2),
152
+ "allowZeroInvoice" BOOLEAN NOT NULL DEFAULT false,
153
+ "status" "PromoCodeStatus" NOT NULL DEFAULT 'ACTIVE',
154
+ "description" TEXT,
155
+ "campaignTag" TEXT,
156
+ "revenueDeductionAccount" TEXT,
157
+ "createdById" TEXT NOT NULL,
158
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
159
+ "updatedAt" TIMESTAMP(3) NOT NULL,
160
+ "deletedAt" TIMESTAMP(3),
161
+
162
+ CONSTRAINT "promo_codes_pkey" PRIMARY KEY ("id")
163
+ );
164
+
165
+ -- CreateTable
166
+ CREATE TABLE "promo_code_redemptions" (
167
+ "id" TEXT NOT NULL,
168
+ "promoCodeId" TEXT NOT NULL,
169
+ "subscriptionId" TEXT NOT NULL,
170
+ "tenantId" TEXT NOT NULL,
171
+ "appliedValueType" "PromoCodeValueType" NOT NULL,
172
+ "appliedValue" DECIMAL(8,2) NOT NULL,
173
+ "appliedDurationType" "PromoCodeDurationType" NOT NULL,
174
+ "appliedDurationValue" INTEGER,
175
+ "startsAt" TIMESTAMP(3) NOT NULL,
176
+ "endsAt" TIMESTAMP(3),
177
+ "status" "PromoCodeRedemptionStatus" NOT NULL DEFAULT 'ACTIVE',
178
+ "redeemedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
179
+ "reversedAt" TIMESTAMP(3),
180
+
181
+ CONSTRAINT "promo_code_redemptions_pkey" PRIMARY KEY ("id")
182
+ );
183
+
184
+ -- CreateTable
185
+ CREATE TABLE "promo_code_validation_logs" (
186
+ "id" TEXT NOT NULL,
187
+ "promoCodeId" TEXT,
188
+ "codeAttempt" TEXT NOT NULL,
189
+ "ipHash" TEXT,
190
+ "sessionId" TEXT,
191
+ "result" TEXT NOT NULL,
192
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
193
+
194
+ CONSTRAINT "promo_code_validation_logs_pkey" PRIMARY KEY ("id")
195
+ );
196
+
197
+ -- CreateTable
198
+ CREATE TABLE "plans" (
199
+ "id" TEXT NOT NULL,
200
+ "projectKey" TEXT NOT NULL,
201
+ "planKey" TEXT NOT NULL,
202
+ "label" TEXT NOT NULL,
203
+ "description" TEXT,
204
+ "icon" TEXT,
205
+ "sortOrder" INTEGER NOT NULL DEFAULT 0,
206
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
207
+ "updatedAt" TIMESTAMP(3) NOT NULL,
208
+ "deletedAt" TIMESTAMP(3),
209
+
210
+ CONSTRAINT "plans_pkey" PRIMARY KEY ("id")
211
+ );
212
+
213
+ -- CreateTable
214
+ CREATE TABLE "plan_versions" (
215
+ "id" TEXT NOT NULL,
216
+ "planId" TEXT NOT NULL,
217
+ "version" INTEGER NOT NULL,
218
+ "baseVersionId" TEXT,
219
+ "features" JSONB NOT NULL,
220
+ "quotas" JSONB NOT NULL,
221
+ "monthlyNet" DECIMAL(10,2) NOT NULL,
222
+ "yearlyNet" DECIMAL(10,2) NOT NULL,
223
+ "marketed" BOOLEAN NOT NULL DEFAULT true,
224
+ "publishedAt" TIMESTAMP(3),
225
+ "supersededAt" TIMESTAMP(3),
226
+ "publishedChanges" JSONB,
227
+ "changeNote" TEXT NOT NULL,
228
+ "nonRegressive" BOOLEAN NOT NULL DEFAULT true,
229
+ "createdByUserId" TEXT,
230
+ "publishedByUserId" TEXT,
231
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
232
+ "updatedAt" TIMESTAMP(3) NOT NULL,
233
+
234
+ CONSTRAINT "plan_versions_pkey" PRIMARY KEY ("id")
235
+ );
236
+
237
+ -- CreateTable
238
+ CREATE TABLE "audit_logs" (
239
+ "id" TEXT NOT NULL,
240
+ "tenantId" TEXT,
241
+ "userId" TEXT,
242
+ "entity" TEXT NOT NULL,
243
+ "entityId" TEXT NOT NULL,
244
+ "action" TEXT NOT NULL,
245
+ "changes" JSONB,
246
+ "actorTag" TEXT,
247
+ "ipAddress" TEXT,
248
+ "userAgent" TEXT,
249
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
250
+
251
+ CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id")
252
+ );
253
+
254
+ -- CreateTable
255
+ CREATE TABLE "bundles" (
256
+ "id" TEXT NOT NULL,
257
+ "projectKey" TEXT NOT NULL,
258
+ "bundleKey" TEXT NOT NULL,
259
+ "label" TEXT NOT NULL,
260
+ "description" TEXT,
261
+ "icon" TEXT,
262
+ "sortOrder" INTEGER NOT NULL DEFAULT 0,
263
+ "i18n" JSONB NOT NULL DEFAULT '{}',
264
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
265
+ "updatedAt" TIMESTAMP(3) NOT NULL,
266
+ "deletedAt" TIMESTAMP(3),
267
+
268
+ CONSTRAINT "bundles_pkey" PRIMARY KEY ("id")
269
+ );
270
+
271
+ -- CreateTable
272
+ CREATE TABLE "bundle_versions" (
273
+ "id" TEXT NOT NULL,
274
+ "bundleId" TEXT NOT NULL,
275
+ "version" INTEGER NOT NULL,
276
+ "baseVersionId" TEXT,
277
+ "features" JSONB NOT NULL,
278
+ "quotas" JSONB NOT NULL DEFAULT '{}',
279
+ "compatibility" JSONB NOT NULL DEFAULT '{}',
280
+ "pricingOverrides" JSONB NOT NULL DEFAULT '[]',
281
+ "monthlyNet" DECIMAL(10,2),
282
+ "yearlyNet" DECIMAL(10,2),
283
+ "marketed" BOOLEAN NOT NULL DEFAULT true,
284
+ "publishedAt" TIMESTAMP(3),
285
+ "supersededAt" TIMESTAMP(3),
286
+ "publishedChanges" JSONB,
287
+ "changeNote" TEXT NOT NULL DEFAULT '',
288
+ "nonRegressive" BOOLEAN NOT NULL DEFAULT true,
289
+ "createdByUserId" TEXT,
290
+ "publishedByUserId" TEXT,
291
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
292
+ "updatedAt" TIMESTAMP(3) NOT NULL,
293
+
294
+ CONSTRAINT "bundle_versions_pkey" PRIMARY KEY ("id")
295
+ );
296
+
297
+ -- CreateTable
298
+ CREATE TABLE "business_types" (
299
+ "id" TEXT NOT NULL,
300
+ "projectKey" TEXT NOT NULL,
301
+ "businessTypeKey" TEXT NOT NULL,
302
+ "label" TEXT NOT NULL,
303
+ "description" TEXT,
304
+ "icon" TEXT,
305
+ "sortOrder" INTEGER NOT NULL DEFAULT 0,
306
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
307
+ "updatedAt" TIMESTAMP(3) NOT NULL,
308
+ "deletedAt" TIMESTAMP(3),
309
+
310
+ CONSTRAINT "business_types_pkey" PRIMARY KEY ("id")
311
+ );
312
+
313
+ -- CreateTable
314
+ CREATE TABLE "business_type_versions" (
315
+ "id" TEXT NOT NULL,
316
+ "businessTypeId" TEXT NOT NULL,
317
+ "version" INTEGER NOT NULL,
318
+ "baseVersionId" TEXT,
319
+ "quotaOverrides" JSONB NOT NULL DEFAULT '{}',
320
+ "monthlyNet" DECIMAL(10,2),
321
+ "yearlyNet" DECIMAL(10,2),
322
+ "marketed" BOOLEAN NOT NULL DEFAULT true,
323
+ "publishedAt" TIMESTAMP(3),
324
+ "supersededAt" TIMESTAMP(3),
325
+ "publishedChanges" JSONB,
326
+ "changeNote" TEXT NOT NULL DEFAULT '',
327
+ "nonRegressive" BOOLEAN NOT NULL DEFAULT true,
328
+ "createdByUserId" TEXT,
329
+ "publishedByUserId" TEXT,
330
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
331
+ "updatedAt" TIMESTAMP(3) NOT NULL,
332
+
333
+ CONSTRAINT "business_type_versions_pkey" PRIMARY KEY ("id")
334
+ );
335
+
336
+ -- CreateTable
337
+ CREATE TABLE "business_type_bundles" (
338
+ "businessTypeVersionId" TEXT NOT NULL,
339
+ "bundleVersionId" TEXT NOT NULL,
340
+ "sortOrder" INTEGER NOT NULL DEFAULT 0,
341
+
342
+ CONSTRAINT "business_type_bundles_pkey" PRIMARY KEY ("businessTypeVersionId","bundleVersionId")
343
+ );
344
+
345
+ -- CreateTable
346
+ CREATE TABLE "capability_catalog_entries" (
347
+ "id" TEXT NOT NULL,
348
+ "projectKey" TEXT NOT NULL,
349
+ "capabilityKey" TEXT NOT NULL,
350
+ "label" TEXT NOT NULL,
351
+ "description" TEXT,
352
+ "featureKey" TEXT,
353
+ "bundleKey" TEXT,
354
+ "codeStatus" TEXT NOT NULL DEFAULT 'active',
355
+ "owner" TEXT,
356
+ "kind" TEXT NOT NULL,
357
+ "replacementKey" TEXT,
358
+ "deprecatedAt" TIMESTAMP(3),
359
+ "removalPlannedAt" TIMESTAMP(3),
360
+ "reason" TEXT,
361
+ "i18n" JSONB NOT NULL DEFAULT '{}',
362
+ "sortOrder" INTEGER NOT NULL DEFAULT 0,
363
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
364
+ "updatedAt" TIMESTAMP(3) NOT NULL,
365
+ "deletedAt" TIMESTAMP(3),
366
+
367
+ CONSTRAINT "capability_catalog_entries_pkey" PRIMARY KEY ("id")
368
+ );
369
+
370
+ -- CreateTable
371
+ CREATE TABLE "feature_catalog_entries" (
372
+ "id" TEXT NOT NULL,
373
+ "projectKey" TEXT NOT NULL,
374
+ "featureKey" TEXT NOT NULL,
375
+ "label" TEXT NOT NULL,
376
+ "description" TEXT,
377
+ "marketingLabel" TEXT,
378
+ "marketingDescription" TEXT,
379
+ "icon" TEXT,
380
+ "tier" TEXT,
381
+ "core" BOOLEAN NOT NULL DEFAULT false,
382
+ "requires" TEXT[],
383
+ "replaces" TEXT[],
384
+ "successorKey" TEXT,
385
+ "discoveryStatus" TEXT NOT NULL DEFAULT 'pending',
386
+ "approvedAt" TIMESTAMP(3),
387
+ "approvedBy" TEXT,
388
+ "approvedSignature" TEXT,
389
+ "plannedOnly" BOOLEAN NOT NULL DEFAULT false,
390
+ "i18n" JSONB NOT NULL DEFAULT '{}',
391
+ "sortOrder" INTEGER NOT NULL DEFAULT 0,
392
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
393
+ "updatedAt" TIMESTAMP(3) NOT NULL,
394
+ "deletedAt" TIMESTAMP(3),
395
+
396
+ CONSTRAINT "feature_catalog_entries_pkey" PRIMARY KEY ("id")
397
+ );
398
+
399
+ -- CreateTable
400
+ CREATE TABLE "quota_catalog_entries" (
401
+ "id" TEXT NOT NULL,
402
+ "projectKey" TEXT NOT NULL,
403
+ "quotaKey" TEXT NOT NULL,
404
+ "label" TEXT NOT NULL,
405
+ "description" TEXT,
406
+ "unit" TEXT NOT NULL,
407
+ "featureKey" TEXT,
408
+ "usageProvider" TEXT,
409
+ "enforcementMode" TEXT NOT NULL DEFAULT 'soft',
410
+ "discoveryStatus" TEXT NOT NULL DEFAULT 'pending',
411
+ "approvedAt" TIMESTAMP(3),
412
+ "approvedBy" TEXT,
413
+ "approvedSignature" TEXT,
414
+ "i18n" JSONB NOT NULL DEFAULT '{}',
415
+ "sortOrder" INTEGER NOT NULL DEFAULT 0,
416
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
417
+ "updatedAt" TIMESTAMP(3) NOT NULL,
418
+ "deletedAt" TIMESTAMP(3),
419
+
420
+ CONSTRAINT "quota_catalog_entries_pkey" PRIMARY KEY ("id")
421
+ );
422
+
423
+ -- CreateTable
424
+ CREATE TABLE "marketing_projections" (
425
+ "id" TEXT NOT NULL,
426
+ "projectKey" TEXT NOT NULL,
427
+ "targetType" TEXT NOT NULL,
428
+ "targetVersionId" TEXT NOT NULL,
429
+ "locale" TEXT NOT NULL DEFAULT 'de',
430
+ "displayLabel" TEXT NOT NULL,
431
+ "description" TEXT NOT NULL,
432
+ "visible" BOOLEAN NOT NULL DEFAULT true,
433
+ "badge" TEXT NOT NULL DEFAULT '',
434
+ "topFeatures" JSONB NOT NULL DEFAULT '[]',
435
+ "trialEnabled" BOOLEAN NOT NULL DEFAULT false,
436
+ "trialDays" INTEGER NOT NULL DEFAULT 30,
437
+ "priceTag" TEXT,
438
+ "ctaLabel" TEXT,
439
+ "priority" INTEGER NOT NULL DEFAULT 0,
440
+ "highlight" BOOLEAN NOT NULL DEFAULT false,
441
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
442
+ "updatedAt" TIMESTAMP(3) NOT NULL,
443
+
444
+ CONSTRAINT "marketing_projections_pkey" PRIMARY KEY ("id")
445
+ );
446
+
447
+ -- CreateTable
448
+ CREATE TABLE "marketing_settings" (
449
+ "id" TEXT NOT NULL,
450
+ "projectKey" TEXT NOT NULL,
451
+ "activeLocales" JSONB NOT NULL DEFAULT '[]',
452
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
453
+ "updatedAt" TIMESTAMP(3) NOT NULL,
454
+
455
+ CONSTRAINT "marketing_settings_pkey" PRIMARY KEY ("id")
456
+ );
457
+
458
+ -- CreateTable
459
+ CREATE TABLE "promotions" (
460
+ "id" TEXT NOT NULL,
461
+ "projectKey" TEXT NOT NULL,
462
+ "internalLabel" TEXT NOT NULL,
463
+ "type" TEXT NOT NULL,
464
+ "value" JSONB NOT NULL,
465
+ "targetType" TEXT NOT NULL DEFAULT 'PLAN',
466
+ "appliesTo" JSONB NOT NULL DEFAULT '[]',
467
+ "billingCycle" TEXT NOT NULL DEFAULT 'both',
468
+ "validFrom" TIMESTAMP(3) NOT NULL,
469
+ "validTo" TIMESTAMP(3) NOT NULL,
470
+ "priority" INTEGER NOT NULL DEFAULT 0,
471
+ "onlyLocales" JSONB,
472
+ "requiresCoupon" BOOLEAN NOT NULL DEFAULT false,
473
+ "codes" JSONB NOT NULL DEFAULT '[]',
474
+ "color" TEXT NOT NULL DEFAULT '#2563eb',
475
+ "i18n" JSONB NOT NULL DEFAULT '{}',
476
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
477
+ "updatedAt" TIMESTAMP(3) NOT NULL,
478
+
479
+ CONSTRAINT "promotions_pkey" PRIMARY KEY ("id")
480
+ );
481
+
482
+ -- CreateTable
483
+ CREATE TABLE "subscription_contracts" (
484
+ "id" TEXT NOT NULL,
485
+ "projectKey" TEXT NOT NULL,
486
+ "tenantId" TEXT NOT NULL,
487
+ "status" "SubscriptionContractStatus" NOT NULL DEFAULT 'active',
488
+ "effectiveFrom" TIMESTAMP(3) NOT NULL,
489
+ "effectiveUntil" TIMESTAMP(3),
490
+ "originalOfferId" TEXT,
491
+ "originalPlanVersionId" TEXT,
492
+ "originalBundleVersionIds" JSONB NOT NULL DEFAULT '[]',
493
+ "entitlementSnapshot" JSONB,
494
+ "priceSnapshot" JSONB NOT NULL,
495
+ "promotionSnapshots" JSONB NOT NULL DEFAULT '[]',
496
+ "promoCodeSnapshots" JSONB NOT NULL DEFAULT '[]',
497
+ "termsSnapshot" JSONB,
498
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
499
+ "updatedAt" TIMESTAMP(3) NOT NULL,
500
+
501
+ CONSTRAINT "subscription_contracts_pkey" PRIMARY KEY ("id")
502
+ );
503
+
504
+ -- CreateTable
505
+ CREATE TABLE "contract_line_items" (
506
+ "id" TEXT NOT NULL,
507
+ "contractId" TEXT NOT NULL,
508
+ "kind" "ContractLineItemKind" NOT NULL,
509
+ "sourceKey" TEXT NOT NULL,
510
+ "sourceVersionId" TEXT,
511
+ "titleSnapshot" TEXT NOT NULL,
512
+ "descriptionSnapshot" TEXT,
513
+ "quantity" INTEGER NOT NULL DEFAULT 1,
514
+ "unit" TEXT,
515
+ "priceNet" DECIMAL(10,2) NOT NULL,
516
+ "priceGross" DECIMAL(10,2) NOT NULL,
517
+ "billingCycle" TEXT NOT NULL,
518
+ "minimumTermUntil" TIMESTAMP(3),
519
+ "featuresSnapshot" JSONB NOT NULL DEFAULT '[]',
520
+ "quotaEffectsSnapshot" JSONB NOT NULL DEFAULT '{}',
521
+ "metadata" JSONB,
522
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
523
+
524
+ CONSTRAINT "contract_line_items_pkey" PRIMARY KEY ("id")
525
+ );
526
+
527
+ -- CreateTable
528
+ CREATE TABLE "PendingRegistration" (
529
+ "id" TEXT NOT NULL,
530
+ "tenantName" TEXT NOT NULL,
531
+ "tenantSlug" TEXT,
532
+ "salutation" TEXT,
533
+ "firstName" TEXT NOT NULL,
534
+ "lastName" TEXT NOT NULL,
535
+ "email" TEXT NOT NULL,
536
+ "passwordHash" TEXT NOT NULL,
537
+ "locale" TEXT NOT NULL DEFAULT 'de',
538
+ "status" "RegistrationStatus" NOT NULL DEFAULT 'PENDING_EMAIL_VERIFICATION',
539
+ "currentStep" INTEGER NOT NULL DEFAULT 2,
540
+ "emailVerifiedAt" TIMESTAMP(3),
541
+ "otpHash" TEXT,
542
+ "otpExpiresAt" TIMESTAMP(3),
543
+ "otpSendCount" INTEGER NOT NULL DEFAULT 1,
544
+ "lastOtpSentAt" TIMESTAMP(3),
545
+ "otpAttemptCount" INTEGER NOT NULL DEFAULT 0,
546
+ "selectedPlanId" TEXT,
547
+ "configJson" JSONB,
548
+ "billingCycle" TEXT,
549
+ "appliedPromoCode" TEXT,
550
+ "checkoutSessionId" TEXT,
551
+ "checkoutStartedAt" TIMESTAMP(3),
552
+ "expiresAt" TIMESTAMP(3) NOT NULL,
553
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
554
+ "updatedAt" TIMESTAMP(3) NOT NULL,
555
+
556
+ CONSTRAINT "PendingRegistration_pkey" PRIMARY KEY ("id")
557
+ );
558
+
559
+ -- CreateTable
560
+ CREATE TABLE "PaymentEventLog" (
561
+ "id" TEXT NOT NULL,
562
+ "eventId" TEXT NOT NULL,
563
+ "provider" TEXT NOT NULL,
564
+ "sessionId" TEXT,
565
+ "status" TEXT NOT NULL,
566
+ "payload" JSONB,
567
+ "processedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
568
+
569
+ CONSTRAINT "PaymentEventLog_pkey" PRIMARY KEY ("id")
570
+ );
571
+
572
+ -- CreateTable
573
+ CREATE TABLE "super_admin_users" (
574
+ "id" TEXT NOT NULL,
575
+ "email" TEXT NOT NULL,
576
+ "passwordHash" TEXT NOT NULL,
577
+ "firstName" TEXT,
578
+ "lastName" TEXT,
579
+ "platformRole" TEXT NOT NULL DEFAULT 'SUPER_ADMIN',
580
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
581
+ "lastLoginAt" TIMESTAMP(3),
582
+ "deletedAt" TIMESTAMP(3),
583
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
584
+ "updatedAt" TIMESTAMP(3) NOT NULL,
585
+
586
+ CONSTRAINT "super_admin_users_pkey" PRIMARY KEY ("id")
587
+ );
588
+
589
+ -- CreateTable
590
+ CREATE TABLE "super_admin_mfa" (
591
+ "userId" TEXT NOT NULL,
592
+ "secret" TEXT,
593
+ "enabledAt" TIMESTAMP(3),
594
+ "updatedAt" TIMESTAMP(3) NOT NULL,
595
+
596
+ CONSTRAINT "super_admin_mfa_pkey" PRIMARY KEY ("userId")
597
+ );
598
+
599
+ -- CreateIndex
600
+ CREATE UNIQUE INDEX "subscriptions_tenantId_key" ON "subscriptions"("tenantId");
601
+
602
+ -- CreateIndex
603
+ CREATE INDEX "subscriptions_tenantId_idx" ON "subscriptions"("tenantId");
604
+
605
+ -- CreateIndex
606
+ CREATE INDEX "subscriptions_planVersionId_idx" ON "subscriptions"("planVersionId");
607
+
608
+ -- CreateIndex
609
+ CREATE INDEX "subscriptions_pendingPlanVersionId_idx" ON "subscriptions"("pendingPlanVersionId");
610
+
611
+ -- CreateIndex
612
+ CREATE INDEX "subscriptions_businessTypeVersionId_idx" ON "subscriptions"("businessTypeVersionId");
613
+
614
+ -- CreateIndex
615
+ CREATE INDEX "subscriptions_currentPeriodEnd_idx" ON "subscriptions"("currentPeriodEnd");
616
+
617
+ -- CreateIndex
618
+ CREATE UNIQUE INDEX "subscription_payment_methods_subscriptionId_key" ON "subscription_payment_methods"("subscriptionId");
619
+
620
+ -- CreateIndex
621
+ CREATE INDEX "checkout_offers_projectKey_status_idx" ON "checkout_offers"("projectKey", "status");
622
+
623
+ -- CreateIndex
624
+ CREATE UNIQUE INDEX "promo_codes_code_key" ON "promo_codes"("code");
625
+
626
+ -- CreateIndex
627
+ CREATE INDEX "promo_codes_status_validFrom_validUntil_idx" ON "promo_codes"("status", "validFrom", "validUntil");
628
+
629
+ -- CreateIndex
630
+ CREATE INDEX "promo_codes_campaignTag_idx" ON "promo_codes"("campaignTag");
631
+
632
+ -- CreateIndex
633
+ CREATE UNIQUE INDEX "promo_code_redemptions_subscriptionId_key" ON "promo_code_redemptions"("subscriptionId");
634
+
635
+ -- CreateIndex
636
+ CREATE INDEX "promo_code_redemptions_tenantId_idx" ON "promo_code_redemptions"("tenantId");
637
+
638
+ -- CreateIndex
639
+ CREATE INDEX "promo_code_redemptions_promoCodeId_status_idx" ON "promo_code_redemptions"("promoCodeId", "status");
640
+
641
+ -- CreateIndex
642
+ CREATE INDEX "promo_code_redemptions_startsAt_endsAt_idx" ON "promo_code_redemptions"("startsAt", "endsAt");
643
+
644
+ -- CreateIndex
645
+ CREATE INDEX "promo_code_validation_logs_codeAttempt_createdAt_idx" ON "promo_code_validation_logs"("codeAttempt", "createdAt");
646
+
647
+ -- CreateIndex
648
+ CREATE INDEX "promo_code_validation_logs_ipHash_createdAt_idx" ON "promo_code_validation_logs"("ipHash", "createdAt");
649
+
650
+ -- CreateIndex
651
+ CREATE INDEX "promo_code_validation_logs_sessionId_createdAt_idx" ON "promo_code_validation_logs"("sessionId", "createdAt");
652
+
653
+ -- CreateIndex
654
+ CREATE INDEX "plans_projectKey_deletedAt_idx" ON "plans"("projectKey", "deletedAt");
655
+
656
+ -- CreateIndex
657
+ CREATE UNIQUE INDEX "plans_projectKey_planKey_key" ON "plans"("projectKey", "planKey");
658
+
659
+ -- CreateIndex
660
+ CREATE INDEX "plan_versions_planId_supersededAt_idx" ON "plan_versions"("planId", "supersededAt");
661
+
662
+ -- CreateIndex
663
+ CREATE INDEX "plan_versions_planId_publishedAt_idx" ON "plan_versions"("planId", "publishedAt");
664
+
665
+ -- CreateIndex
666
+ CREATE UNIQUE INDEX "plan_versions_planId_version_key" ON "plan_versions"("planId", "version");
667
+
668
+ -- CreateIndex
669
+ CREATE INDEX "audit_logs_tenantId_entity_entityId_idx" ON "audit_logs"("tenantId", "entity", "entityId");
670
+
671
+ -- CreateIndex
672
+ CREATE INDEX "audit_logs_tenantId_createdAt_idx" ON "audit_logs"("tenantId", "createdAt");
673
+
674
+ -- CreateIndex
675
+ CREATE INDEX "audit_logs_entity_action_createdAt_idx" ON "audit_logs"("entity", "action", "createdAt");
676
+
677
+ -- CreateIndex
678
+ CREATE INDEX "audit_logs_actorTag_createdAt_idx" ON "audit_logs"("actorTag", "createdAt");
679
+
680
+ -- CreateIndex
681
+ CREATE INDEX "bundles_projectKey_deletedAt_idx" ON "bundles"("projectKey", "deletedAt");
682
+
683
+ -- CreateIndex
684
+ CREATE UNIQUE INDEX "bundles_projectKey_bundleKey_key" ON "bundles"("projectKey", "bundleKey");
685
+
686
+ -- CreateIndex
687
+ CREATE INDEX "bundle_versions_bundleId_supersededAt_idx" ON "bundle_versions"("bundleId", "supersededAt");
688
+
689
+ -- CreateIndex
690
+ CREATE INDEX "bundle_versions_bundleId_publishedAt_idx" ON "bundle_versions"("bundleId", "publishedAt");
691
+
692
+ -- CreateIndex
693
+ CREATE UNIQUE INDEX "bundle_versions_bundleId_version_key" ON "bundle_versions"("bundleId", "version");
694
+
695
+ -- CreateIndex
696
+ CREATE INDEX "business_types_projectKey_deletedAt_idx" ON "business_types"("projectKey", "deletedAt");
697
+
698
+ -- CreateIndex
699
+ CREATE UNIQUE INDEX "business_types_projectKey_businessTypeKey_key" ON "business_types"("projectKey", "businessTypeKey");
700
+
701
+ -- CreateIndex
702
+ CREATE INDEX "business_type_versions_businessTypeId_supersededAt_idx" ON "business_type_versions"("businessTypeId", "supersededAt");
703
+
704
+ -- CreateIndex
705
+ CREATE INDEX "business_type_versions_businessTypeId_publishedAt_idx" ON "business_type_versions"("businessTypeId", "publishedAt");
706
+
707
+ -- CreateIndex
708
+ CREATE UNIQUE INDEX "business_type_versions_businessTypeId_version_key" ON "business_type_versions"("businessTypeId", "version");
709
+
710
+ -- CreateIndex
711
+ CREATE INDEX "business_type_bundles_bundleVersionId_idx" ON "business_type_bundles"("bundleVersionId");
712
+
713
+ -- CreateIndex
714
+ CREATE INDEX "capability_catalog_entries_projectKey_codeStatus_idx" ON "capability_catalog_entries"("projectKey", "codeStatus");
715
+
716
+ -- CreateIndex
717
+ CREATE INDEX "capability_catalog_entries_projectKey_featureKey_idx" ON "capability_catalog_entries"("projectKey", "featureKey");
718
+
719
+ -- CreateIndex
720
+ CREATE UNIQUE INDEX "capability_catalog_entries_projectKey_capabilityKey_key" ON "capability_catalog_entries"("projectKey", "capabilityKey");
721
+
722
+ -- CreateIndex
723
+ CREATE INDEX "feature_catalog_entries_projectKey_discoveryStatus_idx" ON "feature_catalog_entries"("projectKey", "discoveryStatus");
724
+
725
+ -- CreateIndex
726
+ CREATE INDEX "feature_catalog_entries_projectKey_plannedOnly_idx" ON "feature_catalog_entries"("projectKey", "plannedOnly");
727
+
728
+ -- CreateIndex
729
+ CREATE UNIQUE INDEX "feature_catalog_entries_projectKey_featureKey_key" ON "feature_catalog_entries"("projectKey", "featureKey");
730
+
731
+ -- CreateIndex
732
+ CREATE INDEX "quota_catalog_entries_projectKey_discoveryStatus_idx" ON "quota_catalog_entries"("projectKey", "discoveryStatus");
733
+
734
+ -- CreateIndex
735
+ CREATE UNIQUE INDEX "quota_catalog_entries_projectKey_quotaKey_key" ON "quota_catalog_entries"("projectKey", "quotaKey");
736
+
737
+ -- CreateIndex
738
+ CREATE INDEX "marketing_projections_projectKey_targetType_locale_priority_idx" ON "marketing_projections"("projectKey", "targetType", "locale", "priority");
739
+
740
+ -- CreateIndex
741
+ CREATE UNIQUE INDEX "marketing_projections_targetType_targetVersionId_locale_key" ON "marketing_projections"("targetType", "targetVersionId", "locale");
742
+
743
+ -- CreateIndex
744
+ CREATE UNIQUE INDEX "marketing_settings_projectKey_key" ON "marketing_settings"("projectKey");
745
+
746
+ -- CreateIndex
747
+ CREATE INDEX "promotions_projectKey_targetType_validFrom_validTo_idx" ON "promotions"("projectKey", "targetType", "validFrom", "validTo");
748
+
749
+ -- CreateIndex
750
+ CREATE INDEX "subscription_contracts_tenantId_status_effectiveFrom_idx" ON "subscription_contracts"("tenantId", "status", "effectiveFrom");
751
+
752
+ -- CreateIndex
753
+ CREATE INDEX "subscription_contracts_projectKey_status_idx" ON "subscription_contracts"("projectKey", "status");
754
+
755
+ -- CreateIndex
756
+ CREATE INDEX "subscription_contracts_originalOfferId_idx" ON "subscription_contracts"("originalOfferId");
757
+
758
+ -- CreateIndex
759
+ CREATE INDEX "contract_line_items_contractId_kind_idx" ON "contract_line_items"("contractId", "kind");
760
+
761
+ -- CreateIndex
762
+ CREATE INDEX "contract_line_items_sourceVersionId_idx" ON "contract_line_items"("sourceVersionId");
763
+
764
+ -- CreateIndex
765
+ CREATE UNIQUE INDEX "PendingRegistration_email_key" ON "PendingRegistration"("email");
766
+
767
+ -- CreateIndex
768
+ CREATE INDEX "PendingRegistration_status_expiresAt_idx" ON "PendingRegistration"("status", "expiresAt");
769
+
770
+ -- CreateIndex
771
+ CREATE INDEX "PendingRegistration_tenantSlug_idx" ON "PendingRegistration"("tenantSlug");
772
+
773
+ -- CreateIndex
774
+ CREATE UNIQUE INDEX "PaymentEventLog_eventId_key" ON "PaymentEventLog"("eventId");
775
+
776
+ -- CreateIndex
777
+ CREATE INDEX "PaymentEventLog_sessionId_idx" ON "PaymentEventLog"("sessionId");
778
+
779
+ -- CreateIndex
780
+ CREATE INDEX "PaymentEventLog_status_processedAt_idx" ON "PaymentEventLog"("status", "processedAt");
781
+
782
+ -- CreateIndex
783
+ CREATE UNIQUE INDEX "super_admin_users_email_key" ON "super_admin_users"("email");
784
+
785
+ -- CreateIndex
786
+ CREATE INDEX "super_admin_users_isActive_deletedAt_idx" ON "super_admin_users"("isActive", "deletedAt");
787
+
788
+ -- AddForeignKey
789
+ ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_planVersionId_fkey" FOREIGN KEY ("planVersionId") REFERENCES "plan_versions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
790
+
791
+ -- AddForeignKey
792
+ ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_pendingPlanVersionId_fkey" FOREIGN KEY ("pendingPlanVersionId") REFERENCES "plan_versions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
793
+
794
+ -- AddForeignKey
795
+ ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_businessTypeVersionId_fkey" FOREIGN KEY ("businessTypeVersionId") REFERENCES "business_type_versions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
796
+
797
+ -- AddForeignKey
798
+ ALTER TABLE "subscription_payment_methods" ADD CONSTRAINT "subscription_payment_methods_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "subscriptions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
799
+
800
+ -- AddForeignKey
801
+ ALTER TABLE "promo_code_redemptions" ADD CONSTRAINT "promo_code_redemptions_promoCodeId_fkey" FOREIGN KEY ("promoCodeId") REFERENCES "promo_codes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
802
+
803
+ -- AddForeignKey
804
+ ALTER TABLE "promo_code_redemptions" ADD CONSTRAINT "promo_code_redemptions_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "subscriptions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
805
+
806
+ -- AddForeignKey
807
+ ALTER TABLE "promo_code_validation_logs" ADD CONSTRAINT "promo_code_validation_logs_promoCodeId_fkey" FOREIGN KEY ("promoCodeId") REFERENCES "promo_codes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
808
+
809
+ -- AddForeignKey
810
+ ALTER TABLE "plan_versions" ADD CONSTRAINT "plan_versions_baseVersionId_fkey" FOREIGN KEY ("baseVersionId") REFERENCES "plan_versions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
811
+
812
+ -- AddForeignKey
813
+ ALTER TABLE "bundle_versions" ADD CONSTRAINT "bundle_versions_bundleId_fkey" FOREIGN KEY ("bundleId") REFERENCES "bundles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
814
+
815
+ -- AddForeignKey
816
+ ALTER TABLE "bundle_versions" ADD CONSTRAINT "bundle_versions_baseVersionId_fkey" FOREIGN KEY ("baseVersionId") REFERENCES "bundle_versions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
817
+
818
+ -- AddForeignKey
819
+ ALTER TABLE "business_type_versions" ADD CONSTRAINT "business_type_versions_businessTypeId_fkey" FOREIGN KEY ("businessTypeId") REFERENCES "business_types"("id") ON DELETE CASCADE ON UPDATE CASCADE;
820
+
821
+ -- AddForeignKey
822
+ ALTER TABLE "business_type_versions" ADD CONSTRAINT "business_type_versions_baseVersionId_fkey" FOREIGN KEY ("baseVersionId") REFERENCES "business_type_versions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
823
+
824
+ -- AddForeignKey
825
+ ALTER TABLE "business_type_bundles" ADD CONSTRAINT "business_type_bundles_businessTypeVersionId_fkey" FOREIGN KEY ("businessTypeVersionId") REFERENCES "business_type_versions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
826
+
827
+ -- AddForeignKey
828
+ ALTER TABLE "business_type_bundles" ADD CONSTRAINT "business_type_bundles_bundleVersionId_fkey" FOREIGN KEY ("bundleVersionId") REFERENCES "bundle_versions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
829
+
830
+ -- AddForeignKey
831
+ ALTER TABLE "contract_line_items" ADD CONSTRAINT "contract_line_items_contractId_fkey" FOREIGN KEY ("contractId") REFERENCES "subscription_contracts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
832
+
833
+ -- =============================================================================
834
+ -- SaaSicat — normative PostgreSQL constraints the Prisma DSL cannot express.
835
+ -- =============================================================================
836
+ --
837
+ -- Consumers add these statements to their SQL migration after creating the
838
+ -- tables from the prisma-fragments. The adapter contract tests
839
+ -- (@saasicat/persistence-testing) run against a database with these
840
+ -- constraints applied — they are part of the canonical schema, not optional
841
+ -- hardening.
842
+ --
843
+ -- Column names are camelCase (the fragments map table names via @@map but
844
+ -- keep Prisma's default field→column naming), hence the quoting.
845
+
846
+ -- At most ONE draft (publishedAt IS NULL) per version lineage.
847
+ CREATE UNIQUE INDEX IF NOT EXISTS plan_versions_draft_per_plan
848
+ ON plan_versions ("planId") WHERE "publishedAt" IS NULL;
849
+
850
+ CREATE UNIQUE INDEX IF NOT EXISTS bundle_versions_draft_per_bundle
851
+ ON bundle_versions ("bundleId") WHERE "publishedAt" IS NULL;
852
+
853
+ CREATE UNIQUE INDEX IF NOT EXISTS business_type_versions_draft_per_business_type
854
+ ON business_type_versions ("businessTypeId") WHERE "publishedAt" IS NULL;
855
+
856
+ -- A subscription binds a PlanVersion, a BusinessTypeVersion, or both —
857
+ -- never neither (SPEC_V2 §11.1 M5).
858
+ ALTER TABLE subscriptions
859
+ ADD CONSTRAINT subscriptions_plan_or_bt_check
860
+ CHECK ("planVersionId" IS NOT NULL OR "businessTypeVersionId" IS NOT NULL);