@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.cjs CHANGED
@@ -23,7 +23,9 @@ var index_exports = {};
23
23
  __export(index_exports, {
24
24
  AsyncLocalRlsBypassAdapter: () => AsyncLocalRlsBypassAdapter,
25
25
  PASSWORD_HASHER_TOKEN: () => PASSWORD_HASHER_TOKEN,
26
+ PRISMA_BUNDLE_REPOSITORY_OPTIONS: () => PRISMA_BUNDLE_REPOSITORY_OPTIONS,
26
27
  PRISMA_CLIENT_TOKEN: () => PRISMA_CLIENT_TOKEN,
28
+ PRISMA_SCHEMA_OPTIONS_TOKEN: () => PRISMA_SCHEMA_OPTIONS_TOKEN,
27
29
  PrismaAuditAdapter: () => PrismaAuditAdapter,
28
30
  PrismaAuditQueryAdapter: () => PrismaAuditQueryAdapter,
29
31
  PrismaAuditStatsAdapter: () => PrismaAuditStatsAdapter,
@@ -49,7 +51,10 @@ __export(index_exports, {
49
51
  PrismaTransactionRunner: () => PrismaTransactionRunner,
50
52
  ZeroPromoRevenueDeductionAggregator: () => ZeroPromoRevenueDeductionAggregator,
51
53
  buildActorTag: () => buildActorTag,
52
- prismaPersistence: () => prismaPersistence
54
+ createPrismaPlanBindingResolver: () => createPrismaPlanBindingResolver,
55
+ getPrismaDelegate: () => getPrismaDelegate,
56
+ prismaPersistence: () => prismaPersistence,
57
+ resolvePrismaSchemaOptions: () => resolvePrismaSchemaOptions
53
58
  });
54
59
  module.exports = __toCommonJS(index_exports);
55
60
 
@@ -138,7 +143,7 @@ PrismaAuditAdapter = _ts_decorate2([
138
143
  _ts_param(0, (0, import_common2.Inject)(PRISMA_CLIENT_TOKEN)),
139
144
  _ts_metadata("design:type", Function),
140
145
  _ts_metadata("design:paramtypes", [
141
- typeof PrismaLike === "undefined" ? Object : PrismaLike
146
+ typeof Pick === "undefined" ? Object : Pick
142
147
  ])
143
148
  ], PrismaAuditAdapter);
144
149
 
@@ -201,7 +206,7 @@ PrismaAuditQueryAdapter = _ts_decorate3([
201
206
  _ts_param2(0, (0, import_common3.Inject)(PRISMA_CLIENT_TOKEN)),
202
207
  _ts_metadata2("design:type", Function),
203
208
  _ts_metadata2("design:paramtypes", [
204
- typeof PrismaLike === "undefined" ? Object : PrismaLike
209
+ typeof Pick === "undefined" ? Object : Pick
205
210
  ])
206
211
  ], PrismaAuditQueryAdapter);
207
212
  function toActorTagFilter(actorTag) {
@@ -273,7 +278,7 @@ PrismaAuditStatsAdapter = _ts_decorate4([
273
278
  _ts_param3(0, (0, import_common4.Inject)(PRISMA_CLIENT_TOKEN)),
274
279
  _ts_metadata3("design:type", Function),
275
280
  _ts_metadata3("design:paramtypes", [
276
- typeof PrismaLike === "undefined" ? Object : PrismaLike
281
+ typeof Pick === "undefined" ? Object : Pick
277
282
  ])
278
283
  ], PrismaAuditStatsAdapter);
279
284
 
@@ -343,12 +348,149 @@ PrismaMfaAdapter = _ts_decorate5([
343
348
  _ts_param4(0, (0, import_common5.Inject)(PRISMA_CLIENT_TOKEN)),
344
349
  _ts_metadata4("design:type", Function),
345
350
  _ts_metadata4("design:paramtypes", [
346
- typeof PrismaLike === "undefined" ? Object : PrismaLike
351
+ typeof Pick === "undefined" ? Object : Pick
347
352
  ])
348
353
  ], PrismaMfaAdapter);
349
354
 
350
355
  // src/prisma-plan-catalog-import-sink.ts
351
356
  var import_common6 = require("@nestjs/common");
357
+
358
+ // src/prisma-plan-binding.ts
359
+ var PRISMA_SCHEMA_OPTIONS_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/adapter-prisma/PrismaSchemaOptions");
360
+ var DEFAULT_SCHEMA_OPTIONS = {
361
+ planBinding: {
362
+ mode: "legacy-plan-key"
363
+ },
364
+ delegates: {
365
+ catalogPlanVersion: "planVersion",
366
+ entitlementPlanVersion: "planVersion"
367
+ },
368
+ planVersionFields: {
369
+ catalog: {
370
+ validityWindows: false,
371
+ endsAt: false
372
+ },
373
+ entitlement: {
374
+ validityWindows: false,
375
+ endsAt: false
376
+ }
377
+ },
378
+ tenantSubscription: {
379
+ delegate: "subscription",
380
+ subscriptionBundleDelegate: false,
381
+ synchronizePlanVersion: false,
382
+ atomicOnboardingSelection: false,
383
+ activeVersionSelection: "latest-live",
384
+ withEndsAt: false
385
+ }
386
+ };
387
+ function resolvePrismaSchemaOptions(options) {
388
+ const mode = options?.planBinding?.mode ?? "legacy-plan-key";
389
+ const projectKey = options?.planBinding?.projectKey;
390
+ if (mode === "normalized-plan-id" && !projectKey?.trim()) {
391
+ throw new Error("Prisma plan binding mode 'normalized-plan-id' requires a non-empty projectKey.");
392
+ }
393
+ const sharedFields = options?.planVersionFields;
394
+ const catalogFields = sharedFields?.catalog;
395
+ const entitlementFields = sharedFields?.entitlement;
396
+ return {
397
+ planBinding: {
398
+ mode,
399
+ ...projectKey ? {
400
+ projectKey
401
+ } : {}
402
+ },
403
+ delegates: {
404
+ catalogPlanVersion: options?.delegates?.catalogPlanVersion ?? DEFAULT_SCHEMA_OPTIONS.delegates.catalogPlanVersion,
405
+ entitlementPlanVersion: options?.delegates?.entitlementPlanVersion ?? DEFAULT_SCHEMA_OPTIONS.delegates.entitlementPlanVersion
406
+ },
407
+ planVersionFields: {
408
+ catalog: {
409
+ validityWindows: catalogFields?.validityWindows ?? sharedFields?.validityWindows ?? false,
410
+ endsAt: catalogFields?.endsAt ?? sharedFields?.endsAt ?? false
411
+ },
412
+ entitlement: {
413
+ validityWindows: entitlementFields?.validityWindows ?? sharedFields?.validityWindows ?? false,
414
+ endsAt: entitlementFields?.endsAt ?? sharedFields?.endsAt ?? false
415
+ }
416
+ },
417
+ tenantSubscription: {
418
+ delegate: options?.tenantSubscription?.delegate ?? DEFAULT_SCHEMA_OPTIONS.tenantSubscription.delegate,
419
+ subscriptionBundleDelegate: options?.tenantSubscription?.subscriptionBundleDelegate ?? DEFAULT_SCHEMA_OPTIONS.tenantSubscription.subscriptionBundleDelegate,
420
+ synchronizePlanVersion: options?.tenantSubscription?.synchronizePlanVersion ?? DEFAULT_SCHEMA_OPTIONS.tenantSubscription.synchronizePlanVersion,
421
+ atomicOnboardingSelection: options?.tenantSubscription?.atomicOnboardingSelection ?? DEFAULT_SCHEMA_OPTIONS.tenantSubscription.atomicOnboardingSelection,
422
+ activeVersionSelection: options?.tenantSubscription?.activeVersionSelection ?? DEFAULT_SCHEMA_OPTIONS.tenantSubscription.activeVersionSelection,
423
+ withEndsAt: options?.tenantSubscription?.withEndsAt ?? DEFAULT_SCHEMA_OPTIONS.tenantSubscription.withEndsAt
424
+ }
425
+ };
426
+ }
427
+ __name(resolvePrismaSchemaOptions, "resolvePrismaSchemaOptions");
428
+ function createPrismaPlanBindingResolver(options) {
429
+ const resolved = resolvePrismaSchemaOptions({
430
+ planBinding: options
431
+ }).planBinding;
432
+ return {
433
+ mode: resolved.mode,
434
+ projectKey: resolved.projectKey,
435
+ async toStoragePlanId(client, planKey, projectKey) {
436
+ if (resolved.mode === "legacy-plan-key") return planKey;
437
+ const scope = resolveProjectKey(resolved.projectKey, projectKey);
438
+ const plan = await asPlanIdentityClient(client).plan.findFirst({
439
+ where: {
440
+ projectKey: scope,
441
+ planKey,
442
+ deletedAt: null
443
+ }
444
+ });
445
+ if (!plan) {
446
+ throw new Error(`Plan '${planKey}' not found in project '${scope}'.`);
447
+ }
448
+ return plan.id;
449
+ },
450
+ async toPlanKey(client, storedPlanId, projectKey) {
451
+ if (resolved.mode === "legacy-plan-key") return storedPlanId;
452
+ const scope = resolveProjectKey(resolved.projectKey, projectKey);
453
+ const plan = await asPlanIdentityClient(client).plan.findUnique({
454
+ where: {
455
+ id: storedPlanId
456
+ }
457
+ });
458
+ if (!plan || plan.projectKey !== scope) {
459
+ throw new Error(`Plan id '${storedPlanId}' not found in project '${scope}'.`);
460
+ }
461
+ return plan.planKey;
462
+ }
463
+ };
464
+ }
465
+ __name(createPrismaPlanBindingResolver, "createPrismaPlanBindingResolver");
466
+ function getPrismaDelegate(client, delegateName) {
467
+ const delegate = client?.[delegateName];
468
+ if (!delegate || typeof delegate !== "object") {
469
+ throw new Error(`Prisma client has no '${delegateName}' delegate.`);
470
+ }
471
+ return delegate;
472
+ }
473
+ __name(getPrismaDelegate, "getPrismaDelegate");
474
+ function resolveProjectKey(configured, requested) {
475
+ const projectKey = requested ?? configured;
476
+ if (!projectKey) {
477
+ throw new Error("Prisma plan binding mode 'normalized-plan-id' requires a projectKey.");
478
+ }
479
+ if (configured && requested && configured !== requested) {
480
+ throw new Error(`Prisma plan binding is configured for project '${configured}', not '${requested}'.`);
481
+ }
482
+ return projectKey;
483
+ }
484
+ __name(resolveProjectKey, "resolveProjectKey");
485
+ function asPlanIdentityClient(client) {
486
+ if (!client || typeof client !== "object" || !("plan" in client)) {
487
+ throw new Error("Prisma client has no 'plan' delegate.");
488
+ }
489
+ return client;
490
+ }
491
+ __name(asPlanIdentityClient, "asPlanIdentityClient");
492
+
493
+ // src/prisma-plan-catalog-import-sink.ts
352
494
  function _ts_decorate6(decorators, target, key, desc) {
353
495
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
354
496
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -371,11 +513,23 @@ var PrismaPlanCatalogImportSink = class {
371
513
  __name(this, "PrismaPlanCatalogImportSink");
372
514
  }
373
515
  prisma;
374
- constructor(prisma) {
516
+ binding;
517
+ delegateName;
518
+ constructor(prisma, options) {
375
519
  this.prisma = prisma;
520
+ const schema = resolvePrismaSchemaOptions(options);
521
+ this.binding = createPrismaPlanBindingResolver(options?.planBinding);
522
+ this.delegateName = schema.delegates.catalogPlanVersion;
523
+ }
524
+ db() {
525
+ return this.prisma;
376
526
  }
377
527
  async upsertPlan(input) {
378
- const existing = await this.prisma.plan.findFirst({
528
+ if (this.binding.mode === "normalized-plan-id" && this.binding.projectKey !== input.projectKey) {
529
+ throw new Error(`Prisma plan binding is configured for project '${this.binding.projectKey}', not '${input.projectKey}'.`);
530
+ }
531
+ const db = this.db();
532
+ const existing = await db.plan.findFirst({
379
533
  where: {
380
534
  projectKey: input.projectKey,
381
535
  planKey: input.planKey
@@ -385,7 +539,7 @@ var PrismaPlanCatalogImportSink = class {
385
539
  created: false,
386
540
  skipReason: "exists"
387
541
  };
388
- await this.prisma.plan.create({
542
+ await db.plan.create({
389
543
  data: {
390
544
  projectKey: input.projectKey,
391
545
  planKey: input.planKey,
@@ -399,9 +553,11 @@ var PrismaPlanCatalogImportSink = class {
399
553
  };
400
554
  }
401
555
  async upsertPlanVersion(input) {
402
- const existing = await this.prisma.planVersion.findFirst({
556
+ const planVersion = this.planVersions();
557
+ const storedPlanId = await this.binding.toStoragePlanId(this.prisma, input.planKey);
558
+ const existing = await planVersion.findFirst({
403
559
  where: {
404
- planId: input.planKey,
560
+ planId: storedPlanId,
405
561
  version: input.version
406
562
  }
407
563
  });
@@ -411,9 +567,9 @@ var PrismaPlanCatalogImportSink = class {
411
567
  };
412
568
  const now = /* @__PURE__ */ new Date();
413
569
  if (input.publish) {
414
- await this.prisma.planVersion.updateMany({
570
+ await planVersion.updateMany({
415
571
  where: {
416
- planId: input.planKey,
572
+ planId: storedPlanId,
417
573
  publishedAt: {
418
574
  not: null
419
575
  },
@@ -427,9 +583,9 @@ var PrismaPlanCatalogImportSink = class {
427
583
  }
428
584
  });
429
585
  }
430
- await this.prisma.planVersion.create({
586
+ await planVersion.create({
431
587
  data: {
432
- planId: input.planKey,
588
+ planId: storedPlanId,
433
589
  version: input.version,
434
590
  features: input.features,
435
591
  quotas: input.quotas,
@@ -444,8 +600,12 @@ var PrismaPlanCatalogImportSink = class {
444
600
  created: true
445
601
  };
446
602
  }
603
+ planVersions() {
604
+ return getPrismaDelegate(this.prisma, this.delegateName);
605
+ }
447
606
  async upsertFeatureCatalogEntry(input) {
448
- const existing = await this.prisma.featureCatalogEntry.findFirst({
607
+ const db = this.db();
608
+ const existing = await db.featureCatalogEntry.findFirst({
449
609
  where: {
450
610
  projectKey: input.projectKey,
451
611
  featureKey: input.featureKey
@@ -455,7 +615,7 @@ var PrismaPlanCatalogImportSink = class {
455
615
  created: false,
456
616
  skipReason: "exists"
457
617
  };
458
- await this.prisma.featureCatalogEntry.create({
618
+ await db.featureCatalogEntry.create({
459
619
  data: {
460
620
  projectKey: input.projectKey,
461
621
  featureKey: input.featureKey,
@@ -474,9 +634,12 @@ var PrismaPlanCatalogImportSink = class {
474
634
  PrismaPlanCatalogImportSink = _ts_decorate6([
475
635
  (0, import_common6.Injectable)(),
476
636
  _ts_param5(0, (0, import_common6.Inject)(PRISMA_CLIENT_TOKEN)),
637
+ _ts_param5(1, (0, import_common6.Optional)()),
638
+ _ts_param5(1, (0, import_common6.Inject)(PRISMA_SCHEMA_OPTIONS_TOKEN)),
477
639
  _ts_metadata5("design:type", Function),
478
640
  _ts_metadata5("design:paramtypes", [
479
- typeof PrismaLike === "undefined" ? Object : PrismaLike
641
+ typeof PlanCatalogImportClient === "undefined" ? Object : PlanCatalogImportClient,
642
+ typeof PrismaSchemaOptions === "undefined" ? Object : PrismaSchemaOptions
480
643
  ])
481
644
  ], PrismaPlanCatalogImportSink);
482
645
 
@@ -523,11 +686,25 @@ var PrismaPlanCatalogReadSink = class {
523
686
  __name(this, "PrismaPlanCatalogReadSink");
524
687
  }
525
688
  prisma;
526
- constructor(prisma) {
689
+ binding;
690
+ delegateName;
691
+ fields;
692
+ constructor(prisma, options) {
527
693
  this.prisma = prisma;
694
+ const schema = resolvePrismaSchemaOptions(options);
695
+ this.binding = createPrismaPlanBindingResolver(options?.planBinding);
696
+ this.delegateName = schema.delegates.catalogPlanVersion;
697
+ this.fields = schema.planVersionFields.catalog;
698
+ }
699
+ db() {
700
+ return this.prisma;
528
701
  }
529
702
  async loadSnapshot(projectKey) {
530
- const plans = await this.prisma.plan.findMany({
703
+ if (this.binding.mode === "normalized-plan-id" && this.binding.projectKey !== projectKey) {
704
+ throw new Error(`Prisma plan binding is configured for project '${this.binding.projectKey}', not '${projectKey}'.`);
705
+ }
706
+ const db = this.db();
707
+ const plans = await db.plan.findMany({
531
708
  where: {
532
709
  projectKey,
533
710
  deletedAt: null
@@ -536,11 +713,17 @@ var PrismaPlanCatalogReadSink = class {
536
713
  sortOrder: "asc"
537
714
  }
538
715
  });
539
- const planKeys = plans.map((plan) => plan.planKey);
540
- const livePlanVersions = planKeys.length === 0 ? [] : await this.prisma.planVersion.findMany({
716
+ const planKeysByStoredId = new Map(plans.map((plan) => [
717
+ this.binding.mode === "normalized-plan-id" ? plan.id : plan.planKey,
718
+ plan.planKey
719
+ ]));
720
+ const storedPlanIds = [
721
+ ...planKeysByStoredId.keys()
722
+ ];
723
+ const livePlanVersions = storedPlanIds.length === 0 ? [] : await this.planVersions().findMany({
541
724
  where: {
542
725
  planId: {
543
- in: planKeys
726
+ in: storedPlanIds
544
727
  },
545
728
  publishedAt: {
546
729
  not: null
@@ -548,7 +731,7 @@ var PrismaPlanCatalogReadSink = class {
548
731
  supersededAt: null
549
732
  }
550
733
  });
551
- const featureEntries = await this.prisma.featureCatalogEntry.findMany({
734
+ const featureEntries = await db.featureCatalogEntry.findMany({
552
735
  where: {
553
736
  projectKey,
554
737
  deletedAt: null
@@ -559,17 +742,29 @@ var PrismaPlanCatalogReadSink = class {
559
742
  });
560
743
  return {
561
744
  plans: plans.map(toPlanRow),
562
- livePlanVersions: livePlanVersions.map(toPlanVersionRow),
745
+ livePlanVersions: livePlanVersions.map((row) => {
746
+ const planKey = planKeysByStoredId.get(row.planId);
747
+ if (!planKey) {
748
+ throw new Error(`PlanVersion ${row.id} references plan '${row.planId}' outside project '${projectKey}'.`);
749
+ }
750
+ return toPlanVersionRow(row, planKey, this.fields);
751
+ }),
563
752
  featureEntries: featureEntries.map(toFeatureCatalogEntryRow)
564
753
  };
565
754
  }
755
+ planVersions() {
756
+ return getPrismaDelegate(this.prisma, this.delegateName);
757
+ }
566
758
  };
567
759
  PrismaPlanCatalogReadSink = _ts_decorate7([
568
760
  (0, import_common7.Injectable)(),
569
761
  _ts_param6(0, (0, import_common7.Inject)(PRISMA_CLIENT_TOKEN)),
762
+ _ts_param6(1, (0, import_common7.Optional)()),
763
+ _ts_param6(1, (0, import_common7.Inject)(PRISMA_SCHEMA_OPTIONS_TOKEN)),
570
764
  _ts_metadata6("design:type", Function),
571
765
  _ts_metadata6("design:paramtypes", [
572
- typeof PrismaLike === "undefined" ? Object : PrismaLike
766
+ typeof PlanCatalogReadClient === "undefined" ? Object : PlanCatalogReadClient,
767
+ typeof PrismaSchemaOptions === "undefined" ? Object : PrismaSchemaOptions
573
768
  ])
574
769
  ], PrismaPlanCatalogReadSink);
575
770
  function toPlanRow(row) {
@@ -587,10 +782,10 @@ function toPlanRow(row) {
587
782
  };
588
783
  }
589
784
  __name(toPlanRow, "toPlanRow");
590
- function toPlanVersionRow(row) {
591
- return {
785
+ function toPlanVersionRow(row, planKey, fields) {
786
+ const mapped = {
592
787
  id: row.id,
593
- planId: row.planId,
788
+ planId: planKey,
594
789
  version: row.version,
595
790
  baseVersionId: row.baseVersionId,
596
791
  publishedAt: row.publishedAt ? row.publishedAt.toISOString() : null,
@@ -598,8 +793,8 @@ function toPlanVersionRow(row) {
598
793
  publishedChanges: row.publishedChanges ?? null,
599
794
  changeNote: row.changeNote,
600
795
  nonRegressive: row.nonRegressive,
601
- validFrom: null,
602
- validUntil: null,
796
+ validFrom: fields.validityWindows && row.validFrom ? row.validFrom.toISOString() : null,
797
+ validUntil: fields.validityWindows && row.validUntil ? row.validUntil.toISOString() : null,
603
798
  createdByUserId: row.createdByUserId,
604
799
  publishedByUserId: row.publishedByUserId,
605
800
  createdAt: row.createdAt.toISOString(),
@@ -610,6 +805,10 @@ function toPlanVersionRow(row) {
610
805
  yearlyNet: String(row.yearlyNet),
611
806
  marketed: row.marketed
612
807
  };
808
+ if (fields.endsAt) {
809
+ mapped.endsAt = row.endsAt?.toISOString() ?? null;
810
+ }
811
+ return mapped;
613
812
  }
614
813
  __name(toPlanVersionRow, "toPlanVersionRow");
615
814
  function toFeatureCatalogEntryRow(row) {
@@ -643,6 +842,7 @@ __name(toFeatureCatalogEntryRow, "toFeatureCatalogEntryRow");
643
842
 
644
843
  // src/prisma-plan-version.repository.ts
645
844
  var import_common8 = require("@nestjs/common");
845
+ var import_types = require("@saasicat/types");
646
846
  function _ts_decorate8(decorators, target, key, desc) {
647
847
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
648
848
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -665,14 +865,29 @@ var PrismaPlanVersionRepository = class {
665
865
  __name(this, "PrismaPlanVersionRepository");
666
866
  }
667
867
  prisma;
668
- constructor(prisma) {
868
+ findActive;
869
+ binding;
870
+ delegateName;
871
+ fields;
872
+ constructor(prisma, options) {
669
873
  this.prisma = prisma;
874
+ const schema = resolvePrismaSchemaOptions(options);
875
+ this.binding = createPrismaPlanBindingResolver(options?.planBinding);
876
+ this.delegateName = schema.delegates.entitlementPlanVersion;
877
+ this.fields = schema.planVersionFields.entitlement;
878
+ if (this.fields.validityWindows) {
879
+ this.findActive = (planId, asOf = /* @__PURE__ */ new Date(), tx) => this.findActivePlanVersion(planId, asOf, tx);
880
+ }
881
+ }
882
+ db(tx) {
883
+ return tx ?? this.prisma;
670
884
  }
671
885
  async findLatestLive(planId, tx) {
672
- const db = resolveClient(this.prisma, tx);
673
- const row = await db.planVersion.findFirst({
886
+ const db = this.db(tx);
887
+ const storedPlanId = await this.binding.toStoragePlanId(db, planId);
888
+ const row = await this.versions(db).findFirst({
674
889
  where: {
675
- planId,
890
+ planId: storedPlanId,
676
891
  publishedAt: {
677
892
  not: null
678
893
  },
@@ -683,8 +898,40 @@ var PrismaPlanVersionRepository = class {
683
898
  }
684
899
  });
685
900
  if (!row) return null;
901
+ return this.toRecord(db, row);
902
+ }
903
+ async findActivePlanVersion(planId, asOf, tx) {
904
+ const db = this.db(tx);
905
+ const storedPlanId = await this.binding.toStoragePlanId(db, planId);
906
+ const activeWhere = this.fields.endsAt ? (0, import_types.buildActivePlanVersionWhere)(asOf, {
907
+ withEndsAt: true
908
+ }) : (0, import_types.buildActivePlanVersionWhere)(asOf);
909
+ const row = await this.versions(db).findFirst({
910
+ where: {
911
+ planId: storedPlanId,
912
+ ...activeWhere
913
+ },
914
+ orderBy: [
915
+ {
916
+ validFrom: {
917
+ sort: "desc",
918
+ nulls: "last"
919
+ }
920
+ },
921
+ {
922
+ version: "desc"
923
+ }
924
+ ]
925
+ });
926
+ if (!row) return null;
927
+ return this.toRecord(db, row);
928
+ }
929
+ versions(client) {
930
+ return getPrismaDelegate(client, this.delegateName);
931
+ }
932
+ async toRecord(client, row) {
686
933
  return {
687
- planId: row.planId,
934
+ planId: await this.binding.toPlanKey(client, row.planId),
688
935
  quotas: toQuotaMap(row.quotas),
689
936
  features: toStringArray(row.features)
690
937
  };
@@ -693,9 +940,12 @@ var PrismaPlanVersionRepository = class {
693
940
  PrismaPlanVersionRepository = _ts_decorate8([
694
941
  (0, import_common8.Injectable)(),
695
942
  _ts_param7(0, (0, import_common8.Inject)(PRISMA_CLIENT_TOKEN)),
943
+ _ts_param7(1, (0, import_common8.Optional)()),
944
+ _ts_param7(1, (0, import_common8.Inject)(PRISMA_SCHEMA_OPTIONS_TOKEN)),
696
945
  _ts_metadata7("design:type", Function),
697
946
  _ts_metadata7("design:paramtypes", [
698
- typeof PrismaLike === "undefined" ? Object : PrismaLike
947
+ typeof PlanVersionRepositoryClient === "undefined" ? Object : PlanVersionRepositoryClient,
948
+ typeof PrismaSchemaOptions === "undefined" ? Object : PrismaSchemaOptions
699
949
  ])
700
950
  ], PrismaPlanVersionRepository);
701
951
 
@@ -1163,19 +1413,35 @@ var PrismaSubscriptionRepository = class {
1163
1413
  __name(this, "PrismaSubscriptionRepository");
1164
1414
  }
1165
1415
  prisma;
1166
- constructor(prisma) {
1416
+ countByBundleVersionId;
1417
+ binding;
1418
+ planVersionDelegateName;
1419
+ subscriptionDelegateName;
1420
+ subscriptionBundleDelegateName;
1421
+ constructor(prisma, options) {
1167
1422
  this.prisma = prisma;
1423
+ const schema = resolvePrismaSchemaOptions(options);
1424
+ this.binding = createPrismaPlanBindingResolver(options?.planBinding);
1425
+ this.planVersionDelegateName = schema.delegates.entitlementPlanVersion;
1426
+ this.subscriptionDelegateName = schema.tenantSubscription.delegate;
1427
+ this.subscriptionBundleDelegateName = schema.tenantSubscription.subscriptionBundleDelegate;
1428
+ if (this.subscriptionBundleDelegateName) {
1429
+ this.countByBundleVersionId = (bundleVersionId) => this.countActiveBundleBindings(bundleVersionId);
1430
+ }
1431
+ }
1432
+ db(tx) {
1433
+ return tx ?? this.prisma;
1168
1434
  }
1169
1435
  async findByTenantId(tenantId) {
1170
- return this.loadByTenantId(this.prisma, tenantId);
1436
+ return this.loadByTenantId(this.db(), tenantId);
1171
1437
  }
1172
1438
  async findByTenantIdLocked(tenantId, tx) {
1173
- const db = resolveClient(this.prisma, tx);
1439
+ const db = this.db(tx);
1174
1440
  await db.$queryRaw`SELECT id FROM subscriptions WHERE "tenantId" = ${tenantId} FOR UPDATE`;
1175
1441
  return this.loadByTenantId(db, tenantId);
1176
1442
  }
1177
1443
  async countByPlanVersionId(planVersionId) {
1178
- return this.prisma.subscription.count({
1444
+ return this.subscriptions(this.db()).count({
1179
1445
  where: {
1180
1446
  OR: [
1181
1447
  {
@@ -1188,22 +1454,72 @@ var PrismaSubscriptionRepository = class {
1188
1454
  }
1189
1455
  });
1190
1456
  }
1191
- async countActiveByPlanKey(_projectKey) {
1192
- const rows = await this.prisma.subscription.findMany({
1457
+ async countActiveByPlanKey(projectKey) {
1458
+ const db = this.db();
1459
+ const subscriptions = await this.subscriptions(db).findMany({
1193
1460
  where: {
1194
1461
  status: {
1195
1462
  in: ACTIVE_STATUSES
1196
1463
  }
1464
+ },
1465
+ select: {
1466
+ planVersionId: true
1197
1467
  }
1198
1468
  });
1469
+ const versionIds = [
1470
+ ...new Set(subscriptions.map((subscription) => subscription.planVersionId))
1471
+ ];
1472
+ if (versionIds.length === 0) return {};
1473
+ const planVersions = await this.planVersions(db).findMany({
1474
+ where: {
1475
+ id: {
1476
+ in: versionIds
1477
+ }
1478
+ },
1479
+ select: {
1480
+ id: true,
1481
+ planId: true
1482
+ }
1483
+ });
1484
+ const planVersionById = new Map(planVersions.map((planVersion) => [
1485
+ planVersion.id,
1486
+ planVersion
1487
+ ]));
1488
+ const storedPlanIds = [
1489
+ ...new Set(planVersions.map((planVersion) => planVersion.planId))
1490
+ ];
1491
+ const planKeyByStoredId = await this.planKeysForProject(db, storedPlanIds, projectKey);
1199
1492
  const counts = {};
1200
- for (const row of rows) {
1201
- counts[row.plan] = (counts[row.plan] ?? 0) + 1;
1493
+ for (const subscription of subscriptions) {
1494
+ const planVersion = planVersionById.get(subscription.planVersionId);
1495
+ const planKey = planVersion ? planKeyByStoredId.get(planVersion.planId) : void 0;
1496
+ if (!planKey) continue;
1497
+ counts[planKey] = (counts[planKey] ?? 0) + 1;
1202
1498
  }
1203
1499
  return counts;
1204
1500
  }
1501
+ async countActiveBundleBindings(bundleVersionId) {
1502
+ if (!this.subscriptionBundleDelegateName) {
1503
+ throw new Error("SubscriptionBundle counting is not configured.");
1504
+ }
1505
+ return getPrismaDelegate(this.prisma, this.subscriptionBundleDelegateName).count({
1506
+ where: {
1507
+ bundleVersionId,
1508
+ OR: [
1509
+ {
1510
+ canceledAt: null
1511
+ },
1512
+ {
1513
+ canceledEffectiveAt: {
1514
+ gt: /* @__PURE__ */ new Date()
1515
+ }
1516
+ }
1517
+ ]
1518
+ }
1519
+ });
1520
+ }
1205
1521
  async loadByTenantId(db, tenantId) {
1206
- const row = await db.subscription.findUnique({
1522
+ const row = await this.subscriptions(db).findUnique({
1207
1523
  where: {
1208
1524
  tenantId
1209
1525
  }
@@ -1212,7 +1528,7 @@ var PrismaSubscriptionRepository = class {
1212
1528
  return this.toRecord(db, row);
1213
1529
  }
1214
1530
  async toRecord(db, row) {
1215
- const planVersion = await db.planVersion.findUnique({
1531
+ const planVersion = await this.planVersions(db).findUnique({
1216
1532
  where: {
1217
1533
  id: row.planVersionId
1218
1534
  }
@@ -1220,10 +1536,13 @@ var PrismaSubscriptionRepository = class {
1220
1536
  if (!planVersion) {
1221
1537
  throw new Error(`Subscription ${row.id} references missing PlanVersion ${row.planVersionId}.`);
1222
1538
  }
1539
+ const planKey = await this.binding.toPlanKey(db, planVersion.planId);
1223
1540
  return {
1224
1541
  id: row.id,
1225
1542
  tenantId: row.tenantId,
1226
- plan: row.plan,
1543
+ // The concrete PlanVersion is authoritative. This also normalizes
1544
+ // legacy rows whose denormalized `Subscription.plan` drifted.
1545
+ plan: planKey,
1227
1546
  status: row.status,
1228
1547
  isPilot: row.isPilot,
1229
1548
  trialEntitlementPlan: row.trialEntitlementPlan,
@@ -1232,25 +1551,84 @@ var PrismaSubscriptionRepository = class {
1232
1551
  customLimits: row.customLimits ?? null,
1233
1552
  planVersionId: row.planVersionId,
1234
1553
  planVersion: {
1235
- planId: planVersion.planId,
1554
+ planId: planKey,
1236
1555
  quotas: toQuotaMap(planVersion.quotas),
1237
1556
  features: toStringArray(planVersion.features)
1238
1557
  }
1239
1558
  };
1240
1559
  }
1560
+ planVersions(client) {
1561
+ return getPrismaDelegate(client, this.planVersionDelegateName);
1562
+ }
1563
+ subscriptions(client) {
1564
+ return getPrismaDelegate(client, this.subscriptionDelegateName);
1565
+ }
1566
+ async planKeysForProject(db, storedPlanIds, projectKey) {
1567
+ if (storedPlanIds.length === 0) return /* @__PURE__ */ new Map();
1568
+ if (this.binding.mode === "normalized-plan-id") {
1569
+ if (this.binding.projectKey && this.binding.projectKey !== projectKey) {
1570
+ throw new Error(`Prisma plan binding is configured for project '${this.binding.projectKey}', not '${projectKey}'.`);
1571
+ }
1572
+ const plans2 = await db.plan.findMany({
1573
+ where: {
1574
+ projectKey,
1575
+ id: {
1576
+ in: storedPlanIds
1577
+ }
1578
+ },
1579
+ select: {
1580
+ id: true,
1581
+ planKey: true
1582
+ }
1583
+ });
1584
+ return new Map(plans2.map((plan) => [
1585
+ plan.id,
1586
+ plan.planKey
1587
+ ]));
1588
+ }
1589
+ const plans = await db.plan.findMany({
1590
+ where: {
1591
+ planKey: {
1592
+ in: storedPlanIds
1593
+ }
1594
+ },
1595
+ select: {
1596
+ projectKey: true,
1597
+ planKey: true
1598
+ }
1599
+ });
1600
+ const projectsByPlanKey = /* @__PURE__ */ new Map();
1601
+ for (const plan of plans) {
1602
+ const projects = projectsByPlanKey.get(plan.planKey) ?? /* @__PURE__ */ new Set();
1603
+ projects.add(plan.projectKey);
1604
+ projectsByPlanKey.set(plan.planKey, projects);
1605
+ }
1606
+ return new Map(storedPlanIds.flatMap((planKey) => {
1607
+ const projects = projectsByPlanKey.get(planKey);
1608
+ return projects?.size === 1 && projects.has(projectKey) ? [
1609
+ [
1610
+ planKey,
1611
+ planKey
1612
+ ]
1613
+ ] : [];
1614
+ }));
1615
+ }
1241
1616
  };
1242
1617
  PrismaSubscriptionRepository = _ts_decorate13([
1243
1618
  (0, import_common13.Injectable)(),
1244
1619
  _ts_param12(0, (0, import_common13.Inject)(PRISMA_CLIENT_TOKEN)),
1620
+ _ts_param12(1, (0, import_common13.Optional)()),
1621
+ _ts_param12(1, (0, import_common13.Inject)(PRISMA_SCHEMA_OPTIONS_TOKEN)),
1245
1622
  _ts_metadata12("design:type", Function),
1246
1623
  _ts_metadata12("design:paramtypes", [
1247
- typeof PrismaLike === "undefined" ? Object : PrismaLike
1624
+ typeof SubscriptionRepositoryClient === "undefined" ? Object : SubscriptionRepositoryClient,
1625
+ typeof PrismaSchemaOptions === "undefined" ? Object : PrismaSchemaOptions
1248
1626
  ])
1249
1627
  ], PrismaSubscriptionRepository);
1250
1628
 
1251
1629
  // src/prisma-super-admin-bootstrap.adapter.ts
1252
1630
  var import_common14 = require("@nestjs/common");
1253
- var import_types = require("@saasicat/types");
1631
+ var import_types2 = require("@saasicat/types");
1254
1632
  function _ts_decorate14(decorators, target, key, desc) {
1255
1633
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1256
1634
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1295,7 +1673,7 @@ var PrismaSuperAdminBootstrapAdapter = class {
1295
1673
  }
1296
1674
  });
1297
1675
  if (existing) {
1298
- throw new import_types.PlatformUserExistsError(email, existing.platformRole);
1676
+ throw new import_types2.PlatformUserExistsError(email, existing.platformRole);
1299
1677
  }
1300
1678
  const row = await this.prisma.superAdminUser.create({
1301
1679
  data: {
@@ -1356,7 +1734,8 @@ var PrismaTransactionRunner = class {
1356
1734
  this.prisma = prisma;
1357
1735
  }
1358
1736
  async run(fn) {
1359
- return this.prisma.$transaction((tx) => fn(tx));
1737
+ const transaction = this.prisma.$transaction.bind(this.prisma);
1738
+ return transaction((tx) => fn(tx));
1360
1739
  }
1361
1740
  };
1362
1741
  PrismaTransactionRunner = _ts_decorate15([
@@ -1364,7 +1743,7 @@ PrismaTransactionRunner = _ts_decorate15([
1364
1743
  _ts_param14(0, (0, import_common15.Inject)(PRISMA_CLIENT_TOKEN)),
1365
1744
  _ts_metadata14("design:type", Function),
1366
1745
  _ts_metadata14("design:paramtypes", [
1367
- typeof PrismaLike === "undefined" ? Object : PrismaLike
1746
+ typeof Record === "undefined" ? Object : Record
1368
1747
  ])
1369
1748
  ], PrismaTransactionRunner);
1370
1749
 
@@ -1415,8 +1794,8 @@ function prismaPersistence(options) {
1415
1794
  superAdminProvisioning: buildProvisioning(client, options.passwordHasher)
1416
1795
  },
1417
1796
  entitlement: {
1418
- subscriptionRepository: provide((prisma) => new PrismaSubscriptionRepository(prisma)),
1419
- planVersionRepository: provide((prisma) => new PrismaPlanVersionRepository(prisma))
1797
+ subscriptionRepository: provide((prisma) => new PrismaSubscriptionRepository(prisma, options.schema)),
1798
+ planVersionRepository: provide((prisma) => new PrismaPlanVersionRepository(prisma, options.schema))
1420
1799
  },
1421
1800
  promo: {
1422
1801
  promoCodeRepository: provide((prisma) => new PrismaPromoCodeRepository(prisma)),
@@ -1425,8 +1804,8 @@ function prismaPersistence(options) {
1425
1804
  subscriptionLookup: provide((prisma) => new PrismaPromoSubscriptionLookup(prisma)),
1426
1805
  revenueAggregator: new ZeroPromoRevenueDeductionAggregator()
1427
1806
  },
1428
- planCatalogReadSink: provide((prisma) => new PrismaPlanCatalogReadSink(prisma)),
1429
- planCatalogImportSink: provide((prisma) => new PrismaPlanCatalogImportSink(prisma))
1807
+ planCatalogReadSink: provide((prisma) => new PrismaPlanCatalogReadSink(prisma, options.schema)),
1808
+ planCatalogImportSink: provide((prisma) => new PrismaPlanCatalogImportSink(prisma, options.schema))
1430
1809
  };
1431
1810
  }
1432
1811
  __name(prismaPersistence, "prismaPersistence");
@@ -1495,7 +1874,7 @@ var PrismaSubscriptionBundleRepository = class {
1495
1874
  this.prisma = prisma;
1496
1875
  }
1497
1876
  db(tx) {
1498
- return resolveClient(this.prisma, tx);
1877
+ return tx ?? this.prisma;
1499
1878
  }
1500
1879
  async listBySubscription(subscriptionId) {
1501
1880
  const rows = await this.db().subscriptionBundle.findMany({
@@ -1595,7 +1974,7 @@ PrismaSubscriptionBundleRepository = _ts_decorate17([
1595
1974
  _ts_param15(0, (0, import_common17.Inject)(PRISMA_CLIENT_TOKEN)),
1596
1975
  _ts_metadata15("design:type", Function),
1597
1976
  _ts_metadata15("design:paramtypes", [
1598
- typeof PrismaLike === "undefined" ? Object : PrismaLike
1977
+ typeof SubscriptionBundleClient === "undefined" ? Object : SubscriptionBundleClient
1599
1978
  ])
1600
1979
  ], PrismaSubscriptionBundleRepository);
1601
1980
  function toRecord3(row) {
@@ -1615,6 +1994,7 @@ __name(toRecord3, "toRecord");
1615
1994
 
1616
1995
  // src/prisma-tenant-subscription-write.adapter.ts
1617
1996
  var import_common18 = require("@nestjs/common");
1997
+ var import_types3 = require("@saasicat/types");
1618
1998
  function _ts_decorate18(decorators, target, key, desc) {
1619
1999
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1620
2000
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1637,37 +2017,66 @@ var PrismaTenantSubscriptionWriteAdapter = class {
1637
2017
  __name(this, "PrismaTenantSubscriptionWriteAdapter");
1638
2018
  }
1639
2019
  prisma;
1640
- constructor(prisma) {
2020
+ applyOnboardingSelection;
2021
+ schema;
2022
+ planBinding;
2023
+ constructor(prisma, options) {
1641
2024
  this.prisma = prisma;
1642
- }
1643
- db(tx) {
1644
- return resolveClient(this.prisma, tx);
2025
+ this.schema = resolvePrismaSchemaOptions(options);
2026
+ this.planBinding = createPrismaPlanBindingResolver(options?.planBinding);
2027
+ this.assertConfiguration();
2028
+ if (this.schema.tenantSubscription.atomicOnboardingSelection) {
2029
+ this.applyOnboardingSelection = (tenantId, input, redeemPromo) => this.applyOnboardingSelectionAtomic(tenantId, input, redeemPromo);
2030
+ }
1645
2031
  }
1646
2032
  async changePlanImmediate(tenantId, input) {
1647
- const updated = await this.db().subscription.update({
2033
+ if (this.schema.tenantSubscription.synchronizePlanVersion) {
2034
+ return this.prisma.$transaction((tx) => this.changePlanImmediateInClient(tx, tenantId, input));
2035
+ }
2036
+ return this.changePlanImmediateInClient(this.prisma, tenantId, input);
2037
+ }
2038
+ async changePlanImmediateInClient(client, tenantId, input) {
2039
+ const subscription = this.subscription(client);
2040
+ const data = {
2041
+ plan: input.planId,
2042
+ billingCycle: input.cycle,
2043
+ pendingPlan: null,
2044
+ pendingBillingCycle: null,
2045
+ pendingEffectiveAt: null,
2046
+ ...input.nextStatus ? {
2047
+ status: input.nextStatus
2048
+ } : {},
2049
+ ...input.periodStart && input.periodEnd ? {
2050
+ currentPeriodStart: input.periodStart,
2051
+ currentPeriodEnd: input.periodEnd
2052
+ } : {},
2053
+ // #17: the platform changePlan path computes the carried-over
2054
+ // trial end and passes it through; null/undefined leaves the
2055
+ // existing trialEndsAt untouched.
2056
+ ...input.trialEndsAt ? {
2057
+ trialEndsAt: input.trialEndsAt
2058
+ } : {}
2059
+ };
2060
+ if (this.schema.tenantSubscription.synchronizePlanVersion) {
2061
+ const current = await subscription.findUnique({
2062
+ where: {
2063
+ tenantId
2064
+ }
2065
+ });
2066
+ if (!current) {
2067
+ throw new Error(`No subscription for tenant ${tenantId}.`);
2068
+ }
2069
+ const storagePlanId = await this.planBinding.toStoragePlanId(client, input.planId);
2070
+ data.planVersionId = await this.findTargetPlanVersionId(client, storagePlanId, input.periodStart ?? /* @__PURE__ */ new Date());
2071
+ if (await this.pendingVersionBelongsToAnotherPlan(client, current.pendingPlanVersionId, storagePlanId)) {
2072
+ Object.assign(data, clearedPendingVersionData());
2073
+ }
2074
+ }
2075
+ const updated = await subscription.update({
1648
2076
  where: {
1649
2077
  tenantId
1650
2078
  },
1651
- data: {
1652
- plan: input.planId,
1653
- billingCycle: input.cycle,
1654
- pendingPlan: null,
1655
- pendingBillingCycle: null,
1656
- pendingEffectiveAt: null,
1657
- ...input.nextStatus ? {
1658
- status: input.nextStatus
1659
- } : {},
1660
- ...input.periodStart && input.periodEnd ? {
1661
- currentPeriodStart: input.periodStart,
1662
- currentPeriodEnd: input.periodEnd
1663
- } : {},
1664
- // #17: the platform changePlan path computes the carried-over
1665
- // trial end and passes it through; null/undefined leaves the
1666
- // existing trialEndsAt untouched.
1667
- ...input.trialEndsAt ? {
1668
- trialEndsAt: input.trialEndsAt
1669
- } : {}
1670
- }
2079
+ data
1671
2080
  });
1672
2081
  return {
1673
2082
  plan: updated.plan,
@@ -1675,7 +2084,7 @@ var PrismaTenantSubscriptionWriteAdapter = class {
1675
2084
  };
1676
2085
  }
1677
2086
  async schedulePlanChange(tenantId, input) {
1678
- await this.db().subscription.update({
2087
+ await this.subscription(this.prisma).update({
1679
2088
  where: {
1680
2089
  tenantId
1681
2090
  },
@@ -1687,7 +2096,8 @@ var PrismaTenantSubscriptionWriteAdapter = class {
1687
2096
  });
1688
2097
  }
1689
2098
  async acceptPendingPlanVersion(tenantId, userId, now) {
1690
- const sub = await this.db().subscription.findUnique({
2099
+ const subscription = this.subscription(this.prisma);
2100
+ const sub = await subscription.findUnique({
1691
2101
  where: {
1692
2102
  tenantId
1693
2103
  }
@@ -1695,17 +2105,15 @@ var PrismaTenantSubscriptionWriteAdapter = class {
1695
2105
  if (!sub) {
1696
2106
  throw new Error(`No subscription for tenant ${tenantId}.`);
1697
2107
  }
1698
- if (sub.pendingPlanVersionAccepted) {
1699
- return {
1700
- accepted: true,
1701
- acceptedAt: sub.pendingPlanVersionAcceptedAt,
1702
- effectiveAt: sub.pendingPlanVersionEffectiveAt,
1703
- alreadyAccepted: true
1704
- };
2108
+ if (!sub.pendingPlanVersionId) {
2109
+ throw new Error(`No pending PlanVersion for tenant ${tenantId}.`);
1705
2110
  }
1706
- const updated = await this.db().subscription.update({
2111
+ const pendingPlanVersionId = sub.pendingPlanVersionId;
2112
+ const claimed = await subscription.updateMany({
1707
2113
  where: {
1708
- id: sub.id
2114
+ id: sub.id,
2115
+ pendingPlanVersionId,
2116
+ pendingPlanVersionAccepted: false
1709
2117
  },
1710
2118
  data: {
1711
2119
  pendingPlanVersionAccepted: true,
@@ -1713,15 +2121,66 @@ var PrismaTenantSubscriptionWriteAdapter = class {
1713
2121
  pendingPlanVersionAcceptedByUserId: userId
1714
2122
  }
1715
2123
  });
2124
+ const updated = await subscription.findUnique({
2125
+ where: {
2126
+ id: sub.id
2127
+ }
2128
+ });
2129
+ if (!updated) {
2130
+ throw new Error(`No subscription for tenant ${tenantId}.`);
2131
+ }
2132
+ if (claimed.count === 0 && (updated.pendingPlanVersionId !== pendingPlanVersionId || !updated.pendingPlanVersionAccepted)) {
2133
+ throw new Error(`Pending PlanVersion changed while accepting it for tenant ${tenantId}.`);
2134
+ }
1716
2135
  return {
1717
2136
  accepted: true,
1718
2137
  acceptedAt: updated.pendingPlanVersionAcceptedAt,
1719
2138
  effectiveAt: updated.pendingPlanVersionEffectiveAt,
1720
- alreadyAccepted: false
2139
+ alreadyAccepted: claimed.count === 0
1721
2140
  };
1722
2141
  }
2142
+ async applyOnboardingSelectionAtomic(tenantId, input, redeemPromo) {
2143
+ return this.prisma.$transaction(async (tx) => {
2144
+ const data = {
2145
+ plan: input.planId,
2146
+ billingCycle: input.cycle,
2147
+ pendingPlan: null,
2148
+ pendingBillingCycle: null,
2149
+ pendingEffectiveAt: null,
2150
+ ...clearedPendingVersionData(),
2151
+ ...input.nextStatus ? {
2152
+ status: input.nextStatus
2153
+ } : {},
2154
+ ...input.periodStart && input.periodEnd ? {
2155
+ currentPeriodStart: input.periodStart,
2156
+ currentPeriodEnd: input.periodEnd
2157
+ } : {}
2158
+ };
2159
+ if (this.schema.tenantSubscription.synchronizePlanVersion) {
2160
+ const storagePlanId = await this.planBinding.toStoragePlanId(tx, input.planId);
2161
+ data.planVersionId = await this.findTargetPlanVersionId(tx, storagePlanId, input.periodStart ?? /* @__PURE__ */ new Date());
2162
+ }
2163
+ const updated = await this.subscription(tx).update({
2164
+ where: {
2165
+ tenantId
2166
+ },
2167
+ data
2168
+ });
2169
+ let promoRedemption = null;
2170
+ if (redeemPromo) {
2171
+ promoRedemption = await redeemPromo(tx, updated.id);
2172
+ }
2173
+ return {
2174
+ plan: updated.plan,
2175
+ billingCycle: updated.billingCycle,
2176
+ subscriptionId: updated.id,
2177
+ promoRedemption
2178
+ };
2179
+ });
2180
+ }
1723
2181
  async cancelSubscription(tenantId, immediate, now) {
1724
- const sub = await this.db().subscription.findUnique({
2182
+ const subscription = this.subscription(this.prisma);
2183
+ const sub = await subscription.findUnique({
1725
2184
  where: {
1726
2185
  tenantId
1727
2186
  }
@@ -1730,7 +2189,7 @@ var PrismaTenantSubscriptionWriteAdapter = class {
1730
2189
  throw new Error(`No subscription for tenant ${tenantId}.`);
1731
2190
  }
1732
2191
  const canceledAt = immediate ? now : sub.currentPeriodEnd ?? now;
1733
- const updated = await this.db().subscription.update({
2192
+ const updated = await subscription.update({
1734
2193
  where: {
1735
2194
  tenantId
1736
2195
  },
@@ -1744,18 +2203,107 @@ var PrismaTenantSubscriptionWriteAdapter = class {
1744
2203
  status: updated.status
1745
2204
  };
1746
2205
  }
2206
+ subscription(client) {
2207
+ return getPrismaDelegate(client, this.schema.tenantSubscription.delegate);
2208
+ }
2209
+ planVersions(client) {
2210
+ return getPrismaDelegate(client, this.schema.delegates.entitlementPlanVersion);
2211
+ }
2212
+ async findTargetPlanVersionId(client, storagePlanId, asOf) {
2213
+ const activeWindow = this.schema.tenantSubscription.activeVersionSelection === "validity-window";
2214
+ const activeVersionWhere = this.schema.tenantSubscription.withEndsAt ? (0, import_types3.buildActivePlanVersionWhere)(asOf, {
2215
+ withEndsAt: true
2216
+ }) : (0, import_types3.buildActivePlanVersionWhere)(asOf);
2217
+ const where = activeWindow ? {
2218
+ planId: storagePlanId,
2219
+ ...activeVersionWhere
2220
+ } : {
2221
+ planId: storagePlanId,
2222
+ publishedAt: {
2223
+ not: null
2224
+ },
2225
+ supersededAt: null,
2226
+ ...this.schema.tenantSubscription.withEndsAt ? {
2227
+ OR: [
2228
+ {
2229
+ endsAt: null
2230
+ },
2231
+ {
2232
+ endsAt: {
2233
+ gt: asOf
2234
+ }
2235
+ }
2236
+ ]
2237
+ } : {}
2238
+ };
2239
+ const target = await this.planVersions(client).findFirst({
2240
+ where,
2241
+ orderBy: activeWindow ? [
2242
+ {
2243
+ validFrom: {
2244
+ sort: "desc",
2245
+ nulls: "last"
2246
+ }
2247
+ },
2248
+ {
2249
+ version: "desc"
2250
+ }
2251
+ ] : {
2252
+ version: "desc"
2253
+ }
2254
+ });
2255
+ if (!target) {
2256
+ throw new Error(`No active PlanVersion for plan '${storagePlanId}'.`);
2257
+ }
2258
+ return target.id;
2259
+ }
2260
+ async pendingVersionBelongsToAnotherPlan(client, pendingPlanVersionId, targetStoragePlanId) {
2261
+ if (!pendingPlanVersionId) return false;
2262
+ const pending = await this.planVersions(client).findUnique({
2263
+ where: {
2264
+ id: pendingPlanVersionId
2265
+ }
2266
+ });
2267
+ return !pending || pending.planId !== targetStoragePlanId;
2268
+ }
2269
+ assertConfiguration() {
2270
+ if (!this.schema.tenantSubscription.synchronizePlanVersion) return;
2271
+ const entitlementFields = this.schema.planVersionFields.entitlement;
2272
+ if (this.schema.tenantSubscription.activeVersionSelection === "validity-window" && !entitlementFields.validityWindows) {
2273
+ throw new Error("tenantSubscription.activeVersionSelection='validity-window' requires planVersionFields.entitlement.validityWindows=true.");
2274
+ }
2275
+ if (this.schema.tenantSubscription.withEndsAt && !entitlementFields.endsAt) {
2276
+ throw new Error("tenantSubscription.withEndsAt=true requires planVersionFields.entitlement.endsAt=true.");
2277
+ }
2278
+ }
1747
2279
  };
1748
2280
  PrismaTenantSubscriptionWriteAdapter = _ts_decorate18([
1749
2281
  (0, import_common18.Injectable)(),
1750
2282
  _ts_param16(0, (0, import_common18.Inject)(PRISMA_CLIENT_TOKEN)),
2283
+ _ts_param16(1, (0, import_common18.Optional)()),
2284
+ _ts_param16(1, (0, import_common18.Inject)(PRISMA_SCHEMA_OPTIONS_TOKEN)),
1751
2285
  _ts_metadata16("design:type", Function),
1752
2286
  _ts_metadata16("design:paramtypes", [
1753
- typeof PrismaLike === "undefined" ? Object : PrismaLike
2287
+ typeof TransactionalPrismaClient === "undefined" ? Object : TransactionalPrismaClient,
2288
+ typeof PrismaSchemaOptions === "undefined" ? Object : PrismaSchemaOptions
1754
2289
  ])
1755
2290
  ], PrismaTenantSubscriptionWriteAdapter);
2291
+ function clearedPendingVersionData() {
2292
+ return {
2293
+ pendingPlanVersionId: null,
2294
+ pendingPlanVersionEffectiveAt: null,
2295
+ pendingPlanVersionAccepted: false,
2296
+ pendingPlanVersionAcceptedAt: null,
2297
+ pendingPlanVersionAcceptedByUserId: null,
2298
+ pendingPlanVersionNotifiedAt: null,
2299
+ pendingPlanVersionReminderSentAt: null
2300
+ };
2301
+ }
2302
+ __name(clearedPendingVersionData, "clearedPendingVersionData");
1756
2303
 
1757
2304
  // src/prisma-plan.repository.ts
1758
2305
  var import_common19 = require("@nestjs/common");
2306
+ var import_types4 = require("@saasicat/types");
1759
2307
  function _ts_decorate19(decorators, target, key, desc) {
1760
2308
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1761
2309
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1778,30 +2326,105 @@ var PrismaPlanRepository = class {
1778
2326
  __name(this, "PrismaPlanRepository");
1779
2327
  }
1780
2328
  prisma;
1781
- constructor(prisma) {
2329
+ binding;
2330
+ delegateName;
2331
+ fields;
2332
+ constructor(prisma, options) {
1782
2333
  this.prisma = prisma;
2334
+ const schema = resolvePrismaSchemaOptions(options);
2335
+ this.binding = createPrismaPlanBindingResolver(options?.planBinding);
2336
+ this.delegateName = schema.delegates.catalogPlanVersion;
2337
+ this.fields = schema.planVersionFields.catalog;
1783
2338
  }
1784
2339
  db(tx) {
1785
- return resolveClient(this.prisma, tx);
2340
+ return tx ?? this.prisma;
2341
+ }
2342
+ versions(client) {
2343
+ return getPrismaDelegate(client, this.delegateName);
1786
2344
  }
1787
2345
  // ─── Stem operations (Pack 1) ───
1788
2346
  async list(filter) {
1789
2347
  const excludeDeleted = filter.excludeDeleted ?? true;
2348
+ const db = this.db();
1790
2349
  let publishedKeys = null;
1791
2350
  if (filter.onlyPublished) {
1792
- const live = await this.db().planVersion.findMany({
1793
- where: {
1794
- publishedAt: {
1795
- not: null
1796
- },
1797
- supersededAt: null
2351
+ if (this.binding.mode === "legacy-plan-key") {
2352
+ const projectPlans = await db.plan.findMany({
2353
+ where: {
2354
+ projectKey: filter.projectKey,
2355
+ ...excludeDeleted ? {
2356
+ deletedAt: null
2357
+ } : {}
2358
+ }
2359
+ });
2360
+ const candidateKeys = [
2361
+ ...new Set(projectPlans.map((plan) => plan.planKey))
2362
+ ];
2363
+ const allMatchingPlans = candidateKeys.length === 0 ? [] : await db.plan.findMany({
2364
+ where: {
2365
+ planKey: {
2366
+ in: candidateKeys
2367
+ }
2368
+ }
2369
+ });
2370
+ const projectsByKey = /* @__PURE__ */ new Map();
2371
+ for (const plan of allMatchingPlans) {
2372
+ const projects = projectsByKey.get(plan.planKey) ?? /* @__PURE__ */ new Set();
2373
+ projects.add(plan.projectKey);
2374
+ projectsByKey.set(plan.planKey, projects);
1798
2375
  }
1799
- });
1800
- publishedKeys = [
1801
- ...new Set(live.map((version) => version.planId))
1802
- ];
2376
+ const unambiguousKeys = candidateKeys.filter((planKey) => projectsByKey.get(planKey)?.size === 1);
2377
+ const live = await this.versions(db).findMany({
2378
+ where: {
2379
+ planId: {
2380
+ in: unambiguousKeys
2381
+ },
2382
+ publishedAt: {
2383
+ not: null
2384
+ },
2385
+ supersededAt: null
2386
+ }
2387
+ });
2388
+ publishedKeys = [
2389
+ ...new Set(live.map((version) => version.planId))
2390
+ ];
2391
+ } else {
2392
+ const projectPlans = await db.plan.findMany({
2393
+ where: {
2394
+ projectKey: filter.projectKey,
2395
+ ...excludeDeleted ? {
2396
+ deletedAt: null
2397
+ } : {}
2398
+ }
2399
+ });
2400
+ const planKeyById = new Map(projectPlans.map((plan) => [
2401
+ plan.id,
2402
+ plan.planKey
2403
+ ]));
2404
+ const live = await this.versions(db).findMany({
2405
+ where: {
2406
+ planId: {
2407
+ in: [
2408
+ ...planKeyById.keys()
2409
+ ]
2410
+ },
2411
+ publishedAt: {
2412
+ not: null
2413
+ },
2414
+ supersededAt: null
2415
+ }
2416
+ });
2417
+ publishedKeys = [
2418
+ ...new Set(live.flatMap((version) => {
2419
+ const planKey = planKeyById.get(version.planId);
2420
+ return planKey ? [
2421
+ planKey
2422
+ ] : [];
2423
+ }))
2424
+ ];
2425
+ }
1803
2426
  }
1804
- const rows = await this.db().plan.findMany({
2427
+ const rows = await db.plan.findMany({
1805
2428
  where: {
1806
2429
  projectKey: filter.projectKey,
1807
2430
  ...excludeDeleted ? {
@@ -1896,65 +2519,112 @@ var PrismaPlanRepository = class {
1896
2519
  }
1897
2520
  // ─── Lifecycle operations (Pack 2a) — keyed by planKey ───
1898
2521
  async listVersions(planKey) {
1899
- const rows = await this.db().planVersion.findMany({
2522
+ const db = this.db();
2523
+ const storedPlanId = await this.binding.toStoragePlanId(db, planKey);
2524
+ const rows = await this.versions(db).findMany({
1900
2525
  where: {
1901
- planId: planKey
2526
+ planId: storedPlanId
1902
2527
  },
1903
2528
  orderBy: {
1904
2529
  version: "asc"
1905
2530
  }
1906
2531
  });
1907
- return rows.map(toPlanVersionRow2);
2532
+ return rows.map((row) => this.toPlanVersionRow(row, planKey));
1908
2533
  }
1909
2534
  async findVersionById(versionId) {
1910
- const row = await this.db().planVersion.findUnique({
2535
+ const db = this.db();
2536
+ const row = await this.versions(db).findUnique({
1911
2537
  where: {
1912
2538
  id: versionId
1913
2539
  }
1914
2540
  });
1915
- return row ? toPlanVersionRow2(row) : null;
2541
+ return row ? this.toPlanVersionRow(row, await this.binding.toPlanKey(db, row.planId)) : null;
1916
2542
  }
1917
2543
  async findCurrentDraft(planKey) {
1918
- const row = await this.db().planVersion.findFirst({
2544
+ const db = this.db();
2545
+ const storedPlanId = await this.binding.toStoragePlanId(db, planKey);
2546
+ const row = await this.versions(db).findFirst({
1919
2547
  where: {
1920
- planId: planKey,
2548
+ planId: storedPlanId,
1921
2549
  publishedAt: null
1922
2550
  }
1923
2551
  });
1924
- return row ? toPlanVersionRow2(row) : null;
2552
+ return row ? this.toPlanVersionRow(row, planKey) : null;
1925
2553
  }
1926
2554
  async findLatestLivePlanVersion(planKey, tx) {
1927
- const row = await this.db(tx).planVersion.findFirst({
2555
+ const db = this.db(tx);
2556
+ const storedPlanId = await this.binding.toStoragePlanId(db, planKey);
2557
+ const row = await this.versions(db).findFirst({
1928
2558
  where: {
1929
- planId: planKey,
2559
+ planId: storedPlanId,
1930
2560
  publishedAt: {
1931
2561
  not: null
1932
2562
  },
1933
- supersededAt: null
2563
+ supersededAt: null,
2564
+ ...this.fields.endsAt ? {
2565
+ OR: [
2566
+ {
2567
+ endsAt: null
2568
+ },
2569
+ {
2570
+ endsAt: {
2571
+ gt: /* @__PURE__ */ new Date()
2572
+ }
2573
+ }
2574
+ ]
2575
+ } : {}
1934
2576
  },
1935
2577
  orderBy: {
1936
2578
  version: "desc"
1937
2579
  }
1938
2580
  });
1939
- return row ? toPlanVersionRow2(row) : null;
2581
+ return row ? this.toPlanVersionRow(row, planKey) : null;
1940
2582
  }
1941
- async findActivePlanVersion() {
1942
- throw new Error("findActivePlanVersion is not supported by the shipped @saasicat/adapter-prisma PlanRepository: the canonical plan_versions schema (03-plan-versions.prisma) has no validFrom/validUntil columns, so a version cannot be resolved by validity window. Use findLatestLivePlanVersion for the newest live version, or provide a custom PlanRepository adapter on a schema that carries the validity-window columns.");
2583
+ async findActivePlanVersion(planKey, asOf = /* @__PURE__ */ new Date(), tx) {
2584
+ if (!this.fields.validityWindows) {
2585
+ throw new Error("findActivePlanVersion requires schema.planVersionFields.catalog.validityWindows=true and the current @saasicat/spec PlanVersion validity columns. Apply the additive schema first, or use findLatestLivePlanVersion for a 0.6-compatible newest-live lookup.");
2586
+ }
2587
+ const db = this.db(tx);
2588
+ const storedPlanId = await this.binding.toStoragePlanId(db, planKey);
2589
+ const activeWhere = this.fields.endsAt ? (0, import_types4.buildActivePlanVersionWhere)(asOf, {
2590
+ withEndsAt: true
2591
+ }) : (0, import_types4.buildActivePlanVersionWhere)(asOf);
2592
+ const row = await this.versions(db).findFirst({
2593
+ where: {
2594
+ planId: storedPlanId,
2595
+ ...activeWhere
2596
+ },
2597
+ orderBy: [
2598
+ {
2599
+ validFrom: {
2600
+ sort: "desc",
2601
+ nulls: "last"
2602
+ }
2603
+ },
2604
+ {
2605
+ version: "desc"
2606
+ }
2607
+ ]
2608
+ });
2609
+ return row ? this.toPlanVersionRow(row, planKey) : null;
1943
2610
  }
1944
2611
  async createPlanVersionDraft(data) {
1945
2612
  const planKey = data.planId;
1946
- const latest = await this.db().planVersion.findFirst({
2613
+ const db = this.db();
2614
+ const planVersion = this.versions(db);
2615
+ const storedPlanId = await this.binding.toStoragePlanId(db, planKey);
2616
+ const latest = await planVersion.findFirst({
1947
2617
  where: {
1948
- planId: planKey
2618
+ planId: storedPlanId
1949
2619
  },
1950
2620
  orderBy: {
1951
2621
  version: "desc"
1952
2622
  }
1953
2623
  });
1954
2624
  const nextVersion = (latest?.version ?? 0) + 1;
1955
- const created = await this.db().planVersion.create({
2625
+ const created = await planVersion.create({
1956
2626
  data: {
1957
- planId: planKey,
2627
+ planId: storedPlanId,
1958
2628
  version: nextVersion,
1959
2629
  baseVersionId: data.baseVersionId ?? null,
1960
2630
  features: data.features,
@@ -1963,13 +2633,17 @@ var PrismaPlanRepository = class {
1963
2633
  yearlyNet: data.yearlyNet,
1964
2634
  marketed: data.marketed ?? true,
1965
2635
  changeNote: data.changeNote ?? "",
1966
- createdByUserId: data.createdByUserId ?? null
2636
+ createdByUserId: data.createdByUserId ?? null,
2637
+ ...this.fields.validityWindows ? {
2638
+ validFrom: data.validFrom ? new Date(data.validFrom) : null,
2639
+ validUntil: data.validUntil ? new Date(data.validUntil) : null
2640
+ } : {}
1967
2641
  }
1968
2642
  });
1969
- return toPlanVersionRow2(created);
2643
+ return this.toPlanVersionRow(created, planKey);
1970
2644
  }
1971
2645
  async updatePlanVersionDraft(versionId, data) {
1972
- const updated = await this.db().planVersion.update({
2646
+ const updated = await this.versions(this.db()).update({
1973
2647
  where: {
1974
2648
  id: versionId
1975
2649
  },
@@ -1991,13 +2665,21 @@ var PrismaPlanRepository = class {
1991
2665
  } : {},
1992
2666
  ...data.changeNote !== void 0 ? {
1993
2667
  changeNote: data.changeNote
2668
+ } : {},
2669
+ ...this.fields.validityWindows && data.validFrom !== void 0 ? {
2670
+ validFrom: data.validFrom ? new Date(data.validFrom) : null
2671
+ } : {},
2672
+ ...this.fields.validityWindows && data.validUntil !== void 0 ? {
2673
+ validUntil: data.validUntil ? new Date(data.validUntil) : null
1994
2674
  } : {}
1995
2675
  }
1996
2676
  });
1997
- return toPlanVersionRow2(updated);
2677
+ const planKey = await this.binding.toPlanKey(this.db(), updated.planId);
2678
+ return this.toPlanVersionRow(updated, planKey);
1998
2679
  }
1999
2680
  async publishPlanVersionDraft(versionId, publishMeta, tx) {
2000
- const draft = await this.db(tx).planVersion.findUnique({
2681
+ const operationDb = this.db(tx);
2682
+ const draft = await this.versions(operationDb).findUnique({
2001
2683
  where: {
2002
2684
  id: versionId
2003
2685
  }
@@ -2005,11 +2687,12 @@ var PrismaPlanRepository = class {
2005
2687
  if (!draft) {
2006
2688
  throw new Error(`PlanVersion ${versionId} not found.`);
2007
2689
  }
2008
- const planKey = draft.planId;
2690
+ const storedPlanId = draft.planId;
2009
2691
  const publish = /* @__PURE__ */ __name(async (db) => {
2010
- const previous = await db.planVersion.findFirst({
2692
+ const planVersion = this.versions(db);
2693
+ const previous = await planVersion.findFirst({
2011
2694
  where: {
2012
- planId: planKey,
2695
+ planId: storedPlanId,
2013
2696
  publishedAt: {
2014
2697
  not: null
2015
2698
  },
@@ -2024,16 +2707,20 @@ var PrismaPlanRepository = class {
2024
2707
  });
2025
2708
  const now = /* @__PURE__ */ new Date();
2026
2709
  if (previous) {
2027
- await db.planVersion.update({
2710
+ const predecessorValidUntil = new Date(publishMeta.validFrom.getTime() - 24 * 60 * 60 * 1e3);
2711
+ await planVersion.update({
2028
2712
  where: {
2029
2713
  id: previous.id
2030
2714
  },
2031
2715
  data: {
2032
- supersededAt: now
2716
+ supersededAt: now,
2717
+ ...this.fields.validityWindows ? {
2718
+ validUntil: predecessorValidUntil
2719
+ } : {}
2033
2720
  }
2034
2721
  });
2035
2722
  }
2036
- return db.planVersion.update({
2723
+ return planVersion.update({
2037
2724
  where: {
2038
2725
  id: versionId
2039
2726
  },
@@ -2041,15 +2728,21 @@ var PrismaPlanRepository = class {
2041
2728
  publishedAt: now,
2042
2729
  publishedChanges: publishMeta.publishedChanges,
2043
2730
  nonRegressive: publishMeta.nonRegressive,
2044
- publishedByUserId: publishMeta.publishedByUserId
2731
+ publishedByUserId: publishMeta.publishedByUserId,
2732
+ ...this.fields.validityWindows ? {
2733
+ validFrom: publishMeta.validFrom,
2734
+ validUntil: publishMeta.validUntil
2735
+ } : {}
2045
2736
  }
2046
2737
  });
2047
2738
  }, "publish");
2048
2739
  const published = tx ? await publish(this.db(tx)) : await this.prisma.$transaction((txClient) => publish(txClient));
2049
- return toPlanVersionRow2(published);
2740
+ const planKey = await this.binding.toPlanKey(operationDb, storedPlanId);
2741
+ return this.toPlanVersionRow(published, planKey);
2050
2742
  }
2051
2743
  async deletePlanVersionDraft(versionId) {
2052
- const row = await this.db().planVersion.findUnique({
2744
+ const planVersion = this.versions(this.db());
2745
+ const row = await planVersion.findUnique({
2053
2746
  where: {
2054
2747
  id: versionId
2055
2748
  }
@@ -2058,23 +2751,42 @@ var PrismaPlanRepository = class {
2058
2751
  if (row.publishedAt !== null) {
2059
2752
  throw new Error(`PlanVersion ${versionId} is already published and cannot be discarded (published versions are immutable \u2014 contract protection P1).`);
2060
2753
  }
2061
- await this.db().planVersion.deleteMany({
2754
+ await planVersion.deleteMany({
2062
2755
  where: {
2063
2756
  id: versionId,
2064
2757
  publishedAt: null
2065
2758
  }
2066
2759
  });
2067
2760
  }
2068
- async terminate() {
2069
- throw new Error("terminate is not supported by the shipped @saasicat/adapter-prisma PlanRepository: the canonical plan_versions schema (03-plan-versions.prisma) has no endsAt column. Provide a custom PlanRepository adapter on a schema that carries endsAt to support SuperAdmin-initiated plan-version termination.");
2761
+ async terminate(versionId, endsAt) {
2762
+ if (!this.fields.endsAt) {
2763
+ throw new Error("terminate requires schema.planVersionFields.catalog.endsAt=true and the current @saasicat/spec PlanVersion.endsAt column. Apply the additive schema before enabling SuperAdmin-initiated plan-version termination.");
2764
+ }
2765
+ const db = this.db();
2766
+ const updated = await this.versions(db).update({
2767
+ where: {
2768
+ id: versionId
2769
+ },
2770
+ data: {
2771
+ endsAt
2772
+ }
2773
+ });
2774
+ const planKey = await this.binding.toPlanKey(db, updated.planId);
2775
+ return this.toPlanVersionRow(updated, planKey);
2776
+ }
2777
+ toPlanVersionRow(row, planKey) {
2778
+ return toPlanVersionRow2(row, planKey, this.fields);
2070
2779
  }
2071
2780
  };
2072
2781
  PrismaPlanRepository = _ts_decorate19([
2073
2782
  (0, import_common19.Injectable)(),
2074
2783
  _ts_param17(0, (0, import_common19.Inject)(PRISMA_CLIENT_TOKEN)),
2784
+ _ts_param17(1, (0, import_common19.Optional)()),
2785
+ _ts_param17(1, (0, import_common19.Inject)(PRISMA_SCHEMA_OPTIONS_TOKEN)),
2075
2786
  _ts_metadata17("design:type", Function),
2076
2787
  _ts_metadata17("design:paramtypes", [
2077
- typeof PrismaLike === "undefined" ? Object : PrismaLike
2788
+ typeof PlanRepositoryClient === "undefined" ? Object : PlanRepositoryClient,
2789
+ typeof PrismaSchemaOptions === "undefined" ? Object : PrismaSchemaOptions
2078
2790
  ])
2079
2791
  ], PrismaPlanRepository);
2080
2792
  function toPlanRow2(row) {
@@ -2092,12 +2804,12 @@ function toPlanRow2(row) {
2092
2804
  };
2093
2805
  }
2094
2806
  __name(toPlanRow2, "toPlanRow");
2095
- function toPlanVersionRow2(row) {
2096
- return {
2807
+ function toPlanVersionRow2(row, planKey, fields) {
2808
+ const mapped = {
2097
2809
  id: row.id,
2098
2810
  version: row.version,
2099
2811
  baseVersionId: row.baseVersionId,
2100
- planId: row.planId,
2812
+ planId: planKey,
2101
2813
  features: toStringArray(row.features),
2102
2814
  quotas: toQuotaMap(row.quotas),
2103
2815
  monthlyNet: row.monthlyNet.toString(),
@@ -2108,19 +2820,23 @@ function toPlanVersionRow2(row) {
2108
2820
  publishedChanges: Array.isArray(row.publishedChanges) ? row.publishedChanges : null,
2109
2821
  changeNote: row.changeNote,
2110
2822
  nonRegressive: row.nonRegressive,
2111
- // The canonical plan_versions schema carries no validity-window columns.
2112
- validFrom: null,
2113
- validUntil: null,
2823
+ validFrom: fields.validityWindows && row.validFrom ? row.validFrom.toISOString() : null,
2824
+ validUntil: fields.validityWindows && row.validUntil ? row.validUntil.toISOString() : null,
2114
2825
  createdByUserId: row.createdByUserId,
2115
2826
  publishedByUserId: row.publishedByUserId,
2116
2827
  createdAt: row.createdAt.toISOString(),
2117
2828
  updatedAt: row.updatedAt.toISOString()
2118
2829
  };
2830
+ if (fields.endsAt) {
2831
+ mapped.endsAt = row.endsAt?.toISOString() ?? null;
2832
+ }
2833
+ return mapped;
2119
2834
  }
2120
2835
  __name(toPlanVersionRow2, "toPlanVersionRow");
2121
2836
 
2122
2837
  // src/prisma-bundle.repository.ts
2123
2838
  var import_common20 = require("@nestjs/common");
2839
+ var import_types5 = require("@saasicat/types");
2124
2840
  function _ts_decorate20(decorators, target, key, desc) {
2125
2841
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2126
2842
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2138,16 +2854,32 @@ function _ts_param18(paramIndex, decorator) {
2138
2854
  };
2139
2855
  }
2140
2856
  __name(_ts_param18, "_ts_param");
2857
+ var PRISMA_BUNDLE_REPOSITORY_OPTIONS = /* @__PURE__ */ Symbol.for("saasicat/adapter-prisma/PrismaBundleRepositoryOptions");
2141
2858
  var PrismaBundleRepository = class {
2142
2859
  static {
2143
2860
  __name(this, "PrismaBundleRepository");
2144
2861
  }
2145
2862
  prisma;
2146
- constructor(prisma) {
2863
+ validityWindows;
2864
+ /**
2865
+ * Present only when validity-window mode is enabled. This mirrors the
2866
+ * optional port capability and lets 0.6-schema consumers detect that an
2867
+ * active-at-time lookup is unavailable.
2868
+ */
2869
+ findActiveBundleVersion;
2870
+ constructor(prisma, options = {}) {
2147
2871
  this.prisma = prisma;
2872
+ this.validityWindows = options.validityWindows ?? false;
2873
+ if (this.validityWindows) {
2874
+ this.findActiveBundleVersion = (bundleId, asOf, tx) => this.findActiveBundleVersionWithValidity(bundleId, asOf ?? /* @__PURE__ */ new Date(), tx);
2875
+ }
2148
2876
  }
2149
2877
  db(tx) {
2150
- return resolveClient(this.prisma, tx);
2878
+ return tx ?? this.prisma;
2879
+ }
2880
+ transaction(work) {
2881
+ const transaction = this.prisma.$transaction;
2882
+ return transaction.call(this.prisma, (tx) => work(tx));
2151
2883
  }
2152
2884
  // ─── Stem operations ───
2153
2885
  async list(filter) {
@@ -2253,7 +2985,7 @@ var PrismaBundleRepository = class {
2253
2985
  version: "asc"
2254
2986
  }
2255
2987
  });
2256
- return rows.map((row) => toBundleVersionRow(row, bundle));
2988
+ return rows.map((row) => toBundleVersionRow(row, bundle, this.validityWindows));
2257
2989
  }
2258
2990
  async findVersionById(versionId) {
2259
2991
  const row = await this.db().bundleVersion.findUnique({
@@ -2267,7 +2999,7 @@ var PrismaBundleRepository = class {
2267
2999
  id: row.bundleId
2268
3000
  }
2269
3001
  });
2270
- return toBundleVersionRow(row, bundle);
3002
+ return toBundleVersionRow(row, bundle, this.validityWindows);
2271
3003
  }
2272
3004
  async findCurrentDraft(bundleId) {
2273
3005
  const row = await this.db().bundleVersion.findFirst({
@@ -2282,7 +3014,7 @@ var PrismaBundleRepository = class {
2282
3014
  id: bundleId
2283
3015
  }
2284
3016
  });
2285
- return toBundleVersionRow(row, bundle);
3017
+ return toBundleVersionRow(row, bundle, this.validityWindows);
2286
3018
  }
2287
3019
  async findLatestLive(bundleId, tx) {
2288
3020
  const db = this.db(tx);
@@ -2304,7 +3036,34 @@ var PrismaBundleRepository = class {
2304
3036
  id: bundleId
2305
3037
  }
2306
3038
  });
2307
- return toBundleVersionRow(row, bundle);
3039
+ return toBundleVersionRow(row, bundle, this.validityWindows);
3040
+ }
3041
+ async findActiveBundleVersionWithValidity(bundleId, asOf, tx) {
3042
+ const db = this.db(tx);
3043
+ const row = await db.bundleVersion.findFirst({
3044
+ where: {
3045
+ bundleId,
3046
+ ...(0, import_types5.buildActiveVersionWhere)(asOf)
3047
+ },
3048
+ orderBy: [
3049
+ {
3050
+ validFrom: {
3051
+ sort: "desc",
3052
+ nulls: "last"
3053
+ }
3054
+ },
3055
+ {
3056
+ version: "desc"
3057
+ }
3058
+ ]
3059
+ });
3060
+ if (!row) return null;
3061
+ const bundle = await db.bundle.findUnique({
3062
+ where: {
3063
+ id: bundleId
3064
+ }
3065
+ });
3066
+ return toBundleVersionRow(row, bundle, true);
2308
3067
  }
2309
3068
  async createDraft(data) {
2310
3069
  const db = this.db();
@@ -2339,7 +3098,11 @@ var PrismaBundleRepository = class {
2339
3098
  yearlyNet: data.yearlyNet ?? null,
2340
3099
  marketed: data.marketed ?? true,
2341
3100
  changeNote: data.changeNote ?? "",
2342
- createdByUserId: data.createdByUserId ?? null
3101
+ createdByUserId: data.createdByUserId ?? null,
3102
+ ...this.validityWindows ? {
3103
+ validFrom: toNullableDate(data.validFrom),
3104
+ validUntil: toNullableDate(data.validUntil)
3105
+ } : {}
2343
3106
  }
2344
3107
  });
2345
3108
  const bundle = await db.bundle.findUnique({
@@ -2347,7 +3110,7 @@ var PrismaBundleRepository = class {
2347
3110
  id: data.bundleId
2348
3111
  }
2349
3112
  });
2350
- return toBundleVersionRow(created, bundle);
3113
+ return toBundleVersionRow(created, bundle, this.validityWindows);
2351
3114
  }
2352
3115
  async updateDraft(versionId, data) {
2353
3116
  const db = this.db();
@@ -2379,6 +3142,12 @@ var PrismaBundleRepository = class {
2379
3142
  } : {},
2380
3143
  ...data.changeNote !== void 0 ? {
2381
3144
  changeNote: data.changeNote
3145
+ } : {},
3146
+ ...this.validityWindows && data.validFrom !== void 0 ? {
3147
+ validFrom: toNullableDate(data.validFrom)
3148
+ } : {},
3149
+ ...this.validityWindows && data.validUntil !== void 0 ? {
3150
+ validUntil: toNullableDate(data.validUntil)
2382
3151
  } : {}
2383
3152
  }
2384
3153
  });
@@ -2387,9 +3156,15 @@ var PrismaBundleRepository = class {
2387
3156
  id: updated.bundleId
2388
3157
  }
2389
3158
  });
2390
- return toBundleVersionRow(updated, bundle);
3159
+ return toBundleVersionRow(updated, bundle, this.validityWindows);
2391
3160
  }
2392
3161
  async publishDraft(versionId, publishMeta, tx) {
3162
+ if (this.validityWindows && tx === void 0) {
3163
+ return this.transaction((transaction) => this.publishDraftWithValidity(transaction, versionId, publishMeta));
3164
+ }
3165
+ if (this.validityWindows) {
3166
+ return this.publishDraftWithValidity(this.db(tx), versionId, publishMeta);
3167
+ }
2393
3168
  const db = this.db(tx);
2394
3169
  const draft = await db.bundleVersion.findUnique({
2395
3170
  where: {
@@ -2430,7 +3205,53 @@ var PrismaBundleRepository = class {
2430
3205
  id: published.bundleId
2431
3206
  }
2432
3207
  });
2433
- return toBundleVersionRow(published, bundle);
3208
+ return toBundleVersionRow(published, bundle, false);
3209
+ }
3210
+ async publishDraftWithValidity(db, versionId, publishMeta) {
3211
+ const draft = await db.bundleVersion.findUnique({
3212
+ where: {
3213
+ id: versionId
3214
+ }
3215
+ });
3216
+ if (!draft) {
3217
+ throw new Error(`BundleVersion '${versionId}' not found.`);
3218
+ }
3219
+ const now = /* @__PURE__ */ new Date();
3220
+ await db.bundleVersion.updateMany({
3221
+ where: {
3222
+ bundleId: draft.bundleId,
3223
+ publishedAt: {
3224
+ not: null
3225
+ },
3226
+ supersededAt: null,
3227
+ NOT: {
3228
+ id: versionId
3229
+ }
3230
+ },
3231
+ data: {
3232
+ supersededAt: now,
3233
+ validUntil: previousUtcDay(publishMeta.validFrom)
3234
+ }
3235
+ });
3236
+ const published = await db.bundleVersion.update({
3237
+ where: {
3238
+ id: versionId
3239
+ },
3240
+ data: {
3241
+ publishedAt: now,
3242
+ publishedByUserId: publishMeta.publishedByUserId,
3243
+ publishedChanges: publishMeta.publishedChanges,
3244
+ nonRegressive: publishMeta.nonRegressive,
3245
+ validFrom: publishMeta.validFrom,
3246
+ validUntil: publishMeta.validUntil
3247
+ }
3248
+ });
3249
+ const bundle = await db.bundle.findUnique({
3250
+ where: {
3251
+ id: published.bundleId
3252
+ }
3253
+ });
3254
+ return toBundleVersionRow(published, bundle, true);
2434
3255
  }
2435
3256
  async deleteDraft(versionId) {
2436
3257
  const db = this.db();
@@ -2458,9 +3279,12 @@ var PrismaBundleRepository = class {
2458
3279
  PrismaBundleRepository = _ts_decorate20([
2459
3280
  (0, import_common20.Injectable)(),
2460
3281
  _ts_param18(0, (0, import_common20.Inject)(PRISMA_CLIENT_TOKEN)),
3282
+ _ts_param18(1, (0, import_common20.Optional)()),
3283
+ _ts_param18(1, (0, import_common20.Inject)(PRISMA_BUNDLE_REPOSITORY_OPTIONS)),
2461
3284
  _ts_metadata18("design:type", Function),
2462
3285
  _ts_metadata18("design:paramtypes", [
2463
- typeof PrismaLike === "undefined" ? Object : PrismaLike
3286
+ typeof BundlePrismaClient === "undefined" ? Object : BundlePrismaClient,
3287
+ typeof PrismaBundleRepositoryOptions === "undefined" ? Object : PrismaBundleRepositoryOptions
2464
3288
  ])
2465
3289
  ], PrismaBundleRepository);
2466
3290
  function isPlainObject(value) {
@@ -2471,6 +3295,16 @@ function toDecimalString(value) {
2471
3295
  return value == null ? null : value.toString();
2472
3296
  }
2473
3297
  __name(toDecimalString, "toDecimalString");
3298
+ function toNullableDate(value) {
3299
+ return value ? new Date(value) : null;
3300
+ }
3301
+ __name(toNullableDate, "toNullableDate");
3302
+ function previousUtcDay(value) {
3303
+ const result = new Date(value);
3304
+ result.setUTCDate(result.getUTCDate() - 1);
3305
+ return result;
3306
+ }
3307
+ __name(previousUtcDay, "previousUtcDay");
2474
3308
  function toVersionChanges(value) {
2475
3309
  return Array.isArray(value) ? value : null;
2476
3310
  }
@@ -2503,7 +3337,7 @@ function toBundleRow(row) {
2503
3337
  };
2504
3338
  }
2505
3339
  __name(toBundleRow, "toBundleRow");
2506
- function toBundleVersionRow(row, bundle) {
3340
+ function toBundleVersionRow(row, bundle, validityWindows) {
2507
3341
  return {
2508
3342
  id: row.id,
2509
3343
  bundleId: row.bundleId,
@@ -2520,10 +3354,8 @@ function toBundleVersionRow(row, bundle) {
2520
3354
  marketed: row.marketed,
2521
3355
  publishedAt: row.publishedAt?.toISOString() ?? null,
2522
3356
  supersededAt: row.supersededAt?.toISOString() ?? null,
2523
- // The canonical `bundle_versions` table has no validFrom/validUntil
2524
- // columns; time-aware validity is not expressible on this schema.
2525
- validFrom: null,
2526
- validUntil: null,
3357
+ validFrom: validityWindows && row.validFrom instanceof Date ? row.validFrom.toISOString() : null,
3358
+ validUntil: validityWindows && row.validUntil instanceof Date ? row.validUntil.toISOString() : null,
2527
3359
  publishedChanges: toVersionChanges(row.publishedChanges),
2528
3360
  changeNote: row.changeNote,
2529
3361
  nonRegressive: row.nonRegressive,
@@ -2925,7 +3757,7 @@ PrismaCatalogEntryRepository = _ts_decorate21([
2925
3757
  _ts_param19(0, (0, import_common21.Inject)(PRISMA_CLIENT_TOKEN)),
2926
3758
  _ts_metadata19("design:type", Function),
2927
3759
  _ts_metadata19("design:paramtypes", [
2928
- typeof PrismaLike === "undefined" ? Object : PrismaLike
3760
+ typeof CatalogEntryRepositoryClient === "undefined" ? Object : CatalogEntryRepositoryClient
2929
3761
  ])
2930
3762
  ], PrismaCatalogEntryRepository);
2931
3763
  function toI18n2(value) {
@@ -3166,7 +3998,7 @@ PrismaMarketingProjectionRepository = _ts_decorate22([
3166
3998
  _ts_param20(0, (0, import_common22.Inject)(PRISMA_CLIENT_TOKEN)),
3167
3999
  _ts_metadata20("design:type", Function),
3168
4000
  _ts_metadata20("design:paramtypes", [
3169
- typeof PrismaLike === "undefined" ? Object : PrismaLike
4001
+ typeof MarketingProjectionRepositoryClient === "undefined" ? Object : MarketingProjectionRepositoryClient
3170
4002
  ])
3171
4003
  ], PrismaMarketingProjectionRepository);
3172
4004
  function toTopFeatures(value) {
@@ -3256,7 +4088,7 @@ PrismaMarketingSettingsRepository = _ts_decorate23([
3256
4088
  _ts_param21(0, (0, import_common23.Inject)(PRISMA_CLIENT_TOKEN)),
3257
4089
  _ts_metadata21("design:type", Function),
3258
4090
  _ts_metadata21("design:paramtypes", [
3259
- typeof PrismaLike === "undefined" ? Object : PrismaLike
4091
+ typeof MarketingSettingsRepositoryClient === "undefined" ? Object : MarketingSettingsRepositoryClient
3260
4092
  ])
3261
4093
  ], PrismaMarketingSettingsRepository);
3262
4094
  function toRow2(row) {
@@ -3347,7 +4179,8 @@ var PrismaPromotionRepository = class {
3347
4179
  }
3348
4180
  async update(id, data) {
3349
4181
  if (data.onlyLocales === null) {
3350
- await this.prisma.$executeRaw`
4182
+ const executeRaw = this.prisma.$executeRaw.bind(this.prisma);
4183
+ await executeRaw`
3351
4184
  UPDATE promotions SET "onlyLocales" = NULL, "updatedAt" = NOW() WHERE id = ${id}`;
3352
4185
  }
3353
4186
  const row = await this.db.promotion.update({
@@ -3414,7 +4247,7 @@ PrismaPromotionRepository = _ts_decorate24([
3414
4247
  _ts_param22(0, (0, import_common24.Inject)(PRISMA_CLIENT_TOKEN)),
3415
4248
  _ts_metadata22("design:type", Function),
3416
4249
  _ts_metadata22("design:paramtypes", [
3417
- typeof PrismaLike === "undefined" ? Object : PrismaLike
4250
+ typeof PromotionRepositoryClient === "undefined" ? Object : PromotionRepositoryClient
3418
4251
  ])
3419
4252
  ], PrismaPromotionRepository);
3420
4253
  function toPromotionValue(value) {
@@ -3723,7 +4556,9 @@ __name(toRecord4, "toRecord");
3723
4556
  0 && (module.exports = {
3724
4557
  AsyncLocalRlsBypassAdapter,
3725
4558
  PASSWORD_HASHER_TOKEN,
4559
+ PRISMA_BUNDLE_REPOSITORY_OPTIONS,
3726
4560
  PRISMA_CLIENT_TOKEN,
4561
+ PRISMA_SCHEMA_OPTIONS_TOKEN,
3727
4562
  PrismaAuditAdapter,
3728
4563
  PrismaAuditQueryAdapter,
3729
4564
  PrismaAuditStatsAdapter,
@@ -3749,5 +4584,8 @@ __name(toRecord4, "toRecord");
3749
4584
  PrismaTransactionRunner,
3750
4585
  ZeroPromoRevenueDeductionAggregator,
3751
4586
  buildActorTag,
3752
- prismaPersistence
4587
+ createPrismaPlanBindingResolver,
4588
+ getPrismaDelegate,
4589
+ prismaPersistence,
4590
+ resolvePrismaSchemaOptions
3753
4591
  });