@saasicat/spec 0.5.0 → 0.6.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.
@@ -72,13 +72,8 @@ model Subscription {
72
72
  // same plan; `pendingPlan` the change to a different plan. Both
73
73
  // can coexist.
74
74
  //
75
- // Since SPEC_V2 §11.1 M5, `planVersionId` is **nullable**: a
76
- // Subscription may have either `planVersionId` or
77
- // `businessTypeVersionId` (or both) set. The DB CHECK
78
- // constraint "at least one of the two set" is added by the consumer in
79
- // the SQL migration (Prisma cannot express this directly) — see
80
- // sql/constraints.postgres.sql (`subscriptions_plan_or_bt_check`).
81
- planVersionId String?
75
+ // Every subscription binds a concrete published PlanVersion.
76
+ planVersionId String
82
77
  pendingPlanVersionId String?
83
78
  pendingPlanVersionEffectiveAt DateTime?
84
79
  pendingPlanVersionAccepted Boolean @default(false)
@@ -87,14 +82,6 @@ model Subscription {
87
82
  pendingPlanVersionNotifiedAt DateTime?
88
83
  pendingPlanVersionReminderSentAt DateTime?
89
84
 
90
- // BusinessType composition (SPEC_V2 §11.1 M5). Optional per app: apps
91
- // with a pure plan model leave the field at null; apps with domain
92
- // verticals (e.g. club types)
93
- // set the Subscription to a concrete published BusinessTypeVersion.
94
- // Aggregation in EntitlementService.computeLimits see
95
- // GESCHAEFTSTYP_SPEC.md §6.
96
- businessTypeVersionId String?
97
-
98
85
  // Trial / scheduled plan changes
99
86
  trialEntitlementPlan String? // Default entitlement during TRIAL
100
87
  postTrialPlan String? // Target package after trial
@@ -125,14 +112,14 @@ model Subscription {
125
112
  // tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
126
113
  paymentMethod SubscriptionPaymentMethod?
127
114
  promoRedemption PromoCodeRedemption? // see 02-promo-code.prisma
128
- planVersion PlanVersion? @relation("SubscriptionPlanVersion", fields: [planVersionId], references: [id])
115
+ planVersion PlanVersion @relation("SubscriptionPlanVersion", fields: [planVersionId], references: [id])
129
116
  pendingPlanVersion PlanVersion? @relation("SubscriptionPendingPlanVersion", fields: [pendingPlanVersionId], references: [id])
130
- businessTypeVersion BusinessTypeVersion? @relation("SubscriptionBusinessTypeVersion", fields: [businessTypeVersionId], references: [id])
117
+ // Standalone add-on bookings (see 11-subscription-bundle.prisma).
118
+ bundles SubscriptionBundle[]
131
119
 
132
120
  @@index([tenantId])
133
121
  @@index([planVersionId])
134
122
  @@index([pendingPlanVersionId])
135
- @@index([businessTypeVersionId])
136
123
  @@index([currentPeriodEnd])
137
124
  @@map("subscriptions")
138
125
  }
@@ -27,7 +27,7 @@
27
27
  //
28
28
  // Soft-delete via `deletedAt`. A deleted plan root hides its
29
29
  // PlanVersions in the UI, but keeps them effective for existing subscriptions
30
- // (contract protection P1) — analogous to Bundle/BusinessType.
30
+ // (contract protection P1) — analogous to Bundle.
31
31
  // -----------------------------------------------------------------------------
32
32
 
33
33
  model Plan {
@@ -0,0 +1,96 @@
1
+ // =============================================================================
2
+ // SaaS platform Prisma fragment: Bundle (plan catalog extension)
3
+ // =============================================================================
4
+ //
5
+ // REFERENCE SNIPPET — see 03-plan-versions.prisma for versioning conventions.
6
+ //
7
+ // Global catalog tables (no tenantId, no RLS). Write access only via the
8
+ // SUPER_ADMIN guard in the AdminController of the `@saasicat/nest`
9
+ // package. Entries are assigned to the respective consumer app via
10
+ // `projectKey`.
11
+ //
12
+ // Versioning pattern identical to PlanVersion:
13
+ // - at most 1 draft (publishedAt IS NULL) per identity — constraint via a
14
+ // partial unique index in the migration.
15
+ // - multiple published versions, monotonically incremental `version`.
16
+ // - `supersededAt` marks "no longer actively marketed"; existing bookings
17
+ // stay on the committed version (contractual plan guarantee P1).
18
+ //
19
+ // -----------------------------------------------------------------------------
20
+ // Bundle — reusable component of features + quotas + pricing.
21
+ //
22
+ // Root entity without content — the purchasable fields (features, quotas,
23
+ // pricing) live on BundleVersion.
24
+ // -----------------------------------------------------------------------------
25
+
26
+ model Bundle {
27
+ id String @id @default(uuid())
28
+ projectKey String // e.g. 'clubapp' | 'demoapp' | …
29
+ bundleKey String // SCREAMING_SNAKE_CASE, e.g. 'SPORT', 'ACCOUNTING', 'EVERSEND_PEPPOL'
30
+ label String
31
+ description String?
32
+ icon String?
33
+ sortOrder Int @default(0)
34
+
35
+ // Locale translations { "en": { label, description }, … }. SPEC_V2 §6.4.
36
+ i18n Json @default("{}")
37
+
38
+ createdAt DateTime @default(now())
39
+ updatedAt DateTime @updatedAt
40
+ deletedAt DateTime?
41
+
42
+ versions BundleVersion[]
43
+
44
+ @@unique([projectKey, bundleKey])
45
+ @@index([projectKey, deletedAt])
46
+ @@map("bundles")
47
+ }
48
+
49
+ // -----------------------------------------------------------------------------
50
+ // BundleVersion — versioned composition (features, quotas, pricing).
51
+ // `features` is FeatureKey[] (references Discovery / FeatureCatalogEntry).
52
+ // `quotas` is Record<QuotaKey, number>; -1 = unlimited; missing key = 0.
53
+ // `compatibility` is BundleCompatibility (`planIds?` whitelist). Empty =
54
+ // usable with every plan. `pricingOverrides` is an array of
55
+ // BundlePricingOverride values for plan-dependent prices.
56
+ // -----------------------------------------------------------------------------
57
+
58
+ model BundleVersion {
59
+ id String @id @default(uuid())
60
+ bundleId String
61
+ version Int
62
+ baseVersionId String? // predecessor; null for v1
63
+
64
+ features Json // FeatureKey[]
65
+ quotas Json @default("{}") // Record<QuotaKey, number>
66
+ compatibility Json @default("{}") // BundleCompatibility
67
+ pricingOverrides Json @default("[]") // BundlePricingOverride[]
68
+
69
+ monthlyNet Decimal? @db.Decimal(10, 2) // default price; null = only via override
70
+ yearlyNet Decimal? @db.Decimal(10, 2)
71
+ marketed Boolean @default(true)
72
+
73
+ publishedAt DateTime?
74
+ supersededAt DateTime?
75
+ publishedChanges Json? // VersionChange[] — diff to predecessor version
76
+ changeNote String @default("")
77
+ nonRegressive Boolean @default(true)
78
+
79
+ createdByUserId String?
80
+ publishedByUserId String?
81
+
82
+ createdAt DateTime @default(now())
83
+ updatedAt DateTime @updatedAt
84
+
85
+ bundle Bundle @relation(fields: [bundleId], references: [id], onDelete: Cascade)
86
+ baseVersion BundleVersion? @relation("BundleVersionLineage", fields: [baseVersionId], references: [id])
87
+ derivedVersions BundleVersion[] @relation("BundleVersionLineage")
88
+ // Standalone add-on bookings pinned to this version (see
89
+ // 11-subscription-bundle.prisma).
90
+ subscriptionBundles SubscriptionBundle[]
91
+
92
+ @@unique([bundleId, version])
93
+ @@index([bundleId, supersededAt])
94
+ @@index([bundleId, publishedAt])
95
+ @@map("bundle_versions")
96
+ }
@@ -9,7 +9,7 @@
9
9
  // capability (discovery projection)
10
10
  // - FeatureCatalogEntry — ditto for features + marketing fields
11
11
  // - MarketingProjection — marketing texts/pricing/highlights per
12
- // plan/bundle/business-type version (locale pivot)
12
+ // plan/bundle version (locale pivot)
13
13
  //
14
14
  // Approval lifecycle (#20, feature-/quota-centric):
15
15
  // pending → approved (↔ revoke) · outdated (drift) · obsolete
@@ -179,6 +179,11 @@ model QuotaCatalogEntry {
179
179
  // Approval lifecycle (#20): pending | approved | outdated | obsolete.
180
180
  discoveryStatus String @default("pending")
181
181
 
182
+ // Predecessor quotaKeys this quota replaces + a successor pointer (#39),
183
+ // analogous to FeatureCatalogEntry. The discovery sync overwrites both.
184
+ replaces String[]
185
+ successorKey String?
186
+
182
187
  // Approval signature (#20): code-derived quota facts at approval time
183
188
  // ('unit|enforcementMode|usageProvider|featureKey'); on drift approved →
184
189
  // outdated.
@@ -202,7 +207,7 @@ model QuotaCatalogEntry {
202
207
 
203
208
  // -----------------------------------------------------------------------------
204
209
  // MarketingProjection — locale-specific marketing texts per
205
- // plan/bundle/business-type version.
210
+ // plan/bundle version.
206
211
  //
207
212
  // Read and projected by the public catalog controller
208
213
  // (`GET /public/catalog?locale=de`). Only versions with status `marketed` and
@@ -222,8 +227,8 @@ model MarketingProjection {
222
227
  projectKey String
223
228
 
224
229
  // Polymorphic reference to a versioned entity.
225
- targetType String // 'PLAN' | 'BUNDLE' | 'BUSINESS_TYPE'
226
- targetVersionId String // PlanVersion.id | BundleVersion.id | BusinessTypeVersion.id
230
+ targetType String // 'PLAN' | 'BUNDLE'
231
+ targetVersionId String // PlanVersion.id | BundleVersion.id
227
232
 
228
233
  locale String @default("de") // ISO-639-1, optionally with a region suffix ('de', 'en', 'de-AT')
229
234
 
@@ -0,0 +1,49 @@
1
+ // =============================================================================
2
+ // SaaS-Platform Prisma fragment: SubscriptionBundle (standalone add-on booking)
3
+ // =============================================================================
4
+ //
5
+ // REFERENCE SNIPPET — not a standalone schema. Consumers copy the model into
6
+ // their `schema.prisma`.
7
+ //
8
+ // A SubscriptionBundle is a bundle booked ON TOP of a subscription's plan —
9
+ // the tenant "bundle store". It pins a concrete BundleVersion (features/quotas
10
+ // frozen at booking), carries its own minimum term and cancellation window,
11
+ // and is billed independently of the plan.
12
+ //
13
+ // Accompanying fragments:
14
+ // - 01-subscription.prisma (Subscription — the FK target)
15
+ // - 05-bundle.prisma (BundleVersion — the pinned version)
16
+ //
17
+ // Conventions: see 01-subscription.prisma. `@@map` name is canonical — do not
18
+ // rename. The platform's `SubscriptionBundleRepository` adapter and the tenant
19
+ // bundle-store endpoints depend on it.
20
+
21
+ model SubscriptionBundle {
22
+ id String @id @default(uuid())
23
+ subscriptionId String
24
+ bundleVersionId String
25
+
26
+ startedAt DateTime
27
+
28
+ // Minimum contractual term; a cancellation before this date takes effect
29
+ // only at `minimumTermEndsAt`.
30
+ minimumTermEndsAt DateTime?
31
+
32
+ // Cancellation: requested at `canceledAt`, effective at
33
+ // `canceledEffectiveAt` (period/term end). Both null = active booking.
34
+ canceledAt DateTime?
35
+ canceledEffectiveAt DateTime?
36
+
37
+ createdAt DateTime @default(now())
38
+ updatedAt DateTime @updatedAt
39
+
40
+ // The subscription this add-on is booked on. Restrict the BundleVersion so
41
+ // a booked version cannot be hard-deleted from the catalog.
42
+ subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
43
+ bundleVersion BundleVersion @relation(fields: [bundleVersionId], references: [id], onDelete: Restrict)
44
+
45
+ @@index([subscriptionId])
46
+ @@index([bundleVersionId])
47
+ @@index([canceledEffectiveAt])
48
+ @@map("subscription_bundles")
49
+ }
@@ -28,18 +28,18 @@ regenerated after fragment changes (`tests/reference-sql-drift.test.js`).
28
28
 
29
29
  ## Files
30
30
 
31
- | File | Models |
32
- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
33
- | [`01-subscription.prisma`](01-subscription.prisma) | `Subscription`, `SubscriptionPaymentMethod`, `CheckoutOffer` + Enums |
34
- | [`02-promo-code.prisma`](02-promo-code.prisma) | `PromoCode`, `PromoCodeRedemption`, `PromoCodeValidationLog` + Enums |
35
- | [`03-plan-versions.prisma`](03-plan-versions.prisma) | `Plan`, `PlanVersion` |
36
- | [`04-audit-log.prisma`](04-audit-log.prisma) | `AuditLog` |
37
- | [`05-bundle-business-type.prisma`](05-bundle-business-type.prisma) | `Bundle`, `BundleVersion`, `BusinessType`, `BusinessTypeVersion`, `BusinessTypeBundle` |
38
- | [`06-catalog-entries.prisma`](06-catalog-entries.prisma) | `CapabilityCatalogEntry`, `FeatureCatalogEntry`, `MarketingProjection` |
39
- | [`07-promotion.prisma`](07-promotion.prisma) | `Promotion` |
40
- | [`08-subscription-contract.prisma`](08-subscription-contract.prisma) | `SubscriptionContract`, `ContractLineItem` |
41
- | [`09-pending-registration.prisma`](09-pending-registration.prisma) | `PendingRegistration`, `PaymentEventLog` + `RegistrationStatus` |
42
- | [`10-super-admin.prisma`](10-super-admin.prisma) | `SuperAdminUser`, `SuperAdminMfa` |
31
+ | File | Models |
32
+ | -------------------------------------------------------------------- | ---------------------------------------------------------------------- |
33
+ | [`01-subscription.prisma`](01-subscription.prisma) | `Subscription`, `SubscriptionPaymentMethod`, `CheckoutOffer` + Enums |
34
+ | [`02-promo-code.prisma`](02-promo-code.prisma) | `PromoCode`, `PromoCodeRedemption`, `PromoCodeValidationLog` + Enums |
35
+ | [`03-plan-versions.prisma`](03-plan-versions.prisma) | `Plan`, `PlanVersion` |
36
+ | [`04-audit-log.prisma`](04-audit-log.prisma) | `AuditLog` |
37
+ | [`05-bundle.prisma`](05-bundle.prisma) | `Bundle`, `BundleVersion` |
38
+ | [`06-catalog-entries.prisma`](06-catalog-entries.prisma) | `CapabilityCatalogEntry`, `FeatureCatalogEntry`, `MarketingProjection` |
39
+ | [`07-promotion.prisma`](07-promotion.prisma) | `Promotion` |
40
+ | [`08-subscription-contract.prisma`](08-subscription-contract.prisma) | `SubscriptionContract`, `ContractLineItem` |
41
+ | [`09-pending-registration.prisma`](09-pending-registration.prisma) | `PendingRegistration`, `PaymentEventLog` + `RegistrationStatus` |
42
+ | [`10-super-admin.prisma`](10-super-admin.prisma) | `SuperAdminUser`, `SuperAdminMfa` |
43
43
 
44
44
  ## How the consumer uses the fragments
45
45
 
@@ -85,7 +85,6 @@ comment. The consumer enables them using their own `Tenant`/`User` model names.
85
85
 
86
86
  `subscriptions`, `plan_versions`, `promo_codes`, `promo_code_redemptions`,
87
87
  `promo_code_validation_logs`, `audit_logs`, `bundles`, `bundle_versions`,
88
- `business_types`, `business_type_versions`, `business_type_bundles`,
89
88
  `capability_catalog_entries`, `feature_catalog_entries`,
90
89
  `marketing_projections`, `subscription_contracts`, `contract_line_items`.
91
90
  Please do **not change** them — otherwise platform migration scripts and the
@@ -99,9 +98,8 @@ this precision.
99
98
 
100
99
  ### 5. Constraints Prisma cannot express
101
100
 
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
101
+ `PlanVersion` and `BundleVersion` allow **exactly one** draft per identity key
102
+ (`publishedAt IS NULL`). The partial indexes live as SQL in
105
103
  [`../sql/constraints.postgres.sql`](../sql/constraints.postgres.sql) —
106
104
  add that file to your migration verbatim. Note that column names are
107
105
  **camelCase** (the fragments `@@map` table names only), e.g.:
@@ -133,7 +133,6 @@
133
133
  "pilots": { "$ref": "#/$defs/StandardPageDef" },
134
134
  "discovery": { "$ref": "#/$defs/StandardPageDef" },
135
135
  "bundles": { "$ref": "#/$defs/StandardPageDef" },
136
- "businessTypes": { "$ref": "#/$defs/StandardPageDef" },
137
136
  "marketingCatalog": { "$ref": "#/$defs/StandardPageDef" },
138
137
  "platformEmail": { "$ref": "#/$defs/StandardPageDef" },
139
138
  "platformEmailHistory": { "$ref": "#/$defs/StandardPageDef" }
@@ -17,12 +17,3 @@ CREATE UNIQUE INDEX IF NOT EXISTS plan_versions_draft_per_plan
17
17
 
18
18
  CREATE UNIQUE INDEX IF NOT EXISTS bundle_versions_draft_per_bundle
19
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);
@@ -7,12 +7,13 @@
7
7
  -- prisma-fragments/02-promo-code.prisma
8
8
  -- prisma-fragments/03-plan-versions.prisma
9
9
  -- prisma-fragments/04-audit-log.prisma
10
- -- prisma-fragments/05-bundle-business-type.prisma
10
+ -- prisma-fragments/05-bundle.prisma
11
11
  -- prisma-fragments/06-catalog-entries.prisma
12
12
  -- prisma-fragments/07-promotion.prisma
13
13
  -- prisma-fragments/08-subscription-contract.prisma
14
14
  -- prisma-fragments/09-pending-registration.prisma
15
15
  -- prisma-fragments/10-super-admin.prisma
16
+ -- prisma-fragments/11-subscription-bundle.prisma
16
17
  -- plus the normative constraints from sql/constraints.postgres.sql.
17
18
  -- Do not edit by hand — change the fragments/constraints and regenerate.
18
19
 
@@ -61,7 +62,7 @@ CREATE TABLE "subscriptions" (
61
62
  "canceledAt" TIMESTAMP(3),
62
63
  "currentPeriodStart" TIMESTAMP(3),
63
64
  "currentPeriodEnd" TIMESTAMP(3),
64
- "planVersionId" TEXT,
65
+ "planVersionId" TEXT NOT NULL,
65
66
  "pendingPlanVersionId" TEXT,
66
67
  "pendingPlanVersionEffectiveAt" TIMESTAMP(3),
67
68
  "pendingPlanVersionAccepted" BOOLEAN NOT NULL DEFAULT false,
@@ -69,7 +70,6 @@ CREATE TABLE "subscriptions" (
69
70
  "pendingPlanVersionAcceptedByUserId" TEXT,
70
71
  "pendingPlanVersionNotifiedAt" TIMESTAMP(3),
71
72
  "pendingPlanVersionReminderSentAt" TIMESTAMP(3),
72
- "businessTypeVersionId" TEXT,
73
73
  "trialEntitlementPlan" TEXT,
74
74
  "postTrialPlan" TEXT,
75
75
  "pendingPlan" TEXT,
@@ -294,54 +294,6 @@ CREATE TABLE "bundle_versions" (
294
294
  CONSTRAINT "bundle_versions_pkey" PRIMARY KEY ("id")
295
295
  );
296
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
297
  -- CreateTable
346
298
  CREATE TABLE "capability_catalog_entries" (
347
299
  "id" TEXT NOT NULL,
@@ -408,6 +360,8 @@ CREATE TABLE "quota_catalog_entries" (
408
360
  "usageProvider" TEXT,
409
361
  "enforcementMode" TEXT NOT NULL DEFAULT 'soft',
410
362
  "discoveryStatus" TEXT NOT NULL DEFAULT 'pending',
363
+ "replaces" TEXT[],
364
+ "successorKey" TEXT,
411
365
  "approvedAt" TIMESTAMP(3),
412
366
  "approvedBy" TEXT,
413
367
  "approvedSignature" TEXT,
@@ -596,6 +550,21 @@ CREATE TABLE "super_admin_mfa" (
596
550
  CONSTRAINT "super_admin_mfa_pkey" PRIMARY KEY ("userId")
597
551
  );
598
552
 
553
+ -- CreateTable
554
+ CREATE TABLE "subscription_bundles" (
555
+ "id" TEXT NOT NULL,
556
+ "subscriptionId" TEXT NOT NULL,
557
+ "bundleVersionId" TEXT NOT NULL,
558
+ "startedAt" TIMESTAMP(3) NOT NULL,
559
+ "minimumTermEndsAt" TIMESTAMP(3),
560
+ "canceledAt" TIMESTAMP(3),
561
+ "canceledEffectiveAt" TIMESTAMP(3),
562
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
563
+ "updatedAt" TIMESTAMP(3) NOT NULL,
564
+
565
+ CONSTRAINT "subscription_bundles_pkey" PRIMARY KEY ("id")
566
+ );
567
+
599
568
  -- CreateIndex
600
569
  CREATE UNIQUE INDEX "subscriptions_tenantId_key" ON "subscriptions"("tenantId");
601
570
 
@@ -608,9 +577,6 @@ CREATE INDEX "subscriptions_planVersionId_idx" ON "subscriptions"("planVersionId
608
577
  -- CreateIndex
609
578
  CREATE INDEX "subscriptions_pendingPlanVersionId_idx" ON "subscriptions"("pendingPlanVersionId");
610
579
 
611
- -- CreateIndex
612
- CREATE INDEX "subscriptions_businessTypeVersionId_idx" ON "subscriptions"("businessTypeVersionId");
613
-
614
580
  -- CreateIndex
615
581
  CREATE INDEX "subscriptions_currentPeriodEnd_idx" ON "subscriptions"("currentPeriodEnd");
616
582
 
@@ -692,24 +658,6 @@ CREATE INDEX "bundle_versions_bundleId_publishedAt_idx" ON "bundle_versions"("bu
692
658
  -- CreateIndex
693
659
  CREATE UNIQUE INDEX "bundle_versions_bundleId_version_key" ON "bundle_versions"("bundleId", "version");
694
660
 
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
661
  -- CreateIndex
714
662
  CREATE INDEX "capability_catalog_entries_projectKey_codeStatus_idx" ON "capability_catalog_entries"("projectKey", "codeStatus");
715
663
 
@@ -785,14 +733,20 @@ CREATE UNIQUE INDEX "super_admin_users_email_key" ON "super_admin_users"("email"
785
733
  -- CreateIndex
786
734
  CREATE INDEX "super_admin_users_isActive_deletedAt_idx" ON "super_admin_users"("isActive", "deletedAt");
787
735
 
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;
736
+ -- CreateIndex
737
+ CREATE INDEX "subscription_bundles_subscriptionId_idx" ON "subscription_bundles"("subscriptionId");
738
+
739
+ -- CreateIndex
740
+ CREATE INDEX "subscription_bundles_bundleVersionId_idx" ON "subscription_bundles"("bundleVersionId");
741
+
742
+ -- CreateIndex
743
+ CREATE INDEX "subscription_bundles_canceledEffectiveAt_idx" ON "subscription_bundles"("canceledEffectiveAt");
790
744
 
791
745
  -- AddForeignKey
792
- ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_pendingPlanVersionId_fkey" FOREIGN KEY ("pendingPlanVersionId") REFERENCES "plan_versions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
746
+ ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_planVersionId_fkey" FOREIGN KEY ("planVersionId") REFERENCES "plan_versions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
793
747
 
794
748
  -- 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;
749
+ ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_pendingPlanVersionId_fkey" FOREIGN KEY ("pendingPlanVersionId") REFERENCES "plan_versions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
796
750
 
797
751
  -- AddForeignKey
798
752
  ALTER TABLE "subscription_payment_methods" ADD CONSTRAINT "subscription_payment_methods_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "subscriptions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -816,19 +770,13 @@ ALTER TABLE "bundle_versions" ADD CONSTRAINT "bundle_versions_bundleId_fkey" FOR
816
770
  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
771
 
818
772
  -- 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;
773
+ 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;
826
774
 
827
775
  -- 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;
776
+ ALTER TABLE "subscription_bundles" ADD CONSTRAINT "subscription_bundles_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "subscriptions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
829
777
 
830
778
  -- 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;
779
+ ALTER TABLE "subscription_bundles" ADD CONSTRAINT "subscription_bundles_bundleVersionId_fkey" FOREIGN KEY ("bundleVersionId") REFERENCES "bundle_versions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
832
780
 
833
781
  -- =============================================================================
834
782
  -- SaaSicat — normative PostgreSQL constraints the Prisma DSL cannot express.
@@ -849,12 +797,3 @@ CREATE UNIQUE INDEX IF NOT EXISTS plan_versions_draft_per_plan
849
797
 
850
798
  CREATE UNIQUE INDEX IF NOT EXISTS bundle_versions_draft_per_bundle
851
799
  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);