@saasicat/spec 1.0.0-rc.2 → 1.0.0-rc.21

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.
Files changed (36) hide show
  1. package/README.md +30 -0
  2. package/acceptance/README.md +3 -3
  3. package/acceptance/mfa/totp-verify-good-and-bad-code.yaml +6 -6
  4. package/admin-api.openapi.yaml +331 -77
  5. package/cli-conventions.md +19 -19
  6. package/index.cjs +3 -0
  7. package/index.d.cts +3 -1
  8. package/index.d.ts +2 -0
  9. package/index.js +9 -1
  10. package/package.json +4 -2
  11. package/prisma-fragments/01-subscription.prisma +34 -47
  12. package/prisma-fragments/02-promo-code.prisma +38 -0
  13. package/prisma-fragments/03-plan-versions.prisma +3 -4
  14. package/prisma-fragments/05-bundle.prisma +4 -5
  15. package/prisma-fragments/06-catalog-entries.prisma +24 -24
  16. package/prisma-fragments/07-promotion.prisma +2 -3
  17. package/prisma-fragments/08-subscription-contract.prisma +42 -5
  18. package/prisma-fragments/09-pending-registration.prisma +32 -27
  19. package/prisma-fragments/11-subscription-bundle.prisma +17 -0
  20. package/prisma-fragments/12-applied-settings.prisma +55 -0
  21. package/prisma-fragments/13-subscriber.prisma +94 -0
  22. package/prisma-fragments/14-payments.prisma +108 -0
  23. package/prisma-fragments/README.md +40 -22
  24. package/schemas/admin-manifest.schema.json +10 -3
  25. package/schemas/plan-catalog.schema.json +239 -18
  26. package/schemas/promo-code.schema.json +7 -1
  27. package/schemas/tenant-ledger.schema.json +236 -0
  28. package/sql/1.0-a-contract-names-its-subscriber.postgres.sql +383 -0
  29. package/sql/1.0-a-payment-method-is-a-gateway-reference.postgres.sql +177 -0
  30. package/sql/1.0-a-promo-slot-is-held-through-checkout.postgres.sql +69 -0
  31. package/sql/1.0-a-settings-change-carries-its-order.postgres.sql +73 -0
  32. package/sql/1.0-line-items-record-their-money.postgres.sql +134 -0
  33. package/sql/1.0-remove-project-key.postgres.sql +203 -0
  34. package/sql/1.0-the-applied-settings-are-recorded.postgres.sql +67 -0
  35. package/sql/constraints.postgres.sql +76 -0
  36. package/sql/reference-schema.postgres.sql +324 -77
@@ -14,6 +14,9 @@
14
14
  -- prisma-fragments/09-pending-registration.prisma
15
15
  -- prisma-fragments/10-super-admin.prisma
16
16
  -- prisma-fragments/11-subscription-bundle.prisma
17
+ -- prisma-fragments/12-applied-settings.prisma
18
+ -- prisma-fragments/13-subscriber.prisma
19
+ -- prisma-fragments/14-payments.prisma
17
20
  -- plus the normative constraints from sql/constraints.postgres.sql.
18
21
  -- Do not edit by hand — change the fragments/constraints and regenerate.
19
22
 
@@ -26,9 +29,6 @@ CREATE TYPE "BillingCycle" AS ENUM ('MONTHLY', 'YEARLY');
26
29
  -- CreateEnum
27
30
  CREATE TYPE "SubscriptionStatus" AS ENUM ('TRIAL', 'ACTIVE', 'PAST_DUE', 'CANCELED', 'PENDING_SALES');
28
31
 
29
- -- CreateEnum
30
- CREATE TYPE "SubscriptionPaymentType" AS ENUM ('CARD', 'SEPA', 'PAYPAL', 'KLARNA', 'INVOICE');
31
-
32
32
  -- CreateEnum
33
33
  CREATE TYPE "PromoCodeValueType" AS ENUM ('PERCENT', 'ABSOLUTE');
34
34
 
@@ -60,8 +60,11 @@ CREATE TABLE "subscriptions" (
60
60
  "trialEndsAt" TIMESTAMP(3),
61
61
  "startedAt" TIMESTAMP(3),
62
62
  "canceledAt" TIMESTAMP(3),
63
+ "canceledEffectiveAt" TIMESTAMP(3),
63
64
  "currentPeriodStart" TIMESTAMP(3),
64
65
  "currentPeriodEnd" TIMESTAMP(3),
66
+ "minimumTermUntil" TIMESTAMP(3),
67
+ "billingAnchorDay" INTEGER,
65
68
  "planVersionId" TEXT NOT NULL,
66
69
  "pendingPlanVersionId" TEXT,
67
70
  "pendingPlanVersionEffectiveAt" TIMESTAMP(3),
@@ -89,29 +92,9 @@ CREATE TABLE "subscriptions" (
89
92
  CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("id")
90
93
  );
91
94
 
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
95
  -- CreateTable
112
96
  CREATE TABLE "checkout_offers" (
113
97
  "id" TEXT NOT NULL,
114
- "projectKey" TEXT NOT NULL,
115
98
  "planKey" TEXT NOT NULL,
116
99
  "planVersionId" TEXT,
117
100
  "billingCycle" TEXT NOT NULL,
@@ -145,6 +128,7 @@ CREATE TABLE "promo_codes" (
145
128
  "validUntil" TIMESTAMP(3),
146
129
  "maxRedemptions" INTEGER,
147
130
  "redemptionsCount" INTEGER NOT NULL DEFAULT 0,
131
+ "heldCount" INTEGER NOT NULL DEFAULT 0,
148
132
  "appliesToPlans" TEXT[],
149
133
  "appliesToBilling" "BillingCycle",
150
134
  "firstTimeCustomersOnly" BOOLEAN NOT NULL DEFAULT true,
@@ -181,6 +165,18 @@ CREATE TABLE "promo_code_redemptions" (
181
165
  CONSTRAINT "promo_code_redemptions_pkey" PRIMARY KEY ("id")
182
166
  );
183
167
 
168
+ -- CreateTable
169
+ CREATE TABLE "promo_code_holds" (
170
+ "id" TEXT NOT NULL,
171
+ "promoCodeId" TEXT NOT NULL,
172
+ "checkoutOfferId" TEXT NOT NULL,
173
+ "expiresAt" TIMESTAMP(3) NOT NULL,
174
+ "handedOverTx" BIGINT,
175
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
176
+
177
+ CONSTRAINT "promo_code_holds_pkey" PRIMARY KEY ("id")
178
+ );
179
+
184
180
  -- CreateTable
185
181
  CREATE TABLE "promo_code_validation_logs" (
186
182
  "id" TEXT NOT NULL,
@@ -197,7 +193,6 @@ CREATE TABLE "promo_code_validation_logs" (
197
193
  -- CreateTable
198
194
  CREATE TABLE "plans" (
199
195
  "id" TEXT NOT NULL,
200
- "projectKey" TEXT NOT NULL,
201
196
  "planKey" TEXT NOT NULL,
202
197
  "label" TEXT NOT NULL,
203
198
  "description" TEXT,
@@ -257,7 +252,6 @@ CREATE TABLE "audit_logs" (
257
252
  -- CreateTable
258
253
  CREATE TABLE "bundles" (
259
254
  "id" TEXT NOT NULL,
260
- "projectKey" TEXT NOT NULL,
261
255
  "bundleKey" TEXT NOT NULL,
262
256
  "label" TEXT NOT NULL,
263
257
  "description" TEXT,
@@ -302,7 +296,6 @@ CREATE TABLE "bundle_versions" (
302
296
  -- CreateTable
303
297
  CREATE TABLE "capability_catalog_entries" (
304
298
  "id" TEXT NOT NULL,
305
- "projectKey" TEXT NOT NULL,
306
299
  "capabilityKey" TEXT NOT NULL,
307
300
  "label" TEXT NOT NULL,
308
301
  "description" TEXT,
@@ -327,7 +320,6 @@ CREATE TABLE "capability_catalog_entries" (
327
320
  -- CreateTable
328
321
  CREATE TABLE "feature_catalog_entries" (
329
322
  "id" TEXT NOT NULL,
330
- "projectKey" TEXT NOT NULL,
331
323
  "featureKey" TEXT NOT NULL,
332
324
  "label" TEXT NOT NULL,
333
325
  "description" TEXT,
@@ -356,7 +348,6 @@ CREATE TABLE "feature_catalog_entries" (
356
348
  -- CreateTable
357
349
  CREATE TABLE "quota_catalog_entries" (
358
350
  "id" TEXT NOT NULL,
359
- "projectKey" TEXT NOT NULL,
360
351
  "quotaKey" TEXT NOT NULL,
361
352
  "label" TEXT NOT NULL,
362
353
  "description" TEXT,
@@ -382,7 +373,6 @@ CREATE TABLE "quota_catalog_entries" (
382
373
  -- CreateTable
383
374
  CREATE TABLE "marketing_projections" (
384
375
  "id" TEXT NOT NULL,
385
- "projectKey" TEXT NOT NULL,
386
376
  "targetType" TEXT NOT NULL,
387
377
  "targetVersionId" TEXT NOT NULL,
388
378
  "locale" TEXT NOT NULL DEFAULT 'de',
@@ -405,8 +395,7 @@ CREATE TABLE "marketing_projections" (
405
395
 
406
396
  -- CreateTable
407
397
  CREATE TABLE "marketing_settings" (
408
- "id" TEXT NOT NULL,
409
- "projectKey" TEXT NOT NULL,
398
+ "id" TEXT NOT NULL DEFAULT 'marketing-settings',
410
399
  "activeLocales" JSONB NOT NULL DEFAULT '[]',
411
400
  "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
412
401
  "updatedAt" TIMESTAMP(3) NOT NULL,
@@ -417,7 +406,6 @@ CREATE TABLE "marketing_settings" (
417
406
  -- CreateTable
418
407
  CREATE TABLE "promotions" (
419
408
  "id" TEXT NOT NULL,
420
- "projectKey" TEXT NOT NULL,
421
409
  "internalLabel" TEXT NOT NULL,
422
410
  "type" TEXT NOT NULL,
423
411
  "value" JSONB NOT NULL,
@@ -441,8 +429,11 @@ CREATE TABLE "promotions" (
441
429
  -- CreateTable
442
430
  CREATE TABLE "subscription_contracts" (
443
431
  "id" TEXT NOT NULL,
444
- "projectKey" TEXT NOT NULL,
445
432
  "tenantId" TEXT NOT NULL,
433
+ "subscriberId" TEXT NOT NULL,
434
+ "subscriberSnapshot" JSONB NOT NULL,
435
+ "issuerSnapshot" JSONB,
436
+ "partiesMigrated" BOOLEAN NOT NULL DEFAULT false,
446
437
  "status" "SubscriptionContractStatus" NOT NULL DEFAULT 'active',
447
438
  "effectiveFrom" TIMESTAMP(3) NOT NULL,
448
439
  "effectiveUntil" TIMESTAMP(3),
@@ -474,6 +465,9 @@ CREATE TABLE "contract_line_items" (
474
465
  "priceNet" DECIMAL(10,2) NOT NULL,
475
466
  "priceGross" DECIMAL(10,2) NOT NULL,
476
467
  "billingCycle" TEXT NOT NULL,
468
+ "currency" TEXT NOT NULL,
469
+ "taxRate" DECIMAL(5,2) NOT NULL,
470
+ "taxAmount" DECIMAL(10,2) NOT NULL,
477
471
  "minimumTermUntil" TIMESTAMP(3),
478
472
  "featuresSnapshot" JSONB NOT NULL DEFAULT '[]',
479
473
  "quotaEffectsSnapshot" JSONB NOT NULL DEFAULT '{}',
@@ -506,7 +500,16 @@ CREATE TABLE "PendingRegistration" (
506
500
  "configJson" JSONB,
507
501
  "billingCycle" TEXT,
508
502
  "appliedPromoCode" TEXT,
503
+ "addressLine1" TEXT,
504
+ "addressLine2" TEXT,
505
+ "postalCode" TEXT,
506
+ "city" TEXT,
507
+ "country" TEXT,
508
+ "vatId" TEXT,
509
+ "taxNumber" TEXT,
509
510
  "checkoutSessionId" TEXT,
511
+ "checkoutGatewayAccount" TEXT,
512
+ "gatewayCustomerRef" TEXT,
510
513
  "checkoutStartedAt" TIMESTAMP(3),
511
514
  "expiresAt" TIMESTAMP(3) NOT NULL,
512
515
  "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -515,19 +518,6 @@ CREATE TABLE "PendingRegistration" (
515
518
  CONSTRAINT "PendingRegistration_pkey" PRIMARY KEY ("id")
516
519
  );
517
520
 
518
- -- CreateTable
519
- CREATE TABLE "PaymentEventLog" (
520
- "id" TEXT NOT NULL,
521
- "eventId" TEXT NOT NULL,
522
- "provider" TEXT NOT NULL,
523
- "sessionId" TEXT,
524
- "status" TEXT NOT NULL,
525
- "payload" JSONB,
526
- "processedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
527
-
528
- CONSTRAINT "PaymentEventLog_pkey" PRIMARY KEY ("id")
529
- );
530
-
531
521
  -- CreateTable
532
522
  CREATE TABLE "super_admin_users" (
533
523
  "id" TEXT NOT NULL,
@@ -562,6 +552,9 @@ CREATE TABLE "subscription_bundles" (
562
552
  "bundleVersionId" TEXT NOT NULL,
563
553
  "startedAt" TIMESTAMP(3) NOT NULL,
564
554
  "minimumTermEndsAt" TIMESTAMP(3),
555
+ "billingCycle" TEXT,
556
+ "currentPeriodStart" TIMESTAMP(3),
557
+ "currentPeriodEnd" TIMESTAMP(3),
565
558
  "canceledAt" TIMESTAMP(3),
566
559
  "canceledEffectiveAt" TIMESTAMP(3),
567
560
  "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -570,6 +563,127 @@ CREATE TABLE "subscription_bundles" (
570
563
  CONSTRAINT "subscription_bundles_pkey" PRIMARY KEY ("id")
571
564
  );
572
565
 
566
+ -- CreateTable
567
+ CREATE TABLE "applied_settings" (
568
+ "id" TEXT NOT NULL DEFAULT 'installation',
569
+ "fingerprint" TEXT NOT NULL,
570
+ "settings" JSONB NOT NULL,
571
+ "source" TEXT NOT NULL,
572
+ "appliedAt" TIMESTAMP(3) NOT NULL,
573
+
574
+ CONSTRAINT "applied_settings_pkey" PRIMARY KEY ("id")
575
+ );
576
+
577
+ -- CreateTable
578
+ CREATE TABLE "settings_changes" (
579
+ "id" TEXT NOT NULL,
580
+ "seq" SERIAL NOT NULL,
581
+ "noticedAt" TIMESTAMP(3) NOT NULL,
582
+ "source" TEXT NOT NULL,
583
+ "previous" JSONB NOT NULL,
584
+ "current" JSONB NOT NULL,
585
+ "acknowledgedAt" TIMESTAMP(3),
586
+ "acknowledgedBy" TEXT,
587
+
588
+ CONSTRAINT "settings_changes_pkey" PRIMARY KEY ("id")
589
+ );
590
+
591
+ -- CreateTable
592
+ CREATE TABLE "subscribers" (
593
+ "id" TEXT NOT NULL,
594
+ "customerSequence" SERIAL NOT NULL,
595
+ "customerNumberPrefix" TEXT NOT NULL DEFAULT '',
596
+ "legalName" TEXT NOT NULL,
597
+ "vatId" TEXT,
598
+ "taxNumber" TEXT,
599
+ "addressLine1" TEXT,
600
+ "addressLine2" TEXT,
601
+ "postalCode" TEXT,
602
+ "city" TEXT,
603
+ "country" TEXT,
604
+ "invoiceEmail" TEXT,
605
+ "migrated" BOOLEAN NOT NULL DEFAULT false,
606
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
607
+ "updatedAt" TIMESTAMP(3) NOT NULL,
608
+
609
+ CONSTRAINT "subscribers_pkey" PRIMARY KEY ("id")
610
+ );
611
+
612
+ -- CreateTable
613
+ CREATE TABLE "subscriber_tenants" (
614
+ "id" TEXT NOT NULL,
615
+ "subscriberId" TEXT NOT NULL,
616
+ "tenantId" TEXT NOT NULL,
617
+ "linkedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
618
+ "unlinkedAt" TIMESTAMP(3),
619
+
620
+ CONSTRAINT "subscriber_tenants_pkey" PRIMARY KEY ("id")
621
+ );
622
+
623
+ -- CreateTable
624
+ CREATE TABLE "subscriber_corrections" (
625
+ "id" TEXT NOT NULL,
626
+ "subscriberId" TEXT NOT NULL,
627
+ "previous" JSONB NOT NULL,
628
+ "corrected" JSONB NOT NULL,
629
+ "reason" TEXT NOT NULL,
630
+ "correctedBy" TEXT NOT NULL,
631
+ "correctedAt" TIMESTAMP(3) NOT NULL,
632
+
633
+ CONSTRAINT "subscriber_corrections_pkey" PRIMARY KEY ("id")
634
+ );
635
+
636
+ -- CreateTable
637
+ CREATE TABLE "subscriber_payment_methods" (
638
+ "id" TEXT NOT NULL,
639
+ "subscriberId" TEXT NOT NULL,
640
+ "gatewayAccount" TEXT NOT NULL,
641
+ "provider" TEXT NOT NULL,
642
+ "customerRef" TEXT NOT NULL,
643
+ "paymentMethodRef" TEXT NOT NULL,
644
+ "type" TEXT NOT NULL,
645
+ "brand" TEXT,
646
+ "last4" TEXT NOT NULL,
647
+ "expiryMonth" INTEGER,
648
+ "expiryYear" INTEGER,
649
+ "country" TEXT,
650
+ "bankCode" TEXT,
651
+ "mandateReference" TEXT,
652
+ "status" TEXT NOT NULL DEFAULT 'ACTIVE',
653
+ "confirmedAt" TIMESTAMP(3) NOT NULL,
654
+ "replacedAt" TIMESTAMP(3),
655
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
656
+
657
+ CONSTRAINT "subscriber_payment_methods_pkey" PRIMARY KEY ("id")
658
+ );
659
+
660
+ -- CreateTable
661
+ CREATE TABLE "subscriber_payment_method_setups" (
662
+ "id" TEXT NOT NULL,
663
+ "subscriberId" TEXT NOT NULL,
664
+ "gatewayAccount" TEXT NOT NULL,
665
+ "sessionRef" TEXT NOT NULL,
666
+ "customerRef" TEXT NOT NULL,
667
+ "startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
668
+ "completedAt" TIMESTAMP(3),
669
+
670
+ CONSTRAINT "subscriber_payment_method_setups_pkey" PRIMARY KEY ("id")
671
+ );
672
+
673
+ -- CreateTable
674
+ CREATE TABLE "PaymentEventLog" (
675
+ "id" TEXT NOT NULL,
676
+ "gatewayAccount" TEXT NOT NULL,
677
+ "eventId" TEXT NOT NULL,
678
+ "provider" TEXT NOT NULL,
679
+ "sessionId" TEXT,
680
+ "status" TEXT NOT NULL,
681
+ "payload" JSONB,
682
+ "processedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
683
+
684
+ CONSTRAINT "PaymentEventLog_pkey" PRIMARY KEY ("id")
685
+ );
686
+
573
687
  -- CreateIndex
574
688
  CREATE UNIQUE INDEX "subscriptions_tenantId_key" ON "subscriptions"("tenantId");
575
689
 
@@ -586,10 +700,7 @@ CREATE INDEX "subscriptions_pendingPlanVersionId_idx" ON "subscriptions"("pendin
586
700
  CREATE INDEX "subscriptions_currentPeriodEnd_idx" ON "subscriptions"("currentPeriodEnd");
587
701
 
588
702
  -- CreateIndex
589
- CREATE UNIQUE INDEX "subscription_payment_methods_subscriptionId_key" ON "subscription_payment_methods"("subscriptionId");
590
-
591
- -- CreateIndex
592
- CREATE INDEX "checkout_offers_projectKey_status_idx" ON "checkout_offers"("projectKey", "status");
703
+ CREATE INDEX "checkout_offers_status_idx" ON "checkout_offers"("status");
593
704
 
594
705
  -- CreateIndex
595
706
  CREATE UNIQUE INDEX "promo_codes_code_key" ON "promo_codes"("code");
@@ -612,6 +723,15 @@ CREATE INDEX "promo_code_redemptions_promoCodeId_status_idx" ON "promo_code_rede
612
723
  -- CreateIndex
613
724
  CREATE INDEX "promo_code_redemptions_startsAt_endsAt_idx" ON "promo_code_redemptions"("startsAt", "endsAt");
614
725
 
726
+ -- CreateIndex
727
+ CREATE UNIQUE INDEX "promo_code_holds_checkoutOfferId_key" ON "promo_code_holds"("checkoutOfferId");
728
+
729
+ -- CreateIndex
730
+ CREATE INDEX "promo_code_holds_promoCodeId_expiresAt_idx" ON "promo_code_holds"("promoCodeId", "expiresAt");
731
+
732
+ -- CreateIndex
733
+ CREATE INDEX "promo_code_holds_expiresAt_idx" ON "promo_code_holds"("expiresAt");
734
+
615
735
  -- CreateIndex
616
736
  CREATE INDEX "promo_code_validation_logs_codeAttempt_createdAt_idx" ON "promo_code_validation_logs"("codeAttempt", "createdAt");
617
737
 
@@ -622,10 +742,10 @@ CREATE INDEX "promo_code_validation_logs_ipHash_createdAt_idx" ON "promo_code_va
622
742
  CREATE INDEX "promo_code_validation_logs_sessionId_createdAt_idx" ON "promo_code_validation_logs"("sessionId", "createdAt");
623
743
 
624
744
  -- CreateIndex
625
- CREATE INDEX "plans_projectKey_deletedAt_idx" ON "plans"("projectKey", "deletedAt");
745
+ CREATE INDEX "plans_deletedAt_idx" ON "plans"("deletedAt");
626
746
 
627
747
  -- CreateIndex
628
- CREATE UNIQUE INDEX "plans_projectKey_planKey_key" ON "plans"("projectKey", "planKey");
748
+ CREATE UNIQUE INDEX "plans_planKey_key" ON "plans"("planKey");
629
749
 
630
750
  -- CreateIndex
631
751
  CREATE INDEX "plan_versions_planId_supersededAt_idx" ON "plan_versions"("planId", "supersededAt");
@@ -652,10 +772,10 @@ CREATE INDEX "audit_logs_entity_action_createdAt_idx" ON "audit_logs"("entity",
652
772
  CREATE INDEX "audit_logs_actorTag_createdAt_idx" ON "audit_logs"("actorTag", "createdAt");
653
773
 
654
774
  -- CreateIndex
655
- CREATE INDEX "bundles_projectKey_deletedAt_idx" ON "bundles"("projectKey", "deletedAt");
775
+ CREATE INDEX "bundles_deletedAt_idx" ON "bundles"("deletedAt");
656
776
 
657
777
  -- CreateIndex
658
- CREATE UNIQUE INDEX "bundles_projectKey_bundleKey_key" ON "bundles"("projectKey", "bundleKey");
778
+ CREATE UNIQUE INDEX "bundles_bundleKey_key" ON "bundles"("bundleKey");
659
779
 
660
780
  -- CreateIndex
661
781
  CREATE INDEX "bundle_versions_bundleId_supersededAt_idx" ON "bundle_versions"("bundleId", "supersededAt");
@@ -670,46 +790,46 @@ CREATE INDEX "bundle_versions_bundleId_validFrom_idx" ON "bundle_versions"("bund
670
790
  CREATE UNIQUE INDEX "bundle_versions_bundleId_version_key" ON "bundle_versions"("bundleId", "version");
671
791
 
672
792
  -- CreateIndex
673
- CREATE INDEX "capability_catalog_entries_projectKey_codeStatus_idx" ON "capability_catalog_entries"("projectKey", "codeStatus");
793
+ CREATE INDEX "capability_catalog_entries_codeStatus_idx" ON "capability_catalog_entries"("codeStatus");
674
794
 
675
795
  -- CreateIndex
676
- CREATE INDEX "capability_catalog_entries_projectKey_featureKey_idx" ON "capability_catalog_entries"("projectKey", "featureKey");
796
+ CREATE INDEX "capability_catalog_entries_featureKey_idx" ON "capability_catalog_entries"("featureKey");
677
797
 
678
798
  -- CreateIndex
679
- CREATE UNIQUE INDEX "capability_catalog_entries_projectKey_capabilityKey_key" ON "capability_catalog_entries"("projectKey", "capabilityKey");
799
+ CREATE UNIQUE INDEX "capability_catalog_entries_capabilityKey_key" ON "capability_catalog_entries"("capabilityKey");
680
800
 
681
801
  -- CreateIndex
682
- CREATE INDEX "feature_catalog_entries_projectKey_discoveryStatus_idx" ON "feature_catalog_entries"("projectKey", "discoveryStatus");
802
+ CREATE INDEX "feature_catalog_entries_discoveryStatus_idx" ON "feature_catalog_entries"("discoveryStatus");
683
803
 
684
804
  -- CreateIndex
685
- CREATE INDEX "feature_catalog_entries_projectKey_plannedOnly_idx" ON "feature_catalog_entries"("projectKey", "plannedOnly");
805
+ CREATE INDEX "feature_catalog_entries_plannedOnly_idx" ON "feature_catalog_entries"("plannedOnly");
686
806
 
687
807
  -- CreateIndex
688
- CREATE UNIQUE INDEX "feature_catalog_entries_projectKey_featureKey_key" ON "feature_catalog_entries"("projectKey", "featureKey");
808
+ CREATE UNIQUE INDEX "feature_catalog_entries_featureKey_key" ON "feature_catalog_entries"("featureKey");
689
809
 
690
810
  -- CreateIndex
691
- CREATE INDEX "quota_catalog_entries_projectKey_discoveryStatus_idx" ON "quota_catalog_entries"("projectKey", "discoveryStatus");
811
+ CREATE INDEX "quota_catalog_entries_discoveryStatus_idx" ON "quota_catalog_entries"("discoveryStatus");
692
812
 
693
813
  -- CreateIndex
694
- CREATE UNIQUE INDEX "quota_catalog_entries_projectKey_quotaKey_key" ON "quota_catalog_entries"("projectKey", "quotaKey");
814
+ CREATE UNIQUE INDEX "quota_catalog_entries_quotaKey_key" ON "quota_catalog_entries"("quotaKey");
695
815
 
696
816
  -- CreateIndex
697
- CREATE INDEX "marketing_projections_projectKey_targetType_locale_priority_idx" ON "marketing_projections"("projectKey", "targetType", "locale", "priority");
817
+ CREATE INDEX "marketing_projections_targetType_locale_priority_idx" ON "marketing_projections"("targetType", "locale", "priority");
698
818
 
699
819
  -- CreateIndex
700
820
  CREATE UNIQUE INDEX "marketing_projections_targetType_targetVersionId_locale_key" ON "marketing_projections"("targetType", "targetVersionId", "locale");
701
821
 
702
822
  -- CreateIndex
703
- CREATE UNIQUE INDEX "marketing_settings_projectKey_key" ON "marketing_settings"("projectKey");
823
+ CREATE INDEX "promotions_targetType_validFrom_validTo_idx" ON "promotions"("targetType", "validFrom", "validTo");
704
824
 
705
825
  -- CreateIndex
706
- CREATE INDEX "promotions_projectKey_targetType_validFrom_validTo_idx" ON "promotions"("projectKey", "targetType", "validFrom", "validTo");
826
+ CREATE INDEX "subscription_contracts_tenantId_status_effectiveFrom_idx" ON "subscription_contracts"("tenantId", "status", "effectiveFrom");
707
827
 
708
828
  -- CreateIndex
709
- CREATE INDEX "subscription_contracts_tenantId_status_effectiveFrom_idx" ON "subscription_contracts"("tenantId", "status", "effectiveFrom");
829
+ CREATE INDEX "subscription_contracts_subscriberId_idx" ON "subscription_contracts"("subscriberId");
710
830
 
711
831
  -- CreateIndex
712
- CREATE INDEX "subscription_contracts_projectKey_status_idx" ON "subscription_contracts"("projectKey", "status");
832
+ CREATE INDEX "subscription_contracts_status_idx" ON "subscription_contracts"("status");
713
833
 
714
834
  -- CreateIndex
715
835
  CREATE INDEX "subscription_contracts_originalOfferId_idx" ON "subscription_contracts"("originalOfferId");
@@ -730,13 +850,7 @@ CREATE INDEX "PendingRegistration_status_expiresAt_idx" ON "PendingRegistration"
730
850
  CREATE INDEX "PendingRegistration_tenantSlug_idx" ON "PendingRegistration"("tenantSlug");
731
851
 
732
852
  -- CreateIndex
733
- CREATE UNIQUE INDEX "PaymentEventLog_eventId_key" ON "PaymentEventLog"("eventId");
734
-
735
- -- CreateIndex
736
- CREATE INDEX "PaymentEventLog_sessionId_idx" ON "PaymentEventLog"("sessionId");
737
-
738
- -- CreateIndex
739
- CREATE INDEX "PaymentEventLog_status_processedAt_idx" ON "PaymentEventLog"("status", "processedAt");
853
+ CREATE INDEX "PendingRegistration_checkoutGatewayAccount_checkoutSessionI_idx" ON "PendingRegistration"("checkoutGatewayAccount", "checkoutSessionId");
740
854
 
741
855
  -- CreateIndex
742
856
  CREATE UNIQUE INDEX "super_admin_users_email_key" ON "super_admin_users"("email");
@@ -753,21 +867,63 @@ CREATE INDEX "subscription_bundles_bundleVersionId_idx" ON "subscription_bundles
753
867
  -- CreateIndex
754
868
  CREATE INDEX "subscription_bundles_canceledEffectiveAt_idx" ON "subscription_bundles"("canceledEffectiveAt");
755
869
 
870
+ -- CreateIndex
871
+ CREATE UNIQUE INDEX "settings_changes_seq_key" ON "settings_changes"("seq");
872
+
873
+ -- CreateIndex
874
+ CREATE INDEX "settings_changes_acknowledgedAt_noticedAt_idx" ON "settings_changes"("acknowledgedAt", "noticedAt");
875
+
876
+ -- CreateIndex
877
+ CREATE UNIQUE INDEX "subscribers_customerSequence_key" ON "subscribers"("customerSequence");
878
+
879
+ -- CreateIndex
880
+ CREATE INDEX "subscriber_tenants_tenantId_idx" ON "subscriber_tenants"("tenantId");
881
+
882
+ -- CreateIndex
883
+ CREATE INDEX "subscriber_tenants_subscriberId_idx" ON "subscriber_tenants"("subscriberId");
884
+
885
+ -- CreateIndex
886
+ CREATE INDEX "subscriber_corrections_subscriberId_correctedAt_idx" ON "subscriber_corrections"("subscriberId", "correctedAt");
887
+
888
+ -- CreateIndex
889
+ CREATE INDEX "subscriber_payment_methods_subscriberId_status_idx" ON "subscriber_payment_methods"("subscriberId", "status");
890
+
891
+ -- CreateIndex
892
+ CREATE INDEX "subscriber_payment_methods_gatewayAccount_status_idx" ON "subscriber_payment_methods"("gatewayAccount", "status");
893
+
894
+ -- CreateIndex
895
+ CREATE UNIQUE INDEX "subscriber_payment_methods_gatewayAccount_paymentMethodRef_key" ON "subscriber_payment_methods"("gatewayAccount", "paymentMethodRef");
896
+
897
+ -- CreateIndex
898
+ CREATE INDEX "subscriber_payment_method_setups_subscriberId_idx" ON "subscriber_payment_method_setups"("subscriberId");
899
+
900
+ -- CreateIndex
901
+ CREATE UNIQUE INDEX "subscriber_payment_method_setups_gatewayAccount_sessionRef_key" ON "subscriber_payment_method_setups"("gatewayAccount", "sessionRef");
902
+
903
+ -- CreateIndex
904
+ CREATE INDEX "PaymentEventLog_sessionId_idx" ON "PaymentEventLog"("sessionId");
905
+
906
+ -- CreateIndex
907
+ CREATE INDEX "PaymentEventLog_status_processedAt_idx" ON "PaymentEventLog"("status", "processedAt");
908
+
909
+ -- CreateIndex
910
+ CREATE UNIQUE INDEX "PaymentEventLog_gatewayAccount_eventId_key" ON "PaymentEventLog"("gatewayAccount", "eventId");
911
+
756
912
  -- AddForeignKey
757
913
  ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_planVersionId_fkey" FOREIGN KEY ("planVersionId") REFERENCES "plan_versions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
758
914
 
759
915
  -- AddForeignKey
760
916
  ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_pendingPlanVersionId_fkey" FOREIGN KEY ("pendingPlanVersionId") REFERENCES "plan_versions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
761
917
 
762
- -- AddForeignKey
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;
764
-
765
918
  -- AddForeignKey
766
919
  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;
767
920
 
768
921
  -- AddForeignKey
769
922
  ALTER TABLE "promo_code_redemptions" ADD CONSTRAINT "promo_code_redemptions_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "subscriptions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
770
923
 
924
+ -- AddForeignKey
925
+ ALTER TABLE "promo_code_holds" ADD CONSTRAINT "promo_code_holds_promoCodeId_fkey" FOREIGN KEY ("promoCodeId") REFERENCES "promo_codes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
926
+
771
927
  -- AddForeignKey
772
928
  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;
773
929
 
@@ -780,6 +936,9 @@ ALTER TABLE "bundle_versions" ADD CONSTRAINT "bundle_versions_bundleId_fkey" FOR
780
936
  -- AddForeignKey
781
937
  ALTER TABLE "bundle_versions" ADD CONSTRAINT "bundle_versions_baseVersionId_fkey" FOREIGN KEY ("baseVersionId") REFERENCES "bundle_versions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
782
938
 
939
+ -- AddForeignKey
940
+ ALTER TABLE "subscription_contracts" ADD CONSTRAINT "subscription_contracts_subscriberId_fkey" FOREIGN KEY ("subscriberId") REFERENCES "subscribers"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
941
+
783
942
  -- AddForeignKey
784
943
  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;
785
944
 
@@ -789,6 +948,18 @@ ALTER TABLE "subscription_bundles" ADD CONSTRAINT "subscription_bundles_subscrip
789
948
  -- AddForeignKey
790
949
  ALTER TABLE "subscription_bundles" ADD CONSTRAINT "subscription_bundles_bundleVersionId_fkey" FOREIGN KEY ("bundleVersionId") REFERENCES "bundle_versions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
791
950
 
951
+ -- AddForeignKey
952
+ ALTER TABLE "subscriber_tenants" ADD CONSTRAINT "subscriber_tenants_subscriberId_fkey" FOREIGN KEY ("subscriberId") REFERENCES "subscribers"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
953
+
954
+ -- AddForeignKey
955
+ ALTER TABLE "subscriber_corrections" ADD CONSTRAINT "subscriber_corrections_subscriberId_fkey" FOREIGN KEY ("subscriberId") REFERENCES "subscribers"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
956
+
957
+ -- AddForeignKey
958
+ ALTER TABLE "subscriber_payment_methods" ADD CONSTRAINT "subscriber_payment_methods_subscriberId_fkey" FOREIGN KEY ("subscriberId") REFERENCES "subscribers"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
959
+
960
+ -- AddForeignKey
961
+ ALTER TABLE "subscriber_payment_method_setups" ADD CONSTRAINT "subscriber_payment_method_setups_subscriberId_fkey" FOREIGN KEY ("subscriberId") REFERENCES "subscribers"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
962
+
792
963
  -- =============================================================================
793
964
  -- SaaSiCat — normative PostgreSQL constraints the Prisma DSL cannot express.
794
965
  -- =============================================================================
@@ -808,3 +979,79 @@ CREATE UNIQUE INDEX IF NOT EXISTS plan_versions_draft_per_plan
808
979
 
809
980
  CREATE UNIQUE INDEX IF NOT EXISTS bundle_versions_draft_per_bundle
810
981
  ON bundle_versions ("bundleId") WHERE "publishedAt" IS NULL;
982
+
983
+ -- `marketing_settings` holds at most ONE row.
984
+ --
985
+ -- The Prisma DSL cannot say this, and the constant default on `id` does not: a
986
+ -- default applies only where the caller omits the value, and a primary key
987
+ -- accepts every distinct one. A consumer writing the row directly could make as
988
+ -- many as it liked, and the repository would then read one and ignore the rest.
989
+ --
990
+ -- `ADD CONSTRAINT` has no `IF NOT EXISTS`, and this file is applied again on
991
+ -- every deployment — so it is dropped first. Two plain statements rather than a
992
+ -- dollar-quoted `DO` block on purpose: every consumer of this file splits it at
993
+ -- the statement separator, and a dollar-quoted body would need each of those
994
+ -- splitters to become a SQL lexer. For the same reason no comment in this file
995
+ -- may contain that separator — one here did, and three splitters cut the
996
+ -- sentence in half.
997
+ ALTER TABLE marketing_settings
998
+ DROP CONSTRAINT IF EXISTS marketing_settings_is_a_singleton;
999
+ ALTER TABLE marketing_settings
1000
+ ADD CONSTRAINT marketing_settings_is_a_singleton CHECK ("id" = 'marketing-settings');
1001
+
1002
+ -- `applied_settings` holds at most ONE row: the installation's.
1003
+ --
1004
+ -- Same reasoning as above, and the same two plain statements. The row mirrors
1005
+ -- the settings the installation applied at its last start, and an installation
1006
+ -- serves one application, so there is exactly one configuration to record.
1007
+ ALTER TABLE applied_settings
1008
+ DROP CONSTRAINT IF EXISTS applied_settings_is_a_singleton;
1009
+ ALTER TABLE applied_settings
1010
+ ADD CONSTRAINT applied_settings_is_a_singleton CHECK ("id" = 'installation');
1011
+
1012
+ -- A subscriber is live for at most ONE tenant, and a tenant has at most ONE
1013
+ -- live subscriber. A link that ended keeps its row with `unlinkedAt` set, so the
1014
+ -- tenants a subscriber had before stay in its history. Two partial unique
1015
+ -- indexes, because a link that is over must not count against the next one.
1016
+ CREATE UNIQUE INDEX IF NOT EXISTS subscriber_tenants_live_per_tenant
1017
+ ON subscriber_tenants ("tenantId") WHERE "unlinkedAt" IS NULL;
1018
+
1019
+ CREATE UNIQUE INDEX IF NOT EXISTS subscriber_tenants_live_per_subscriber
1020
+ ON subscriber_tenants ("subscriberId") WHERE "unlinkedAt" IS NULL;
1021
+
1022
+ -- A subscriber has at most ONE payment method in use. The one a newer payment
1023
+ -- method replaced keeps its row as `REPLACED`, so it does not count here.
1024
+ CREATE UNIQUE INDEX IF NOT EXISTS subscriber_payment_methods_active_per_subscriber
1025
+ ON subscriber_payment_methods ("subscriberId") WHERE "status" = 'ACTIVE';
1026
+
1027
+ -- A gateway session is confirmed ONCE.
1028
+ --
1029
+ -- A gateway delivers every event at least once, and it may report one session
1030
+ -- through more than one event — a completed form and the payment method behind
1031
+ -- it can arrive as two. `eventId` tells those apart, so both would be claimed,
1032
+ -- and both would record the session's payment method: for a sign-up that means
1033
+ -- a second tenant for one registration.
1034
+ --
1035
+ -- Both adapters claim with `ON CONFLICT DO NOTHING` and no conflict target, so
1036
+ -- this index turns the second confirmation into no row rather than an error,
1037
+ -- and the caller reads it as the duplicate it is. Partial on the kind, because
1038
+ -- a failed setup and an event SaaSiCat does not act on say nothing about the
1039
+ -- session being confirmed. A claim carrying no session does not collide:
1040
+ -- PostgreSQL counts nulls as distinct in a unique index.
1041
+ CREATE UNIQUE INDEX IF NOT EXISTS payment_event_log_confirmation_per_session
1042
+ ON "PaymentEventLog" ("gatewayAccount", "sessionId")
1043
+ WHERE "status" = 'payment-method-confirmed';
1044
+
1045
+ -- Customer numbers count from 10001, so a number has five digits up to 99999
1046
+ -- and none reads as a count of subscribers. Two statements: the first makes
1047
+ -- 10001 where the sequence starts over, which a restart of its identity reads,
1048
+ -- and the second moves a sequence that has never handed out a number there. A
1049
+ -- second run finds the start already set and the sequence used, and an
1050
+ -- installation without the subscriber tables has no such sequence.
1051
+ ALTER SEQUENCE IF EXISTS "subscribers_customerSequence_seq" START WITH 10001;
1052
+
1053
+ SELECT setval(format('%I.%I', schemaname, sequencename)::regclass, 10001, false)
1054
+ FROM pg_sequences
1055
+ WHERE schemaname = current_schema()
1056
+ AND sequencename = 'subscribers_customerSequence_seq'
1057
+ AND last_value IS NULL;