@saasicat/persistence-testing 1.0.0-rc.6 → 1.0.0-rc.8

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/README.md CHANGED
@@ -46,7 +46,6 @@ import { persistenceAdapterContract } from '@saasicat/persistence-testing';
46
46
 
47
47
  persistenceAdapterContract({
48
48
  name: 'adapter-drizzle @ postgres',
49
- projectKey: 'my-app',
50
49
  create: async () => ({
51
50
  adapter: {
52
51
  capabilities: { transactions: true, pessimisticLocking: true /* … */ },
@@ -81,4 +80,5 @@ scenarios gate off, visibly).
81
80
  ## Next
82
81
 
83
82
  - [Ports and adapters](../../docs/explanation/adr/0007-ports-and-adapters.md) — what a port promises
84
- - [Test coverage](../../docs/explanation/test-coverage.md) — what this contract reaches, and what it does not
83
+ - [Test coverage](../../docs/explanation/test-coverage.md) — what this contract reaches, and what it
84
+ does not
package/dist/.build-stamp CHANGED
@@ -1 +1 @@
1
- ebc7b4a96e9a57e93d8e7e38f708e2c44f7953784e74366e0391473dd2908f3f
1
+ 88264ec951b4befd31dea5d94e1cd92fe5953dca67eb08539e2e0f1d2453fabb
package/dist/index.cjs CHANGED
@@ -260,11 +260,10 @@ function persistenceAdapterContract(options) {
260
260
  return;
261
261
  }
262
262
  const plan = await repository.create({
263
- projectKey: options.projectKey,
264
263
  planKey: "STANDARD",
265
264
  label: "Standard"
266
265
  });
267
- import_strict.default.equal(plan.projectKey, options.projectKey);
266
+ import_strict.default.equal(plan.planKey, "STANDARD");
268
267
  const firstDraft = await repository.createPlanVersionDraft({
269
268
  planId: "STANDARD",
270
269
  features: ["CORE"],
@@ -325,11 +324,10 @@ function persistenceAdapterContract(options) {
325
324
  return;
326
325
  }
327
326
  const bundle = await repository.create({
328
- projectKey: options.projectKey,
329
327
  bundleKey: "REPORTING",
330
328
  label: "Reporting"
331
329
  });
332
- import_strict.default.equal(bundle.projectKey, options.projectKey);
330
+ import_strict.default.equal(bundle.bundleKey, "REPORTING");
333
331
  const firstDraft = await repository.createDraft({
334
332
  bundleId: bundle.id,
335
333
  features: ["REPORTS"],
@@ -377,6 +375,375 @@ function persistenceAdapterContract(options) {
377
375
  second.id
378
376
  );
379
377
  });
378
+ (0, import_node_test.test)("a booking keeps the rhythm and the window it was made in", async (t) => {
379
+ const repository = harness.adapter.subscriptionBundleRepository;
380
+ const { seed } = harness;
381
+ if (!repository || !seed.createBundleVersion) {
382
+ t.skip("adapter provides no SubscriptionBundleRepository or bundle catalog");
383
+ return;
384
+ }
385
+ const { planVersionId } = await seed.createPlanVersion({
386
+ planKey: "PRO",
387
+ version: 1,
388
+ quotas: {},
389
+ features: ["CORE"],
390
+ published: true
391
+ });
392
+ const { subscriptionId } = await seed.createSubscription({
393
+ tenantId: "tenant-bundle-period",
394
+ plan: "PRO",
395
+ planVersionId,
396
+ billingCycle: "YEARLY"
397
+ });
398
+ const { bundleVersionId } = await seed.createBundleVersion({
399
+ bundleKey: "ANALYTICS",
400
+ features: ["REPORTS"]
401
+ });
402
+ const booked = await repository.add({
403
+ subscriptionId,
404
+ bundleVersionId,
405
+ startedAt: /* @__PURE__ */ new Date("2026-02-21T00:00:00.000Z"),
406
+ minimumTermEndsAt: null,
407
+ billingCycle: "MONTHLY",
408
+ currentPeriodStart: /* @__PURE__ */ new Date("2026-02-21T00:00:00.000Z"),
409
+ currentPeriodEnd: /* @__PURE__ */ new Date("2026-02-28T00:00:00.000Z")
410
+ });
411
+ import_strict.default.equal(booked.billingCycle, "MONTHLY");
412
+ const [readBack] = await repository.listBySubscription(subscriptionId);
413
+ import_strict.default.ok(readBack, "the booking must be readable back");
414
+ import_strict.default.equal(readBack.billingCycle, "MONTHLY");
415
+ import_strict.default.equal(
416
+ readBack.currentPeriodEnd?.toISOString(),
417
+ "2026-02-28T00:00:00.000Z",
418
+ "the period end must survive the round trip"
419
+ );
420
+ import_strict.default.equal(readBack.currentPeriodStart?.toISOString(), "2026-02-21T00:00:00.000Z");
421
+ const legacy = await repository.add({
422
+ subscriptionId,
423
+ bundleVersionId,
424
+ startedAt: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
425
+ minimumTermEndsAt: null
426
+ });
427
+ const legacyReadBack = await repository.findById(legacy.id);
428
+ import_strict.default.ok(legacyReadBack, "the legacy booking must be readable back");
429
+ import_strict.default.equal(legacyReadBack.billingCycle, null);
430
+ import_strict.default.equal(legacyReadBack.currentPeriodStart, null);
431
+ import_strict.default.equal(legacyReadBack.currentPeriodEnd, null);
432
+ });
433
+ (0, import_node_test.test)("a second cancellation of one booking is refused, not applied", async (t) => {
434
+ const repository = harness.adapter.subscriptionBundleRepository;
435
+ const { seed } = harness;
436
+ if (!repository || !seed.createBundleVersion) {
437
+ t.skip("adapter provides no SubscriptionBundleRepository or bundle catalog");
438
+ return;
439
+ }
440
+ const { planVersionId } = await seed.createPlanVersion({
441
+ planKey: "PRO",
442
+ version: 1,
443
+ quotas: {},
444
+ features: ["CORE"],
445
+ published: true
446
+ });
447
+ const { subscriptionId } = await seed.createSubscription({
448
+ tenantId: "tenant-double-cancel",
449
+ plan: "PRO",
450
+ planVersionId
451
+ });
452
+ const { bundleVersionId } = await seed.createBundleVersion({
453
+ bundleKey: "ANALYTICS",
454
+ features: ["REPORTS"]
455
+ });
456
+ const booking = await repository.add({
457
+ subscriptionId,
458
+ bundleVersionId,
459
+ startedAt: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
460
+ minimumTermEndsAt: null
461
+ });
462
+ const first = await repository.cancel(booking.id, {
463
+ canceledAt: /* @__PURE__ */ new Date("2026-03-01T00:00:00.000Z"),
464
+ canceledEffectiveAt: /* @__PURE__ */ new Date("2026-04-01T00:00:00.000Z")
465
+ });
466
+ import_strict.default.equal(first.canceledEffectiveAt?.toISOString(), "2026-04-01T00:00:00.000Z");
467
+ await import_strict.default.rejects(
468
+ () => repository.cancel(booking.id, {
469
+ canceledAt: /* @__PURE__ */ new Date("2026-03-02T00:00:00.000Z"),
470
+ canceledEffectiveAt: /* @__PURE__ */ new Date("2026-09-01T00:00:00.000Z")
471
+ }),
472
+ "a second cancellation must be refused"
473
+ );
474
+ const readBack = await repository.findById(booking.id);
475
+ import_strict.default.equal(
476
+ readBack?.canceledEffectiveAt?.toISOString(),
477
+ "2026-04-01T00:00:00.000Z",
478
+ "the first cancellation must still stand"
479
+ );
480
+ await repository.reactivate(booking.id);
481
+ const again = await repository.cancel(booking.id, {
482
+ canceledAt: /* @__PURE__ */ new Date("2026-03-02T00:00:00.000Z"),
483
+ canceledEffectiveAt: /* @__PURE__ */ new Date("2026-09-01T00:00:00.000Z")
484
+ });
485
+ import_strict.default.equal(again.canceledEffectiveAt?.toISOString(), "2026-09-01T00:00:00.000Z");
486
+ });
487
+ (0, import_node_test.test)("a subscription's bookings come back newest first", async (t) => {
488
+ const repository = harness.adapter.subscriptionBundleRepository;
489
+ const { seed } = harness;
490
+ if (!repository || !seed.createBundleVersion) {
491
+ t.skip("adapter provides no SubscriptionBundleRepository or bundle catalog");
492
+ return;
493
+ }
494
+ const { planVersionId } = await seed.createPlanVersion({
495
+ planKey: "PRO",
496
+ version: 1,
497
+ quotas: {},
498
+ features: ["CORE"],
499
+ published: true
500
+ });
501
+ const { subscriptionId } = await seed.createSubscription({
502
+ tenantId: "tenant-booking-order",
503
+ plan: "PRO",
504
+ planVersionId
505
+ });
506
+ for (const [key, startedAt] of [
507
+ ["OLDEST", "2026-01-01T00:00:00.000Z"],
508
+ ["MIDDLE", "2026-02-01T00:00:00.000Z"],
509
+ ["NEWEST", "2026-03-01T00:00:00.000Z"]
510
+ ]) {
511
+ const { bundleVersionId } = await seed.createBundleVersion({
512
+ bundleKey: key,
513
+ features: ["REPORTS"]
514
+ });
515
+ await repository.add({
516
+ subscriptionId,
517
+ bundleVersionId,
518
+ startedAt: new Date(startedAt),
519
+ minimumTermEndsAt: null
520
+ });
521
+ }
522
+ const listed = await repository.listBySubscription(subscriptionId);
523
+ import_strict.default.deepEqual(
524
+ listed.map((row) => row.startedAt.toISOString()),
525
+ [
526
+ "2026-03-01T00:00:00.000Z",
527
+ "2026-02-01T00:00:00.000Z",
528
+ "2026-01-01T00:00:00.000Z"
529
+ ]
530
+ );
531
+ });
532
+ (0, import_node_test.test)("a booking with no request date is active, whatever its effective date says", async (t) => {
533
+ const repository = harness.adapter.subscriptionBundleRepository;
534
+ const { seed } = harness;
535
+ if (!repository || !seed.createBundleVersion) {
536
+ t.skip("adapter provides no SubscriptionBundleRepository or bundle catalog");
537
+ return;
538
+ }
539
+ const { planVersionId } = await seed.createPlanVersion({
540
+ planKey: "PRO",
541
+ version: 1,
542
+ quotas: {},
543
+ features: ["CORE"],
544
+ published: true
545
+ });
546
+ const { subscriptionId } = await seed.createSubscription({
547
+ tenantId: "tenant-half-cancelled",
548
+ plan: "PRO",
549
+ planVersionId
550
+ });
551
+ const { bundleVersionId } = await seed.createBundleVersion({
552
+ bundleKey: "ANALYTICS",
553
+ features: ["REPORTS"]
554
+ });
555
+ const booking = await repository.add({
556
+ subscriptionId,
557
+ bundleVersionId,
558
+ startedAt: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
559
+ minimumTermEndsAt: null
560
+ });
561
+ await repository.cancel(booking.id, {
562
+ canceledAt: /* @__PURE__ */ new Date("2026-02-01T00:00:00.000Z"),
563
+ canceledEffectiveAt: /* @__PURE__ */ new Date("2026-03-01T00:00:00.000Z")
564
+ });
565
+ const clearRequestDate = harness.seed.clearBookingRequestDate;
566
+ if (!clearRequestDate) {
567
+ t.skip("adapter harness cannot write the half-cancelled shape");
568
+ return;
569
+ }
570
+ await clearRequestDate(booking.id);
571
+ const active = await repository.listActiveBySubscription(
572
+ subscriptionId,
573
+ /* @__PURE__ */ new Date("2026-06-01T00:00:00.000Z")
574
+ );
575
+ import_strict.default.equal(active.length, 1, "no request date means nobody asked to cancel it");
576
+ import_strict.default.equal(
577
+ await repository.countActiveByBundleVersionId(
578
+ bundleVersionId,
579
+ /* @__PURE__ */ new Date("2026-06-01T00:00:00.000Z")
580
+ ),
581
+ 1
582
+ );
583
+ });
584
+ (0, import_node_test.test)("discarding a draft cannot remove a version published meanwhile", async (t) => {
585
+ const catalog = harness.adapter.bundleRepository;
586
+ const discardDraft = catalog?.deleteDraft?.bind(catalog);
587
+ if (!catalog || !discardDraft) {
588
+ t.skip("adapter provides no BundleRepository");
589
+ return;
590
+ }
591
+ const bundle = await catalog.create({
592
+ bundleKey: "RACE",
593
+ label: "Race"
594
+ });
595
+ const draft = await catalog.createDraft({
596
+ bundleId: bundle.id,
597
+ features: ["REPORTS"],
598
+ quotas: {}
599
+ });
600
+ await catalog.publishDraft(draft.id, {
601
+ publishedByUserId: null,
602
+ publishedChanges: [],
603
+ nonRegressive: true,
604
+ validFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
605
+ validUntil: null
606
+ });
607
+ await import_strict.default.rejects(
608
+ () => discardDraft(draft.id),
609
+ "a published version must not be discardable"
610
+ );
611
+ import_strict.default.ok(
612
+ await catalog.findVersionById(draft.id),
613
+ "and it must still be there afterwards"
614
+ );
615
+ });
616
+ (0, import_node_test.test)("publishing one draft twice claims it once, and the windows stay adjacent", async (t) => {
617
+ const catalog = harness.adapter.bundleRepository;
618
+ const publish = catalog?.publishDraft?.bind(catalog);
619
+ if (!catalog || !publish) {
620
+ t.skip("adapter provides no BundleRepository");
621
+ return;
622
+ }
623
+ const bundle = await catalog.create({
624
+ bundleKey: "CLAIM",
625
+ label: "Claim"
626
+ });
627
+ const first = await catalog.createDraft({
628
+ bundleId: bundle.id,
629
+ features: ["A"],
630
+ quotas: {}
631
+ });
632
+ await publish(first.id, {
633
+ publishedByUserId: null,
634
+ publishedChanges: [],
635
+ nonRegressive: true,
636
+ validFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
637
+ validUntil: null
638
+ });
639
+ const second = await catalog.createDraft({
640
+ bundleId: bundle.id,
641
+ baseVersionId: first.id,
642
+ features: ["A", "B"],
643
+ quotas: {}
644
+ });
645
+ const publishedAt = /* @__PURE__ */ new Date("2026-03-01T00:00:00.000Z");
646
+ await publish(second.id, {
647
+ publishedByUserId: null,
648
+ publishedChanges: [],
649
+ nonRegressive: true,
650
+ validFrom: publishedAt,
651
+ validUntil: null
652
+ });
653
+ await import_strict.default.rejects(
654
+ () => publish(second.id, {
655
+ publishedByUserId: null,
656
+ publishedChanges: [],
657
+ nonRegressive: true,
658
+ validFrom: /* @__PURE__ */ new Date("2026-06-01T00:00:00.000Z"),
659
+ validUntil: null
660
+ }),
661
+ "a version that is already published must not be published again"
662
+ );
663
+ const successor = await catalog.findVersionById(second.id);
664
+ const predecessor = await catalog.findVersionById(first.id);
665
+ import_strict.default.equal(
666
+ successor?.validFrom && new Date(successor.validFrom).toISOString(),
667
+ publishedAt.toISOString(),
668
+ "the winning date must still stand"
669
+ );
670
+ import_strict.default.ok(predecessor?.supersededAt, "the predecessor must be superseded");
671
+ if (predecessor?.validUntil) {
672
+ const closesAt = new Date(predecessor.validUntil);
673
+ import_strict.default.equal(
674
+ closesAt.toISOString().slice(0, 10),
675
+ "2026-02-28",
676
+ "the predecessor must close the day before its successor opens"
677
+ );
678
+ }
679
+ });
680
+ (0, import_node_test.test)("a plan key names one plan for the whole installation", async (t) => {
681
+ const repository = harness.adapter.planRepository;
682
+ if (!repository) {
683
+ t.skip("adapter provides no PlanRepository");
684
+ return;
685
+ }
686
+ await repository.create({ planKey: "DOUBLE", label: "First" });
687
+ await import_strict.default.rejects(
688
+ () => repository.create({ planKey: "DOUBLE", label: "Second" }),
689
+ "a plan key is taken once"
690
+ );
691
+ });
692
+ (0, import_node_test.test)("a bundle key names one bundle for the whole installation", async (t) => {
693
+ const catalog = harness.adapter.bundleRepository;
694
+ if (!catalog) {
695
+ t.skip("adapter provides no BundleRepository");
696
+ return;
697
+ }
698
+ await catalog.create({ bundleKey: "DOUBLE", label: "First" });
699
+ await import_strict.default.rejects(
700
+ () => catalog.create({ bundleKey: "DOUBLE", label: "Second" }),
701
+ "a bundle key is taken once"
702
+ );
703
+ });
704
+ (0, import_node_test.test)("a retired plan still occupies its key", async (t) => {
705
+ const repository = harness.adapter.planRepository;
706
+ const retire = repository?.softDelete?.bind(repository);
707
+ const byKey = repository?.findByKey?.bind(repository);
708
+ if (!repository || !retire || !byKey) {
709
+ t.skip("adapter provides no PlanRepository");
710
+ return;
711
+ }
712
+ const plan = await repository.create({ planKey: "RETIRED_PLAN", label: "Retired" });
713
+ await retire(plan.id);
714
+ const stillThere = await byKey("RETIRED_PLAN");
715
+ import_strict.default.equal(stillThere?.id, plan.id, "the key is not free again");
716
+ import_strict.default.ok(stillThere?.deletedAt, "and the row says it is retired");
717
+ import_strict.default.equal(
718
+ (await repository.list({})).some((row) => row.id === plan.id),
719
+ false,
720
+ "a retired plan is not in the catalogue an operator browses"
721
+ );
722
+ });
723
+ (0, import_node_test.test)("a retired bundle still occupies its key", async (t) => {
724
+ const catalog = harness.adapter.bundleRepository;
725
+ const retire = catalog?.softDelete?.bind(catalog);
726
+ const byKey = catalog?.findByKey?.bind(catalog);
727
+ if (!catalog || !retire || !byKey) {
728
+ t.skip("adapter provides no BundleRepository");
729
+ return;
730
+ }
731
+ const bundle = await catalog.create({
732
+ bundleKey: "RETIRED_KEY",
733
+ label: "Retired"
734
+ });
735
+ import_strict.default.equal((await byKey("RETIRED_KEY"))?.id, bundle.id);
736
+ await retire(bundle.id);
737
+ const stillThere = await byKey("RETIRED_KEY");
738
+ import_strict.default.equal(stillThere?.id, bundle.id, "the key is not free again");
739
+ import_strict.default.ok(stillThere?.deletedAt, "and the row says it is retired");
740
+ const listed = await catalog.list({});
741
+ import_strict.default.equal(
742
+ listed.some((row) => row.id === bundle.id),
743
+ false,
744
+ "a retired bundle is not in the catalogue an operator browses"
745
+ );
746
+ });
380
747
  (0, import_node_test.test)("countByPlanVersionId counts current AND pending bindings in one query", async (t) => {
381
748
  const { seed, adapter } = harness;
382
749
  if (!adapter.subscriptionRepository.countByPlanVersionId) {
@@ -747,13 +1114,245 @@ function persistenceAdapterContract(options) {
747
1114
  import_strict.default.equal(seen?.id, subscriptionId);
748
1115
  import_strict.default.equal(seen?.tenantId, "tenant-a");
749
1116
  });
750
- (0, import_node_test.test)("immutable subscription contracts (append-only, terminate-only)", (t) => {
751
- if (!harness.adapter.subscriptionContractRepository) {
752
- t.skip("adapter provides no SubscriptionContractRepository \u2014 scenario pending");
1117
+ (0, import_node_test.test)("a contract keeps what was agreed, and ending it does not rewrite it", async (t) => {
1118
+ const contracts = harness.adapter.subscriptionContractRepository;
1119
+ if (!contracts) {
1120
+ t.skip("adapter provides no SubscriptionContractRepository");
1121
+ return;
1122
+ }
1123
+ const tenantId = "tenant-contract-lifecycle";
1124
+ const signedAt = /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z");
1125
+ const created = await contracts.create({
1126
+ tenantId,
1127
+ effectiveFrom: signedAt,
1128
+ priceSnapshot: {
1129
+ currency: "EUR",
1130
+ billingCycle: "monthly",
1131
+ subtotalNet: 29.9,
1132
+ discountNet: 0,
1133
+ totalNet: 29.9,
1134
+ vatRate: 19,
1135
+ totalGross: 35.58
1136
+ },
1137
+ entitlementSnapshot: {
1138
+ plan: "STANDARD",
1139
+ features: ["CORE"],
1140
+ quotas: { users: 5 }
1141
+ },
1142
+ originalBundleVersionIds: ["bundle-version-1"],
1143
+ termsSnapshot: { noticePeriodDays: 30 },
1144
+ lineItems: [
1145
+ {
1146
+ kind: "plan",
1147
+ sourceKey: "STANDARD",
1148
+ sourceVersionId: "plan-version-1",
1149
+ titleSnapshot: "Standard",
1150
+ descriptionSnapshot: "The plan as it was signed",
1151
+ quantity: 1,
1152
+ unit: null,
1153
+ priceNet: 19.9,
1154
+ priceGross: 23.68,
1155
+ billingCycle: "monthly",
1156
+ minimumTermUntil: /* @__PURE__ */ new Date("2027-01-01T00:00:00.000Z"),
1157
+ featuresSnapshot: ["CORE"],
1158
+ quotaEffectsSnapshot: { users: 5 },
1159
+ metadata: { origin: "onboarding" }
1160
+ },
1161
+ {
1162
+ kind: "bundle",
1163
+ sourceKey: "EXTRA-SEATS",
1164
+ sourceVersionId: "bundle-version-1",
1165
+ titleSnapshot: "Extra seats",
1166
+ descriptionSnapshot: null,
1167
+ quantity: 1,
1168
+ unit: "seat",
1169
+ priceNet: 10,
1170
+ priceGross: 11.9,
1171
+ billingCycle: "monthly",
1172
+ minimumTermUntil: null,
1173
+ featuresSnapshot: [],
1174
+ quotaEffectsSnapshot: { users: 5 },
1175
+ metadata: null
1176
+ }
1177
+ ]
1178
+ });
1179
+ import_strict.default.equal(created.status, "active");
1180
+ import_strict.default.equal(created.tenantId, tenantId);
1181
+ import_strict.default.equal(created.lineItems.length, 2);
1182
+ import_strict.default.deepEqual(created.originalBundleVersionIds, ["bundle-version-1"]);
1183
+ import_strict.default.deepEqual(created.termsSnapshot, { noticePeriodDays: 30 });
1184
+ const planLine = created.lineItems.find((item) => item.kind === "plan");
1185
+ import_strict.default.ok(planLine, "plan line expected");
1186
+ import_strict.default.equal(planLine.priceNet, 19.9, "money must survive the round trip unrounded");
1187
+ import_strict.default.equal(planLine.priceGross, 23.68);
1188
+ import_strict.default.equal(planLine.billingCycle, "monthly");
1189
+ import_strict.default.deepEqual(planLine.quotaEffectsSnapshot, { users: 5 });
1190
+ import_strict.default.equal(planLine.descriptionSnapshot, "The plan as it was signed");
1191
+ import_strict.default.equal(
1192
+ planLine.minimumTermUntil?.getTime(),
1193
+ (/* @__PURE__ */ new Date("2027-01-01T00:00:00.000Z")).getTime(),
1194
+ "the commitment is part of what was agreed"
1195
+ );
1196
+ import_strict.default.deepEqual(planLine.metadata, { origin: "onboarding" });
1197
+ const bundleLine = created.lineItems.find((item) => item.kind === "bundle");
1198
+ import_strict.default.ok(bundleLine, "bundle line expected");
1199
+ import_strict.default.equal(bundleLine.descriptionSnapshot, null);
1200
+ import_strict.default.equal(bundleLine.unit, "seat");
1201
+ import_strict.default.equal(bundleLine.minimumTermUntil, null);
1202
+ import_strict.default.equal(bundleLine.metadata, null);
1203
+ const readBack = await contracts.findById(created.id);
1204
+ import_strict.default.ok(readBack, "contract expected by id");
1205
+ import_strict.default.equal(
1206
+ readBack.lineItems.length,
1207
+ 2,
1208
+ "lines belong to the contract, not the call"
1209
+ );
1210
+ import_strict.default.equal(
1211
+ (await contracts.findActiveByTenantId(
1212
+ tenantId,
1213
+ /* @__PURE__ */ new Date("2026-06-01T00:00:00.000Z")
1214
+ ))?.id,
1215
+ created.id
1216
+ );
1217
+ import_strict.default.equal(
1218
+ await contracts.findActiveByTenantId(
1219
+ tenantId,
1220
+ /* @__PURE__ */ new Date("2025-12-31T23:59:59.999Z")
1221
+ ),
1222
+ null,
1223
+ "a contract is not active before it starts"
1224
+ );
1225
+ const endsAt = /* @__PURE__ */ new Date("2026-07-01T00:00:00.000Z");
1226
+ const terminated = await contracts.terminate(created.id, {
1227
+ effectiveUntil: endsAt,
1228
+ status: null
1229
+ });
1230
+ import_strict.default.equal(
1231
+ terminated.status,
1232
+ "active",
1233
+ "a null status leaves the contract in the state it had"
1234
+ );
1235
+ import_strict.default.equal(terminated.effectiveUntil?.getTime(), endsAt.getTime());
1236
+ import_strict.default.equal(terminated.lineItems.length, 2, "ending a contract keeps its lines");
1237
+ import_strict.default.equal(
1238
+ terminated.priceSnapshot.totalNet,
1239
+ created.priceSnapshot.totalNet,
1240
+ "ending a contract does not restate its price"
1241
+ );
1242
+ const afterEnd = await contracts.findById(created.id);
1243
+ import_strict.default.ok(afterEnd, "a terminated contract is still readable");
1244
+ import_strict.default.equal(afterEnd.lineItems.length, 2);
1245
+ import_strict.default.equal(
1246
+ (await contracts.findActiveByTenantId(
1247
+ tenantId,
1248
+ /* @__PURE__ */ new Date("2026-06-30T00:00:00.000Z")
1249
+ ))?.id,
1250
+ created.id,
1251
+ "active up to the moment it ends"
1252
+ );
1253
+ import_strict.default.equal(
1254
+ await contracts.findActiveByTenantId(tenantId, endsAt),
1255
+ null,
1256
+ "and not at that moment"
1257
+ );
1258
+ });
1259
+ (0, import_node_test.test)("a successor takes over without erasing the contract it replaces", async (t) => {
1260
+ const contracts = harness.adapter.subscriptionContractRepository;
1261
+ if (!contracts) {
1262
+ t.skip("adapter provides no SubscriptionContractRepository");
753
1263
  return;
754
1264
  }
755
- import_strict.default.fail(
756
- "SubscriptionContractRepository present but the contract kit has no scenario yet \u2014 extend the kit"
1265
+ const tenantId = "tenant-contract-succession";
1266
+ const handover = /* @__PURE__ */ new Date("2026-04-01T00:00:00.000Z");
1267
+ const lineAt = (priceNet, priceGross) => ({
1268
+ kind: "plan",
1269
+ sourceKey: "STANDARD",
1270
+ sourceVersionId: null,
1271
+ titleSnapshot: "Standard",
1272
+ descriptionSnapshot: null,
1273
+ quantity: 1,
1274
+ unit: null,
1275
+ priceNet,
1276
+ priceGross,
1277
+ billingCycle: "monthly",
1278
+ minimumTermUntil: null,
1279
+ featuresSnapshot: [],
1280
+ quotaEffectsSnapshot: {},
1281
+ metadata: null
1282
+ });
1283
+ const priceAt = (net, gross) => ({
1284
+ currency: "EUR",
1285
+ billingCycle: "monthly",
1286
+ subtotalNet: net,
1287
+ discountNet: 0,
1288
+ totalNet: net,
1289
+ vatRate: 19,
1290
+ totalGross: gross
1291
+ });
1292
+ const first = await contracts.create({
1293
+ tenantId,
1294
+ effectiveFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
1295
+ priceSnapshot: priceAt(19.9, 23.68),
1296
+ lineItems: [lineAt(19.9, 23.68)]
1297
+ });
1298
+ import_strict.default.equal(
1299
+ (await contracts.findActiveByTenantId(
1300
+ tenantId,
1301
+ /* @__PURE__ */ new Date("2026-03-31T00:00:00.000Z")
1302
+ ))?.id,
1303
+ first.id
1304
+ );
1305
+ await contracts.terminate(first.id, {
1306
+ effectiveUntil: handover,
1307
+ status: "superseded"
1308
+ });
1309
+ const second = await contracts.create({
1310
+ tenantId,
1311
+ effectiveFrom: handover,
1312
+ priceSnapshot: priceAt(24.9, 29.63),
1313
+ lineItems: [lineAt(24.9, 29.63)]
1314
+ });
1315
+ import_strict.default.equal(
1316
+ (await contracts.findActiveByTenantId(tenantId, handover))?.id,
1317
+ second.id,
1318
+ "the successor takes over at the moment the predecessor ends"
1319
+ );
1320
+ import_strict.default.equal(
1321
+ await contracts.findActiveByTenantId(
1322
+ tenantId,
1323
+ /* @__PURE__ */ new Date("2026-03-31T00:00:00.000Z")
1324
+ ),
1325
+ null,
1326
+ "a superseded contract is not live at any asOf"
1327
+ );
1328
+ const superseded = await contracts.findById(first.id);
1329
+ import_strict.default.equal(superseded?.status, "superseded");
1330
+ import_strict.default.equal(
1331
+ superseded?.priceSnapshot.totalNet,
1332
+ 19.9,
1333
+ "the replaced contract keeps the price it was signed at"
1334
+ );
1335
+ const history = await contracts.list({ tenantId });
1336
+ import_strict.default.equal(history.length, 2, "both contracts remain in the history");
1337
+ import_strict.default.deepEqual(
1338
+ history.map((contract) => contract.lineItems.map((item) => item.priceNet)),
1339
+ [[24.9], [19.9]]
1340
+ );
1341
+ import_strict.default.deepEqual(
1342
+ history.map((contract) => contract.id),
1343
+ [second.id, first.id],
1344
+ "newest first"
1345
+ );
1346
+ import_strict.default.deepEqual(
1347
+ (await contracts.list({ tenantId, asOf: /* @__PURE__ */ new Date("2026-03-31T00:00:00.000Z") })).map((contract) => contract.id),
1348
+ [first.id],
1349
+ "asOf narrows the history to what was in force then"
1350
+ );
1351
+ import_strict.default.deepEqual(
1352
+ (await contracts.list({ tenantId, status: "superseded" })).map(
1353
+ (contract) => contract.id
1354
+ ),
1355
+ [first.id]
757
1356
  );
758
1357
  });
759
1358
  });