@saasicat/adapter-prisma 0.6.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.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { PersistenceInjectionToken, PasswordHasher, SaasicatPersistenceAdapter, TransactionRunner, TransactionContext, MfaPort, AuditPort, AdminActor, AuditQueryPort, AuditQuery, AuditEntry, AuditStatsPort, RlsBypassPort, SubscriptionRepository, SubscriptionRecord, SubscriptionBundleRepository, SubscriptionBundleRecord, CreateSubscriptionBundleData, CancelSubscriptionBundleData, TenantSubscriptionWritePort, 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, BundleListFilter, BundleRow, CreateBundleData, UpdateBundleData, BundleVersionRow, 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';
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. */
@@ -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
  }
@@ -578,6 +581,102 @@ interface PrismaModelDelegateLike<Row> {
578
581
  count(args?: unknown): Promise<number>;
579
582
  }
580
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>;
679
+
581
680
  interface PrismaPersistenceOptions {
582
681
  /**
583
682
  * The app's Prisma client: either its injection token (typically the
@@ -599,6 +698,12 @@ interface PrismaPersistenceOptions {
599
698
  * middleware. Default false.
600
699
  */
601
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;
602
707
  }
603
708
  /**
604
709
  * Builds the `SaasicatPersistenceAdapter` bundle for Prisma + PostgreSQL on
@@ -629,7 +734,7 @@ declare function prismaPersistence(options: PrismaPersistenceOptions): SaasicatP
629
734
  */
630
735
  declare class PrismaTransactionRunner implements TransactionRunner {
631
736
  private readonly prisma;
632
- constructor(prisma: PrismaLike);
737
+ constructor(prisma: Record<'$transaction', unknown>);
633
738
  run<T>(fn: (tx: TransactionContext) => Promise<T>): Promise<T>;
634
739
  }
635
740
 
@@ -650,7 +755,7 @@ declare class PrismaTransactionRunner implements TransactionRunner {
650
755
  */
651
756
  declare class PrismaMfaAdapter implements MfaPort {
652
757
  private readonly prisma;
653
- constructor(prisma: PrismaLike);
758
+ constructor(prisma: Pick<PrismaLike, 'superAdminMfa'>);
654
759
  getSecret(userId: string): Promise<string | null>;
655
760
  setSecret(userId: string, secret: string | null): Promise<void>;
656
761
  isEnabled(userId: string): Promise<boolean>;
@@ -667,7 +772,7 @@ declare function buildActorTag(actor: AdminActor): string;
667
772
  */
668
773
  declare class PrismaAuditAdapter implements AuditPort {
669
774
  private readonly prisma;
670
- constructor(prisma: PrismaLike);
775
+ constructor(prisma: Pick<PrismaLike, 'auditLog'>);
671
776
  write(input: {
672
777
  actor: AdminActor;
673
778
  entity: string;
@@ -683,14 +788,14 @@ declare class PrismaAuditAdapter implements AuditPort {
683
788
  */
684
789
  declare class PrismaAuditQueryAdapter implements AuditQueryPort {
685
790
  private readonly prisma;
686
- constructor(prisma: PrismaLike);
791
+ constructor(prisma: Pick<PrismaLike, 'auditLog'>);
687
792
  list(filter: AuditQuery): Promise<AuditEntry[]>;
688
793
  }
689
794
 
690
795
  /** `AuditStatsPort` against the canonical `audit_logs` table. */
691
796
  declare class PrismaAuditStatsAdapter implements AuditStatsPort {
692
797
  private readonly prisma;
693
- constructor(prisma: PrismaLike);
798
+ constructor(prisma: Pick<PrismaLike, 'auditLog'>);
694
799
  countSince(since: Date): Promise<number>;
695
800
  }
696
801
 
@@ -727,25 +832,44 @@ declare class AsyncLocalRlsBypassAdapter implements RlsBypassPort {
727
832
  isBypassActive(): boolean;
728
833
  }
729
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
+ }
730
840
  /**
731
841
  * `SubscriptionRepository` against the canonical `subscriptions` +
732
842
  * `plan_versions` tables.
733
843
  *
734
- * `countByBundleVersionId` is deliberately not implemented (the
735
- * `subscription_bundles` junction is not part of this adapter's slice);
736
- * 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).
737
848
  */
738
849
  declare class PrismaSubscriptionRepository implements SubscriptionRepository {
739
850
  private readonly prisma;
740
- 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;
741
858
  findByTenantId(tenantId: string): Promise<SubscriptionRecord | null>;
742
859
  findByTenantIdLocked(tenantId: string, tx: TransactionContext): Promise<SubscriptionRecord | null>;
743
860
  countByPlanVersionId(planVersionId: string): Promise<number>;
744
- countActiveByPlanKey(_projectKey: string): Promise<Record<string, number>>;
861
+ countActiveByPlanKey(projectKey: string): Promise<Record<string, number>>;
862
+ private countActiveBundleBindings;
745
863
  private loadByTenantId;
746
864
  private toRecord;
865
+ private planVersions;
866
+ private subscriptions;
867
+ private planKeysForProject;
747
868
  }
748
869
 
870
+ interface SubscriptionBundleClient {
871
+ subscriptionBundle: unknown;
872
+ }
749
873
  /**
750
874
  * `SubscriptionBundleRepository` against the canonical `subscription_bundles`
751
875
  * junction (SPEC_V2 §11.1 M6 Pack 2e). Dumb persistence: domain constraints
@@ -756,7 +880,7 @@ declare class PrismaSubscriptionRepository implements SubscriptionRepository {
756
880
  */
757
881
  declare class PrismaSubscriptionBundleRepository implements SubscriptionBundleRepository {
758
882
  private readonly prisma;
759
- constructor(prisma: PrismaLike);
883
+ constructor(prisma: SubscriptionBundleClient);
760
884
  private db;
761
885
  listBySubscription(subscriptionId: string): Promise<SubscriptionBundleRecord[]>;
762
886
  findById(subscriptionBundleId: string): Promise<SubscriptionBundleRecord | null>;
@@ -767,24 +891,42 @@ declare class PrismaSubscriptionBundleRepository implements SubscriptionBundleRe
767
891
  countActiveByBundleVersionId(bundleVersionId: string, asOf?: Date): Promise<number>;
768
892
  }
769
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
+ }
770
898
  /**
771
- * `TenantSubscriptionWritePort` against the canonical `subscriptions` table.
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
+ *
772
909
  * Pure persistence: trial carry-over (#17) and contract freeze (#18) are
773
910
  * resolved in the platform `changePlan` path and handed down as plain values —
774
911
  * this adapter only writes what it receives.
775
912
  *
776
- * The optional `applyOnboardingSelection` is not implemented; the platform
777
- * service then falls back to sequential `changePlanImmediate` + promo redeem
778
- * (best-effort, port contract P10.1.1).
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.
779
918
  */
780
919
  declare class PrismaTenantSubscriptionWriteAdapter implements TenantSubscriptionWritePort {
781
920
  private readonly prisma;
782
- constructor(prisma: PrismaLike);
783
- private db;
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);
784
925
  changePlanImmediate(tenantId: string, input: ImmediatePlanChangeInput): Promise<{
785
926
  plan: string;
786
927
  billingCycle: string;
787
928
  }>;
929
+ private changePlanImmediateInClient;
788
930
  schedulePlanChange(tenantId: string, input: ScheduledPlanChangeInput): Promise<void>;
789
931
  acceptPendingPlanVersion(tenantId: string, userId: string, now: Date): Promise<{
790
932
  accepted: boolean;
@@ -792,24 +934,44 @@ declare class PrismaTenantSubscriptionWriteAdapter implements TenantSubscription
792
934
  effectiveAt: Date | null;
793
935
  alreadyAccepted: boolean;
794
936
  }>;
937
+ private applyOnboardingSelectionAtomic;
795
938
  cancelSubscription(tenantId: string, immediate: boolean, now: Date): Promise<{
796
939
  canceledAt: Date | null;
797
940
  status: string;
798
941
  }>;
942
+ private subscription;
943
+ private planVersions;
944
+ private findTargetPlanVersionId;
945
+ private pendingVersionBelongsToAnotherPlan;
946
+ private assertConfiguration;
799
947
  }
800
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
+ }
801
956
  /**
802
957
  * `PlanVersionRepository` against the canonical `plan_versions` table.
803
958
  *
804
- * `findActive` is deliberately not implemented: the canonical schema carries
805
- * no `validFrom`/`validUntil` columns yet, so time-aware resolution is not
806
- * expressible consumers fall back to `findLatestLive` (documented port
807
- * 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.
808
962
  */
809
963
  declare class PrismaPlanVersionRepository implements PlanVersionRepository {
810
964
  private readonly prisma;
811
- 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;
812
971
  findLatestLive(planId: string, tx?: TransactionContext): Promise<PlanVersionRecord | null>;
972
+ private findActivePlanVersion;
973
+ private versions;
974
+ private toRecord;
813
975
  }
814
976
 
815
977
  /**
@@ -909,58 +1071,82 @@ declare class PrismaSuperAdminBootstrapAdapter implements SuperAdminProvisioning
909
1071
  createSuperAdmin(input: CreateSuperAdminCliInput): Promise<PlatformUserDto>;
910
1072
  }
911
1073
 
1074
+ /** Root-client fields used directly; the PlanVersion delegate is configurable. */
1075
+ interface PlanCatalogReadClient {
1076
+ plan: unknown;
1077
+ featureCatalogEntry: unknown;
1078
+ }
912
1079
  /**
913
1080
  * `PlanCatalogReadSink` against the canonical `plans`, `plan_versions` and
914
1081
  * `feature_catalog_entries` tables — DB hydration of the plan catalog at
915
1082
  * boot (`PlanCatalogModule.forRoot({ sink })`).
916
1083
  *
917
- * `validFrom`/`validUntil` are reported as null: the canonical schema does
918
- * 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.
919
1087
  */
920
1088
  declare class PrismaPlanCatalogReadSink implements PlanCatalogReadSink {
921
1089
  private readonly prisma;
922
- constructor(prisma: PrismaLike);
1090
+ private readonly binding;
1091
+ private readonly delegateName;
1092
+ private readonly fields;
1093
+ constructor(prisma: PlanCatalogReadClient, options?: PrismaSchemaOptions);
1094
+ private db;
923
1095
  loadSnapshot(projectKey: string): Promise<PlanCatalogReadSnapshot>;
1096
+ private planVersions;
924
1097
  }
925
1098
 
1099
+ /** Root-client fields used directly; the PlanVersion delegate is configurable. */
1100
+ interface PlanCatalogImportClient {
1101
+ plan: unknown;
1102
+ featureCatalogEntry: unknown;
1103
+ }
926
1104
  /**
927
1105
  * `PlanCatalogImportSink` against the canonical catalog tables — the
928
1106
  * one-shot `saas.yaml → DB` import at boot.
929
1107
  *
930
1108
  * Idempotency per the port contract: existing rows (identity match) are
931
- * 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`.
932
1111
  */
933
1112
  declare class PrismaPlanCatalogImportSink implements PlanCatalogImportSink {
934
1113
  private readonly prisma;
935
- constructor(prisma: PrismaLike);
1114
+ private readonly binding;
1115
+ private readonly delegateName;
1116
+ constructor(prisma: PlanCatalogImportClient, options?: PrismaSchemaOptions);
1117
+ private db;
936
1118
  upsertPlan(input: UpsertPlanInput): Promise<UpsertResult>;
937
1119
  upsertPlanVersion(input: UpsertPlanVersionInput): Promise<UpsertResult>;
1120
+ private planVersions;
938
1121
  upsertFeatureCatalogEntry(input: UpsertFeatureCatalogEntryInput): Promise<UpsertResult>;
939
1122
  }
940
1123
 
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
+ }
941
1129
  /**
942
1130
  * `PlanRepository` against the canonical `plans` + `plan_versions` tables
943
1131
  * (SPEC_V2 §11.1 M6). Plan stem CRUD (Pack 1) and PlanVersion lifecycle
944
- * (Pack 2a) live in one adapter; the soft binding is
945
- * `PlanVersion.planId === Plan.planKey`, so the lifecycle methods take the
946
- * **planKey**, not the plan UUID.
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.
947
1136
  *
948
- * Schema limitation: the canonical `plan_versions` fragment
949
- * (03-plan-versions.prisma) intentionally has no validity-window columns
950
- * (`validFrom`/`validUntil`) and no `endsAt` column it carries a generic
951
- * `quotas Json` instead of fixed quota columns. Consequences:
952
- * - Every `PlanVersionRow.validFrom`/`validUntil` maps to `null`.
953
- * - `publishPlanVersionDraft` persists only the publish/supersede state; the
954
- * `validFrom`/`validUntil` in `publishMeta` cannot be stored, so
955
- * auto-succession reduces to setting `supersededAt` on the previous live version.
956
- * - `findActivePlanVersion` (validity-window read) and `terminate` (endsAt)
957
- * throw, because their contract depends on columns this schema does not have.
958
- * Consumers that need them provide a custom adapter on an extended schema.
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`.
959
1141
  */
960
1142
  declare class PrismaPlanRepository implements PlanRepository {
961
1143
  private readonly prisma;
962
- constructor(prisma: PrismaLike);
1144
+ private readonly binding;
1145
+ private readonly delegateName;
1146
+ private readonly fields;
1147
+ constructor(prisma: PlanRepositoryClient, options?: PrismaSchemaOptions);
963
1148
  private db;
1149
+ private versions;
964
1150
  list(filter: PlanListFilter): Promise<PlanRow[]>;
965
1151
  findById(planId: string): Promise<PlanRow | null>;
966
1152
  findByKey(projectKey: string, planKey: string): Promise<PlanRow | null>;
@@ -972,7 +1158,7 @@ declare class PrismaPlanRepository implements PlanRepository {
972
1158
  findVersionById(versionId: string): Promise<PlanVersionRow | null>;
973
1159
  findCurrentDraft(planKey: string): Promise<PlanVersionRow | null>;
974
1160
  findLatestLivePlanVersion(planKey: string, tx?: TransactionContext): Promise<PlanVersionRow | null>;
975
- findActivePlanVersion(): Promise<PlanVersionRow | null>;
1161
+ findActivePlanVersion(planKey: string, asOf?: Date, tx?: TransactionContext): Promise<PlanVersionRow | null>;
976
1162
  createPlanVersionDraft(data: CreatePlanVersionDraftData): Promise<PlanVersionRow>;
977
1163
  updatePlanVersionDraft(versionId: string, data: UpdatePlanVersionDraftData): Promise<PlanVersionRow>;
978
1164
  publishPlanVersionDraft(versionId: string, publishMeta: {
@@ -983,30 +1169,64 @@ declare class PrismaPlanRepository implements PlanRepository {
983
1169
  validUntil: Date | null;
984
1170
  }, tx?: TransactionContext): Promise<PlanVersionRow>;
985
1171
  deletePlanVersionDraft(versionId: string): Promise<void>;
986
- terminate(): Promise<PlanVersionRow>;
1172
+ terminate(versionId: string, endsAt: Date): Promise<PlanVersionRow>;
1173
+ private toPlanVersionRow;
987
1174
  }
988
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
+ }
989
1202
  /**
990
1203
  * `BundleRepository` against the canonical `bundles` + `bundle_versions`
991
1204
  * tables (SPEC_V2 §5 + §11.1 M3). Versioning mirrors `PlanVersion`: at most one
992
1205
  * draft (`publishedAt IS NULL`) per bundle, monotonically incrementing
993
1206
  * `version`, `supersededAt` marking the previous live version on publish.
994
1207
  *
995
- * Divergence from time-aware consumer schemas: the canonical `bundle_versions`
996
- * table carries **no** `validFrom`/`validUntil` columns. The `BundleVersionRow`
997
- * therefore always reports both as `null`, and the `validFrom`/`validUntil`
998
- * inputs on the Create/Update DTOs and on `publishDraft`'s `publishMeta` are
999
- * accepted (to satisfy the port signature) but not persisted — consistent with
1000
- * `PrismaPlanVersionRepository`, which documents the same schema gap.
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.
1001
1212
  *
1002
- * Atomicity of `publishDraft` (supersede-previous + publish-draft) is delegated
1003
- * to the caller's `TransactionContext`, following the package convention that
1004
- * repositories never open their own `$transaction`.
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.
1005
1217
  */
1006
1218
  declare class PrismaBundleRepository implements BundleRepository {
1007
1219
  private readonly prisma;
1008
- constructor(prisma: PrismaLike);
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);
1009
1228
  private db;
1229
+ private transaction;
1010
1230
  list(filter: BundleListFilter): Promise<BundleRow[]>;
1011
1231
  findById(bundleId: string): Promise<BundleRow | null>;
1012
1232
  findByKey(projectKey: string, bundleKey: string): Promise<BundleRow | null>;
@@ -1017,6 +1237,7 @@ declare class PrismaBundleRepository implements BundleRepository {
1017
1237
  findVersionById(versionId: string): Promise<BundleVersionRow | null>;
1018
1238
  findCurrentDraft(bundleId: string): Promise<BundleVersionRow | null>;
1019
1239
  findLatestLive(bundleId: string, tx?: TransactionContext): Promise<BundleVersionRow | null>;
1240
+ private findActiveBundleVersionWithValidity;
1020
1241
  createDraft(data: CreateBundleVersionDraftData): Promise<BundleVersionRow>;
1021
1242
  updateDraft(versionId: string, data: UpdateBundleVersionDraftData): Promise<BundleVersionRow>;
1022
1243
  publishDraft(versionId: string, publishMeta: {
@@ -1026,9 +1247,15 @@ declare class PrismaBundleRepository implements BundleRepository {
1026
1247
  validFrom: Date;
1027
1248
  validUntil: Date | null;
1028
1249
  }, tx?: TransactionContext): Promise<BundleVersionRow>;
1250
+ private publishDraftWithValidity;
1029
1251
  deleteDraft(versionId: string): Promise<void>;
1030
1252
  }
1031
1253
 
1254
+ interface CatalogEntryRepositoryClient {
1255
+ capabilityCatalogEntry: unknown;
1256
+ featureCatalogEntry: unknown;
1257
+ quotaCatalogEntry: unknown;
1258
+ }
1032
1259
  /**
1033
1260
  * `CatalogEntryRepository` against the canonical `capability_catalog_entries`,
1034
1261
  * `feature_catalog_entries` and `quota_catalog_entries` tables (SPEC_V2 §6.3 —
@@ -1041,7 +1268,7 @@ declare class PrismaBundleRepository implements BundleRepository {
1041
1268
  */
1042
1269
  declare class PrismaCatalogEntryRepository implements CatalogEntryRepository {
1043
1270
  private readonly prisma;
1044
- constructor(prisma: PrismaLike);
1271
+ constructor(prisma: CatalogEntryRepositoryClient);
1045
1272
  private get db();
1046
1273
  listCapabilities(filter: CatalogEntryFilter): Promise<CapabilityCatalogEntryRow[]>;
1047
1274
  listFeatures(filter: CatalogEntryFilter): Promise<FeatureCatalogEntryRow[]>;
@@ -1062,6 +1289,9 @@ declare class PrismaCatalogEntryRepository implements CatalogEntryRepository {
1062
1289
  setQuotaBase(projectKey: string, quotaKey: string, data: UpdateCatalogEntryBaseData): Promise<QuotaCatalogEntryRow>;
1063
1290
  }
1064
1291
 
1292
+ interface MarketingProjectionRepositoryClient {
1293
+ marketingProjection: unknown;
1294
+ }
1065
1295
  /**
1066
1296
  * `MarketingProjectionRepository` against the canonical `marketing_projections`
1067
1297
  * table. Not versioned: per (`targetType`, `targetVersionId`, `locale`) there is
@@ -1070,7 +1300,7 @@ declare class PrismaCatalogEntryRepository implements CatalogEntryRepository {
1070
1300
  */
1071
1301
  declare class PrismaMarketingProjectionRepository implements MarketingProjectionRepository {
1072
1302
  private readonly prisma;
1073
- constructor(prisma: PrismaLike);
1303
+ constructor(prisma: MarketingProjectionRepositoryClient);
1074
1304
  private get db();
1075
1305
  list(filter: MarketingProjectionFilter): Promise<MarketingProjectionRow[]>;
1076
1306
  findById(id: string): Promise<MarketingProjectionRow | null>;
@@ -1080,6 +1310,9 @@ declare class PrismaMarketingProjectionRepository implements MarketingProjection
1080
1310
  delete(id: string): Promise<void>;
1081
1311
  }
1082
1312
 
1313
+ interface MarketingSettingsRepositoryClient {
1314
+ marketingSettings: unknown;
1315
+ }
1083
1316
  /**
1084
1317
  * `MarketingSettingsRepository` against the canonical `marketing_settings`
1085
1318
  * table (one row per project). A missing row means "full locale pool active",
@@ -1087,12 +1320,16 @@ declare class PrismaMarketingProjectionRepository implements MarketingProjection
1087
1320
  */
1088
1321
  declare class PrismaMarketingSettingsRepository implements MarketingSettingsRepository {
1089
1322
  private readonly prisma;
1090
- constructor(prisma: PrismaLike);
1323
+ constructor(prisma: MarketingSettingsRepositoryClient);
1091
1324
  private get db();
1092
1325
  get(projectKey: string): Promise<MarketingSettingsRow | null>;
1093
1326
  upsert(projectKey: string, data: UpdateMarketingSettingsData): Promise<MarketingSettingsRow>;
1094
1327
  }
1095
1328
 
1329
+ interface PromotionRepositoryClient {
1330
+ promotion: unknown;
1331
+ $executeRaw: unknown;
1332
+ }
1096
1333
  /**
1097
1334
  * `PromotionRepository` against the canonical `promotions` table. Not
1098
1335
  * versioned: promotions are edited directly.
@@ -1105,7 +1342,7 @@ declare class PrismaMarketingSettingsRepository implements MarketingSettingsRepo
1105
1342
  */
1106
1343
  declare class PrismaPromotionRepository implements PromotionRepository {
1107
1344
  private readonly prisma;
1108
- constructor(prisma: PrismaLike);
1345
+ constructor(prisma: PromotionRepositoryClient);
1109
1346
  private get db();
1110
1347
  list(filter: PromotionFilter): Promise<PromotionRow[]>;
1111
1348
  findById(id: string): Promise<PromotionRow | null>;
@@ -1132,4 +1369,4 @@ declare class PrismaSubscriptionContractRepository implements SubscriptionContra
1132
1369
  terminate(contractId: string, data: TerminateSubscriptionContractData): Promise<SubscriptionContractRecord>;
1133
1370
  }
1134
1371
 
1135
- export { AsyncLocalRlsBypassAdapter, type DecimalLike, PASSWORD_HASHER_TOKEN, PRISMA_CLIENT_TOKEN, PrismaAuditAdapter, PrismaAuditQueryAdapter, PrismaAuditStatsAdapter, PrismaBundleRepository, PrismaCatalogEntryRepository, type PrismaLike, PrismaMarketingProjectionRepository, PrismaMarketingSettingsRepository, PrismaMfaAdapter, type PrismaModelDelegateLike, type PrismaPersistenceOptions, PrismaPlanCatalogImportSink, PrismaPlanCatalogReadSink, PrismaPlanRepository, PrismaPlanVersionRepository, PrismaPromoCodeRedemptionRepository, PrismaPromoCodeRepository, PrismaPromoCodeValidationLogRepository, PrismaPromoSubscriptionLookup, PrismaPromotionRepository, PrismaSubscriptionBundleRepository, PrismaSubscriptionContractRepository, PrismaSubscriptionRepository, PrismaSuperAdminBootstrapAdapter, PrismaTenantSubscriptionWriteAdapter, PrismaTransactionRunner, type PrismaTxLike, ZeroPromoRevenueDeductionAggregator, buildActorTag, prismaPersistence };
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 };