@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.
- package/admin-api.openapi.yaml +242 -160
- package/package.json +1 -1
- package/prisma-fragments/01-subscription.prisma +5 -18
- package/prisma-fragments/03-plan-versions.prisma +8 -1
- package/prisma-fragments/05-bundle.prisma +103 -0
- package/prisma-fragments/06-catalog-entries.prisma +9 -4
- package/prisma-fragments/11-subscription-bundle.prisma +49 -0
- package/prisma-fragments/README.md +21 -18
- package/schemas/admin-manifest.schema.json +0 -1
- package/sql/constraints.postgres.sql +0 -9
- package/sql/reference-schema.postgres.sql +43 -93
- package/prisma-fragments/05-bundle-business-type.prisma +0 -203
|
@@ -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
|
-
//
|
|
76
|
-
|
|
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
|
|
115
|
+
planVersion PlanVersion @relation("SubscriptionPlanVersion", fields: [planVersionId], references: [id])
|
|
129
116
|
pendingPlanVersion PlanVersion? @relation("SubscriptionPendingPlanVersion", fields: [pendingPlanVersionId], references: [id])
|
|
130
|
-
|
|
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
|
|
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
|
|
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
|
|
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'
|
|
226
|
-
targetVersionId String // PlanVersion.id | BundleVersion.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
|
|
38
|
-
| [`06-catalog-entries.prisma`](06-catalog-entries.prisma) |
|
|
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`, `
|
|
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
|
-
`
|
|
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
|
|
103
|
-
|
|
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
|
|
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
|
|
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 "
|
|
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
|
-
--
|
|
789
|
-
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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);
|