@saasicat/persistence-testing 1.0.0-rc.13 → 1.0.0-rc.14
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 +33 -10
- package/dist/.build-stamp +1 -1
- package/dist/index.cjs +255 -44
- package/dist/index.d.cts +18 -3
- package/dist/index.d.ts +18 -3
- package/dist/index.js +255 -44
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -27,10 +27,20 @@ Verified scenarios:
|
|
|
27
27
|
concurrently — with the change and the record it supersedes landing together;
|
|
28
28
|
changes listed in the order they were recorded, acknowledged once
|
|
29
29
|
|
|
30
|
-
Scenario groups gate on declared capabilities and provided slices
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
Scenario groups gate on declared capabilities and provided slices. A group the
|
|
31
|
+
capabilities rule out, such as the lock scenarios with `pessimisticLocking:
|
|
32
|
+
false`, reports as **skipped with reason**. A group whose port or seed writer
|
|
33
|
+
the harness does not provide **fails**, unless the adapter names it in `gaps` —
|
|
34
|
+
then it reports as skipped. A gap named there that the harness does provide
|
|
35
|
+
fails the suite, so the list stays true. A skipped scenario is easy to read
|
|
36
|
+
past in a green run; a harness that forgot to wire a port would otherwise pass
|
|
37
|
+
without checking it.
|
|
38
|
+
|
|
39
|
+
The list describes the harness as it is built, not the adapter package. Where a
|
|
40
|
+
port adds a member only under an option — `@saasicat/adapter-prisma`'s
|
|
41
|
+
`validityWindows` and `atomicOnboardingSelection`, off by default for a 0.6
|
|
42
|
+
schema — compute `gaps` from the same option rather than writing a constant, so
|
|
43
|
+
the declaration moves when the schema does.
|
|
34
44
|
|
|
35
45
|
## What this is not
|
|
36
46
|
|
|
@@ -49,26 +59,39 @@ find one.
|
|
|
49
59
|
import { persistenceAdapterContract } from '@saasicat/persistence-testing';
|
|
50
60
|
|
|
51
61
|
persistenceAdapterContract({
|
|
52
|
-
name: 'adapter
|
|
62
|
+
name: 'my-adapter @ postgres',
|
|
53
63
|
create: async () => ({
|
|
54
64
|
adapter: {
|
|
55
65
|
capabilities: { transactions: true, pessimisticLocking: true /* … */ },
|
|
56
66
|
transactionRunner,
|
|
57
67
|
subscriptionRepository,
|
|
58
68
|
planVersionRepository,
|
|
59
|
-
|
|
69
|
+
planRepository,
|
|
70
|
+
bundleRepository,
|
|
71
|
+
subscriptionBundleRepository,
|
|
72
|
+
tenantSubscriptionWrite,
|
|
73
|
+
promoCodeRepository,
|
|
60
74
|
promoCodeRedemptionRepository,
|
|
75
|
+
promoSubscriptionLookup,
|
|
61
76
|
mfa,
|
|
62
77
|
audit,
|
|
63
78
|
auditQuery,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
// Leave a part out and name it in `gaps` below; left out and not
|
|
80
|
+
// named, its scenarios fail.
|
|
81
|
+
},
|
|
82
|
+
seed: {
|
|
83
|
+
createPlanVersion,
|
|
84
|
+
createSubscription,
|
|
85
|
+
createBundleVersion,
|
|
86
|
+
clearBookingRequestDate,
|
|
87
|
+
createPromoCode,
|
|
67
88
|
},
|
|
68
|
-
seed: { createPlanVersion, createSubscription, createPromoCode },
|
|
69
89
|
reset: () => truncatePlatformTables(),
|
|
70
90
|
close: () => pool.end(),
|
|
71
91
|
}),
|
|
92
|
+
// The parts this adapter deliberately does not provide. A part named here
|
|
93
|
+
// that the harness does provide fails the suite as well.
|
|
94
|
+
gaps: ['subscriptionContracts', 'appliedSettings'],
|
|
72
95
|
});
|
|
73
96
|
```
|
|
74
97
|
|
package/dist/.build-stamp
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
fe1ab043363d0764aaf580f2167335fcb0c2659c445bcc9540708782cb751a27
|
package/dist/index.cjs
CHANGED
|
@@ -38,12 +38,133 @@ module.exports = __toCommonJS(index_exports);
|
|
|
38
38
|
var import_strict = __toESM(require("node:assert/strict"), 1);
|
|
39
39
|
var import_node_test = require("node:test");
|
|
40
40
|
var LOCK_HOLD_MS = 150;
|
|
41
|
+
var CONTRACT_GAPS = {
|
|
42
|
+
atomicPlanBinding: {
|
|
43
|
+
reason: "adapter does not expose atomic plan-binding writes",
|
|
44
|
+
present: ({ adapter }) => Boolean(adapter.tenantSubscriptionWrite)
|
|
45
|
+
},
|
|
46
|
+
atomicOnboarding: {
|
|
47
|
+
reason: "adapter does not expose atomic onboarding writes",
|
|
48
|
+
present: ({ adapter }) => Boolean(adapter.tenantSubscriptionWrite?.applyOnboardingSelection)
|
|
49
|
+
},
|
|
50
|
+
promoCodes: {
|
|
51
|
+
reason: "adapter provides no PromoCodeRepository",
|
|
52
|
+
present: ({ adapter }) => Boolean(adapter.promoCodeRepository)
|
|
53
|
+
},
|
|
54
|
+
promoCodeRedemptions: {
|
|
55
|
+
reason: "adapter provides no PromoCodeRedemptionRepository",
|
|
56
|
+
present: ({ adapter }) => Boolean(adapter.promoCodeRedemptionRepository)
|
|
57
|
+
},
|
|
58
|
+
promoSubscriptionLookup: {
|
|
59
|
+
reason: "adapter provides no PromoSubscriptionLookup",
|
|
60
|
+
present: ({ adapter }) => Boolean(adapter.promoSubscriptionLookup)
|
|
61
|
+
},
|
|
62
|
+
planRepository: {
|
|
63
|
+
reason: "adapter provides no PlanRepository",
|
|
64
|
+
present: ({ adapter }) => Boolean(adapter.planRepository)
|
|
65
|
+
},
|
|
66
|
+
planLifecycle: {
|
|
67
|
+
reason: "adapter provides no time-aware PlanRepository lifecycle",
|
|
68
|
+
present: ({ adapter }) => {
|
|
69
|
+
const repository = adapter.planRepository;
|
|
70
|
+
return Boolean(
|
|
71
|
+
repository?.createPlanVersionDraft && repository.publishPlanVersionDraft && repository.findVersionById && repository.findActivePlanVersion
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
planRetirement: {
|
|
76
|
+
reason: "adapter provides no PlanRepository that retires and finds plans by key",
|
|
77
|
+
present: ({ adapter }) => Boolean(adapter.planRepository?.softDelete && adapter.planRepository.findByKey)
|
|
78
|
+
},
|
|
79
|
+
planVersionReads: {
|
|
80
|
+
reason: "adapter provides no PlanRepository that reads versions by plan key",
|
|
81
|
+
present: ({ adapter }) => {
|
|
82
|
+
const repository = adapter.planRepository;
|
|
83
|
+
return Boolean(
|
|
84
|
+
repository?.listVersions && repository.findCurrentDraft && repository.findLatestLivePlanVersion
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
planVersionRetirement: {
|
|
89
|
+
reason: "adapter provides no PlanRepository that reads versions and retires plans",
|
|
90
|
+
present: ({ adapter }) => {
|
|
91
|
+
const repository = adapter.planRepository;
|
|
92
|
+
return Boolean(
|
|
93
|
+
repository?.createPlanVersionDraft && repository.publishPlanVersionDraft && repository.listVersions && repository.findCurrentDraft && repository.findLatestLivePlanVersion && repository.softDelete
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
bundleRepository: {
|
|
98
|
+
reason: "adapter provides no BundleRepository",
|
|
99
|
+
present: ({ adapter }) => Boolean(adapter.bundleRepository)
|
|
100
|
+
},
|
|
101
|
+
bundleValidity: {
|
|
102
|
+
reason: "adapter provides no time-aware BundleRepository",
|
|
103
|
+
present: ({ adapter }) => Boolean(adapter.bundleRepository?.findActiveBundleVersion)
|
|
104
|
+
},
|
|
105
|
+
bundleDraftDiscard: {
|
|
106
|
+
reason: "adapter provides no BundleRepository that discards drafts",
|
|
107
|
+
present: ({ adapter }) => Boolean(adapter.bundleRepository?.deleteDraft)
|
|
108
|
+
},
|
|
109
|
+
bundleDraftPublish: {
|
|
110
|
+
reason: "adapter provides no BundleRepository that publishes drafts",
|
|
111
|
+
present: ({ adapter }) => Boolean(adapter.bundleRepository?.publishDraft)
|
|
112
|
+
},
|
|
113
|
+
bundleRetirement: {
|
|
114
|
+
reason: "adapter provides no BundleRepository that retires and finds bundles by key",
|
|
115
|
+
present: ({ adapter }) => Boolean(adapter.bundleRepository?.softDelete && adapter.bundleRepository.findByKey)
|
|
116
|
+
},
|
|
117
|
+
bundleBookings: {
|
|
118
|
+
reason: "adapter provides no SubscriptionBundleRepository or bundle catalog",
|
|
119
|
+
present: ({ adapter, seed }) => Boolean(adapter.subscriptionBundleRepository && seed.createBundleVersion)
|
|
120
|
+
},
|
|
121
|
+
halfCancelledBookingSeed: {
|
|
122
|
+
reason: "adapter harness cannot write the half-cancelled shape",
|
|
123
|
+
present: ({ seed }) => Boolean(seed.clearBookingRequestDate)
|
|
124
|
+
},
|
|
125
|
+
countByPlanVersionId: {
|
|
126
|
+
reason: "adapter does not implement countByPlanVersionId (fail-closed fallback)",
|
|
127
|
+
present: ({ adapter }) => Boolean(adapter.subscriptionRepository.countByPlanVersionId)
|
|
128
|
+
},
|
|
129
|
+
audit: {
|
|
130
|
+
reason: "adapter provides no AuditPort/AuditQueryPort pair",
|
|
131
|
+
present: ({ adapter }) => Boolean(adapter.audit && adapter.auditQuery)
|
|
132
|
+
},
|
|
133
|
+
mfa: {
|
|
134
|
+
reason: "adapter provides no MfaPort",
|
|
135
|
+
present: ({ adapter }) => Boolean(adapter.mfa)
|
|
136
|
+
},
|
|
137
|
+
subscriptionContracts: {
|
|
138
|
+
reason: "adapter provides no SubscriptionContractRepository",
|
|
139
|
+
present: ({ adapter }) => Boolean(adapter.subscriptionContractRepository)
|
|
140
|
+
},
|
|
141
|
+
appliedSettings: {
|
|
142
|
+
reason: "adapter provides no AppliedSettingsPort",
|
|
143
|
+
present: ({ adapter }) => Boolean(adapter.appliedSettings)
|
|
144
|
+
}
|
|
145
|
+
};
|
|
41
146
|
function sleep(ms) {
|
|
42
147
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
43
148
|
}
|
|
44
149
|
function persistenceAdapterContract(options) {
|
|
150
|
+
const declaredGaps = new Set(options.gaps ?? []);
|
|
151
|
+
let harness;
|
|
152
|
+
function missing(t, gap) {
|
|
153
|
+
const { reason, present } = CONTRACT_GAPS[gap];
|
|
154
|
+
if (present(harness)) {
|
|
155
|
+
import_strict.default.fail(
|
|
156
|
+
`'${gap}' counts as provided, yet this scenario found a member it needs missing or not callable. Check that the harness wires the port itself; if it does, the contract's check for '${gap}' and this scenario disagree, which is a defect in @saasicat/persistence-testing.`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (declaredGaps.has(gap)) {
|
|
160
|
+
t.skip(reason);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
import_strict.default.fail(
|
|
164
|
+
`${reason}. Wire it into the harness, or declare \`gaps: ['${gap}']\` in persistenceAdapterContract if the adapter deliberately does not provide it.`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
45
167
|
(0, import_node_test.describe)(`persistence adapter contract: ${options.name}`, () => {
|
|
46
|
-
let harness;
|
|
47
168
|
(0, import_node_test.before)(async () => {
|
|
48
169
|
harness = await options.create();
|
|
49
170
|
});
|
|
@@ -53,6 +174,20 @@ function persistenceAdapterContract(options) {
|
|
|
53
174
|
(0, import_node_test.beforeEach)(async () => {
|
|
54
175
|
await harness.reset();
|
|
55
176
|
});
|
|
177
|
+
(0, import_node_test.test)("the declared gaps are exactly the parts the harness does not provide", () => {
|
|
178
|
+
const gaps = Object.keys(CONTRACT_GAPS);
|
|
179
|
+
const absent = gaps.filter((gap) => !CONTRACT_GAPS[gap].present(harness));
|
|
180
|
+
const known = (gap) => Object.prototype.hasOwnProperty.call(CONTRACT_GAPS, gap);
|
|
181
|
+
const unknown = [...declaredGaps].filter((gap) => !known(gap));
|
|
182
|
+
const stale = [...declaredGaps].filter((gap) => known(gap) && !absent.includes(gap));
|
|
183
|
+
const undeclared = absent.filter((gap) => !declaredGaps.has(gap));
|
|
184
|
+
const problems = [
|
|
185
|
+
unknown.length > 0 && `declared as gaps but not parts of the contract: ${unknown.join(", ")} (ContractGap lists the names)`,
|
|
186
|
+
stale.length > 0 && `declared as gaps but wired into the harness: ${stale.join(", ")}`,
|
|
187
|
+
undeclared.length > 0 && `not wired into the harness and not declared as gaps: ${undeclared.join(", ")}`
|
|
188
|
+
].filter(Boolean);
|
|
189
|
+
import_strict.default.deepEqual(problems, [], problems.join("; "));
|
|
190
|
+
});
|
|
56
191
|
(0, import_node_test.test)("findByTenantId returns the tenant subscription with plan-version limits", async () => {
|
|
57
192
|
const { seed, adapter } = harness;
|
|
58
193
|
const { planVersionId } = await seed.createPlanVersion({
|
|
@@ -127,7 +262,7 @@ function persistenceAdapterContract(options) {
|
|
|
127
262
|
(0, import_node_test.test)("immediate plan change binds plan and active PlanVersion consistently", async (t) => {
|
|
128
263
|
const { seed, adapter } = harness;
|
|
129
264
|
if (!adapter.tenantSubscriptionWrite) {
|
|
130
|
-
t
|
|
265
|
+
missing(t, "atomicPlanBinding");
|
|
131
266
|
return;
|
|
132
267
|
}
|
|
133
268
|
const oldVersion = await seed.createPlanVersion({
|
|
@@ -172,12 +307,12 @@ function persistenceAdapterContract(options) {
|
|
|
172
307
|
const { seed, adapter } = harness;
|
|
173
308
|
const writer = adapter.tenantSubscriptionWrite;
|
|
174
309
|
if (!writer?.applyOnboardingSelection) {
|
|
175
|
-
t
|
|
310
|
+
missing(t, "atomicOnboarding");
|
|
176
311
|
return;
|
|
177
312
|
}
|
|
178
313
|
const redemptions = adapter.promoCodeRedemptionRepository;
|
|
179
314
|
if (!redemptions) {
|
|
180
|
-
t
|
|
315
|
+
missing(t, "promoCodeRedemptions");
|
|
181
316
|
return;
|
|
182
317
|
}
|
|
183
318
|
const oldVersion = await seed.createPlanVersion({
|
|
@@ -256,7 +391,7 @@ function persistenceAdapterContract(options) {
|
|
|
256
391
|
(0, import_node_test.test)("plan lifecycle keeps semantic identity and auto-succeeds validity windows", async (t) => {
|
|
257
392
|
const repository = harness.adapter.planRepository;
|
|
258
393
|
if (!repository?.createPlanVersionDraft || !repository.publishPlanVersionDraft || !repository.findVersionById || !repository.findActivePlanVersion) {
|
|
259
|
-
t
|
|
394
|
+
missing(t, "planLifecycle");
|
|
260
395
|
return;
|
|
261
396
|
}
|
|
262
397
|
const plan = await repository.create({
|
|
@@ -320,7 +455,7 @@ function persistenceAdapterContract(options) {
|
|
|
320
455
|
(0, import_node_test.test)("bundle lifecycle roundtrips validity and auto-succeeds atomically", async (t) => {
|
|
321
456
|
const repository = harness.adapter.bundleRepository;
|
|
322
457
|
if (!repository?.findActiveBundleVersion) {
|
|
323
|
-
t
|
|
458
|
+
missing(t, "bundleValidity");
|
|
324
459
|
return;
|
|
325
460
|
}
|
|
326
461
|
const bundle = await repository.create({
|
|
@@ -379,7 +514,7 @@ function persistenceAdapterContract(options) {
|
|
|
379
514
|
const repository = harness.adapter.subscriptionBundleRepository;
|
|
380
515
|
const { seed } = harness;
|
|
381
516
|
if (!repository || !seed.createBundleVersion) {
|
|
382
|
-
t
|
|
517
|
+
missing(t, "bundleBookings");
|
|
383
518
|
return;
|
|
384
519
|
}
|
|
385
520
|
const { planVersionId } = await seed.createPlanVersion({
|
|
@@ -434,7 +569,7 @@ function persistenceAdapterContract(options) {
|
|
|
434
569
|
const repository = harness.adapter.subscriptionBundleRepository;
|
|
435
570
|
const { seed } = harness;
|
|
436
571
|
if (!repository || !seed.createBundleVersion) {
|
|
437
|
-
t
|
|
572
|
+
missing(t, "bundleBookings");
|
|
438
573
|
return;
|
|
439
574
|
}
|
|
440
575
|
const { planVersionId } = await seed.createPlanVersion({
|
|
@@ -488,7 +623,7 @@ function persistenceAdapterContract(options) {
|
|
|
488
623
|
const repository = harness.adapter.subscriptionBundleRepository;
|
|
489
624
|
const { seed } = harness;
|
|
490
625
|
if (!repository || !seed.createBundleVersion) {
|
|
491
|
-
t
|
|
626
|
+
missing(t, "bundleBookings");
|
|
492
627
|
return;
|
|
493
628
|
}
|
|
494
629
|
const { planVersionId } = await seed.createPlanVersion({
|
|
@@ -533,7 +668,7 @@ function persistenceAdapterContract(options) {
|
|
|
533
668
|
const repository = harness.adapter.subscriptionBundleRepository;
|
|
534
669
|
const { seed } = harness;
|
|
535
670
|
if (!repository || !seed.createBundleVersion) {
|
|
536
|
-
t
|
|
671
|
+
missing(t, "bundleBookings");
|
|
537
672
|
return;
|
|
538
673
|
}
|
|
539
674
|
const { planVersionId } = await seed.createPlanVersion({
|
|
@@ -564,7 +699,7 @@ function persistenceAdapterContract(options) {
|
|
|
564
699
|
});
|
|
565
700
|
const clearRequestDate = harness.seed.clearBookingRequestDate;
|
|
566
701
|
if (!clearRequestDate) {
|
|
567
|
-
t
|
|
702
|
+
missing(t, "halfCancelledBookingSeed");
|
|
568
703
|
return;
|
|
569
704
|
}
|
|
570
705
|
await clearRequestDate(booking.id);
|
|
@@ -585,7 +720,7 @@ function persistenceAdapterContract(options) {
|
|
|
585
720
|
const catalog = harness.adapter.bundleRepository;
|
|
586
721
|
const discardDraft = catalog?.deleteDraft?.bind(catalog);
|
|
587
722
|
if (!catalog || !discardDraft) {
|
|
588
|
-
t
|
|
723
|
+
missing(t, "bundleDraftDiscard");
|
|
589
724
|
return;
|
|
590
725
|
}
|
|
591
726
|
const bundle = await catalog.create({
|
|
@@ -617,7 +752,7 @@ function persistenceAdapterContract(options) {
|
|
|
617
752
|
const catalog = harness.adapter.bundleRepository;
|
|
618
753
|
const publish = catalog?.publishDraft?.bind(catalog);
|
|
619
754
|
if (!catalog || !publish) {
|
|
620
|
-
t
|
|
755
|
+
missing(t, "bundleDraftPublish");
|
|
621
756
|
return;
|
|
622
757
|
}
|
|
623
758
|
const bundle = await catalog.create({
|
|
@@ -680,7 +815,7 @@ function persistenceAdapterContract(options) {
|
|
|
680
815
|
(0, import_node_test.test)("a plan key names one plan for the whole installation", async (t) => {
|
|
681
816
|
const repository = harness.adapter.planRepository;
|
|
682
817
|
if (!repository) {
|
|
683
|
-
t
|
|
818
|
+
missing(t, "planRepository");
|
|
684
819
|
return;
|
|
685
820
|
}
|
|
686
821
|
await repository.create({ planKey: "DOUBLE", label: "First" });
|
|
@@ -692,7 +827,7 @@ function persistenceAdapterContract(options) {
|
|
|
692
827
|
(0, import_node_test.test)("a bundle key names one bundle for the whole installation", async (t) => {
|
|
693
828
|
const catalog = harness.adapter.bundleRepository;
|
|
694
829
|
if (!catalog) {
|
|
695
|
-
t
|
|
830
|
+
missing(t, "bundleRepository");
|
|
696
831
|
return;
|
|
697
832
|
}
|
|
698
833
|
await catalog.create({ bundleKey: "DOUBLE", label: "First" });
|
|
@@ -706,7 +841,7 @@ function persistenceAdapterContract(options) {
|
|
|
706
841
|
const retire = repository?.softDelete?.bind(repository);
|
|
707
842
|
const byKey = repository?.findByKey?.bind(repository);
|
|
708
843
|
if (!repository || !retire || !byKey) {
|
|
709
|
-
t
|
|
844
|
+
missing(t, "planRetirement");
|
|
710
845
|
return;
|
|
711
846
|
}
|
|
712
847
|
const plan = await repository.create({ planKey: "RETIRED_PLAN", label: "Retired" });
|
|
@@ -720,12 +855,90 @@ function persistenceAdapterContract(options) {
|
|
|
720
855
|
"a retired plan is not in the catalogue an operator browses"
|
|
721
856
|
);
|
|
722
857
|
});
|
|
858
|
+
(0, import_node_test.test)("a plan key no plan has finds no versions, rather than failing", async (t) => {
|
|
859
|
+
const repository = harness.adapter.planRepository;
|
|
860
|
+
if (!repository?.listVersions || !repository.findCurrentDraft || !repository.findLatestLivePlanVersion) {
|
|
861
|
+
missing(t, "planVersionReads");
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const entitlementVersions = harness.adapter.planVersionRepository;
|
|
865
|
+
import_strict.default.deepEqual(await repository.listVersions("NO_SUCH_PLAN"), []);
|
|
866
|
+
import_strict.default.equal(await repository.findCurrentDraft("NO_SUCH_PLAN"), null);
|
|
867
|
+
import_strict.default.equal(await repository.findLatestLivePlanVersion("NO_SUCH_PLAN"), null);
|
|
868
|
+
if (repository.findActivePlanVersion) {
|
|
869
|
+
import_strict.default.equal(
|
|
870
|
+
await repository.findActivePlanVersion("NO_SUCH_PLAN", /* @__PURE__ */ new Date()),
|
|
871
|
+
null
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
import_strict.default.equal(await entitlementVersions.findLatestLive("NO_SUCH_PLAN"), null);
|
|
875
|
+
if (entitlementVersions.findActive) {
|
|
876
|
+
import_strict.default.equal(
|
|
877
|
+
await entitlementVersions.findActive("NO_SUCH_PLAN", /* @__PURE__ */ new Date()),
|
|
878
|
+
null
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
(0, import_node_test.test)("retiring a plan hides none of its versions", async (t) => {
|
|
883
|
+
const repository = harness.adapter.planRepository;
|
|
884
|
+
if (!repository?.createPlanVersionDraft || !repository.publishPlanVersionDraft || !repository.listVersions || !repository.findCurrentDraft || !repository.findLatestLivePlanVersion || !repository.softDelete) {
|
|
885
|
+
missing(t, "planVersionRetirement");
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const listVersions = repository.listVersions.bind(repository);
|
|
889
|
+
const findCurrentDraft = repository.findCurrentDraft.bind(repository);
|
|
890
|
+
const findLatestLive = repository.findLatestLivePlanVersion.bind(repository);
|
|
891
|
+
const entitlementVersions = harness.adapter.planVersionRepository;
|
|
892
|
+
const plan = await repository.create({ planKey: "RETIRING", label: "Retiring" });
|
|
893
|
+
const firstDraft = await repository.createPlanVersionDraft({
|
|
894
|
+
planId: "RETIRING",
|
|
895
|
+
features: ["CORE"],
|
|
896
|
+
quotas: { users: 5 },
|
|
897
|
+
monthlyNet: "10.00",
|
|
898
|
+
yearlyNet: "100.00",
|
|
899
|
+
validFrom: "2026-01-01"
|
|
900
|
+
});
|
|
901
|
+
const live = await repository.publishPlanVersionDraft(firstDraft.id, {
|
|
902
|
+
publishedByUserId: null,
|
|
903
|
+
publishedChanges: [],
|
|
904
|
+
nonRegressive: true,
|
|
905
|
+
validFrom: /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z"),
|
|
906
|
+
validUntil: null
|
|
907
|
+
});
|
|
908
|
+
const openDraft = await repository.createPlanVersionDraft({
|
|
909
|
+
planId: "RETIRING",
|
|
910
|
+
baseVersionId: live.id,
|
|
911
|
+
features: ["CORE", "PLUS"],
|
|
912
|
+
quotas: { users: 10 },
|
|
913
|
+
monthlyNet: "15.00",
|
|
914
|
+
yearlyNet: "150.00",
|
|
915
|
+
validFrom: "2026-06-01"
|
|
916
|
+
});
|
|
917
|
+
const reads = async () => ({
|
|
918
|
+
versions: (await listVersions("RETIRING")).map((row) => row.id),
|
|
919
|
+
draft: (await findCurrentDraft("RETIRING"))?.id ?? null,
|
|
920
|
+
latestLive: (await findLatestLive("RETIRING"))?.id ?? null,
|
|
921
|
+
entitlementFeatures: (await entitlementVersions.findLatestLive("RETIRING"))?.features ?? null
|
|
922
|
+
});
|
|
923
|
+
const beforeRetiring = await reads();
|
|
924
|
+
import_strict.default.deepEqual(
|
|
925
|
+
{
|
|
926
|
+
versions: beforeRetiring.versions,
|
|
927
|
+
draft: beforeRetiring.draft,
|
|
928
|
+
latestLive: beforeRetiring.latestLive
|
|
929
|
+
},
|
|
930
|
+
{ versions: [live.id, openDraft.id], draft: openDraft.id, latestLive: live.id },
|
|
931
|
+
"a live plan reads its versions, so the comparison below has a subject"
|
|
932
|
+
);
|
|
933
|
+
await repository.softDelete(plan.id);
|
|
934
|
+
import_strict.default.deepEqual(await reads(), beforeRetiring, "retiring the plan hid its versions");
|
|
935
|
+
});
|
|
723
936
|
(0, import_node_test.test)("a retired bundle still occupies its key", async (t) => {
|
|
724
937
|
const catalog = harness.adapter.bundleRepository;
|
|
725
938
|
const retire = catalog?.softDelete?.bind(catalog);
|
|
726
939
|
const byKey = catalog?.findByKey?.bind(catalog);
|
|
727
940
|
if (!catalog || !retire || !byKey) {
|
|
728
|
-
t
|
|
941
|
+
missing(t, "bundleRetirement");
|
|
729
942
|
return;
|
|
730
943
|
}
|
|
731
944
|
const bundle = await catalog.create({
|
|
@@ -747,7 +960,7 @@ function persistenceAdapterContract(options) {
|
|
|
747
960
|
(0, import_node_test.test)("countByPlanVersionId counts current AND pending bindings in one query", async (t) => {
|
|
748
961
|
const { seed, adapter } = harness;
|
|
749
962
|
if (!adapter.subscriptionRepository.countByPlanVersionId) {
|
|
750
|
-
t
|
|
963
|
+
missing(t, "countByPlanVersionId");
|
|
751
964
|
return;
|
|
752
965
|
}
|
|
753
966
|
const v1 = await seed.createPlanVersion({
|
|
@@ -787,9 +1000,7 @@ function persistenceAdapterContract(options) {
|
|
|
787
1000
|
return;
|
|
788
1001
|
}
|
|
789
1002
|
if (!adapter.promoCodeRedemptionRepository) {
|
|
790
|
-
t
|
|
791
|
-
"adapter provides no PromoCodeRedemptionRepository (needed as tx write probe)"
|
|
792
|
-
);
|
|
1003
|
+
missing(t, "promoCodeRedemptions");
|
|
793
1004
|
return;
|
|
794
1005
|
}
|
|
795
1006
|
const redemptions = adapter.promoCodeRedemptionRepository;
|
|
@@ -872,7 +1083,7 @@ function persistenceAdapterContract(options) {
|
|
|
872
1083
|
(0, import_node_test.test)("concurrent claimSlot grants exactly maxRedemptions slots", async (t) => {
|
|
873
1084
|
const { seed, adapter } = harness;
|
|
874
1085
|
if (!adapter.promoCodeRepository) {
|
|
875
|
-
t
|
|
1086
|
+
missing(t, "promoCodes");
|
|
876
1087
|
return;
|
|
877
1088
|
}
|
|
878
1089
|
const promoCodes = adapter.promoCodeRepository;
|
|
@@ -893,7 +1104,7 @@ function persistenceAdapterContract(options) {
|
|
|
893
1104
|
(0, import_node_test.test)("claimSlot / markExhaustedIfFull / releaseSlot lifecycle", async (t) => {
|
|
894
1105
|
const { seed, adapter } = harness;
|
|
895
1106
|
if (!adapter.promoCodeRepository) {
|
|
896
|
-
t
|
|
1107
|
+
missing(t, "promoCodes");
|
|
897
1108
|
return;
|
|
898
1109
|
}
|
|
899
1110
|
const promoCodes = adapter.promoCodeRepository;
|
|
@@ -913,7 +1124,7 @@ function persistenceAdapterContract(options) {
|
|
|
913
1124
|
(0, import_node_test.test)("a subscription cannot redeem twice (unique guard)", async (t) => {
|
|
914
1125
|
const { seed, adapter } = harness;
|
|
915
1126
|
if (!adapter.promoCodeRedemptionRepository) {
|
|
916
|
-
t
|
|
1127
|
+
missing(t, "promoCodeRedemptions");
|
|
917
1128
|
return;
|
|
918
1129
|
}
|
|
919
1130
|
const redemptions = adapter.promoCodeRedemptionRepository;
|
|
@@ -953,7 +1164,7 @@ function persistenceAdapterContract(options) {
|
|
|
953
1164
|
(0, import_node_test.test)("audit write \u2192 query roundtrip with actorTag filters", async (t) => {
|
|
954
1165
|
const { adapter } = harness;
|
|
955
1166
|
if (!adapter.audit || !adapter.auditQuery) {
|
|
956
|
-
t
|
|
1167
|
+
missing(t, "audit");
|
|
957
1168
|
return;
|
|
958
1169
|
}
|
|
959
1170
|
await adapter.audit.write({
|
|
@@ -993,7 +1204,7 @@ function persistenceAdapterContract(options) {
|
|
|
993
1204
|
(0, import_node_test.test)("MFA secret roundtrip", async (t) => {
|
|
994
1205
|
const { adapter } = harness;
|
|
995
1206
|
if (!adapter.mfa) {
|
|
996
|
-
t
|
|
1207
|
+
missing(t, "mfa");
|
|
997
1208
|
return;
|
|
998
1209
|
}
|
|
999
1210
|
import_strict.default.equal(await adapter.mfa.getSecret("admin-1"), null);
|
|
@@ -1008,7 +1219,7 @@ function persistenceAdapterContract(options) {
|
|
|
1008
1219
|
(0, import_node_test.test)("finds the subscription the id names, not merely a subscription", async (t) => {
|
|
1009
1220
|
const { adapter, seed } = harness;
|
|
1010
1221
|
if (!adapter.promoSubscriptionLookup) {
|
|
1011
|
-
t
|
|
1222
|
+
missing(t, "promoSubscriptionLookup");
|
|
1012
1223
|
return;
|
|
1013
1224
|
}
|
|
1014
1225
|
const { planVersionId } = await seed.createPlanVersion({
|
|
@@ -1037,7 +1248,7 @@ function persistenceAdapterContract(options) {
|
|
|
1037
1248
|
(0, import_node_test.test)("returns null for an id that does not exist", async (t) => {
|
|
1038
1249
|
const { adapter, seed } = harness;
|
|
1039
1250
|
if (!adapter.promoSubscriptionLookup) {
|
|
1040
|
-
t
|
|
1251
|
+
missing(t, "promoSubscriptionLookup");
|
|
1041
1252
|
return;
|
|
1042
1253
|
}
|
|
1043
1254
|
const { planVersionId } = await seed.createPlanVersion({
|
|
@@ -1058,7 +1269,7 @@ function persistenceAdapterContract(options) {
|
|
|
1058
1269
|
(0, import_node_test.test)("carries the fields a promo rule reads: cycle and start date", async (t) => {
|
|
1059
1270
|
const { adapter, seed } = harness;
|
|
1060
1271
|
if (!adapter.promoSubscriptionLookup) {
|
|
1061
|
-
t
|
|
1272
|
+
missing(t, "promoSubscriptionLookup");
|
|
1062
1273
|
return;
|
|
1063
1274
|
}
|
|
1064
1275
|
const { planVersionId } = await seed.createPlanVersion({
|
|
@@ -1093,7 +1304,7 @@ function persistenceAdapterContract(options) {
|
|
|
1093
1304
|
(0, import_node_test.test)("reads inside a transaction, so validation and redemption agree", async (t) => {
|
|
1094
1305
|
const { adapter, seed } = harness;
|
|
1095
1306
|
if (!adapter.promoSubscriptionLookup) {
|
|
1096
|
-
t
|
|
1307
|
+
missing(t, "promoSubscriptionLookup");
|
|
1097
1308
|
return;
|
|
1098
1309
|
}
|
|
1099
1310
|
const { planVersionId } = await seed.createPlanVersion({
|
|
@@ -1117,7 +1328,7 @@ function persistenceAdapterContract(options) {
|
|
|
1117
1328
|
(0, import_node_test.test)("a contract keeps what was agreed, and ending it does not rewrite it", async (t) => {
|
|
1118
1329
|
const contracts = harness.adapter.subscriptionContractRepository;
|
|
1119
1330
|
if (!contracts) {
|
|
1120
|
-
t
|
|
1331
|
+
missing(t, "subscriptionContracts");
|
|
1121
1332
|
return;
|
|
1122
1333
|
}
|
|
1123
1334
|
const tenantId = "tenant-contract-lifecycle";
|
|
@@ -1273,7 +1484,7 @@ function persistenceAdapterContract(options) {
|
|
|
1273
1484
|
(0, import_node_test.test)("a successor takes over without erasing the contract it replaces", async (t) => {
|
|
1274
1485
|
const contracts = harness.adapter.subscriptionContractRepository;
|
|
1275
1486
|
if (!contracts) {
|
|
1276
|
-
t
|
|
1487
|
+
missing(t, "subscriptionContracts");
|
|
1277
1488
|
return;
|
|
1278
1489
|
}
|
|
1279
1490
|
const tenantId = "tenant-contract-succession";
|
|
@@ -1375,7 +1586,7 @@ function persistenceAdapterContract(options) {
|
|
|
1375
1586
|
(0, import_node_test.test)("a line keeps the currency and the tax it was booked with", async (t) => {
|
|
1376
1587
|
const contracts = harness.adapter.subscriptionContractRepository;
|
|
1377
1588
|
if (!contracts) {
|
|
1378
|
-
t
|
|
1589
|
+
missing(t, "subscriptionContracts");
|
|
1379
1590
|
return;
|
|
1380
1591
|
}
|
|
1381
1592
|
const tenantId = "tenant-contract-money-facts";
|
|
@@ -1473,7 +1684,7 @@ function persistenceAdapterContract(options) {
|
|
|
1473
1684
|
(0, import_node_test.test)("no record before the first boot that could write one", async (t) => {
|
|
1474
1685
|
const port = harness.adapter.appliedSettings;
|
|
1475
1686
|
if (!port) {
|
|
1476
|
-
t
|
|
1687
|
+
missing(t, "appliedSettings");
|
|
1477
1688
|
return;
|
|
1478
1689
|
}
|
|
1479
1690
|
import_strict.default.equal(await port.readApplied(), null);
|
|
@@ -1482,7 +1693,7 @@ function persistenceAdapterContract(options) {
|
|
|
1482
1693
|
(0, import_node_test.test)("the record comes back as it was written \u2014 values, source and moment", async (t) => {
|
|
1483
1694
|
const port = harness.adapter.appliedSettings;
|
|
1484
1695
|
if (!port) {
|
|
1485
|
-
t
|
|
1696
|
+
missing(t, "appliedSettings");
|
|
1486
1697
|
return;
|
|
1487
1698
|
}
|
|
1488
1699
|
import_strict.default.equal(await port.writeApplied(applied("sha256-a", FIRST_START), null), true);
|
|
@@ -1496,7 +1707,7 @@ function persistenceAdapterContract(options) {
|
|
|
1496
1707
|
(0, import_node_test.test)("writing again replaces the one row rather than adding a second", async (t) => {
|
|
1497
1708
|
const port = harness.adapter.appliedSettings;
|
|
1498
1709
|
if (!port) {
|
|
1499
|
-
t
|
|
1710
|
+
missing(t, "appliedSettings");
|
|
1500
1711
|
return;
|
|
1501
1712
|
}
|
|
1502
1713
|
await port.writeApplied(applied("sha256-a", FIRST_START), null);
|
|
@@ -1512,7 +1723,7 @@ function persistenceAdapterContract(options) {
|
|
|
1512
1723
|
(0, import_node_test.test)("the first record is written once: a second writer that read none is refused", async (t) => {
|
|
1513
1724
|
const port = harness.adapter.appliedSettings;
|
|
1514
1725
|
if (!port) {
|
|
1515
|
-
t
|
|
1726
|
+
missing(t, "appliedSettings");
|
|
1516
1727
|
return;
|
|
1517
1728
|
}
|
|
1518
1729
|
import_strict.default.equal(await port.writeApplied(applied("sha256-a", FIRST_START), null), true);
|
|
@@ -1522,7 +1733,7 @@ function persistenceAdapterContract(options) {
|
|
|
1522
1733
|
(0, import_node_test.test)("a write guarded on a fingerprint the row no longer carries is refused", async (t) => {
|
|
1523
1734
|
const port = harness.adapter.appliedSettings;
|
|
1524
1735
|
if (!port) {
|
|
1525
|
-
t
|
|
1736
|
+
missing(t, "appliedSettings");
|
|
1526
1737
|
return;
|
|
1527
1738
|
}
|
|
1528
1739
|
await port.writeApplied(applied("sha256-a", FIRST_START), null);
|
|
@@ -1545,7 +1756,7 @@ function persistenceAdapterContract(options) {
|
|
|
1545
1756
|
(0, import_node_test.test)("a change lands with the record it supersedes, and is listed newest first", async (t) => {
|
|
1546
1757
|
const port = harness.adapter.appliedSettings;
|
|
1547
1758
|
if (!port) {
|
|
1548
|
-
t
|
|
1759
|
+
missing(t, "appliedSettings");
|
|
1549
1760
|
return;
|
|
1550
1761
|
}
|
|
1551
1762
|
await port.writeApplied(applied("sha256-a", FIRST_START), null);
|
|
@@ -1583,7 +1794,7 @@ function persistenceAdapterContract(options) {
|
|
|
1583
1794
|
(0, import_node_test.test)("a change whose record has moved on is refused whole: no change, and the record as it was", async (t) => {
|
|
1584
1795
|
const port = harness.adapter.appliedSettings;
|
|
1585
1796
|
if (!port) {
|
|
1586
|
-
t
|
|
1797
|
+
missing(t, "appliedSettings");
|
|
1587
1798
|
return;
|
|
1588
1799
|
}
|
|
1589
1800
|
await port.writeApplied(applied("sha256-a", FIRST_START), null);
|
|
@@ -1601,7 +1812,7 @@ function persistenceAdapterContract(options) {
|
|
|
1601
1812
|
(0, import_node_test.test)("starts noticing the same difference at once record it once", async (t) => {
|
|
1602
1813
|
const port = harness.adapter.appliedSettings;
|
|
1603
1814
|
if (!port) {
|
|
1604
|
-
t
|
|
1815
|
+
missing(t, "appliedSettings");
|
|
1605
1816
|
return;
|
|
1606
1817
|
}
|
|
1607
1818
|
await port.writeApplied(applied("sha256-a", FIRST_START), null);
|
|
@@ -1625,7 +1836,7 @@ function persistenceAdapterContract(options) {
|
|
|
1625
1836
|
(0, import_node_test.test)("several first starts write the record once", async (t) => {
|
|
1626
1837
|
const port = harness.adapter.appliedSettings;
|
|
1627
1838
|
if (!port) {
|
|
1628
|
-
t
|
|
1839
|
+
missing(t, "appliedSettings");
|
|
1629
1840
|
return;
|
|
1630
1841
|
}
|
|
1631
1842
|
const written = await Promise.all(
|
|
@@ -1637,7 +1848,7 @@ function persistenceAdapterContract(options) {
|
|
|
1637
1848
|
(0, import_node_test.test)("changes are listed in the order they were recorded, latest first \u2014 not by the moment they carry", async (t) => {
|
|
1638
1849
|
const port = harness.adapter.appliedSettings;
|
|
1639
1850
|
if (!port) {
|
|
1640
|
-
t
|
|
1851
|
+
missing(t, "appliedSettings");
|
|
1641
1852
|
return;
|
|
1642
1853
|
}
|
|
1643
1854
|
await port.writeApplied(applied("sha256-0", FIRST_START, {}), null);
|
|
@@ -1669,7 +1880,7 @@ function persistenceAdapterContract(options) {
|
|
|
1669
1880
|
(0, import_node_test.test)("acknowledging a change is recorded once, and filters it out of what is owed", async (t) => {
|
|
1670
1881
|
const port = harness.adapter.appliedSettings;
|
|
1671
1882
|
if (!port) {
|
|
1672
|
-
t
|
|
1883
|
+
missing(t, "appliedSettings");
|
|
1673
1884
|
return;
|
|
1674
1885
|
}
|
|
1675
1886
|
await port.writeApplied(applied("sha256-a", FIRST_START), null);
|