@saasicat/adapter-prisma 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/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { PersistenceInjectionToken, PasswordHasher, SaasicatPersistenceAdapter, TransactionRunner, TransactionContext, MfaPort, AuditPort, AdminActor, AuditQueryPort, AuditQuery, AuditEntry, AuditStatsPort, RlsBypassPort, SubscriptionRepository, SubscriptionRecord, PlanVersionRepository, PlanVersionRecord, PromoCodeRepository, PromoCodeRecord, PromoCodeFilter, CreatePromoCodeData, UpdatePromoCodeData, PromoCodeRedemptionRepository, PromoCodeRedemptionRecord, PromoCodeRedemptionStatus, PromoCodeRedemptionListItem, PromoCodeValidationLogRepository, PromoSubscriptionLookup, BillingCycle, PromoRevenueDeductionAggregator, SuperAdminProvisioningPort, CreateSuperAdminCliInput, PlatformUserDto, PlanCatalogReadSink, PlanCatalogReadSnapshot, PlanCatalogImportSink, UpsertPlanInput, UpsertResult, UpsertPlanVersionInput, UpsertFeatureCatalogEntryInput } from '@saasicat/types';
1
+ import { PersistenceInjectionToken, PasswordHasher, SaasicatPersistenceAdapter, TransactionRunner, TransactionContext, MfaPort, AuditPort, AdminActor, AuditQueryPort, AuditQuery, AuditEntry, AuditStatsPort, RlsBypassPort, SubscriptionRepository, SubscriptionRecord, SubscriptionBundleRepository, SubscriptionBundleRecord, CreateSubscriptionBundleData, CancelSubscriptionBundleData, TenantSubscriptionWritePort, ApplyOnboardingSelectionInput, RedeemPromoInTransactionCallback, ApplyOnboardingSelectionResult, ImmediatePlanChangeInput, ScheduledPlanChangeInput, PlanVersionRepository, PlanVersionRecord, PromoCodeRepository, PromoCodeRecord, PromoCodeFilter, CreatePromoCodeData, UpdatePromoCodeData, PromoCodeRedemptionRepository, PromoCodeRedemptionRecord, PromoCodeRedemptionStatus, PromoCodeRedemptionListItem, PromoCodeValidationLogRepository, PromoSubscriptionLookup, BillingCycle, PromoRevenueDeductionAggregator, SuperAdminProvisioningPort, CreateSuperAdminCliInput, PlatformUserDto, PlanCatalogReadSink, PlanCatalogReadSnapshot, PlanCatalogImportSink, UpsertPlanInput, UpsertResult, UpsertPlanVersionInput, UpsertFeatureCatalogEntryInput, PlanRepository, PlanListFilter, PlanRow, CreatePlanData, UpdatePlanData, PlanVersionRow, CreatePlanVersionDraftData, UpdatePlanVersionDraftData, VersionChange, BundleRepository, BundleVersionRow, BundleListFilter, BundleRow, CreateBundleData, UpdateBundleData, CreateBundleVersionDraftData, UpdateBundleVersionDraftData, CatalogEntryRepository, CatalogEntryFilter, CapabilityCatalogEntryRow, FeatureCatalogEntryRow, QuotaCatalogEntryRow, UpsertCapabilityEntryData, UpsertFeatureEntryData, UpsertQuotaEntryData, SetCatalogEntryReviewData, CatalogEntryI18n, UpdateCatalogEntryBaseData, MarketingProjectionRepository, MarketingProjectionFilter, MarketingProjectionRow, CreateMarketingProjectionData, UpdateMarketingProjectionData, MarketingSettingsRepository, MarketingSettingsRow, UpdateMarketingSettingsData, PromotionRepository, PromotionFilter, PromotionRow, CreatePromotionData, UpdatePromotionData, SubscriptionContractRepository, SubscriptionContractFilter, SubscriptionContractRecord, CreateSubscriptionContractData, TerminateSubscriptionContractData } from '@saasicat/types';
2
2
 
3
3
  declare const PRISMA_CLIENT_TOKEN: unique symbol;
4
4
  /** Prisma `Decimal` values arrive as objects with `toString()`; tests may use plain values. */
@@ -16,7 +16,7 @@ interface SubscriptionRowLike {
16
16
  pendingPlan: string | null;
17
17
  pendingEffectiveAt: Date | null;
18
18
  customLimits: unknown;
19
- planVersionId: string | null;
19
+ planVersionId: string;
20
20
  pendingPlanVersionId: string | null;
21
21
  startedAt: Date | null;
22
22
  }
@@ -37,6 +37,9 @@ interface PlanVersionRowLike {
37
37
  nonRegressive: boolean;
38
38
  createdByUserId: string | null;
39
39
  publishedByUserId: string | null;
40
+ validFrom?: Date | null;
41
+ validUntil?: Date | null;
42
+ endsAt?: Date | null;
40
43
  createdAt: Date;
41
44
  updatedAt: Date;
42
45
  }
@@ -547,6 +550,132 @@ interface PrismaTxLike {
547
550
  interface PrismaLike extends PrismaTxLike {
548
551
  $transaction<T>(fn: (tx: PrismaTxLike) => Promise<T>): Promise<T>;
549
552
  }
553
+ /**
554
+ * Generic structural minimum of a Prisma model delegate. The catalog-plane
555
+ * repositories (bundle, plan, catalog-entry, marketing,
556
+ * promotion, contract) declare their own DB-row interfaces and view the
557
+ * injected client through `{ model: PrismaModelDelegateLike<Row> }` casts —
558
+ * this keeps each repo self-contained and avoids hard-coding every delegate on
559
+ * `PrismaTxLike`. Args mirror Prisma's `where`/`data`/`select`/`orderBy` shapes
560
+ * and are typed `unknown`: the repos build them inline and Prisma validates
561
+ * them at runtime; only the results are typed, matching the package's
562
+ * structural-minimum philosophy.
563
+ */
564
+ interface PrismaModelDelegateLike<Row> {
565
+ findMany(args?: unknown): Promise<Row[]>;
566
+ findUnique(args: unknown): Promise<Row | null>;
567
+ findFirst(args?: unknown): Promise<Row | null>;
568
+ create(args: unknown): Promise<Row>;
569
+ update(args: unknown): Promise<Row>;
570
+ delete(args: unknown): Promise<Row>;
571
+ upsert(args: unknown): Promise<Row>;
572
+ updateMany(args: unknown): Promise<{
573
+ count: number;
574
+ }>;
575
+ createMany(args: unknown): Promise<{
576
+ count: number;
577
+ }>;
578
+ deleteMany(args: unknown): Promise<{
579
+ count: number;
580
+ }>;
581
+ count(args?: unknown): Promise<number>;
582
+ }
583
+
584
+ /**
585
+ * How `PlanVersion.planId` is persisted.
586
+ *
587
+ * - `legacy-plan-key`: SaaSiCat 0.6 behavior. `planId` stores the semantic
588
+ * `Plan.planKey` directly and no lookup is performed.
589
+ * - `normalized-plan-id`: `planId` stores the database `Plan.id` foreign key.
590
+ * Adapter ports still accept and return semantic plan keys.
591
+ */
592
+ type PrismaPlanBindingMode = 'legacy-plan-key' | 'normalized-plan-id';
593
+ type PrismaPlanBindingOptions = {
594
+ mode?: 'legacy-plan-key';
595
+ projectKey?: string;
596
+ } | {
597
+ mode: 'normalized-plan-id';
598
+ projectKey: string;
599
+ };
600
+ /**
601
+ * Prisma delegate names used by the two independent plan-version slices.
602
+ * Both default to `planVersion`, preserving the 0.6 canonical schema.
603
+ */
604
+ interface PrismaPlanDelegateOptions {
605
+ catalogPlanVersion?: string;
606
+ entitlementPlanVersion?: string;
607
+ }
608
+ interface PrismaPlanVersionFieldCapabilities {
609
+ /** The delegate carries `validFrom` and `validUntil` columns. */
610
+ validityWindows?: boolean;
611
+ /** The delegate carries the optional, precise `endsAt` timestamp. */
612
+ endsAt?: boolean;
613
+ }
614
+ /**
615
+ * Shared field defaults plus optional per-slice overrides. A consumer with
616
+ * one plan-version model can set the top-level flags; split schemas can
617
+ * configure catalog and entitlement independently.
618
+ */
619
+ interface PrismaPlanVersionFieldOptions extends PrismaPlanVersionFieldCapabilities {
620
+ catalog?: PrismaPlanVersionFieldCapabilities;
621
+ entitlement?: PrismaPlanVersionFieldCapabilities;
622
+ }
623
+ interface PrismaTenantSubscriptionOptions {
624
+ /**
625
+ * Prisma model delegate used for every Subscription ORM operation.
626
+ * Locked reads still address the canonical physical `subscriptions`
627
+ * table, so a differently named model must use `@@map("subscriptions")`.
628
+ */
629
+ delegate?: string;
630
+ /**
631
+ * Optional SubscriptionBundle delegate used for BundleVersion booking
632
+ * counts. `false` keeps the capability absent for schemas without the
633
+ * junction table.
634
+ */
635
+ subscriptionBundleDelegate?: string | false;
636
+ synchronizePlanVersion?: boolean;
637
+ /** Expose the optional atomic onboarding + promo callback capability. */
638
+ atomicOnboardingSelection?: boolean;
639
+ activeVersionSelection?: 'latest-live' | 'validity-window';
640
+ withEndsAt?: boolean;
641
+ }
642
+ /** Schema differences understood by the plan-related Prisma adapters. */
643
+ interface PrismaSchemaOptions {
644
+ planBinding?: PrismaPlanBindingOptions;
645
+ delegates?: PrismaPlanDelegateOptions;
646
+ planVersionFields?: PrismaPlanVersionFieldOptions;
647
+ tenantSubscription?: PrismaTenantSubscriptionOptions;
648
+ }
649
+ interface ResolvedPrismaSchemaOptions {
650
+ planBinding: {
651
+ mode: PrismaPlanBindingMode;
652
+ projectKey?: string;
653
+ };
654
+ delegates: {
655
+ catalogPlanVersion: string;
656
+ entitlementPlanVersion: string;
657
+ };
658
+ planVersionFields: {
659
+ catalog: Required<PrismaPlanVersionFieldCapabilities>;
660
+ entitlement: Required<PrismaPlanVersionFieldCapabilities>;
661
+ };
662
+ tenantSubscription: Required<PrismaTenantSubscriptionOptions>;
663
+ }
664
+ /**
665
+ * Optional DI token for direct Nest registration of individual adapters.
666
+ * `prismaPersistence()` passes the options to constructors itself.
667
+ */
668
+ declare const PRISMA_SCHEMA_OPTIONS_TOKEN: unique symbol;
669
+ interface PrismaPlanBindingResolver {
670
+ readonly mode: PrismaPlanBindingMode;
671
+ readonly projectKey?: string;
672
+ toStoragePlanId(client: unknown, planKey: string, projectKey?: string): Promise<string>;
673
+ toPlanKey(client: unknown, storedPlanId: string, projectKey?: string): Promise<string>;
674
+ }
675
+ declare function resolvePrismaSchemaOptions(options?: PrismaSchemaOptions): ResolvedPrismaSchemaOptions;
676
+ declare function createPrismaPlanBindingResolver(options?: PrismaPlanBindingOptions): PrismaPlanBindingResolver;
677
+ /** Resolves a configurable Prisma delegate and fails early on misspellings. */
678
+ declare function getPrismaDelegate<Row>(client: unknown, delegateName: string): PrismaModelDelegateLike<Row>;
550
679
 
551
680
  interface PrismaPersistenceOptions {
552
681
  /**
@@ -569,6 +698,12 @@ interface PrismaPersistenceOptions {
569
698
  * middleware. Default false.
570
699
  */
571
700
  rlsIntegration?: boolean;
701
+ /**
702
+ * Optional plan-schema adaptations. Omitted means the exact 0.6 layout:
703
+ * soft `planId === planKey`, one `planVersion` delegate, no validity or
704
+ * termination columns.
705
+ */
706
+ schema?: PrismaSchemaOptions;
572
707
  }
573
708
  /**
574
709
  * Builds the `SaasicatPersistenceAdapter` bundle for Prisma + PostgreSQL on
@@ -581,9 +716,14 @@ interface PrismaPersistenceOptions {
581
716
  * });
582
717
  * ```
583
718
  *
584
- * Slices not shipped by this adapter (contracts, bundles, registration,
585
- * tenant-billing write ports) stay absent consumers keep providing custom
586
- * adapters for those features.
719
+ * This bundle covers the core/entitlement/promo/plan-catalog slices consumed
720
+ * by `SaasPlatformModule`. The catalog plane (CatalogModule) and the V3
721
+ * contract loop take their repositories directly as `forRoot` options — wire
722
+ * the standalone `PrismaPlanRepository` / `PrismaBundleRepository` /
723
+ * `PrismaCatalogEntryRepository` /
724
+ * `PrismaMarketingProjectionRepository` / `PrismaMarketingSettingsRepository` /
725
+ * `PrismaPromotionRepository` / `PrismaSubscriptionContractRepository` exports
726
+ * there. The registration and tenant-billing write ports remain app-specific.
587
727
  */
588
728
  declare function prismaPersistence(options: PrismaPersistenceOptions): SaasicatPersistenceAdapter;
589
729
 
@@ -594,7 +734,7 @@ declare function prismaPersistence(options: PrismaPersistenceOptions): SaasicatP
594
734
  */
595
735
  declare class PrismaTransactionRunner implements TransactionRunner {
596
736
  private readonly prisma;
597
- constructor(prisma: PrismaLike);
737
+ constructor(prisma: Record<'$transaction', unknown>);
598
738
  run<T>(fn: (tx: TransactionContext) => Promise<T>): Promise<T>;
599
739
  }
600
740
 
@@ -615,7 +755,7 @@ declare class PrismaTransactionRunner implements TransactionRunner {
615
755
  */
616
756
  declare class PrismaMfaAdapter implements MfaPort {
617
757
  private readonly prisma;
618
- constructor(prisma: PrismaLike);
758
+ constructor(prisma: Pick<PrismaLike, 'superAdminMfa'>);
619
759
  getSecret(userId: string): Promise<string | null>;
620
760
  setSecret(userId: string, secret: string | null): Promise<void>;
621
761
  isEnabled(userId: string): Promise<boolean>;
@@ -632,7 +772,7 @@ declare function buildActorTag(actor: AdminActor): string;
632
772
  */
633
773
  declare class PrismaAuditAdapter implements AuditPort {
634
774
  private readonly prisma;
635
- constructor(prisma: PrismaLike);
775
+ constructor(prisma: Pick<PrismaLike, 'auditLog'>);
636
776
  write(input: {
637
777
  actor: AdminActor;
638
778
  entity: string;
@@ -648,14 +788,14 @@ declare class PrismaAuditAdapter implements AuditPort {
648
788
  */
649
789
  declare class PrismaAuditQueryAdapter implements AuditQueryPort {
650
790
  private readonly prisma;
651
- constructor(prisma: PrismaLike);
791
+ constructor(prisma: Pick<PrismaLike, 'auditLog'>);
652
792
  list(filter: AuditQuery): Promise<AuditEntry[]>;
653
793
  }
654
794
 
655
795
  /** `AuditStatsPort` against the canonical `audit_logs` table. */
656
796
  declare class PrismaAuditStatsAdapter implements AuditStatsPort {
657
797
  private readonly prisma;
658
- constructor(prisma: PrismaLike);
798
+ constructor(prisma: Pick<PrismaLike, 'auditLog'>);
659
799
  countSince(since: Date): Promise<number>;
660
800
  }
661
801
 
@@ -692,42 +832,146 @@ declare class AsyncLocalRlsBypassAdapter implements RlsBypassPort {
692
832
  isBypassActive(): boolean;
693
833
  }
694
834
 
835
+ /** Root-client fields used directly; subscription and PlanVersion delegates are configurable. */
836
+ interface SubscriptionRepositoryClient {
837
+ /** Used by normalized semantic-key ↔ UUID binding. */
838
+ plan: unknown;
839
+ }
695
840
  /**
696
841
  * `SubscriptionRepository` against the canonical `subscriptions` +
697
842
  * `plan_versions` tables.
698
843
  *
699
- * Limitation: subscriptions that bind ONLY a `businessTypeVersionId` (no
700
- * `planVersionId`) are not supported by this shipped adapter — the
701
- * BusinessType composition needs app-specific aggregation. Such rows raise a
702
- * descriptive error instead of returning wrong entitlements.
703
- *
704
- * `countByBundleVersionId` is deliberately not implemented (the
705
- * `subscription_bundles` junction is not part of this adapter's slice);
706
- * the platform then treats affected bundle versions as frozen (fail-closed).
844
+ * BundleVersion booking counts are an optional capability. Configure
845
+ * `tenantSubscription.subscriptionBundleDelegate` for schemas carrying the
846
+ * `subscription_bundles` junction; without it the platform keeps affected
847
+ * versions frozen (fail-closed).
707
848
  */
708
849
  declare class PrismaSubscriptionRepository implements SubscriptionRepository {
709
850
  private readonly prisma;
710
- constructor(prisma: PrismaLike);
851
+ readonly countByBundleVersionId?: (bundleVersionId: string) => Promise<number>;
852
+ private readonly binding;
853
+ private readonly planVersionDelegateName;
854
+ private readonly subscriptionDelegateName;
855
+ private readonly subscriptionBundleDelegateName;
856
+ constructor(prisma: SubscriptionRepositoryClient, options?: PrismaSchemaOptions);
857
+ private db;
711
858
  findByTenantId(tenantId: string): Promise<SubscriptionRecord | null>;
712
859
  findByTenantIdLocked(tenantId: string, tx: TransactionContext): Promise<SubscriptionRecord | null>;
713
860
  countByPlanVersionId(planVersionId: string): Promise<number>;
714
- countActiveByPlanKey(_projectKey: string): Promise<Record<string, number>>;
861
+ countActiveByPlanKey(projectKey: string): Promise<Record<string, number>>;
862
+ private countActiveBundleBindings;
715
863
  private loadByTenantId;
716
864
  private toRecord;
865
+ private planVersions;
866
+ private subscriptions;
867
+ private planKeysForProject;
868
+ }
869
+
870
+ interface SubscriptionBundleClient {
871
+ subscriptionBundle: unknown;
872
+ }
873
+ /**
874
+ * `SubscriptionBundleRepository` against the canonical `subscription_bundles`
875
+ * junction (SPEC_V2 §11.1 M6 Pack 2e). Dumb persistence: domain constraints
876
+ * (plan compatibility, minimum-term default, cancellation-window computation)
877
+ * live in the platform's `SubscriptionBundlesService`; `add`/`cancel` here only
878
+ * write what they are handed. "Active" is `canceledAt IS NULL OR
879
+ * canceledEffectiveAt > asOf`, mirroring the port contract.
880
+ */
881
+ declare class PrismaSubscriptionBundleRepository implements SubscriptionBundleRepository {
882
+ private readonly prisma;
883
+ constructor(prisma: SubscriptionBundleClient);
884
+ private db;
885
+ listBySubscription(subscriptionId: string): Promise<SubscriptionBundleRecord[]>;
886
+ findById(subscriptionBundleId: string): Promise<SubscriptionBundleRecord | null>;
887
+ listActiveBySubscription(subscriptionId: string, asOf?: Date): Promise<SubscriptionBundleRecord[]>;
888
+ add(data: CreateSubscriptionBundleData): Promise<SubscriptionBundleRecord>;
889
+ cancel(subscriptionBundleId: string, data: CancelSubscriptionBundleData): Promise<SubscriptionBundleRecord>;
890
+ reactivate(subscriptionBundleId: string): Promise<SubscriptionBundleRecord>;
891
+ countActiveByBundleVersionId(bundleVersionId: string, asOf?: Date): Promise<number>;
717
892
  }
718
893
 
894
+ /** Structural minimum of the root client; model delegates are configurable. */
895
+ interface TransactionalPrismaClient {
896
+ $transaction<T>(fn: (tx: unknown) => Promise<T>): Promise<T>;
897
+ }
898
+ /**
899
+ * `TenantSubscriptionWritePort` against a configurable Prisma subscription
900
+ * delegate.
901
+ *
902
+ * The 0.6 default remains deliberately conservative:
903
+ * `tenantSubscription.synchronizePlanVersion` is false, so
904
+ * `changePlanImmediate` writes the semantic `plan` and cycle exactly as
905
+ * before. Opting into synchronization resolves the target plan through the
906
+ * configured plan binding, selects its live/active PlanVersion, and writes
907
+ * `plan` + `planVersionId` in one transaction.
908
+ *
909
+ * Pure persistence: trial carry-over (#17) and contract freeze (#18) are
910
+ * resolved in the platform `changePlan` path and handed down as plain values —
911
+ * this adapter only writes what it receives.
912
+ *
913
+ * `applyOnboardingSelection` is an explicit opt-in capability. When
914
+ * `tenantSubscription.atomicOnboardingSelection` is true it uses one
915
+ * interactive transaction for the subscription write and optional promo
916
+ * callback. In synchronized mode it also binds the concrete PlanVersion and
917
+ * clears stale pending-version state.
918
+ */
919
+ declare class PrismaTenantSubscriptionWriteAdapter implements TenantSubscriptionWritePort {
920
+ private readonly prisma;
921
+ readonly applyOnboardingSelection?: (tenantId: string, input: ApplyOnboardingSelectionInput, redeemPromo: RedeemPromoInTransactionCallback | null) => Promise<ApplyOnboardingSelectionResult>;
922
+ private readonly schema;
923
+ private readonly planBinding;
924
+ constructor(prisma: TransactionalPrismaClient, options?: PrismaSchemaOptions);
925
+ changePlanImmediate(tenantId: string, input: ImmediatePlanChangeInput): Promise<{
926
+ plan: string;
927
+ billingCycle: string;
928
+ }>;
929
+ private changePlanImmediateInClient;
930
+ schedulePlanChange(tenantId: string, input: ScheduledPlanChangeInput): Promise<void>;
931
+ acceptPendingPlanVersion(tenantId: string, userId: string, now: Date): Promise<{
932
+ accepted: boolean;
933
+ acceptedAt: Date | null;
934
+ effectiveAt: Date | null;
935
+ alreadyAccepted: boolean;
936
+ }>;
937
+ private applyOnboardingSelectionAtomic;
938
+ cancelSubscription(tenantId: string, immediate: boolean, now: Date): Promise<{
939
+ canceledAt: Date | null;
940
+ status: string;
941
+ }>;
942
+ private subscription;
943
+ private planVersions;
944
+ private findTargetPlanVersionId;
945
+ private pendingVersionBelongsToAnotherPlan;
946
+ private assertConfiguration;
947
+ }
948
+
949
+ /**
950
+ * Root-client fields needed outside the configurable PlanVersion delegate.
951
+ * `plan` is used by normalized semantic-key ↔ UUID binding.
952
+ */
953
+ interface PlanVersionRepositoryClient {
954
+ plan: unknown;
955
+ }
719
956
  /**
720
957
  * `PlanVersionRepository` against the canonical `plan_versions` table.
721
958
  *
722
- * `findActive` is deliberately not implemented: the canonical schema carries
723
- * no `validFrom`/`validUntil` columns yet, so time-aware resolution is not
724
- * expressible consumers fall back to `findLatestLive` (documented port
725
- * behavior).
959
+ * `findActive` remains absent with the canonical 0.6 defaults. Extended
960
+ * schemas enable it through `schema.planVersionFields.entitlement`; its query
961
+ * then uses the shared day-inclusive validity semantics.
726
962
  */
727
963
  declare class PrismaPlanVersionRepository implements PlanVersionRepository {
728
964
  private readonly prisma;
729
- constructor(prisma: PrismaLike);
965
+ readonly findActive?: (planId: string, asOf?: Date, tx?: TransactionContext) => Promise<PlanVersionRecord | null>;
966
+ private readonly binding;
967
+ private readonly delegateName;
968
+ private readonly fields;
969
+ constructor(prisma: PlanVersionRepositoryClient, options?: PrismaSchemaOptions);
970
+ private db;
730
971
  findLatestLive(planId: string, tx?: TransactionContext): Promise<PlanVersionRecord | null>;
972
+ private findActivePlanVersion;
973
+ private versions;
974
+ private toRecord;
731
975
  }
732
976
 
733
977
  /**
@@ -827,33 +1071,302 @@ declare class PrismaSuperAdminBootstrapAdapter implements SuperAdminProvisioning
827
1071
  createSuperAdmin(input: CreateSuperAdminCliInput): Promise<PlatformUserDto>;
828
1072
  }
829
1073
 
1074
+ /** Root-client fields used directly; the PlanVersion delegate is configurable. */
1075
+ interface PlanCatalogReadClient {
1076
+ plan: unknown;
1077
+ featureCatalogEntry: unknown;
1078
+ }
830
1079
  /**
831
1080
  * `PlanCatalogReadSink` against the canonical `plans`, `plan_versions` and
832
1081
  * `feature_catalog_entries` tables — DB hydration of the plan catalog at
833
1082
  * boot (`PlanCatalogModule.forRoot({ sink })`).
834
1083
  *
835
- * `validFrom`/`validUntil` are reported as null: the canonical schema does
836
- * not persist booking windows yet (see docs/data-model.md, "Known gaps").
1084
+ * The canonical 0.6 defaults report validity fields as null. Extended schemas
1085
+ * can opt into validity/termination fields and can point this sink at a
1086
+ * catalog-specific plan-version delegate.
837
1087
  */
838
1088
  declare class PrismaPlanCatalogReadSink implements PlanCatalogReadSink {
839
1089
  private readonly prisma;
840
- constructor(prisma: PrismaLike);
1090
+ private readonly binding;
1091
+ private readonly delegateName;
1092
+ private readonly fields;
1093
+ constructor(prisma: PlanCatalogReadClient, options?: PrismaSchemaOptions);
1094
+ private db;
841
1095
  loadSnapshot(projectKey: string): Promise<PlanCatalogReadSnapshot>;
1096
+ private planVersions;
842
1097
  }
843
1098
 
1099
+ /** Root-client fields used directly; the PlanVersion delegate is configurable. */
1100
+ interface PlanCatalogImportClient {
1101
+ plan: unknown;
1102
+ featureCatalogEntry: unknown;
1103
+ }
844
1104
  /**
845
1105
  * `PlanCatalogImportSink` against the canonical catalog tables — the
846
1106
  * one-shot `saas.yaml → DB` import at boot.
847
1107
  *
848
1108
  * Idempotency per the port contract: existing rows (identity match) are
849
- * skipped without error; only the created/skipped flags are reported.
1109
+ * skipped without error; only the created/skipped flags are reported. In
1110
+ * normalized mode the semantic `planKey` input is resolved to `Plan.id`.
850
1111
  */
851
1112
  declare class PrismaPlanCatalogImportSink implements PlanCatalogImportSink {
852
1113
  private readonly prisma;
853
- constructor(prisma: PrismaLike);
1114
+ private readonly binding;
1115
+ private readonly delegateName;
1116
+ constructor(prisma: PlanCatalogImportClient, options?: PrismaSchemaOptions);
1117
+ private db;
854
1118
  upsertPlan(input: UpsertPlanInput): Promise<UpsertResult>;
855
1119
  upsertPlanVersion(input: UpsertPlanVersionInput): Promise<UpsertResult>;
1120
+ private planVersions;
856
1121
  upsertFeatureCatalogEntry(input: UpsertFeatureCatalogEntryInput): Promise<UpsertResult>;
857
1122
  }
858
1123
 
859
- export { AsyncLocalRlsBypassAdapter, type DecimalLike, PASSWORD_HASHER_TOKEN, PRISMA_CLIENT_TOKEN, PrismaAuditAdapter, PrismaAuditQueryAdapter, PrismaAuditStatsAdapter, type PrismaLike, PrismaMfaAdapter, type PrismaPersistenceOptions, PrismaPlanCatalogImportSink, PrismaPlanCatalogReadSink, PrismaPlanVersionRepository, PrismaPromoCodeRedemptionRepository, PrismaPromoCodeRepository, PrismaPromoCodeValidationLogRepository, PrismaPromoSubscriptionLookup, PrismaSubscriptionRepository, PrismaSuperAdminBootstrapAdapter, PrismaTransactionRunner, type PrismaTxLike, ZeroPromoRevenueDeductionAggregator, buildActorTag, prismaPersistence };
1124
+ /** Root-client fields used directly; the version delegate is configurable. */
1125
+ interface PlanRepositoryClient {
1126
+ plan: unknown;
1127
+ $transaction<T>(fn: (tx: unknown) => Promise<T>): Promise<T>;
1128
+ }
1129
+ /**
1130
+ * `PlanRepository` against the canonical `plans` + `plan_versions` tables
1131
+ * (SPEC_V2 §11.1 M6). Plan stem CRUD (Pack 1) and PlanVersion lifecycle
1132
+ * (Pack 2a) live in one adapter. Ports always use the semantic **planKey**;
1133
+ * storage defaults to the 0.6 soft binding
1134
+ * `PlanVersion.planId === Plan.planKey`. The opt-in normalized binding resolves
1135
+ * that key to `Plan.id` for every database operation.
1136
+ *
1137
+ * The canonical 0.6 schema has neither validity-window nor `endsAt` columns.
1138
+ * Both capabilities therefore default off for rolling compatibility; apps
1139
+ * that applied the current additive schema opt in through
1140
+ * `schema.planVersionFields.catalog`.
1141
+ */
1142
+ declare class PrismaPlanRepository implements PlanRepository {
1143
+ private readonly prisma;
1144
+ private readonly binding;
1145
+ private readonly delegateName;
1146
+ private readonly fields;
1147
+ constructor(prisma: PlanRepositoryClient, options?: PrismaSchemaOptions);
1148
+ private db;
1149
+ private versions;
1150
+ list(filter: PlanListFilter): Promise<PlanRow[]>;
1151
+ findById(planId: string): Promise<PlanRow | null>;
1152
+ findByKey(projectKey: string, planKey: string): Promise<PlanRow | null>;
1153
+ create(data: CreatePlanData): Promise<PlanRow>;
1154
+ update(planId: string, data: UpdatePlanData): Promise<PlanRow>;
1155
+ softDelete(planId: string): Promise<void>;
1156
+ hardDelete(planId: string): Promise<void>;
1157
+ listVersions(planKey: string): Promise<PlanVersionRow[]>;
1158
+ findVersionById(versionId: string): Promise<PlanVersionRow | null>;
1159
+ findCurrentDraft(planKey: string): Promise<PlanVersionRow | null>;
1160
+ findLatestLivePlanVersion(planKey: string, tx?: TransactionContext): Promise<PlanVersionRow | null>;
1161
+ findActivePlanVersion(planKey: string, asOf?: Date, tx?: TransactionContext): Promise<PlanVersionRow | null>;
1162
+ createPlanVersionDraft(data: CreatePlanVersionDraftData): Promise<PlanVersionRow>;
1163
+ updatePlanVersionDraft(versionId: string, data: UpdatePlanVersionDraftData): Promise<PlanVersionRow>;
1164
+ publishPlanVersionDraft(versionId: string, publishMeta: {
1165
+ publishedByUserId: string | null;
1166
+ publishedChanges: VersionChange[];
1167
+ nonRegressive: boolean;
1168
+ validFrom: Date;
1169
+ validUntil: Date | null;
1170
+ }, tx?: TransactionContext): Promise<PlanVersionRow>;
1171
+ deletePlanVersionDraft(versionId: string): Promise<void>;
1172
+ terminate(versionId: string, endsAt: Date): Promise<PlanVersionRow>;
1173
+ private toPlanVersionRow;
1174
+ }
1175
+
1176
+ /**
1177
+ * Root-client shape accepted by this repository. Delegates and `$transaction`
1178
+ * stay opaque at the injection boundary because generated Prisma delegates
1179
+ * have schema-specific generic signatures; calls are narrowed locally to the
1180
+ * exact Bundle operations below.
1181
+ */
1182
+ interface BundlePrismaClient {
1183
+ bundle: unknown;
1184
+ bundleVersion: unknown;
1185
+ $transaction: unknown;
1186
+ }
1187
+ /**
1188
+ * Optional DI token for apps that register `PrismaBundleRepository` directly
1189
+ * as a Nest provider. Factory users may pass the same options as the second
1190
+ * constructor argument.
1191
+ */
1192
+ declare const PRISMA_BUNDLE_REPOSITORY_OPTIONS: unique symbol;
1193
+ interface PrismaBundleRepositoryOptions {
1194
+ /**
1195
+ * Enables reads and writes of `BundleVersion.validFrom`/`validUntil`.
1196
+ *
1197
+ * Default `false` preserves compatibility with databases and Prisma
1198
+ * clients generated from the 0.6 schema, where the columns do not exist.
1199
+ */
1200
+ validityWindows?: boolean;
1201
+ }
1202
+ /**
1203
+ * `BundleRepository` against the canonical `bundles` + `bundle_versions`
1204
+ * tables (SPEC_V2 §5 + §11.1 M3). Versioning mirrors `PlanVersion`: at most one
1205
+ * draft (`publishedAt IS NULL`) per bundle, monotonically incrementing
1206
+ * `version`, `supersededAt` marking the previous live version on publish.
1207
+ *
1208
+ * Validity-window support is opt-in via `{ validityWindows: true }`. The
1209
+ * default deliberately does not select, read or write `validFrom`/
1210
+ * `validUntil`, and therefore keeps working with the 0.6 schema. In the
1211
+ * enabled mode the repository expects both nullable columns to exist.
1212
+ *
1213
+ * In validity-window mode `publishDraft` opens an internal transaction when
1214
+ * the caller did not provide one. This makes superseding the predecessor,
1215
+ * applying its auto-succession end date and publishing the draft atomic.
1216
+ * Legacy mode retains the 0.6 transaction behavior unchanged.
1217
+ */
1218
+ declare class PrismaBundleRepository implements BundleRepository {
1219
+ private readonly prisma;
1220
+ private readonly validityWindows;
1221
+ /**
1222
+ * Present only when validity-window mode is enabled. This mirrors the
1223
+ * optional port capability and lets 0.6-schema consumers detect that an
1224
+ * active-at-time lookup is unavailable.
1225
+ */
1226
+ readonly findActiveBundleVersion?: (bundleId: string, asOf?: Date, tx?: TransactionContext) => Promise<BundleVersionRow | null>;
1227
+ constructor(prisma: BundlePrismaClient, options?: PrismaBundleRepositoryOptions);
1228
+ private db;
1229
+ private transaction;
1230
+ list(filter: BundleListFilter): Promise<BundleRow[]>;
1231
+ findById(bundleId: string): Promise<BundleRow | null>;
1232
+ findByKey(projectKey: string, bundleKey: string): Promise<BundleRow | null>;
1233
+ create(data: CreateBundleData): Promise<BundleRow>;
1234
+ update(bundleId: string, data: UpdateBundleData): Promise<BundleRow>;
1235
+ softDelete(bundleId: string): Promise<void>;
1236
+ listVersions(bundleId: string): Promise<BundleVersionRow[]>;
1237
+ findVersionById(versionId: string): Promise<BundleVersionRow | null>;
1238
+ findCurrentDraft(bundleId: string): Promise<BundleVersionRow | null>;
1239
+ findLatestLive(bundleId: string, tx?: TransactionContext): Promise<BundleVersionRow | null>;
1240
+ private findActiveBundleVersionWithValidity;
1241
+ createDraft(data: CreateBundleVersionDraftData): Promise<BundleVersionRow>;
1242
+ updateDraft(versionId: string, data: UpdateBundleVersionDraftData): Promise<BundleVersionRow>;
1243
+ publishDraft(versionId: string, publishMeta: {
1244
+ publishedByUserId: string | null;
1245
+ publishedChanges: VersionChange[];
1246
+ nonRegressive: boolean;
1247
+ validFrom: Date;
1248
+ validUntil: Date | null;
1249
+ }, tx?: TransactionContext): Promise<BundleVersionRow>;
1250
+ private publishDraftWithValidity;
1251
+ deleteDraft(versionId: string): Promise<void>;
1252
+ }
1253
+
1254
+ interface CatalogEntryRepositoryClient {
1255
+ capabilityCatalogEntry: unknown;
1256
+ featureCatalogEntry: unknown;
1257
+ quotaCatalogEntry: unknown;
1258
+ }
1259
+ /**
1260
+ * `CatalogEntryRepository` against the canonical `capability_catalog_entries`,
1261
+ * `feature_catalog_entries` and `quota_catalog_entries` tables (SPEC_V2 §6.3 —
1262
+ * discovery review workflow).
1263
+ *
1264
+ * `upsert*` writes only the code-derived fields + the service-resolved status
1265
+ * and leaves `i18n`, `sortOrder`, `createdAt` and the approval fields untouched
1266
+ * on update (Prisma's `update` payload simply omits them). `retireMissing`
1267
+ * marks entries whose key vanished from the code snapshot.
1268
+ */
1269
+ declare class PrismaCatalogEntryRepository implements CatalogEntryRepository {
1270
+ private readonly prisma;
1271
+ constructor(prisma: CatalogEntryRepositoryClient);
1272
+ private get db();
1273
+ listCapabilities(filter: CatalogEntryFilter): Promise<CapabilityCatalogEntryRow[]>;
1274
+ listFeatures(filter: CatalogEntryFilter): Promise<FeatureCatalogEntryRow[]>;
1275
+ listQuotas(filter: CatalogEntryFilter): Promise<QuotaCatalogEntryRow[]>;
1276
+ upsertCapability(data: UpsertCapabilityEntryData): Promise<CapabilityCatalogEntryRow>;
1277
+ upsertFeature(data: UpsertFeatureEntryData): Promise<FeatureCatalogEntryRow>;
1278
+ upsertQuota(data: UpsertQuotaEntryData): Promise<QuotaCatalogEntryRow>;
1279
+ retireMissing(projectKey: string, type: 'capability' | 'feature' | 'quota', presentKeys: string[]): Promise<number>;
1280
+ setFeatureSuccessor(projectKey: string, featureKey: string, successorKey: string | null): Promise<FeatureCatalogEntryRow>;
1281
+ setQuotaSuccessor(projectKey: string, quotaKey: string, successorKey: string | null): Promise<QuotaCatalogEntryRow>;
1282
+ findFeature(projectKey: string, featureKey: string): Promise<FeatureCatalogEntryRow | null>;
1283
+ findQuota(projectKey: string, quotaKey: string): Promise<QuotaCatalogEntryRow | null>;
1284
+ setFeatureReview(projectKey: string, featureKey: string, data: SetCatalogEntryReviewData): Promise<FeatureCatalogEntryRow>;
1285
+ setQuotaReview(projectKey: string, quotaKey: string, data: SetCatalogEntryReviewData): Promise<QuotaCatalogEntryRow>;
1286
+ setFeatureI18n(projectKey: string, featureKey: string, i18n: CatalogEntryI18n): Promise<FeatureCatalogEntryRow>;
1287
+ setQuotaI18n(projectKey: string, quotaKey: string, i18n: CatalogEntryI18n): Promise<QuotaCatalogEntryRow>;
1288
+ setFeatureBase(projectKey: string, featureKey: string, data: UpdateCatalogEntryBaseData): Promise<FeatureCatalogEntryRow>;
1289
+ setQuotaBase(projectKey: string, quotaKey: string, data: UpdateCatalogEntryBaseData): Promise<QuotaCatalogEntryRow>;
1290
+ }
1291
+
1292
+ interface MarketingProjectionRepositoryClient {
1293
+ marketingProjection: unknown;
1294
+ }
1295
+ /**
1296
+ * `MarketingProjectionRepository` against the canonical `marketing_projections`
1297
+ * table. Not versioned: per (`targetType`, `targetVersionId`, `locale`) there is
1298
+ * exactly one row (enforced by a unique index), edited directly. `create` on a
1299
+ * duplicate triple therefore raises the DB unique-constraint error.
1300
+ */
1301
+ declare class PrismaMarketingProjectionRepository implements MarketingProjectionRepository {
1302
+ private readonly prisma;
1303
+ constructor(prisma: MarketingProjectionRepositoryClient);
1304
+ private get db();
1305
+ list(filter: MarketingProjectionFilter): Promise<MarketingProjectionRow[]>;
1306
+ findById(id: string): Promise<MarketingProjectionRow | null>;
1307
+ findByTarget(targetType: string, targetVersionId: string, locale: string): Promise<MarketingProjectionRow | null>;
1308
+ create(data: CreateMarketingProjectionData): Promise<MarketingProjectionRow>;
1309
+ update(id: string, data: UpdateMarketingProjectionData): Promise<MarketingProjectionRow>;
1310
+ delete(id: string): Promise<void>;
1311
+ }
1312
+
1313
+ interface MarketingSettingsRepositoryClient {
1314
+ marketingSettings: unknown;
1315
+ }
1316
+ /**
1317
+ * `MarketingSettingsRepository` against the canonical `marketing_settings`
1318
+ * table (one row per project). A missing row means "full locale pool active",
1319
+ * so `get` returns null and the platform falls back to the pool.
1320
+ */
1321
+ declare class PrismaMarketingSettingsRepository implements MarketingSettingsRepository {
1322
+ private readonly prisma;
1323
+ constructor(prisma: MarketingSettingsRepositoryClient);
1324
+ private get db();
1325
+ get(projectKey: string): Promise<MarketingSettingsRow | null>;
1326
+ upsert(projectKey: string, data: UpdateMarketingSettingsData): Promise<MarketingSettingsRow>;
1327
+ }
1328
+
1329
+ interface PromotionRepositoryClient {
1330
+ promotion: unknown;
1331
+ $executeRaw: unknown;
1332
+ }
1333
+ /**
1334
+ * `PromotionRepository` against the canonical `promotions` table. Not
1335
+ * versioned: promotions are edited directly.
1336
+ *
1337
+ * The nullable `onlyLocales` JSON column cannot be set to null through Prisma's
1338
+ * query API without the `Prisma.DbNull` sentinel — which this Prisma-agnostic
1339
+ * package deliberately does not import. On `create` a null restriction is
1340
+ * therefore written by omission (the column defaults to SQL NULL); on `update`
1341
+ * an explicit null clear runs as a raw statement.
1342
+ */
1343
+ declare class PrismaPromotionRepository implements PromotionRepository {
1344
+ private readonly prisma;
1345
+ constructor(prisma: PromotionRepositoryClient);
1346
+ private get db();
1347
+ list(filter: PromotionFilter): Promise<PromotionRow[]>;
1348
+ findById(id: string): Promise<PromotionRow | null>;
1349
+ create(data: CreatePromotionData): Promise<PromotionRow>;
1350
+ update(id: string, data: UpdatePromotionData): Promise<PromotionRow>;
1351
+ delete(id: string): Promise<void>;
1352
+ }
1353
+
1354
+ /**
1355
+ * Append-only `SubscriptionContractRepository` against the canonical
1356
+ * `subscription_contracts` + `contract_line_items` tables. Contracts store full
1357
+ * snapshots and are never rewritten: `create` writes the contract and its line
1358
+ * items atomically via a single nested-create, and `terminate` only closes a
1359
+ * contract (sets `effectiveUntil` + `status`). There is no line-item mutation.
1360
+ */
1361
+ declare class PrismaSubscriptionContractRepository implements SubscriptionContractRepository {
1362
+ private readonly prisma;
1363
+ constructor(prisma: PrismaLike);
1364
+ private get db();
1365
+ list(filter: SubscriptionContractFilter): Promise<SubscriptionContractRecord[]>;
1366
+ findById(contractId: string): Promise<SubscriptionContractRecord | null>;
1367
+ findActiveByTenantId(tenantId: string, asOf?: Date): Promise<SubscriptionContractRecord | null>;
1368
+ create(data: CreateSubscriptionContractData): Promise<SubscriptionContractRecord>;
1369
+ terminate(contractId: string, data: TerminateSubscriptionContractData): Promise<SubscriptionContractRecord>;
1370
+ }
1371
+
1372
+ export { AsyncLocalRlsBypassAdapter, type DecimalLike, PASSWORD_HASHER_TOKEN, PRISMA_BUNDLE_REPOSITORY_OPTIONS, PRISMA_CLIENT_TOKEN, PRISMA_SCHEMA_OPTIONS_TOKEN, PrismaAuditAdapter, PrismaAuditQueryAdapter, PrismaAuditStatsAdapter, PrismaBundleRepository, type PrismaBundleRepositoryOptions, PrismaCatalogEntryRepository, type PrismaLike, PrismaMarketingProjectionRepository, PrismaMarketingSettingsRepository, PrismaMfaAdapter, type PrismaModelDelegateLike, type PrismaPersistenceOptions, type PrismaPlanBindingMode, type PrismaPlanBindingOptions, type PrismaPlanBindingResolver, PrismaPlanCatalogImportSink, PrismaPlanCatalogReadSink, type PrismaPlanDelegateOptions, PrismaPlanRepository, type PrismaPlanVersionFieldCapabilities, type PrismaPlanVersionFieldOptions, PrismaPlanVersionRepository, PrismaPromoCodeRedemptionRepository, PrismaPromoCodeRepository, PrismaPromoCodeValidationLogRepository, PrismaPromoSubscriptionLookup, PrismaPromotionRepository, type PrismaSchemaOptions, PrismaSubscriptionBundleRepository, PrismaSubscriptionContractRepository, PrismaSubscriptionRepository, PrismaSuperAdminBootstrapAdapter, type PrismaTenantSubscriptionOptions, PrismaTenantSubscriptionWriteAdapter, PrismaTransactionRunner, type PrismaTxLike, type ResolvedPrismaSchemaOptions, ZeroPromoRevenueDeductionAggregator, buildActorTag, createPrismaPlanBindingResolver, getPrismaDelegate, prismaPersistence, resolvePrismaSchemaOptions };