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