@saasicat/spec 0.5.0 → 0.7.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 {
@@ -76,6 +76,12 @@ model PlanVersion {
76
76
  changeNote String
77
77
  nonRegressive Boolean @default(true)
78
78
 
79
+ // Booking window for new subscriptions. Nullable for backwards-compatible
80
+ // legacy rows; `endsAt` is an optional precise administrative termination.
81
+ validFrom DateTime?
82
+ validUntil DateTime?
83
+ endsAt DateTime?
84
+
79
85
  createdByUserId String?
80
86
  publishedByUserId String?
81
87
 
@@ -90,5 +96,6 @@ model PlanVersion {
90
96
  @@unique([planId, version])
91
97
  @@index([planId, supersededAt])
92
98
  @@index([planId, publishedAt])
99
+ @@index([planId, validFrom])
93
100
  @@map("plan_versions")
94
101
  }
@@ -0,0 +1,103 @@
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
+ // Booking window for new subscriptions. Both columns are nullable so this
80
+ // schema change is additive and existing rows remain valid. Legacy adapters
81
+ // may ignore them; time-aware adapters opt in explicitly.
82
+ validFrom DateTime?
83
+ validUntil DateTime?
84
+
85
+ createdByUserId String?
86
+ publishedByUserId String?
87
+
88
+ createdAt DateTime @default(now())
89
+ updatedAt DateTime @updatedAt
90
+
91
+ bundle Bundle @relation(fields: [bundleId], references: [id], onDelete: Cascade)
92
+ baseVersion BundleVersion? @relation("BundleVersionLineage", fields: [baseVersionId], references: [id])
93
+ derivedVersions BundleVersion[] @relation("BundleVersionLineage")
94
+ // Standalone add-on bookings pinned to this version (see
95
+ // 11-subscription-bundle.prisma).
96
+ subscriptionBundles SubscriptionBundle[]
97
+
98
+ @@unique([bundleId, version])
99
+ @@index([bundleId, supersededAt])
100
+ @@index([bundleId, publishedAt])
101
+ @@index([bundleId, validFrom])
102
+ @@map("bundle_versions")
103
+ }
@@ -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,19 @@ 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) | Capability, feature, quota and marketing catalog models |
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
+ | [`11-subscription-bundle.prisma`](11-subscription-bundle.prisma) | `SubscriptionBundle` |
43
44
 
44
45
  ## How the consumer uses the fragments
45
46
 
@@ -83,11 +84,14 @@ comment. The consumer enables them using their own `Tenant`/`User` model names.
83
84
 
84
85
  ### 3. Table names (`@@map`) are canonical
85
86
 
86
- `subscriptions`, `plan_versions`, `promo_codes`, `promo_code_redemptions`,
87
+ `subscriptions`, `subscription_payment_methods`, `checkout_offers`, `plans`,
88
+ `plan_versions`, `promo_codes`, `promo_code_redemptions`,
87
89
  `promo_code_validation_logs`, `audit_logs`, `bundles`, `bundle_versions`,
88
- `business_types`, `business_type_versions`, `business_type_bundles`,
89
90
  `capability_catalog_entries`, `feature_catalog_entries`,
90
- `marketing_projections`, `subscription_contracts`, `contract_line_items`.
91
+ `quota_catalog_entries`, `marketing_projections`, `marketing_settings`,
92
+ `promotions`, `subscription_contracts`, `contract_line_items`,
93
+ `pending_registrations`, `payment_event_logs`, `super_admin_users`,
94
+ `super_admin_mfa`, `subscription_bundles`.
91
95
  Please do **not change** them — otherwise platform migration scripts and the
92
96
  `@saasicat/cli` commands that rely on these names will break.
93
97
 
@@ -99,9 +103,8 @@ this precision.
99
103
 
100
104
  ### 5. Constraints Prisma cannot express
101
105
 
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
106
+ `PlanVersion` and `BundleVersion` allow **exactly one** draft per identity key
107
+ (`publishedAt IS NULL`). The partial indexes live as SQL in
105
108
  [`../sql/constraints.postgres.sql`](../sql/constraints.postgres.sql) —
106
109
  add that file to your migration verbatim. Note that column names are
107
110
  **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,
@@ -226,6 +226,9 @@ CREATE TABLE "plan_versions" (
226
226
  "publishedChanges" JSONB,
227
227
  "changeNote" TEXT NOT NULL,
228
228
  "nonRegressive" BOOLEAN NOT NULL DEFAULT true,
229
+ "validFrom" TIMESTAMP(3),
230
+ "validUntil" TIMESTAMP(3),
231
+ "endsAt" TIMESTAMP(3),
229
232
  "createdByUserId" TEXT,
230
233
  "publishedByUserId" TEXT,
231
234
  "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -286,6 +289,8 @@ CREATE TABLE "bundle_versions" (
286
289
  "publishedChanges" JSONB,
287
290
  "changeNote" TEXT NOT NULL DEFAULT '',
288
291
  "nonRegressive" BOOLEAN NOT NULL DEFAULT true,
292
+ "validFrom" TIMESTAMP(3),
293
+ "validUntil" TIMESTAMP(3),
289
294
  "createdByUserId" TEXT,
290
295
  "publishedByUserId" TEXT,
291
296
  "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -294,54 +299,6 @@ CREATE TABLE "bundle_versions" (
294
299
  CONSTRAINT "bundle_versions_pkey" PRIMARY KEY ("id")
295
300
  );
296
301
 
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
302
  -- CreateTable
346
303
  CREATE TABLE "capability_catalog_entries" (
347
304
  "id" TEXT NOT NULL,
@@ -408,6 +365,8 @@ CREATE TABLE "quota_catalog_entries" (
408
365
  "usageProvider" TEXT,
409
366
  "enforcementMode" TEXT NOT NULL DEFAULT 'soft',
410
367
  "discoveryStatus" TEXT NOT NULL DEFAULT 'pending',
368
+ "replaces" TEXT[],
369
+ "successorKey" TEXT,
411
370
  "approvedAt" TIMESTAMP(3),
412
371
  "approvedBy" TEXT,
413
372
  "approvedSignature" TEXT,
@@ -596,6 +555,21 @@ CREATE TABLE "super_admin_mfa" (
596
555
  CONSTRAINT "super_admin_mfa_pkey" PRIMARY KEY ("userId")
597
556
  );
598
557
 
558
+ -- CreateTable
559
+ CREATE TABLE "subscription_bundles" (
560
+ "id" TEXT NOT NULL,
561
+ "subscriptionId" TEXT NOT NULL,
562
+ "bundleVersionId" TEXT NOT NULL,
563
+ "startedAt" TIMESTAMP(3) NOT NULL,
564
+ "minimumTermEndsAt" TIMESTAMP(3),
565
+ "canceledAt" TIMESTAMP(3),
566
+ "canceledEffectiveAt" TIMESTAMP(3),
567
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
568
+ "updatedAt" TIMESTAMP(3) NOT NULL,
569
+
570
+ CONSTRAINT "subscription_bundles_pkey" PRIMARY KEY ("id")
571
+ );
572
+
599
573
  -- CreateIndex
600
574
  CREATE UNIQUE INDEX "subscriptions_tenantId_key" ON "subscriptions"("tenantId");
601
575
 
@@ -608,9 +582,6 @@ CREATE INDEX "subscriptions_planVersionId_idx" ON "subscriptions"("planVersionId
608
582
  -- CreateIndex
609
583
  CREATE INDEX "subscriptions_pendingPlanVersionId_idx" ON "subscriptions"("pendingPlanVersionId");
610
584
 
611
- -- CreateIndex
612
- CREATE INDEX "subscriptions_businessTypeVersionId_idx" ON "subscriptions"("businessTypeVersionId");
613
-
614
585
  -- CreateIndex
615
586
  CREATE INDEX "subscriptions_currentPeriodEnd_idx" ON "subscriptions"("currentPeriodEnd");
616
587
 
@@ -662,6 +633,9 @@ CREATE INDEX "plan_versions_planId_supersededAt_idx" ON "plan_versions"("planId"
662
633
  -- CreateIndex
663
634
  CREATE INDEX "plan_versions_planId_publishedAt_idx" ON "plan_versions"("planId", "publishedAt");
664
635
 
636
+ -- CreateIndex
637
+ CREATE INDEX "plan_versions_planId_validFrom_idx" ON "plan_versions"("planId", "validFrom");
638
+
665
639
  -- CreateIndex
666
640
  CREATE UNIQUE INDEX "plan_versions_planId_version_key" ON "plan_versions"("planId", "version");
667
641
 
@@ -690,25 +664,10 @@ CREATE INDEX "bundle_versions_bundleId_supersededAt_idx" ON "bundle_versions"("b
690
664
  CREATE INDEX "bundle_versions_bundleId_publishedAt_idx" ON "bundle_versions"("bundleId", "publishedAt");
691
665
 
692
666
  -- 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");
667
+ CREATE INDEX "bundle_versions_bundleId_validFrom_idx" ON "bundle_versions"("bundleId", "validFrom");
706
668
 
707
669
  -- 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");
670
+ CREATE UNIQUE INDEX "bundle_versions_bundleId_version_key" ON "bundle_versions"("bundleId", "version");
712
671
 
713
672
  -- CreateIndex
714
673
  CREATE INDEX "capability_catalog_entries_projectKey_codeStatus_idx" ON "capability_catalog_entries"("projectKey", "codeStatus");
@@ -785,14 +744,20 @@ CREATE UNIQUE INDEX "super_admin_users_email_key" ON "super_admin_users"("email"
785
744
  -- CreateIndex
786
745
  CREATE INDEX "super_admin_users_isActive_deletedAt_idx" ON "super_admin_users"("isActive", "deletedAt");
787
746
 
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;
747
+ -- CreateIndex
748
+ CREATE INDEX "subscription_bundles_subscriptionId_idx" ON "subscription_bundles"("subscriptionId");
749
+
750
+ -- CreateIndex
751
+ CREATE INDEX "subscription_bundles_bundleVersionId_idx" ON "subscription_bundles"("bundleVersionId");
752
+
753
+ -- CreateIndex
754
+ CREATE INDEX "subscription_bundles_canceledEffectiveAt_idx" ON "subscription_bundles"("canceledEffectiveAt");
790
755
 
791
756
  -- AddForeignKey
792
- ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_pendingPlanVersionId_fkey" FOREIGN KEY ("pendingPlanVersionId") REFERENCES "plan_versions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
757
+ ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_planVersionId_fkey" FOREIGN KEY ("planVersionId") REFERENCES "plan_versions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
793
758
 
794
759
  -- 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;
760
+ ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_pendingPlanVersionId_fkey" FOREIGN KEY ("pendingPlanVersionId") REFERENCES "plan_versions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
796
761
 
797
762
  -- AddForeignKey
798
763
  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 +781,13 @@ ALTER TABLE "bundle_versions" ADD CONSTRAINT "bundle_versions_bundleId_fkey" FOR
816
781
  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
782
 
818
783
  -- 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;
784
+ 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
785
 
827
786
  -- 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;
787
+ ALTER TABLE "subscription_bundles" ADD CONSTRAINT "subscription_bundles_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "subscriptions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
829
788
 
830
789
  -- 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;
790
+ ALTER TABLE "subscription_bundles" ADD CONSTRAINT "subscription_bundles_bundleVersionId_fkey" FOREIGN KEY ("bundleVersionId") REFERENCES "bundle_versions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
832
791
 
833
792
  -- =============================================================================
834
793
  -- SaaSicat — normative PostgreSQL constraints the Prisma DSL cannot express.
@@ -849,12 +808,3 @@ CREATE UNIQUE INDEX IF NOT EXISTS plan_versions_draft_per_plan
849
808
 
850
809
  CREATE UNIQUE INDEX IF NOT EXISTS bundle_versions_draft_per_bundle
851
810
  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);