@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.js CHANGED
@@ -86,7 +86,7 @@ PrismaAuditAdapter = _ts_decorate2([
86
86
  _ts_param(0, Inject(PRISMA_CLIENT_TOKEN)),
87
87
  _ts_metadata("design:type", Function),
88
88
  _ts_metadata("design:paramtypes", [
89
- typeof PrismaLike === "undefined" ? Object : PrismaLike
89
+ typeof Pick === "undefined" ? Object : Pick
90
90
  ])
91
91
  ], PrismaAuditAdapter);
92
92
 
@@ -149,7 +149,7 @@ PrismaAuditQueryAdapter = _ts_decorate3([
149
149
  _ts_param2(0, Inject2(PRISMA_CLIENT_TOKEN)),
150
150
  _ts_metadata2("design:type", Function),
151
151
  _ts_metadata2("design:paramtypes", [
152
- typeof PrismaLike === "undefined" ? Object : PrismaLike
152
+ typeof Pick === "undefined" ? Object : Pick
153
153
  ])
154
154
  ], PrismaAuditQueryAdapter);
155
155
  function toActorTagFilter(actorTag) {
@@ -221,7 +221,7 @@ PrismaAuditStatsAdapter = _ts_decorate4([
221
221
  _ts_param3(0, Inject3(PRISMA_CLIENT_TOKEN)),
222
222
  _ts_metadata3("design:type", Function),
223
223
  _ts_metadata3("design:paramtypes", [
224
- typeof PrismaLike === "undefined" ? Object : PrismaLike
224
+ typeof Pick === "undefined" ? Object : Pick
225
225
  ])
226
226
  ], PrismaAuditStatsAdapter);
227
227
 
@@ -291,12 +291,149 @@ PrismaMfaAdapter = _ts_decorate5([
291
291
  _ts_param4(0, Inject4(PRISMA_CLIENT_TOKEN)),
292
292
  _ts_metadata4("design:type", Function),
293
293
  _ts_metadata4("design:paramtypes", [
294
- typeof PrismaLike === "undefined" ? Object : PrismaLike
294
+ typeof Pick === "undefined" ? Object : Pick
295
295
  ])
296
296
  ], PrismaMfaAdapter);
297
297
 
298
298
  // src/prisma-plan-catalog-import-sink.ts
299
- import { Inject as Inject5, Injectable as Injectable6 } from "@nestjs/common";
299
+ import { Inject as Inject5, Injectable as Injectable6, Optional } from "@nestjs/common";
300
+
301
+ // src/prisma-plan-binding.ts
302
+ var PRISMA_SCHEMA_OPTIONS_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/adapter-prisma/PrismaSchemaOptions");
303
+ var DEFAULT_SCHEMA_OPTIONS = {
304
+ planBinding: {
305
+ mode: "legacy-plan-key"
306
+ },
307
+ delegates: {
308
+ catalogPlanVersion: "planVersion",
309
+ entitlementPlanVersion: "planVersion"
310
+ },
311
+ planVersionFields: {
312
+ catalog: {
313
+ validityWindows: false,
314
+ endsAt: false
315
+ },
316
+ entitlement: {
317
+ validityWindows: false,
318
+ endsAt: false
319
+ }
320
+ },
321
+ tenantSubscription: {
322
+ delegate: "subscription",
323
+ subscriptionBundleDelegate: false,
324
+ synchronizePlanVersion: false,
325
+ atomicOnboardingSelection: false,
326
+ activeVersionSelection: "latest-live",
327
+ withEndsAt: false
328
+ }
329
+ };
330
+ function resolvePrismaSchemaOptions(options) {
331
+ const mode = options?.planBinding?.mode ?? "legacy-plan-key";
332
+ const projectKey = options?.planBinding?.projectKey;
333
+ if (mode === "normalized-plan-id" && !projectKey?.trim()) {
334
+ throw new Error("Prisma plan binding mode 'normalized-plan-id' requires a non-empty projectKey.");
335
+ }
336
+ const sharedFields = options?.planVersionFields;
337
+ const catalogFields = sharedFields?.catalog;
338
+ const entitlementFields = sharedFields?.entitlement;
339
+ return {
340
+ planBinding: {
341
+ mode,
342
+ ...projectKey ? {
343
+ projectKey
344
+ } : {}
345
+ },
346
+ delegates: {
347
+ catalogPlanVersion: options?.delegates?.catalogPlanVersion ?? DEFAULT_SCHEMA_OPTIONS.delegates.catalogPlanVersion,
348
+ entitlementPlanVersion: options?.delegates?.entitlementPlanVersion ?? DEFAULT_SCHEMA_OPTIONS.delegates.entitlementPlanVersion
349
+ },
350
+ planVersionFields: {
351
+ catalog: {
352
+ validityWindows: catalogFields?.validityWindows ?? sharedFields?.validityWindows ?? false,
353
+ endsAt: catalogFields?.endsAt ?? sharedFields?.endsAt ?? false
354
+ },
355
+ entitlement: {
356
+ validityWindows: entitlementFields?.validityWindows ?? sharedFields?.validityWindows ?? false,
357
+ endsAt: entitlementFields?.endsAt ?? sharedFields?.endsAt ?? false
358
+ }
359
+ },
360
+ tenantSubscription: {
361
+ delegate: options?.tenantSubscription?.delegate ?? DEFAULT_SCHEMA_OPTIONS.tenantSubscription.delegate,
362
+ subscriptionBundleDelegate: options?.tenantSubscription?.subscriptionBundleDelegate ?? DEFAULT_SCHEMA_OPTIONS.tenantSubscription.subscriptionBundleDelegate,
363
+ synchronizePlanVersion: options?.tenantSubscription?.synchronizePlanVersion ?? DEFAULT_SCHEMA_OPTIONS.tenantSubscription.synchronizePlanVersion,
364
+ atomicOnboardingSelection: options?.tenantSubscription?.atomicOnboardingSelection ?? DEFAULT_SCHEMA_OPTIONS.tenantSubscription.atomicOnboardingSelection,
365
+ activeVersionSelection: options?.tenantSubscription?.activeVersionSelection ?? DEFAULT_SCHEMA_OPTIONS.tenantSubscription.activeVersionSelection,
366
+ withEndsAt: options?.tenantSubscription?.withEndsAt ?? DEFAULT_SCHEMA_OPTIONS.tenantSubscription.withEndsAt
367
+ }
368
+ };
369
+ }
370
+ __name(resolvePrismaSchemaOptions, "resolvePrismaSchemaOptions");
371
+ function createPrismaPlanBindingResolver(options) {
372
+ const resolved = resolvePrismaSchemaOptions({
373
+ planBinding: options
374
+ }).planBinding;
375
+ return {
376
+ mode: resolved.mode,
377
+ projectKey: resolved.projectKey,
378
+ async toStoragePlanId(client, planKey, projectKey) {
379
+ if (resolved.mode === "legacy-plan-key") return planKey;
380
+ const scope = resolveProjectKey(resolved.projectKey, projectKey);
381
+ const plan = await asPlanIdentityClient(client).plan.findFirst({
382
+ where: {
383
+ projectKey: scope,
384
+ planKey,
385
+ deletedAt: null
386
+ }
387
+ });
388
+ if (!plan) {
389
+ throw new Error(`Plan '${planKey}' not found in project '${scope}'.`);
390
+ }
391
+ return plan.id;
392
+ },
393
+ async toPlanKey(client, storedPlanId, projectKey) {
394
+ if (resolved.mode === "legacy-plan-key") return storedPlanId;
395
+ const scope = resolveProjectKey(resolved.projectKey, projectKey);
396
+ const plan = await asPlanIdentityClient(client).plan.findUnique({
397
+ where: {
398
+ id: storedPlanId
399
+ }
400
+ });
401
+ if (!plan || plan.projectKey !== scope) {
402
+ throw new Error(`Plan id '${storedPlanId}' not found in project '${scope}'.`);
403
+ }
404
+ return plan.planKey;
405
+ }
406
+ };
407
+ }
408
+ __name(createPrismaPlanBindingResolver, "createPrismaPlanBindingResolver");
409
+ function getPrismaDelegate(client, delegateName) {
410
+ const delegate = client?.[delegateName];
411
+ if (!delegate || typeof delegate !== "object") {
412
+ throw new Error(`Prisma client has no '${delegateName}' delegate.`);
413
+ }
414
+ return delegate;
415
+ }
416
+ __name(getPrismaDelegate, "getPrismaDelegate");
417
+ function resolveProjectKey(configured, requested) {
418
+ const projectKey = requested ?? configured;
419
+ if (!projectKey) {
420
+ throw new Error("Prisma plan binding mode 'normalized-plan-id' requires a projectKey.");
421
+ }
422
+ if (configured && requested && configured !== requested) {
423
+ throw new Error(`Prisma plan binding is configured for project '${configured}', not '${requested}'.`);
424
+ }
425
+ return projectKey;
426
+ }
427
+ __name(resolveProjectKey, "resolveProjectKey");
428
+ function asPlanIdentityClient(client) {
429
+ if (!client || typeof client !== "object" || !("plan" in client)) {
430
+ throw new Error("Prisma client has no 'plan' delegate.");
431
+ }
432
+ return client;
433
+ }
434
+ __name(asPlanIdentityClient, "asPlanIdentityClient");
435
+
436
+ // src/prisma-plan-catalog-import-sink.ts
300
437
  function _ts_decorate6(decorators, target, key, desc) {
301
438
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
302
439
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -319,11 +456,23 @@ var PrismaPlanCatalogImportSink = class {
319
456
  __name(this, "PrismaPlanCatalogImportSink");
320
457
  }
321
458
  prisma;
322
- constructor(prisma) {
459
+ binding;
460
+ delegateName;
461
+ constructor(prisma, options) {
323
462
  this.prisma = prisma;
463
+ const schema = resolvePrismaSchemaOptions(options);
464
+ this.binding = createPrismaPlanBindingResolver(options?.planBinding);
465
+ this.delegateName = schema.delegates.catalogPlanVersion;
466
+ }
467
+ db() {
468
+ return this.prisma;
324
469
  }
325
470
  async upsertPlan(input) {
326
- const existing = await this.prisma.plan.findFirst({
471
+ if (this.binding.mode === "normalized-plan-id" && this.binding.projectKey !== input.projectKey) {
472
+ throw new Error(`Prisma plan binding is configured for project '${this.binding.projectKey}', not '${input.projectKey}'.`);
473
+ }
474
+ const db = this.db();
475
+ const existing = await db.plan.findFirst({
327
476
  where: {
328
477
  projectKey: input.projectKey,
329
478
  planKey: input.planKey
@@ -333,7 +482,7 @@ var PrismaPlanCatalogImportSink = class {
333
482
  created: false,
334
483
  skipReason: "exists"
335
484
  };
336
- await this.prisma.plan.create({
485
+ await db.plan.create({
337
486
  data: {
338
487
  projectKey: input.projectKey,
339
488
  planKey: input.planKey,
@@ -347,9 +496,11 @@ var PrismaPlanCatalogImportSink = class {
347
496
  };
348
497
  }
349
498
  async upsertPlanVersion(input) {
350
- const existing = await this.prisma.planVersion.findFirst({
499
+ const planVersion = this.planVersions();
500
+ const storedPlanId = await this.binding.toStoragePlanId(this.prisma, input.planKey);
501
+ const existing = await planVersion.findFirst({
351
502
  where: {
352
- planId: input.planKey,
503
+ planId: storedPlanId,
353
504
  version: input.version
354
505
  }
355
506
  });
@@ -359,9 +510,9 @@ var PrismaPlanCatalogImportSink = class {
359
510
  };
360
511
  const now = /* @__PURE__ */ new Date();
361
512
  if (input.publish) {
362
- await this.prisma.planVersion.updateMany({
513
+ await planVersion.updateMany({
363
514
  where: {
364
- planId: input.planKey,
515
+ planId: storedPlanId,
365
516
  publishedAt: {
366
517
  not: null
367
518
  },
@@ -375,9 +526,9 @@ var PrismaPlanCatalogImportSink = class {
375
526
  }
376
527
  });
377
528
  }
378
- await this.prisma.planVersion.create({
529
+ await planVersion.create({
379
530
  data: {
380
- planId: input.planKey,
531
+ planId: storedPlanId,
381
532
  version: input.version,
382
533
  features: input.features,
383
534
  quotas: input.quotas,
@@ -392,8 +543,12 @@ var PrismaPlanCatalogImportSink = class {
392
543
  created: true
393
544
  };
394
545
  }
546
+ planVersions() {
547
+ return getPrismaDelegate(this.prisma, this.delegateName);
548
+ }
395
549
  async upsertFeatureCatalogEntry(input) {
396
- const existing = await this.prisma.featureCatalogEntry.findFirst({
550
+ const db = this.db();
551
+ const existing = await db.featureCatalogEntry.findFirst({
397
552
  where: {
398
553
  projectKey: input.projectKey,
399
554
  featureKey: input.featureKey
@@ -403,7 +558,7 @@ var PrismaPlanCatalogImportSink = class {
403
558
  created: false,
404
559
  skipReason: "exists"
405
560
  };
406
- await this.prisma.featureCatalogEntry.create({
561
+ await db.featureCatalogEntry.create({
407
562
  data: {
408
563
  projectKey: input.projectKey,
409
564
  featureKey: input.featureKey,
@@ -422,14 +577,17 @@ var PrismaPlanCatalogImportSink = class {
422
577
  PrismaPlanCatalogImportSink = _ts_decorate6([
423
578
  Injectable6(),
424
579
  _ts_param5(0, Inject5(PRISMA_CLIENT_TOKEN)),
580
+ _ts_param5(1, Optional()),
581
+ _ts_param5(1, Inject5(PRISMA_SCHEMA_OPTIONS_TOKEN)),
425
582
  _ts_metadata5("design:type", Function),
426
583
  _ts_metadata5("design:paramtypes", [
427
- typeof PrismaLike === "undefined" ? Object : PrismaLike
584
+ typeof PlanCatalogImportClient === "undefined" ? Object : PlanCatalogImportClient,
585
+ typeof PrismaSchemaOptions === "undefined" ? Object : PrismaSchemaOptions
428
586
  ])
429
587
  ], PrismaPlanCatalogImportSink);
430
588
 
431
589
  // src/prisma-plan-catalog-read-sink.ts
432
- import { Inject as Inject6, Injectable as Injectable7 } from "@nestjs/common";
590
+ import { Inject as Inject6, Injectable as Injectable7, Optional as Optional2 } from "@nestjs/common";
433
591
 
434
592
  // src/tx.ts
435
593
  function resolveClient(client, tx) {
@@ -471,11 +629,25 @@ var PrismaPlanCatalogReadSink = class {
471
629
  __name(this, "PrismaPlanCatalogReadSink");
472
630
  }
473
631
  prisma;
474
- constructor(prisma) {
632
+ binding;
633
+ delegateName;
634
+ fields;
635
+ constructor(prisma, options) {
475
636
  this.prisma = prisma;
637
+ const schema = resolvePrismaSchemaOptions(options);
638
+ this.binding = createPrismaPlanBindingResolver(options?.planBinding);
639
+ this.delegateName = schema.delegates.catalogPlanVersion;
640
+ this.fields = schema.planVersionFields.catalog;
641
+ }
642
+ db() {
643
+ return this.prisma;
476
644
  }
477
645
  async loadSnapshot(projectKey) {
478
- const plans = await this.prisma.plan.findMany({
646
+ if (this.binding.mode === "normalized-plan-id" && this.binding.projectKey !== projectKey) {
647
+ throw new Error(`Prisma plan binding is configured for project '${this.binding.projectKey}', not '${projectKey}'.`);
648
+ }
649
+ const db = this.db();
650
+ const plans = await db.plan.findMany({
479
651
  where: {
480
652
  projectKey,
481
653
  deletedAt: null
@@ -484,11 +656,17 @@ var PrismaPlanCatalogReadSink = class {
484
656
  sortOrder: "asc"
485
657
  }
486
658
  });
487
- const planKeys = plans.map((plan) => plan.planKey);
488
- const livePlanVersions = planKeys.length === 0 ? [] : await this.prisma.planVersion.findMany({
659
+ const planKeysByStoredId = new Map(plans.map((plan) => [
660
+ this.binding.mode === "normalized-plan-id" ? plan.id : plan.planKey,
661
+ plan.planKey
662
+ ]));
663
+ const storedPlanIds = [
664
+ ...planKeysByStoredId.keys()
665
+ ];
666
+ const livePlanVersions = storedPlanIds.length === 0 ? [] : await this.planVersions().findMany({
489
667
  where: {
490
668
  planId: {
491
- in: planKeys
669
+ in: storedPlanIds
492
670
  },
493
671
  publishedAt: {
494
672
  not: null
@@ -496,7 +674,7 @@ var PrismaPlanCatalogReadSink = class {
496
674
  supersededAt: null
497
675
  }
498
676
  });
499
- const featureEntries = await this.prisma.featureCatalogEntry.findMany({
677
+ const featureEntries = await db.featureCatalogEntry.findMany({
500
678
  where: {
501
679
  projectKey,
502
680
  deletedAt: null
@@ -507,17 +685,29 @@ var PrismaPlanCatalogReadSink = class {
507
685
  });
508
686
  return {
509
687
  plans: plans.map(toPlanRow),
510
- livePlanVersions: livePlanVersions.map(toPlanVersionRow),
688
+ livePlanVersions: livePlanVersions.map((row) => {
689
+ const planKey = planKeysByStoredId.get(row.planId);
690
+ if (!planKey) {
691
+ throw new Error(`PlanVersion ${row.id} references plan '${row.planId}' outside project '${projectKey}'.`);
692
+ }
693
+ return toPlanVersionRow(row, planKey, this.fields);
694
+ }),
511
695
  featureEntries: featureEntries.map(toFeatureCatalogEntryRow)
512
696
  };
513
697
  }
698
+ planVersions() {
699
+ return getPrismaDelegate(this.prisma, this.delegateName);
700
+ }
514
701
  };
515
702
  PrismaPlanCatalogReadSink = _ts_decorate7([
516
703
  Injectable7(),
517
704
  _ts_param6(0, Inject6(PRISMA_CLIENT_TOKEN)),
705
+ _ts_param6(1, Optional2()),
706
+ _ts_param6(1, Inject6(PRISMA_SCHEMA_OPTIONS_TOKEN)),
518
707
  _ts_metadata6("design:type", Function),
519
708
  _ts_metadata6("design:paramtypes", [
520
- typeof PrismaLike === "undefined" ? Object : PrismaLike
709
+ typeof PlanCatalogReadClient === "undefined" ? Object : PlanCatalogReadClient,
710
+ typeof PrismaSchemaOptions === "undefined" ? Object : PrismaSchemaOptions
521
711
  ])
522
712
  ], PrismaPlanCatalogReadSink);
523
713
  function toPlanRow(row) {
@@ -535,10 +725,10 @@ function toPlanRow(row) {
535
725
  };
536
726
  }
537
727
  __name(toPlanRow, "toPlanRow");
538
- function toPlanVersionRow(row) {
539
- return {
728
+ function toPlanVersionRow(row, planKey, fields) {
729
+ const mapped = {
540
730
  id: row.id,
541
- planId: row.planId,
731
+ planId: planKey,
542
732
  version: row.version,
543
733
  baseVersionId: row.baseVersionId,
544
734
  publishedAt: row.publishedAt ? row.publishedAt.toISOString() : null,
@@ -546,8 +736,8 @@ function toPlanVersionRow(row) {
546
736
  publishedChanges: row.publishedChanges ?? null,
547
737
  changeNote: row.changeNote,
548
738
  nonRegressive: row.nonRegressive,
549
- validFrom: null,
550
- validUntil: null,
739
+ validFrom: fields.validityWindows && row.validFrom ? row.validFrom.toISOString() : null,
740
+ validUntil: fields.validityWindows && row.validUntil ? row.validUntil.toISOString() : null,
551
741
  createdByUserId: row.createdByUserId,
552
742
  publishedByUserId: row.publishedByUserId,
553
743
  createdAt: row.createdAt.toISOString(),
@@ -558,6 +748,10 @@ function toPlanVersionRow(row) {
558
748
  yearlyNet: String(row.yearlyNet),
559
749
  marketed: row.marketed
560
750
  };
751
+ if (fields.endsAt) {
752
+ mapped.endsAt = row.endsAt?.toISOString() ?? null;
753
+ }
754
+ return mapped;
561
755
  }
562
756
  __name(toPlanVersionRow, "toPlanVersionRow");
563
757
  function toFeatureCatalogEntryRow(row) {
@@ -590,7 +784,8 @@ function toFeatureCatalogEntryRow(row) {
590
784
  __name(toFeatureCatalogEntryRow, "toFeatureCatalogEntryRow");
591
785
 
592
786
  // src/prisma-plan-version.repository.ts
593
- import { Inject as Inject7, Injectable as Injectable8 } from "@nestjs/common";
787
+ import { Inject as Inject7, Injectable as Injectable8, Optional as Optional3 } from "@nestjs/common";
788
+ import { buildActivePlanVersionWhere } from "@saasicat/types";
594
789
  function _ts_decorate8(decorators, target, key, desc) {
595
790
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
596
791
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -613,14 +808,29 @@ var PrismaPlanVersionRepository = class {
613
808
  __name(this, "PrismaPlanVersionRepository");
614
809
  }
615
810
  prisma;
616
- constructor(prisma) {
811
+ findActive;
812
+ binding;
813
+ delegateName;
814
+ fields;
815
+ constructor(prisma, options) {
617
816
  this.prisma = prisma;
817
+ const schema = resolvePrismaSchemaOptions(options);
818
+ this.binding = createPrismaPlanBindingResolver(options?.planBinding);
819
+ this.delegateName = schema.delegates.entitlementPlanVersion;
820
+ this.fields = schema.planVersionFields.entitlement;
821
+ if (this.fields.validityWindows) {
822
+ this.findActive = (planId, asOf = /* @__PURE__ */ new Date(), tx) => this.findActivePlanVersion(planId, asOf, tx);
823
+ }
824
+ }
825
+ db(tx) {
826
+ return tx ?? this.prisma;
618
827
  }
619
828
  async findLatestLive(planId, tx) {
620
- const db = resolveClient(this.prisma, tx);
621
- const row = await db.planVersion.findFirst({
829
+ const db = this.db(tx);
830
+ const storedPlanId = await this.binding.toStoragePlanId(db, planId);
831
+ const row = await this.versions(db).findFirst({
622
832
  where: {
623
- planId,
833
+ planId: storedPlanId,
624
834
  publishedAt: {
625
835
  not: null
626
836
  },
@@ -631,8 +841,40 @@ var PrismaPlanVersionRepository = class {
631
841
  }
632
842
  });
633
843
  if (!row) return null;
844
+ return this.toRecord(db, row);
845
+ }
846
+ async findActivePlanVersion(planId, asOf, tx) {
847
+ const db = this.db(tx);
848
+ const storedPlanId = await this.binding.toStoragePlanId(db, planId);
849
+ const activeWhere = this.fields.endsAt ? buildActivePlanVersionWhere(asOf, {
850
+ withEndsAt: true
851
+ }) : buildActivePlanVersionWhere(asOf);
852
+ const row = await this.versions(db).findFirst({
853
+ where: {
854
+ planId: storedPlanId,
855
+ ...activeWhere
856
+ },
857
+ orderBy: [
858
+ {
859
+ validFrom: {
860
+ sort: "desc",
861
+ nulls: "last"
862
+ }
863
+ },
864
+ {
865
+ version: "desc"
866
+ }
867
+ ]
868
+ });
869
+ if (!row) return null;
870
+ return this.toRecord(db, row);
871
+ }
872
+ versions(client) {
873
+ return getPrismaDelegate(client, this.delegateName);
874
+ }
875
+ async toRecord(client, row) {
634
876
  return {
635
- planId: row.planId,
877
+ planId: await this.binding.toPlanKey(client, row.planId),
636
878
  quotas: toQuotaMap(row.quotas),
637
879
  features: toStringArray(row.features)
638
880
  };
@@ -641,9 +883,12 @@ var PrismaPlanVersionRepository = class {
641
883
  PrismaPlanVersionRepository = _ts_decorate8([
642
884
  Injectable8(),
643
885
  _ts_param7(0, Inject7(PRISMA_CLIENT_TOKEN)),
886
+ _ts_param7(1, Optional3()),
887
+ _ts_param7(1, Inject7(PRISMA_SCHEMA_OPTIONS_TOKEN)),
644
888
  _ts_metadata7("design:type", Function),
645
889
  _ts_metadata7("design:paramtypes", [
646
- typeof PrismaLike === "undefined" ? Object : PrismaLike
890
+ typeof PlanVersionRepositoryClient === "undefined" ? Object : PlanVersionRepositoryClient,
891
+ typeof PrismaSchemaOptions === "undefined" ? Object : PrismaSchemaOptions
647
892
  ])
648
893
  ], PrismaPlanVersionRepository);
649
894
 
@@ -1084,7 +1329,7 @@ PrismaPromoSubscriptionLookup = _ts_decorate12([
1084
1329
  ], PrismaPromoSubscriptionLookup);
1085
1330
 
1086
1331
  // src/prisma-subscription.repository.ts
1087
- import { Inject as Inject12, Injectable as Injectable13 } from "@nestjs/common";
1332
+ import { Inject as Inject12, Injectable as Injectable13, Optional as Optional4 } from "@nestjs/common";
1088
1333
  function _ts_decorate13(decorators, target, key, desc) {
1089
1334
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1090
1335
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1111,19 +1356,35 @@ var PrismaSubscriptionRepository = class {
1111
1356
  __name(this, "PrismaSubscriptionRepository");
1112
1357
  }
1113
1358
  prisma;
1114
- constructor(prisma) {
1359
+ countByBundleVersionId;
1360
+ binding;
1361
+ planVersionDelegateName;
1362
+ subscriptionDelegateName;
1363
+ subscriptionBundleDelegateName;
1364
+ constructor(prisma, options) {
1115
1365
  this.prisma = prisma;
1366
+ const schema = resolvePrismaSchemaOptions(options);
1367
+ this.binding = createPrismaPlanBindingResolver(options?.planBinding);
1368
+ this.planVersionDelegateName = schema.delegates.entitlementPlanVersion;
1369
+ this.subscriptionDelegateName = schema.tenantSubscription.delegate;
1370
+ this.subscriptionBundleDelegateName = schema.tenantSubscription.subscriptionBundleDelegate;
1371
+ if (this.subscriptionBundleDelegateName) {
1372
+ this.countByBundleVersionId = (bundleVersionId) => this.countActiveBundleBindings(bundleVersionId);
1373
+ }
1374
+ }
1375
+ db(tx) {
1376
+ return tx ?? this.prisma;
1116
1377
  }
1117
1378
  async findByTenantId(tenantId) {
1118
- return this.loadByTenantId(this.prisma, tenantId);
1379
+ return this.loadByTenantId(this.db(), tenantId);
1119
1380
  }
1120
1381
  async findByTenantIdLocked(tenantId, tx) {
1121
- const db = resolveClient(this.prisma, tx);
1382
+ const db = this.db(tx);
1122
1383
  await db.$queryRaw`SELECT id FROM subscriptions WHERE "tenantId" = ${tenantId} FOR UPDATE`;
1123
1384
  return this.loadByTenantId(db, tenantId);
1124
1385
  }
1125
1386
  async countByPlanVersionId(planVersionId) {
1126
- return this.prisma.subscription.count({
1387
+ return this.subscriptions(this.db()).count({
1127
1388
  where: {
1128
1389
  OR: [
1129
1390
  {
@@ -1136,22 +1397,72 @@ var PrismaSubscriptionRepository = class {
1136
1397
  }
1137
1398
  });
1138
1399
  }
1139
- async countActiveByPlanKey(_projectKey) {
1140
- const rows = await this.prisma.subscription.findMany({
1400
+ async countActiveByPlanKey(projectKey) {
1401
+ const db = this.db();
1402
+ const subscriptions = await this.subscriptions(db).findMany({
1141
1403
  where: {
1142
1404
  status: {
1143
1405
  in: ACTIVE_STATUSES
1144
1406
  }
1407
+ },
1408
+ select: {
1409
+ planVersionId: true
1410
+ }
1411
+ });
1412
+ const versionIds = [
1413
+ ...new Set(subscriptions.map((subscription) => subscription.planVersionId))
1414
+ ];
1415
+ if (versionIds.length === 0) return {};
1416
+ const planVersions = await this.planVersions(db).findMany({
1417
+ where: {
1418
+ id: {
1419
+ in: versionIds
1420
+ }
1421
+ },
1422
+ select: {
1423
+ id: true,
1424
+ planId: true
1145
1425
  }
1146
1426
  });
1427
+ const planVersionById = new Map(planVersions.map((planVersion) => [
1428
+ planVersion.id,
1429
+ planVersion
1430
+ ]));
1431
+ const storedPlanIds = [
1432
+ ...new Set(planVersions.map((planVersion) => planVersion.planId))
1433
+ ];
1434
+ const planKeyByStoredId = await this.planKeysForProject(db, storedPlanIds, projectKey);
1147
1435
  const counts = {};
1148
- for (const row of rows) {
1149
- counts[row.plan] = (counts[row.plan] ?? 0) + 1;
1436
+ for (const subscription of subscriptions) {
1437
+ const planVersion = planVersionById.get(subscription.planVersionId);
1438
+ const planKey = planVersion ? planKeyByStoredId.get(planVersion.planId) : void 0;
1439
+ if (!planKey) continue;
1440
+ counts[planKey] = (counts[planKey] ?? 0) + 1;
1150
1441
  }
1151
1442
  return counts;
1152
1443
  }
1444
+ async countActiveBundleBindings(bundleVersionId) {
1445
+ if (!this.subscriptionBundleDelegateName) {
1446
+ throw new Error("SubscriptionBundle counting is not configured.");
1447
+ }
1448
+ return getPrismaDelegate(this.prisma, this.subscriptionBundleDelegateName).count({
1449
+ where: {
1450
+ bundleVersionId,
1451
+ OR: [
1452
+ {
1453
+ canceledAt: null
1454
+ },
1455
+ {
1456
+ canceledEffectiveAt: {
1457
+ gt: /* @__PURE__ */ new Date()
1458
+ }
1459
+ }
1460
+ ]
1461
+ }
1462
+ });
1463
+ }
1153
1464
  async loadByTenantId(db, tenantId) {
1154
- const row = await db.subscription.findUnique({
1465
+ const row = await this.subscriptions(db).findUnique({
1155
1466
  where: {
1156
1467
  tenantId
1157
1468
  }
@@ -1160,7 +1471,7 @@ var PrismaSubscriptionRepository = class {
1160
1471
  return this.toRecord(db, row);
1161
1472
  }
1162
1473
  async toRecord(db, row) {
1163
- const planVersion = await db.planVersion.findUnique({
1474
+ const planVersion = await this.planVersions(db).findUnique({
1164
1475
  where: {
1165
1476
  id: row.planVersionId
1166
1477
  }
@@ -1168,10 +1479,13 @@ var PrismaSubscriptionRepository = class {
1168
1479
  if (!planVersion) {
1169
1480
  throw new Error(`Subscription ${row.id} references missing PlanVersion ${row.planVersionId}.`);
1170
1481
  }
1482
+ const planKey = await this.binding.toPlanKey(db, planVersion.planId);
1171
1483
  return {
1172
1484
  id: row.id,
1173
1485
  tenantId: row.tenantId,
1174
- plan: row.plan,
1486
+ // The concrete PlanVersion is authoritative. This also normalizes
1487
+ // legacy rows whose denormalized `Subscription.plan` drifted.
1488
+ plan: planKey,
1175
1489
  status: row.status,
1176
1490
  isPilot: row.isPilot,
1177
1491
  trialEntitlementPlan: row.trialEntitlementPlan,
@@ -1180,19 +1494,78 @@ var PrismaSubscriptionRepository = class {
1180
1494
  customLimits: row.customLimits ?? null,
1181
1495
  planVersionId: row.planVersionId,
1182
1496
  planVersion: {
1183
- planId: planVersion.planId,
1497
+ planId: planKey,
1184
1498
  quotas: toQuotaMap(planVersion.quotas),
1185
1499
  features: toStringArray(planVersion.features)
1186
1500
  }
1187
1501
  };
1188
1502
  }
1503
+ planVersions(client) {
1504
+ return getPrismaDelegate(client, this.planVersionDelegateName);
1505
+ }
1506
+ subscriptions(client) {
1507
+ return getPrismaDelegate(client, this.subscriptionDelegateName);
1508
+ }
1509
+ async planKeysForProject(db, storedPlanIds, projectKey) {
1510
+ if (storedPlanIds.length === 0) return /* @__PURE__ */ new Map();
1511
+ if (this.binding.mode === "normalized-plan-id") {
1512
+ if (this.binding.projectKey && this.binding.projectKey !== projectKey) {
1513
+ throw new Error(`Prisma plan binding is configured for project '${this.binding.projectKey}', not '${projectKey}'.`);
1514
+ }
1515
+ const plans2 = await db.plan.findMany({
1516
+ where: {
1517
+ projectKey,
1518
+ id: {
1519
+ in: storedPlanIds
1520
+ }
1521
+ },
1522
+ select: {
1523
+ id: true,
1524
+ planKey: true
1525
+ }
1526
+ });
1527
+ return new Map(plans2.map((plan) => [
1528
+ plan.id,
1529
+ plan.planKey
1530
+ ]));
1531
+ }
1532
+ const plans = await db.plan.findMany({
1533
+ where: {
1534
+ planKey: {
1535
+ in: storedPlanIds
1536
+ }
1537
+ },
1538
+ select: {
1539
+ projectKey: true,
1540
+ planKey: true
1541
+ }
1542
+ });
1543
+ const projectsByPlanKey = /* @__PURE__ */ new Map();
1544
+ for (const plan of plans) {
1545
+ const projects = projectsByPlanKey.get(plan.planKey) ?? /* @__PURE__ */ new Set();
1546
+ projects.add(plan.projectKey);
1547
+ projectsByPlanKey.set(plan.planKey, projects);
1548
+ }
1549
+ return new Map(storedPlanIds.flatMap((planKey) => {
1550
+ const projects = projectsByPlanKey.get(planKey);
1551
+ return projects?.size === 1 && projects.has(projectKey) ? [
1552
+ [
1553
+ planKey,
1554
+ planKey
1555
+ ]
1556
+ ] : [];
1557
+ }));
1558
+ }
1189
1559
  };
1190
1560
  PrismaSubscriptionRepository = _ts_decorate13([
1191
1561
  Injectable13(),
1192
1562
  _ts_param12(0, Inject12(PRISMA_CLIENT_TOKEN)),
1563
+ _ts_param12(1, Optional4()),
1564
+ _ts_param12(1, Inject12(PRISMA_SCHEMA_OPTIONS_TOKEN)),
1193
1565
  _ts_metadata12("design:type", Function),
1194
1566
  _ts_metadata12("design:paramtypes", [
1195
- typeof PrismaLike === "undefined" ? Object : PrismaLike
1567
+ typeof SubscriptionRepositoryClient === "undefined" ? Object : SubscriptionRepositoryClient,
1568
+ typeof PrismaSchemaOptions === "undefined" ? Object : PrismaSchemaOptions
1196
1569
  ])
1197
1570
  ], PrismaSubscriptionRepository);
1198
1571
 
@@ -1304,7 +1677,8 @@ var PrismaTransactionRunner = class {
1304
1677
  this.prisma = prisma;
1305
1678
  }
1306
1679
  async run(fn) {
1307
- return this.prisma.$transaction((tx) => fn(tx));
1680
+ const transaction = this.prisma.$transaction.bind(this.prisma);
1681
+ return transaction((tx) => fn(tx));
1308
1682
  }
1309
1683
  };
1310
1684
  PrismaTransactionRunner = _ts_decorate15([
@@ -1312,7 +1686,7 @@ PrismaTransactionRunner = _ts_decorate15([
1312
1686
  _ts_param14(0, Inject14(PRISMA_CLIENT_TOKEN)),
1313
1687
  _ts_metadata14("design:type", Function),
1314
1688
  _ts_metadata14("design:paramtypes", [
1315
- typeof PrismaLike === "undefined" ? Object : PrismaLike
1689
+ typeof Record === "undefined" ? Object : Record
1316
1690
  ])
1317
1691
  ], PrismaTransactionRunner);
1318
1692
 
@@ -1363,8 +1737,8 @@ function prismaPersistence(options) {
1363
1737
  superAdminProvisioning: buildProvisioning(client, options.passwordHasher)
1364
1738
  },
1365
1739
  entitlement: {
1366
- subscriptionRepository: provide((prisma) => new PrismaSubscriptionRepository(prisma)),
1367
- planVersionRepository: provide((prisma) => new PrismaPlanVersionRepository(prisma))
1740
+ subscriptionRepository: provide((prisma) => new PrismaSubscriptionRepository(prisma, options.schema)),
1741
+ planVersionRepository: provide((prisma) => new PrismaPlanVersionRepository(prisma, options.schema))
1368
1742
  },
1369
1743
  promo: {
1370
1744
  promoCodeRepository: provide((prisma) => new PrismaPromoCodeRepository(prisma)),
@@ -1373,8 +1747,8 @@ function prismaPersistence(options) {
1373
1747
  subscriptionLookup: provide((prisma) => new PrismaPromoSubscriptionLookup(prisma)),
1374
1748
  revenueAggregator: new ZeroPromoRevenueDeductionAggregator()
1375
1749
  },
1376
- planCatalogReadSink: provide((prisma) => new PrismaPlanCatalogReadSink(prisma)),
1377
- planCatalogImportSink: provide((prisma) => new PrismaPlanCatalogImportSink(prisma))
1750
+ planCatalogReadSink: provide((prisma) => new PrismaPlanCatalogReadSink(prisma, options.schema)),
1751
+ planCatalogImportSink: provide((prisma) => new PrismaPlanCatalogImportSink(prisma, options.schema))
1378
1752
  };
1379
1753
  }
1380
1754
  __name(prismaPersistence, "prismaPersistence");
@@ -1443,7 +1817,7 @@ var PrismaSubscriptionBundleRepository = class {
1443
1817
  this.prisma = prisma;
1444
1818
  }
1445
1819
  db(tx) {
1446
- return resolveClient(this.prisma, tx);
1820
+ return tx ?? this.prisma;
1447
1821
  }
1448
1822
  async listBySubscription(subscriptionId) {
1449
1823
  const rows = await this.db().subscriptionBundle.findMany({
@@ -1543,7 +1917,7 @@ PrismaSubscriptionBundleRepository = _ts_decorate17([
1543
1917
  _ts_param15(0, Inject15(PRISMA_CLIENT_TOKEN)),
1544
1918
  _ts_metadata15("design:type", Function),
1545
1919
  _ts_metadata15("design:paramtypes", [
1546
- typeof PrismaLike === "undefined" ? Object : PrismaLike
1920
+ typeof SubscriptionBundleClient === "undefined" ? Object : SubscriptionBundleClient
1547
1921
  ])
1548
1922
  ], PrismaSubscriptionBundleRepository);
1549
1923
  function toRecord3(row) {
@@ -1562,7 +1936,8 @@ function toRecord3(row) {
1562
1936
  __name(toRecord3, "toRecord");
1563
1937
 
1564
1938
  // src/prisma-tenant-subscription-write.adapter.ts
1565
- import { Inject as Inject16, Injectable as Injectable18 } from "@nestjs/common";
1939
+ import { Inject as Inject16, Injectable as Injectable18, Optional as Optional5 } from "@nestjs/common";
1940
+ import { buildActivePlanVersionWhere as buildActivePlanVersionWhere2 } from "@saasicat/types";
1566
1941
  function _ts_decorate18(decorators, target, key, desc) {
1567
1942
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1568
1943
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1585,37 +1960,66 @@ var PrismaTenantSubscriptionWriteAdapter = class {
1585
1960
  __name(this, "PrismaTenantSubscriptionWriteAdapter");
1586
1961
  }
1587
1962
  prisma;
1588
- constructor(prisma) {
1963
+ applyOnboardingSelection;
1964
+ schema;
1965
+ planBinding;
1966
+ constructor(prisma, options) {
1589
1967
  this.prisma = prisma;
1590
- }
1591
- db(tx) {
1592
- return resolveClient(this.prisma, tx);
1968
+ this.schema = resolvePrismaSchemaOptions(options);
1969
+ this.planBinding = createPrismaPlanBindingResolver(options?.planBinding);
1970
+ this.assertConfiguration();
1971
+ if (this.schema.tenantSubscription.atomicOnboardingSelection) {
1972
+ this.applyOnboardingSelection = (tenantId, input, redeemPromo) => this.applyOnboardingSelectionAtomic(tenantId, input, redeemPromo);
1973
+ }
1593
1974
  }
1594
1975
  async changePlanImmediate(tenantId, input) {
1595
- const updated = await this.db().subscription.update({
1976
+ if (this.schema.tenantSubscription.synchronizePlanVersion) {
1977
+ return this.prisma.$transaction((tx) => this.changePlanImmediateInClient(tx, tenantId, input));
1978
+ }
1979
+ return this.changePlanImmediateInClient(this.prisma, tenantId, input);
1980
+ }
1981
+ async changePlanImmediateInClient(client, tenantId, input) {
1982
+ const subscription = this.subscription(client);
1983
+ const data = {
1984
+ plan: input.planId,
1985
+ billingCycle: input.cycle,
1986
+ pendingPlan: null,
1987
+ pendingBillingCycle: null,
1988
+ pendingEffectiveAt: null,
1989
+ ...input.nextStatus ? {
1990
+ status: input.nextStatus
1991
+ } : {},
1992
+ ...input.periodStart && input.periodEnd ? {
1993
+ currentPeriodStart: input.periodStart,
1994
+ currentPeriodEnd: input.periodEnd
1995
+ } : {},
1996
+ // #17: the platform changePlan path computes the carried-over
1997
+ // trial end and passes it through; null/undefined leaves the
1998
+ // existing trialEndsAt untouched.
1999
+ ...input.trialEndsAt ? {
2000
+ trialEndsAt: input.trialEndsAt
2001
+ } : {}
2002
+ };
2003
+ if (this.schema.tenantSubscription.synchronizePlanVersion) {
2004
+ const current = await subscription.findUnique({
2005
+ where: {
2006
+ tenantId
2007
+ }
2008
+ });
2009
+ if (!current) {
2010
+ throw new Error(`No subscription for tenant ${tenantId}.`);
2011
+ }
2012
+ const storagePlanId = await this.planBinding.toStoragePlanId(client, input.planId);
2013
+ data.planVersionId = await this.findTargetPlanVersionId(client, storagePlanId, input.periodStart ?? /* @__PURE__ */ new Date());
2014
+ if (await this.pendingVersionBelongsToAnotherPlan(client, current.pendingPlanVersionId, storagePlanId)) {
2015
+ Object.assign(data, clearedPendingVersionData());
2016
+ }
2017
+ }
2018
+ const updated = await subscription.update({
1596
2019
  where: {
1597
2020
  tenantId
1598
2021
  },
1599
- data: {
1600
- plan: input.planId,
1601
- billingCycle: input.cycle,
1602
- pendingPlan: null,
1603
- pendingBillingCycle: null,
1604
- pendingEffectiveAt: null,
1605
- ...input.nextStatus ? {
1606
- status: input.nextStatus
1607
- } : {},
1608
- ...input.periodStart && input.periodEnd ? {
1609
- currentPeriodStart: input.periodStart,
1610
- currentPeriodEnd: input.periodEnd
1611
- } : {},
1612
- // #17: the platform changePlan path computes the carried-over
1613
- // trial end and passes it through; null/undefined leaves the
1614
- // existing trialEndsAt untouched.
1615
- ...input.trialEndsAt ? {
1616
- trialEndsAt: input.trialEndsAt
1617
- } : {}
1618
- }
2022
+ data
1619
2023
  });
1620
2024
  return {
1621
2025
  plan: updated.plan,
@@ -1623,7 +2027,7 @@ var PrismaTenantSubscriptionWriteAdapter = class {
1623
2027
  };
1624
2028
  }
1625
2029
  async schedulePlanChange(tenantId, input) {
1626
- await this.db().subscription.update({
2030
+ await this.subscription(this.prisma).update({
1627
2031
  where: {
1628
2032
  tenantId
1629
2033
  },
@@ -1635,7 +2039,8 @@ var PrismaTenantSubscriptionWriteAdapter = class {
1635
2039
  });
1636
2040
  }
1637
2041
  async acceptPendingPlanVersion(tenantId, userId, now) {
1638
- const sub = await this.db().subscription.findUnique({
2042
+ const subscription = this.subscription(this.prisma);
2043
+ const sub = await subscription.findUnique({
1639
2044
  where: {
1640
2045
  tenantId
1641
2046
  }
@@ -1643,17 +2048,15 @@ var PrismaTenantSubscriptionWriteAdapter = class {
1643
2048
  if (!sub) {
1644
2049
  throw new Error(`No subscription for tenant ${tenantId}.`);
1645
2050
  }
1646
- if (sub.pendingPlanVersionAccepted) {
1647
- return {
1648
- accepted: true,
1649
- acceptedAt: sub.pendingPlanVersionAcceptedAt,
1650
- effectiveAt: sub.pendingPlanVersionEffectiveAt,
1651
- alreadyAccepted: true
1652
- };
2051
+ if (!sub.pendingPlanVersionId) {
2052
+ throw new Error(`No pending PlanVersion for tenant ${tenantId}.`);
1653
2053
  }
1654
- const updated = await this.db().subscription.update({
2054
+ const pendingPlanVersionId = sub.pendingPlanVersionId;
2055
+ const claimed = await subscription.updateMany({
1655
2056
  where: {
1656
- id: sub.id
2057
+ id: sub.id,
2058
+ pendingPlanVersionId,
2059
+ pendingPlanVersionAccepted: false
1657
2060
  },
1658
2061
  data: {
1659
2062
  pendingPlanVersionAccepted: true,
@@ -1661,15 +2064,66 @@ var PrismaTenantSubscriptionWriteAdapter = class {
1661
2064
  pendingPlanVersionAcceptedByUserId: userId
1662
2065
  }
1663
2066
  });
2067
+ const updated = await subscription.findUnique({
2068
+ where: {
2069
+ id: sub.id
2070
+ }
2071
+ });
2072
+ if (!updated) {
2073
+ throw new Error(`No subscription for tenant ${tenantId}.`);
2074
+ }
2075
+ if (claimed.count === 0 && (updated.pendingPlanVersionId !== pendingPlanVersionId || !updated.pendingPlanVersionAccepted)) {
2076
+ throw new Error(`Pending PlanVersion changed while accepting it for tenant ${tenantId}.`);
2077
+ }
1664
2078
  return {
1665
2079
  accepted: true,
1666
2080
  acceptedAt: updated.pendingPlanVersionAcceptedAt,
1667
2081
  effectiveAt: updated.pendingPlanVersionEffectiveAt,
1668
- alreadyAccepted: false
2082
+ alreadyAccepted: claimed.count === 0
1669
2083
  };
1670
2084
  }
2085
+ async applyOnboardingSelectionAtomic(tenantId, input, redeemPromo) {
2086
+ return this.prisma.$transaction(async (tx) => {
2087
+ const data = {
2088
+ plan: input.planId,
2089
+ billingCycle: input.cycle,
2090
+ pendingPlan: null,
2091
+ pendingBillingCycle: null,
2092
+ pendingEffectiveAt: null,
2093
+ ...clearedPendingVersionData(),
2094
+ ...input.nextStatus ? {
2095
+ status: input.nextStatus
2096
+ } : {},
2097
+ ...input.periodStart && input.periodEnd ? {
2098
+ currentPeriodStart: input.periodStart,
2099
+ currentPeriodEnd: input.periodEnd
2100
+ } : {}
2101
+ };
2102
+ if (this.schema.tenantSubscription.synchronizePlanVersion) {
2103
+ const storagePlanId = await this.planBinding.toStoragePlanId(tx, input.planId);
2104
+ data.planVersionId = await this.findTargetPlanVersionId(tx, storagePlanId, input.periodStart ?? /* @__PURE__ */ new Date());
2105
+ }
2106
+ const updated = await this.subscription(tx).update({
2107
+ where: {
2108
+ tenantId
2109
+ },
2110
+ data
2111
+ });
2112
+ let promoRedemption = null;
2113
+ if (redeemPromo) {
2114
+ promoRedemption = await redeemPromo(tx, updated.id);
2115
+ }
2116
+ return {
2117
+ plan: updated.plan,
2118
+ billingCycle: updated.billingCycle,
2119
+ subscriptionId: updated.id,
2120
+ promoRedemption
2121
+ };
2122
+ });
2123
+ }
1671
2124
  async cancelSubscription(tenantId, immediate, now) {
1672
- const sub = await this.db().subscription.findUnique({
2125
+ const subscription = this.subscription(this.prisma);
2126
+ const sub = await subscription.findUnique({
1673
2127
  where: {
1674
2128
  tenantId
1675
2129
  }
@@ -1678,7 +2132,7 @@ var PrismaTenantSubscriptionWriteAdapter = class {
1678
2132
  throw new Error(`No subscription for tenant ${tenantId}.`);
1679
2133
  }
1680
2134
  const canceledAt = immediate ? now : sub.currentPeriodEnd ?? now;
1681
- const updated = await this.db().subscription.update({
2135
+ const updated = await subscription.update({
1682
2136
  where: {
1683
2137
  tenantId
1684
2138
  },
@@ -1692,18 +2146,107 @@ var PrismaTenantSubscriptionWriteAdapter = class {
1692
2146
  status: updated.status
1693
2147
  };
1694
2148
  }
2149
+ subscription(client) {
2150
+ return getPrismaDelegate(client, this.schema.tenantSubscription.delegate);
2151
+ }
2152
+ planVersions(client) {
2153
+ return getPrismaDelegate(client, this.schema.delegates.entitlementPlanVersion);
2154
+ }
2155
+ async findTargetPlanVersionId(client, storagePlanId, asOf) {
2156
+ const activeWindow = this.schema.tenantSubscription.activeVersionSelection === "validity-window";
2157
+ const activeVersionWhere = this.schema.tenantSubscription.withEndsAt ? buildActivePlanVersionWhere2(asOf, {
2158
+ withEndsAt: true
2159
+ }) : buildActivePlanVersionWhere2(asOf);
2160
+ const where = activeWindow ? {
2161
+ planId: storagePlanId,
2162
+ ...activeVersionWhere
2163
+ } : {
2164
+ planId: storagePlanId,
2165
+ publishedAt: {
2166
+ not: null
2167
+ },
2168
+ supersededAt: null,
2169
+ ...this.schema.tenantSubscription.withEndsAt ? {
2170
+ OR: [
2171
+ {
2172
+ endsAt: null
2173
+ },
2174
+ {
2175
+ endsAt: {
2176
+ gt: asOf
2177
+ }
2178
+ }
2179
+ ]
2180
+ } : {}
2181
+ };
2182
+ const target = await this.planVersions(client).findFirst({
2183
+ where,
2184
+ orderBy: activeWindow ? [
2185
+ {
2186
+ validFrom: {
2187
+ sort: "desc",
2188
+ nulls: "last"
2189
+ }
2190
+ },
2191
+ {
2192
+ version: "desc"
2193
+ }
2194
+ ] : {
2195
+ version: "desc"
2196
+ }
2197
+ });
2198
+ if (!target) {
2199
+ throw new Error(`No active PlanVersion for plan '${storagePlanId}'.`);
2200
+ }
2201
+ return target.id;
2202
+ }
2203
+ async pendingVersionBelongsToAnotherPlan(client, pendingPlanVersionId, targetStoragePlanId) {
2204
+ if (!pendingPlanVersionId) return false;
2205
+ const pending = await this.planVersions(client).findUnique({
2206
+ where: {
2207
+ id: pendingPlanVersionId
2208
+ }
2209
+ });
2210
+ return !pending || pending.planId !== targetStoragePlanId;
2211
+ }
2212
+ assertConfiguration() {
2213
+ if (!this.schema.tenantSubscription.synchronizePlanVersion) return;
2214
+ const entitlementFields = this.schema.planVersionFields.entitlement;
2215
+ if (this.schema.tenantSubscription.activeVersionSelection === "validity-window" && !entitlementFields.validityWindows) {
2216
+ throw new Error("tenantSubscription.activeVersionSelection='validity-window' requires planVersionFields.entitlement.validityWindows=true.");
2217
+ }
2218
+ if (this.schema.tenantSubscription.withEndsAt && !entitlementFields.endsAt) {
2219
+ throw new Error("tenantSubscription.withEndsAt=true requires planVersionFields.entitlement.endsAt=true.");
2220
+ }
2221
+ }
1695
2222
  };
1696
2223
  PrismaTenantSubscriptionWriteAdapter = _ts_decorate18([
1697
2224
  Injectable18(),
1698
2225
  _ts_param16(0, Inject16(PRISMA_CLIENT_TOKEN)),
2226
+ _ts_param16(1, Optional5()),
2227
+ _ts_param16(1, Inject16(PRISMA_SCHEMA_OPTIONS_TOKEN)),
1699
2228
  _ts_metadata16("design:type", Function),
1700
2229
  _ts_metadata16("design:paramtypes", [
1701
- typeof PrismaLike === "undefined" ? Object : PrismaLike
2230
+ typeof TransactionalPrismaClient === "undefined" ? Object : TransactionalPrismaClient,
2231
+ typeof PrismaSchemaOptions === "undefined" ? Object : PrismaSchemaOptions
1702
2232
  ])
1703
2233
  ], PrismaTenantSubscriptionWriteAdapter);
2234
+ function clearedPendingVersionData() {
2235
+ return {
2236
+ pendingPlanVersionId: null,
2237
+ pendingPlanVersionEffectiveAt: null,
2238
+ pendingPlanVersionAccepted: false,
2239
+ pendingPlanVersionAcceptedAt: null,
2240
+ pendingPlanVersionAcceptedByUserId: null,
2241
+ pendingPlanVersionNotifiedAt: null,
2242
+ pendingPlanVersionReminderSentAt: null
2243
+ };
2244
+ }
2245
+ __name(clearedPendingVersionData, "clearedPendingVersionData");
1704
2246
 
1705
2247
  // src/prisma-plan.repository.ts
1706
- import { Inject as Inject17, Injectable as Injectable19 } from "@nestjs/common";
2248
+ import { Inject as Inject17, Injectable as Injectable19, Optional as Optional6 } from "@nestjs/common";
2249
+ import { buildActivePlanVersionWhere as buildActivePlanVersionWhere3 } from "@saasicat/types";
1707
2250
  function _ts_decorate19(decorators, target, key, desc) {
1708
2251
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1709
2252
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1726,30 +2269,105 @@ var PrismaPlanRepository = class {
1726
2269
  __name(this, "PrismaPlanRepository");
1727
2270
  }
1728
2271
  prisma;
1729
- constructor(prisma) {
2272
+ binding;
2273
+ delegateName;
2274
+ fields;
2275
+ constructor(prisma, options) {
1730
2276
  this.prisma = prisma;
2277
+ const schema = resolvePrismaSchemaOptions(options);
2278
+ this.binding = createPrismaPlanBindingResolver(options?.planBinding);
2279
+ this.delegateName = schema.delegates.catalogPlanVersion;
2280
+ this.fields = schema.planVersionFields.catalog;
1731
2281
  }
1732
2282
  db(tx) {
1733
- return resolveClient(this.prisma, tx);
2283
+ return tx ?? this.prisma;
2284
+ }
2285
+ versions(client) {
2286
+ return getPrismaDelegate(client, this.delegateName);
1734
2287
  }
1735
2288
  // ─── Stem operations (Pack 1) ───
1736
2289
  async list(filter) {
1737
2290
  const excludeDeleted = filter.excludeDeleted ?? true;
2291
+ const db = this.db();
1738
2292
  let publishedKeys = null;
1739
2293
  if (filter.onlyPublished) {
1740
- const live = await this.db().planVersion.findMany({
1741
- where: {
1742
- publishedAt: {
1743
- not: null
1744
- },
1745
- supersededAt: null
2294
+ if (this.binding.mode === "legacy-plan-key") {
2295
+ const projectPlans = await db.plan.findMany({
2296
+ where: {
2297
+ projectKey: filter.projectKey,
2298
+ ...excludeDeleted ? {
2299
+ deletedAt: null
2300
+ } : {}
2301
+ }
2302
+ });
2303
+ const candidateKeys = [
2304
+ ...new Set(projectPlans.map((plan) => plan.planKey))
2305
+ ];
2306
+ const allMatchingPlans = candidateKeys.length === 0 ? [] : await db.plan.findMany({
2307
+ where: {
2308
+ planKey: {
2309
+ in: candidateKeys
2310
+ }
2311
+ }
2312
+ });
2313
+ const projectsByKey = /* @__PURE__ */ new Map();
2314
+ for (const plan of allMatchingPlans) {
2315
+ const projects = projectsByKey.get(plan.planKey) ?? /* @__PURE__ */ new Set();
2316
+ projects.add(plan.projectKey);
2317
+ projectsByKey.set(plan.planKey, projects);
1746
2318
  }
1747
- });
1748
- publishedKeys = [
1749
- ...new Set(live.map((version) => version.planId))
1750
- ];
2319
+ const unambiguousKeys = candidateKeys.filter((planKey) => projectsByKey.get(planKey)?.size === 1);
2320
+ const live = await this.versions(db).findMany({
2321
+ where: {
2322
+ planId: {
2323
+ in: unambiguousKeys
2324
+ },
2325
+ publishedAt: {
2326
+ not: null
2327
+ },
2328
+ supersededAt: null
2329
+ }
2330
+ });
2331
+ publishedKeys = [
2332
+ ...new Set(live.map((version) => version.planId))
2333
+ ];
2334
+ } else {
2335
+ const projectPlans = await db.plan.findMany({
2336
+ where: {
2337
+ projectKey: filter.projectKey,
2338
+ ...excludeDeleted ? {
2339
+ deletedAt: null
2340
+ } : {}
2341
+ }
2342
+ });
2343
+ const planKeyById = new Map(projectPlans.map((plan) => [
2344
+ plan.id,
2345
+ plan.planKey
2346
+ ]));
2347
+ const live = await this.versions(db).findMany({
2348
+ where: {
2349
+ planId: {
2350
+ in: [
2351
+ ...planKeyById.keys()
2352
+ ]
2353
+ },
2354
+ publishedAt: {
2355
+ not: null
2356
+ },
2357
+ supersededAt: null
2358
+ }
2359
+ });
2360
+ publishedKeys = [
2361
+ ...new Set(live.flatMap((version) => {
2362
+ const planKey = planKeyById.get(version.planId);
2363
+ return planKey ? [
2364
+ planKey
2365
+ ] : [];
2366
+ }))
2367
+ ];
2368
+ }
1751
2369
  }
1752
- const rows = await this.db().plan.findMany({
2370
+ const rows = await db.plan.findMany({
1753
2371
  where: {
1754
2372
  projectKey: filter.projectKey,
1755
2373
  ...excludeDeleted ? {
@@ -1844,65 +2462,112 @@ var PrismaPlanRepository = class {
1844
2462
  }
1845
2463
  // ─── Lifecycle operations (Pack 2a) — keyed by planKey ───
1846
2464
  async listVersions(planKey) {
1847
- const rows = await this.db().planVersion.findMany({
2465
+ const db = this.db();
2466
+ const storedPlanId = await this.binding.toStoragePlanId(db, planKey);
2467
+ const rows = await this.versions(db).findMany({
1848
2468
  where: {
1849
- planId: planKey
2469
+ planId: storedPlanId
1850
2470
  },
1851
2471
  orderBy: {
1852
2472
  version: "asc"
1853
2473
  }
1854
2474
  });
1855
- return rows.map(toPlanVersionRow2);
2475
+ return rows.map((row) => this.toPlanVersionRow(row, planKey));
1856
2476
  }
1857
2477
  async findVersionById(versionId) {
1858
- const row = await this.db().planVersion.findUnique({
2478
+ const db = this.db();
2479
+ const row = await this.versions(db).findUnique({
1859
2480
  where: {
1860
2481
  id: versionId
1861
2482
  }
1862
2483
  });
1863
- return row ? toPlanVersionRow2(row) : null;
2484
+ return row ? this.toPlanVersionRow(row, await this.binding.toPlanKey(db, row.planId)) : null;
1864
2485
  }
1865
2486
  async findCurrentDraft(planKey) {
1866
- const row = await this.db().planVersion.findFirst({
2487
+ const db = this.db();
2488
+ const storedPlanId = await this.binding.toStoragePlanId(db, planKey);
2489
+ const row = await this.versions(db).findFirst({
1867
2490
  where: {
1868
- planId: planKey,
2491
+ planId: storedPlanId,
1869
2492
  publishedAt: null
1870
2493
  }
1871
2494
  });
1872
- return row ? toPlanVersionRow2(row) : null;
2495
+ return row ? this.toPlanVersionRow(row, planKey) : null;
1873
2496
  }
1874
2497
  async findLatestLivePlanVersion(planKey, tx) {
1875
- const row = await this.db(tx).planVersion.findFirst({
2498
+ const db = this.db(tx);
2499
+ const storedPlanId = await this.binding.toStoragePlanId(db, planKey);
2500
+ const row = await this.versions(db).findFirst({
1876
2501
  where: {
1877
- planId: planKey,
2502
+ planId: storedPlanId,
1878
2503
  publishedAt: {
1879
2504
  not: null
1880
2505
  },
1881
- supersededAt: null
2506
+ supersededAt: null,
2507
+ ...this.fields.endsAt ? {
2508
+ OR: [
2509
+ {
2510
+ endsAt: null
2511
+ },
2512
+ {
2513
+ endsAt: {
2514
+ gt: /* @__PURE__ */ new Date()
2515
+ }
2516
+ }
2517
+ ]
2518
+ } : {}
1882
2519
  },
1883
2520
  orderBy: {
1884
2521
  version: "desc"
1885
2522
  }
1886
2523
  });
1887
- return row ? toPlanVersionRow2(row) : null;
2524
+ return row ? this.toPlanVersionRow(row, planKey) : null;
1888
2525
  }
1889
- async findActivePlanVersion() {
1890
- 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.");
2526
+ async findActivePlanVersion(planKey, asOf = /* @__PURE__ */ new Date(), tx) {
2527
+ if (!this.fields.validityWindows) {
2528
+ 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.");
2529
+ }
2530
+ const db = this.db(tx);
2531
+ const storedPlanId = await this.binding.toStoragePlanId(db, planKey);
2532
+ const activeWhere = this.fields.endsAt ? buildActivePlanVersionWhere3(asOf, {
2533
+ withEndsAt: true
2534
+ }) : buildActivePlanVersionWhere3(asOf);
2535
+ const row = await this.versions(db).findFirst({
2536
+ where: {
2537
+ planId: storedPlanId,
2538
+ ...activeWhere
2539
+ },
2540
+ orderBy: [
2541
+ {
2542
+ validFrom: {
2543
+ sort: "desc",
2544
+ nulls: "last"
2545
+ }
2546
+ },
2547
+ {
2548
+ version: "desc"
2549
+ }
2550
+ ]
2551
+ });
2552
+ return row ? this.toPlanVersionRow(row, planKey) : null;
1891
2553
  }
1892
2554
  async createPlanVersionDraft(data) {
1893
2555
  const planKey = data.planId;
1894
- const latest = await this.db().planVersion.findFirst({
2556
+ const db = this.db();
2557
+ const planVersion = this.versions(db);
2558
+ const storedPlanId = await this.binding.toStoragePlanId(db, planKey);
2559
+ const latest = await planVersion.findFirst({
1895
2560
  where: {
1896
- planId: planKey
2561
+ planId: storedPlanId
1897
2562
  },
1898
2563
  orderBy: {
1899
2564
  version: "desc"
1900
2565
  }
1901
2566
  });
1902
2567
  const nextVersion = (latest?.version ?? 0) + 1;
1903
- const created = await this.db().planVersion.create({
2568
+ const created = await planVersion.create({
1904
2569
  data: {
1905
- planId: planKey,
2570
+ planId: storedPlanId,
1906
2571
  version: nextVersion,
1907
2572
  baseVersionId: data.baseVersionId ?? null,
1908
2573
  features: data.features,
@@ -1911,13 +2576,17 @@ var PrismaPlanRepository = class {
1911
2576
  yearlyNet: data.yearlyNet,
1912
2577
  marketed: data.marketed ?? true,
1913
2578
  changeNote: data.changeNote ?? "",
1914
- createdByUserId: data.createdByUserId ?? null
2579
+ createdByUserId: data.createdByUserId ?? null,
2580
+ ...this.fields.validityWindows ? {
2581
+ validFrom: data.validFrom ? new Date(data.validFrom) : null,
2582
+ validUntil: data.validUntil ? new Date(data.validUntil) : null
2583
+ } : {}
1915
2584
  }
1916
2585
  });
1917
- return toPlanVersionRow2(created);
2586
+ return this.toPlanVersionRow(created, planKey);
1918
2587
  }
1919
2588
  async updatePlanVersionDraft(versionId, data) {
1920
- const updated = await this.db().planVersion.update({
2589
+ const updated = await this.versions(this.db()).update({
1921
2590
  where: {
1922
2591
  id: versionId
1923
2592
  },
@@ -1939,13 +2608,21 @@ var PrismaPlanRepository = class {
1939
2608
  } : {},
1940
2609
  ...data.changeNote !== void 0 ? {
1941
2610
  changeNote: data.changeNote
2611
+ } : {},
2612
+ ...this.fields.validityWindows && data.validFrom !== void 0 ? {
2613
+ validFrom: data.validFrom ? new Date(data.validFrom) : null
2614
+ } : {},
2615
+ ...this.fields.validityWindows && data.validUntil !== void 0 ? {
2616
+ validUntil: data.validUntil ? new Date(data.validUntil) : null
1942
2617
  } : {}
1943
2618
  }
1944
2619
  });
1945
- return toPlanVersionRow2(updated);
2620
+ const planKey = await this.binding.toPlanKey(this.db(), updated.planId);
2621
+ return this.toPlanVersionRow(updated, planKey);
1946
2622
  }
1947
2623
  async publishPlanVersionDraft(versionId, publishMeta, tx) {
1948
- const draft = await this.db(tx).planVersion.findUnique({
2624
+ const operationDb = this.db(tx);
2625
+ const draft = await this.versions(operationDb).findUnique({
1949
2626
  where: {
1950
2627
  id: versionId
1951
2628
  }
@@ -1953,11 +2630,12 @@ var PrismaPlanRepository = class {
1953
2630
  if (!draft) {
1954
2631
  throw new Error(`PlanVersion ${versionId} not found.`);
1955
2632
  }
1956
- const planKey = draft.planId;
2633
+ const storedPlanId = draft.planId;
1957
2634
  const publish = /* @__PURE__ */ __name(async (db) => {
1958
- const previous = await db.planVersion.findFirst({
2635
+ const planVersion = this.versions(db);
2636
+ const previous = await planVersion.findFirst({
1959
2637
  where: {
1960
- planId: planKey,
2638
+ planId: storedPlanId,
1961
2639
  publishedAt: {
1962
2640
  not: null
1963
2641
  },
@@ -1972,16 +2650,20 @@ var PrismaPlanRepository = class {
1972
2650
  });
1973
2651
  const now = /* @__PURE__ */ new Date();
1974
2652
  if (previous) {
1975
- await db.planVersion.update({
2653
+ const predecessorValidUntil = new Date(publishMeta.validFrom.getTime() - 24 * 60 * 60 * 1e3);
2654
+ await planVersion.update({
1976
2655
  where: {
1977
2656
  id: previous.id
1978
2657
  },
1979
2658
  data: {
1980
- supersededAt: now
2659
+ supersededAt: now,
2660
+ ...this.fields.validityWindows ? {
2661
+ validUntil: predecessorValidUntil
2662
+ } : {}
1981
2663
  }
1982
2664
  });
1983
2665
  }
1984
- return db.planVersion.update({
2666
+ return planVersion.update({
1985
2667
  where: {
1986
2668
  id: versionId
1987
2669
  },
@@ -1989,15 +2671,21 @@ var PrismaPlanRepository = class {
1989
2671
  publishedAt: now,
1990
2672
  publishedChanges: publishMeta.publishedChanges,
1991
2673
  nonRegressive: publishMeta.nonRegressive,
1992
- publishedByUserId: publishMeta.publishedByUserId
2674
+ publishedByUserId: publishMeta.publishedByUserId,
2675
+ ...this.fields.validityWindows ? {
2676
+ validFrom: publishMeta.validFrom,
2677
+ validUntil: publishMeta.validUntil
2678
+ } : {}
1993
2679
  }
1994
2680
  });
1995
2681
  }, "publish");
1996
2682
  const published = tx ? await publish(this.db(tx)) : await this.prisma.$transaction((txClient) => publish(txClient));
1997
- return toPlanVersionRow2(published);
2683
+ const planKey = await this.binding.toPlanKey(operationDb, storedPlanId);
2684
+ return this.toPlanVersionRow(published, planKey);
1998
2685
  }
1999
2686
  async deletePlanVersionDraft(versionId) {
2000
- const row = await this.db().planVersion.findUnique({
2687
+ const planVersion = this.versions(this.db());
2688
+ const row = await planVersion.findUnique({
2001
2689
  where: {
2002
2690
  id: versionId
2003
2691
  }
@@ -2006,23 +2694,42 @@ var PrismaPlanRepository = class {
2006
2694
  if (row.publishedAt !== null) {
2007
2695
  throw new Error(`PlanVersion ${versionId} is already published and cannot be discarded (published versions are immutable \u2014 contract protection P1).`);
2008
2696
  }
2009
- await this.db().planVersion.deleteMany({
2697
+ await planVersion.deleteMany({
2010
2698
  where: {
2011
2699
  id: versionId,
2012
2700
  publishedAt: null
2013
2701
  }
2014
2702
  });
2015
2703
  }
2016
- async terminate() {
2017
- 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.");
2704
+ async terminate(versionId, endsAt) {
2705
+ if (!this.fields.endsAt) {
2706
+ 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.");
2707
+ }
2708
+ const db = this.db();
2709
+ const updated = await this.versions(db).update({
2710
+ where: {
2711
+ id: versionId
2712
+ },
2713
+ data: {
2714
+ endsAt
2715
+ }
2716
+ });
2717
+ const planKey = await this.binding.toPlanKey(db, updated.planId);
2718
+ return this.toPlanVersionRow(updated, planKey);
2719
+ }
2720
+ toPlanVersionRow(row, planKey) {
2721
+ return toPlanVersionRow2(row, planKey, this.fields);
2018
2722
  }
2019
2723
  };
2020
2724
  PrismaPlanRepository = _ts_decorate19([
2021
2725
  Injectable19(),
2022
2726
  _ts_param17(0, Inject17(PRISMA_CLIENT_TOKEN)),
2727
+ _ts_param17(1, Optional6()),
2728
+ _ts_param17(1, Inject17(PRISMA_SCHEMA_OPTIONS_TOKEN)),
2023
2729
  _ts_metadata17("design:type", Function),
2024
2730
  _ts_metadata17("design:paramtypes", [
2025
- typeof PrismaLike === "undefined" ? Object : PrismaLike
2731
+ typeof PlanRepositoryClient === "undefined" ? Object : PlanRepositoryClient,
2732
+ typeof PrismaSchemaOptions === "undefined" ? Object : PrismaSchemaOptions
2026
2733
  ])
2027
2734
  ], PrismaPlanRepository);
2028
2735
  function toPlanRow2(row) {
@@ -2040,12 +2747,12 @@ function toPlanRow2(row) {
2040
2747
  };
2041
2748
  }
2042
2749
  __name(toPlanRow2, "toPlanRow");
2043
- function toPlanVersionRow2(row) {
2044
- return {
2750
+ function toPlanVersionRow2(row, planKey, fields) {
2751
+ const mapped = {
2045
2752
  id: row.id,
2046
2753
  version: row.version,
2047
2754
  baseVersionId: row.baseVersionId,
2048
- planId: row.planId,
2755
+ planId: planKey,
2049
2756
  features: toStringArray(row.features),
2050
2757
  quotas: toQuotaMap(row.quotas),
2051
2758
  monthlyNet: row.monthlyNet.toString(),
@@ -2056,19 +2763,23 @@ function toPlanVersionRow2(row) {
2056
2763
  publishedChanges: Array.isArray(row.publishedChanges) ? row.publishedChanges : null,
2057
2764
  changeNote: row.changeNote,
2058
2765
  nonRegressive: row.nonRegressive,
2059
- // The canonical plan_versions schema carries no validity-window columns.
2060
- validFrom: null,
2061
- validUntil: null,
2766
+ validFrom: fields.validityWindows && row.validFrom ? row.validFrom.toISOString() : null,
2767
+ validUntil: fields.validityWindows && row.validUntil ? row.validUntil.toISOString() : null,
2062
2768
  createdByUserId: row.createdByUserId,
2063
2769
  publishedByUserId: row.publishedByUserId,
2064
2770
  createdAt: row.createdAt.toISOString(),
2065
2771
  updatedAt: row.updatedAt.toISOString()
2066
2772
  };
2773
+ if (fields.endsAt) {
2774
+ mapped.endsAt = row.endsAt?.toISOString() ?? null;
2775
+ }
2776
+ return mapped;
2067
2777
  }
2068
2778
  __name(toPlanVersionRow2, "toPlanVersionRow");
2069
2779
 
2070
2780
  // src/prisma-bundle.repository.ts
2071
- import { Inject as Inject18, Injectable as Injectable20 } from "@nestjs/common";
2781
+ import { Inject as Inject18, Injectable as Injectable20, Optional as Optional7 } from "@nestjs/common";
2782
+ import { buildActiveVersionWhere } from "@saasicat/types";
2072
2783
  function _ts_decorate20(decorators, target, key, desc) {
2073
2784
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2074
2785
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2086,16 +2797,32 @@ function _ts_param18(paramIndex, decorator) {
2086
2797
  };
2087
2798
  }
2088
2799
  __name(_ts_param18, "_ts_param");
2800
+ var PRISMA_BUNDLE_REPOSITORY_OPTIONS = /* @__PURE__ */ Symbol.for("saasicat/adapter-prisma/PrismaBundleRepositoryOptions");
2089
2801
  var PrismaBundleRepository = class {
2090
2802
  static {
2091
2803
  __name(this, "PrismaBundleRepository");
2092
2804
  }
2093
2805
  prisma;
2094
- constructor(prisma) {
2806
+ validityWindows;
2807
+ /**
2808
+ * Present only when validity-window mode is enabled. This mirrors the
2809
+ * optional port capability and lets 0.6-schema consumers detect that an
2810
+ * active-at-time lookup is unavailable.
2811
+ */
2812
+ findActiveBundleVersion;
2813
+ constructor(prisma, options = {}) {
2095
2814
  this.prisma = prisma;
2815
+ this.validityWindows = options.validityWindows ?? false;
2816
+ if (this.validityWindows) {
2817
+ this.findActiveBundleVersion = (bundleId, asOf, tx) => this.findActiveBundleVersionWithValidity(bundleId, asOf ?? /* @__PURE__ */ new Date(), tx);
2818
+ }
2096
2819
  }
2097
2820
  db(tx) {
2098
- return resolveClient(this.prisma, tx);
2821
+ return tx ?? this.prisma;
2822
+ }
2823
+ transaction(work) {
2824
+ const transaction = this.prisma.$transaction;
2825
+ return transaction.call(this.prisma, (tx) => work(tx));
2099
2826
  }
2100
2827
  // ─── Stem operations ───
2101
2828
  async list(filter) {
@@ -2201,7 +2928,7 @@ var PrismaBundleRepository = class {
2201
2928
  version: "asc"
2202
2929
  }
2203
2930
  });
2204
- return rows.map((row) => toBundleVersionRow(row, bundle));
2931
+ return rows.map((row) => toBundleVersionRow(row, bundle, this.validityWindows));
2205
2932
  }
2206
2933
  async findVersionById(versionId) {
2207
2934
  const row = await this.db().bundleVersion.findUnique({
@@ -2215,7 +2942,7 @@ var PrismaBundleRepository = class {
2215
2942
  id: row.bundleId
2216
2943
  }
2217
2944
  });
2218
- return toBundleVersionRow(row, bundle);
2945
+ return toBundleVersionRow(row, bundle, this.validityWindows);
2219
2946
  }
2220
2947
  async findCurrentDraft(bundleId) {
2221
2948
  const row = await this.db().bundleVersion.findFirst({
@@ -2230,7 +2957,7 @@ var PrismaBundleRepository = class {
2230
2957
  id: bundleId
2231
2958
  }
2232
2959
  });
2233
- return toBundleVersionRow(row, bundle);
2960
+ return toBundleVersionRow(row, bundle, this.validityWindows);
2234
2961
  }
2235
2962
  async findLatestLive(bundleId, tx) {
2236
2963
  const db = this.db(tx);
@@ -2252,7 +2979,34 @@ var PrismaBundleRepository = class {
2252
2979
  id: bundleId
2253
2980
  }
2254
2981
  });
2255
- return toBundleVersionRow(row, bundle);
2982
+ return toBundleVersionRow(row, bundle, this.validityWindows);
2983
+ }
2984
+ async findActiveBundleVersionWithValidity(bundleId, asOf, tx) {
2985
+ const db = this.db(tx);
2986
+ const row = await db.bundleVersion.findFirst({
2987
+ where: {
2988
+ bundleId,
2989
+ ...buildActiveVersionWhere(asOf)
2990
+ },
2991
+ orderBy: [
2992
+ {
2993
+ validFrom: {
2994
+ sort: "desc",
2995
+ nulls: "last"
2996
+ }
2997
+ },
2998
+ {
2999
+ version: "desc"
3000
+ }
3001
+ ]
3002
+ });
3003
+ if (!row) return null;
3004
+ const bundle = await db.bundle.findUnique({
3005
+ where: {
3006
+ id: bundleId
3007
+ }
3008
+ });
3009
+ return toBundleVersionRow(row, bundle, true);
2256
3010
  }
2257
3011
  async createDraft(data) {
2258
3012
  const db = this.db();
@@ -2287,7 +3041,11 @@ var PrismaBundleRepository = class {
2287
3041
  yearlyNet: data.yearlyNet ?? null,
2288
3042
  marketed: data.marketed ?? true,
2289
3043
  changeNote: data.changeNote ?? "",
2290
- createdByUserId: data.createdByUserId ?? null
3044
+ createdByUserId: data.createdByUserId ?? null,
3045
+ ...this.validityWindows ? {
3046
+ validFrom: toNullableDate(data.validFrom),
3047
+ validUntil: toNullableDate(data.validUntil)
3048
+ } : {}
2291
3049
  }
2292
3050
  });
2293
3051
  const bundle = await db.bundle.findUnique({
@@ -2295,7 +3053,7 @@ var PrismaBundleRepository = class {
2295
3053
  id: data.bundleId
2296
3054
  }
2297
3055
  });
2298
- return toBundleVersionRow(created, bundle);
3056
+ return toBundleVersionRow(created, bundle, this.validityWindows);
2299
3057
  }
2300
3058
  async updateDraft(versionId, data) {
2301
3059
  const db = this.db();
@@ -2327,6 +3085,12 @@ var PrismaBundleRepository = class {
2327
3085
  } : {},
2328
3086
  ...data.changeNote !== void 0 ? {
2329
3087
  changeNote: data.changeNote
3088
+ } : {},
3089
+ ...this.validityWindows && data.validFrom !== void 0 ? {
3090
+ validFrom: toNullableDate(data.validFrom)
3091
+ } : {},
3092
+ ...this.validityWindows && data.validUntil !== void 0 ? {
3093
+ validUntil: toNullableDate(data.validUntil)
2330
3094
  } : {}
2331
3095
  }
2332
3096
  });
@@ -2335,9 +3099,15 @@ var PrismaBundleRepository = class {
2335
3099
  id: updated.bundleId
2336
3100
  }
2337
3101
  });
2338
- return toBundleVersionRow(updated, bundle);
3102
+ return toBundleVersionRow(updated, bundle, this.validityWindows);
2339
3103
  }
2340
3104
  async publishDraft(versionId, publishMeta, tx) {
3105
+ if (this.validityWindows && tx === void 0) {
3106
+ return this.transaction((transaction) => this.publishDraftWithValidity(transaction, versionId, publishMeta));
3107
+ }
3108
+ if (this.validityWindows) {
3109
+ return this.publishDraftWithValidity(this.db(tx), versionId, publishMeta);
3110
+ }
2341
3111
  const db = this.db(tx);
2342
3112
  const draft = await db.bundleVersion.findUnique({
2343
3113
  where: {
@@ -2378,7 +3148,53 @@ var PrismaBundleRepository = class {
2378
3148
  id: published.bundleId
2379
3149
  }
2380
3150
  });
2381
- return toBundleVersionRow(published, bundle);
3151
+ return toBundleVersionRow(published, bundle, false);
3152
+ }
3153
+ async publishDraftWithValidity(db, versionId, publishMeta) {
3154
+ const draft = await db.bundleVersion.findUnique({
3155
+ where: {
3156
+ id: versionId
3157
+ }
3158
+ });
3159
+ if (!draft) {
3160
+ throw new Error(`BundleVersion '${versionId}' not found.`);
3161
+ }
3162
+ const now = /* @__PURE__ */ new Date();
3163
+ await db.bundleVersion.updateMany({
3164
+ where: {
3165
+ bundleId: draft.bundleId,
3166
+ publishedAt: {
3167
+ not: null
3168
+ },
3169
+ supersededAt: null,
3170
+ NOT: {
3171
+ id: versionId
3172
+ }
3173
+ },
3174
+ data: {
3175
+ supersededAt: now,
3176
+ validUntil: previousUtcDay(publishMeta.validFrom)
3177
+ }
3178
+ });
3179
+ const published = await db.bundleVersion.update({
3180
+ where: {
3181
+ id: versionId
3182
+ },
3183
+ data: {
3184
+ publishedAt: now,
3185
+ publishedByUserId: publishMeta.publishedByUserId,
3186
+ publishedChanges: publishMeta.publishedChanges,
3187
+ nonRegressive: publishMeta.nonRegressive,
3188
+ validFrom: publishMeta.validFrom,
3189
+ validUntil: publishMeta.validUntil
3190
+ }
3191
+ });
3192
+ const bundle = await db.bundle.findUnique({
3193
+ where: {
3194
+ id: published.bundleId
3195
+ }
3196
+ });
3197
+ return toBundleVersionRow(published, bundle, true);
2382
3198
  }
2383
3199
  async deleteDraft(versionId) {
2384
3200
  const db = this.db();
@@ -2406,9 +3222,12 @@ var PrismaBundleRepository = class {
2406
3222
  PrismaBundleRepository = _ts_decorate20([
2407
3223
  Injectable20(),
2408
3224
  _ts_param18(0, Inject18(PRISMA_CLIENT_TOKEN)),
3225
+ _ts_param18(1, Optional7()),
3226
+ _ts_param18(1, Inject18(PRISMA_BUNDLE_REPOSITORY_OPTIONS)),
2409
3227
  _ts_metadata18("design:type", Function),
2410
3228
  _ts_metadata18("design:paramtypes", [
2411
- typeof PrismaLike === "undefined" ? Object : PrismaLike
3229
+ typeof BundlePrismaClient === "undefined" ? Object : BundlePrismaClient,
3230
+ typeof PrismaBundleRepositoryOptions === "undefined" ? Object : PrismaBundleRepositoryOptions
2412
3231
  ])
2413
3232
  ], PrismaBundleRepository);
2414
3233
  function isPlainObject(value) {
@@ -2419,6 +3238,16 @@ function toDecimalString(value) {
2419
3238
  return value == null ? null : value.toString();
2420
3239
  }
2421
3240
  __name(toDecimalString, "toDecimalString");
3241
+ function toNullableDate(value) {
3242
+ return value ? new Date(value) : null;
3243
+ }
3244
+ __name(toNullableDate, "toNullableDate");
3245
+ function previousUtcDay(value) {
3246
+ const result = new Date(value);
3247
+ result.setUTCDate(result.getUTCDate() - 1);
3248
+ return result;
3249
+ }
3250
+ __name(previousUtcDay, "previousUtcDay");
2422
3251
  function toVersionChanges(value) {
2423
3252
  return Array.isArray(value) ? value : null;
2424
3253
  }
@@ -2451,7 +3280,7 @@ function toBundleRow(row) {
2451
3280
  };
2452
3281
  }
2453
3282
  __name(toBundleRow, "toBundleRow");
2454
- function toBundleVersionRow(row, bundle) {
3283
+ function toBundleVersionRow(row, bundle, validityWindows) {
2455
3284
  return {
2456
3285
  id: row.id,
2457
3286
  bundleId: row.bundleId,
@@ -2468,10 +3297,8 @@ function toBundleVersionRow(row, bundle) {
2468
3297
  marketed: row.marketed,
2469
3298
  publishedAt: row.publishedAt?.toISOString() ?? null,
2470
3299
  supersededAt: row.supersededAt?.toISOString() ?? null,
2471
- // The canonical `bundle_versions` table has no validFrom/validUntil
2472
- // columns; time-aware validity is not expressible on this schema.
2473
- validFrom: null,
2474
- validUntil: null,
3300
+ validFrom: validityWindows && row.validFrom instanceof Date ? row.validFrom.toISOString() : null,
3301
+ validUntil: validityWindows && row.validUntil instanceof Date ? row.validUntil.toISOString() : null,
2475
3302
  publishedChanges: toVersionChanges(row.publishedChanges),
2476
3303
  changeNote: row.changeNote,
2477
3304
  nonRegressive: row.nonRegressive,
@@ -2873,7 +3700,7 @@ PrismaCatalogEntryRepository = _ts_decorate21([
2873
3700
  _ts_param19(0, Inject19(PRISMA_CLIENT_TOKEN)),
2874
3701
  _ts_metadata19("design:type", Function),
2875
3702
  _ts_metadata19("design:paramtypes", [
2876
- typeof PrismaLike === "undefined" ? Object : PrismaLike
3703
+ typeof CatalogEntryRepositoryClient === "undefined" ? Object : CatalogEntryRepositoryClient
2877
3704
  ])
2878
3705
  ], PrismaCatalogEntryRepository);
2879
3706
  function toI18n2(value) {
@@ -3114,7 +3941,7 @@ PrismaMarketingProjectionRepository = _ts_decorate22([
3114
3941
  _ts_param20(0, Inject20(PRISMA_CLIENT_TOKEN)),
3115
3942
  _ts_metadata20("design:type", Function),
3116
3943
  _ts_metadata20("design:paramtypes", [
3117
- typeof PrismaLike === "undefined" ? Object : PrismaLike
3944
+ typeof MarketingProjectionRepositoryClient === "undefined" ? Object : MarketingProjectionRepositoryClient
3118
3945
  ])
3119
3946
  ], PrismaMarketingProjectionRepository);
3120
3947
  function toTopFeatures(value) {
@@ -3204,7 +4031,7 @@ PrismaMarketingSettingsRepository = _ts_decorate23([
3204
4031
  _ts_param21(0, Inject21(PRISMA_CLIENT_TOKEN)),
3205
4032
  _ts_metadata21("design:type", Function),
3206
4033
  _ts_metadata21("design:paramtypes", [
3207
- typeof PrismaLike === "undefined" ? Object : PrismaLike
4034
+ typeof MarketingSettingsRepositoryClient === "undefined" ? Object : MarketingSettingsRepositoryClient
3208
4035
  ])
3209
4036
  ], PrismaMarketingSettingsRepository);
3210
4037
  function toRow2(row) {
@@ -3295,7 +4122,8 @@ var PrismaPromotionRepository = class {
3295
4122
  }
3296
4123
  async update(id, data) {
3297
4124
  if (data.onlyLocales === null) {
3298
- await this.prisma.$executeRaw`
4125
+ const executeRaw = this.prisma.$executeRaw.bind(this.prisma);
4126
+ await executeRaw`
3299
4127
  UPDATE promotions SET "onlyLocales" = NULL, "updatedAt" = NOW() WHERE id = ${id}`;
3300
4128
  }
3301
4129
  const row = await this.db.promotion.update({
@@ -3362,7 +4190,7 @@ PrismaPromotionRepository = _ts_decorate24([
3362
4190
  _ts_param22(0, Inject22(PRISMA_CLIENT_TOKEN)),
3363
4191
  _ts_metadata22("design:type", Function),
3364
4192
  _ts_metadata22("design:paramtypes", [
3365
- typeof PrismaLike === "undefined" ? Object : PrismaLike
4193
+ typeof PromotionRepositoryClient === "undefined" ? Object : PromotionRepositoryClient
3366
4194
  ])
3367
4195
  ], PrismaPromotionRepository);
3368
4196
  function toPromotionValue(value) {
@@ -3670,7 +4498,9 @@ __name(toRecord4, "toRecord");
3670
4498
  export {
3671
4499
  AsyncLocalRlsBypassAdapter,
3672
4500
  PASSWORD_HASHER_TOKEN,
4501
+ PRISMA_BUNDLE_REPOSITORY_OPTIONS,
3673
4502
  PRISMA_CLIENT_TOKEN,
4503
+ PRISMA_SCHEMA_OPTIONS_TOKEN,
3674
4504
  PrismaAuditAdapter,
3675
4505
  PrismaAuditQueryAdapter,
3676
4506
  PrismaAuditStatsAdapter,
@@ -3696,5 +4526,8 @@ export {
3696
4526
  PrismaTransactionRunner,
3697
4527
  ZeroPromoRevenueDeductionAggregator,
3698
4528
  buildActorTag,
3699
- prismaPersistence
4529
+ createPrismaPlanBindingResolver,
4530
+ getPrismaDelegate,
4531
+ prismaPersistence,
4532
+ resolvePrismaSchemaOptions
3700
4533
  };