@saasicat/persistence-testing 1.0.0-rc.2 → 1.0.0-rc.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,10 +1,11 @@
1
- import { PersistenceCapabilities, TransactionRunner, SubscriptionRepository, PlanVersionRepository, PromoCodeRepository, PromoCodeRedemptionRepository, MfaPort, AuditPort, AuditQueryPort, SubscriptionContractRepository, TenantSubscriptionWritePort, BundleRepository, PlanRepository, PromoSubscriptionLookup } from '@saasicat/core';
1
+ import { PersistenceCapabilities, TransactionRunner, SubscriptionRepository, PlanVersionRepository, PromoCodeRepository, PromoCodeRedemptionRepository, MfaPort, AuditPort, AuditQueryPort, SubscriptionContractRepository, SubscriberRepository, PaymentEventLog, SubscriberPaymentMethodRepository, CheckoutOfferRepository, TenantSubscriptionWritePort, BundleRepository, SubscriptionBundleRepository, PlanRepository, PromoSubscriptionLookup, AppliedSettingsPort } from '@saasicat/core';
2
2
 
3
3
  /**
4
4
  * Port instances under test. Required members define the minimum an adapter
5
5
  * must ship to call itself a SaaSiCat persistence adapter; optional members
6
- * activate additional scenario groups (absent the group reports as
7
- * skipped, never silently).
6
+ * activate additional scenario groups. An absent member fails its scenarios
7
+ * unless the contract options declare it in `gaps`, in which case they report
8
+ * as skipped.
8
9
  */
9
10
  interface ContractAdapterInstances {
10
11
  capabilities: PersistenceCapabilities;
@@ -17,6 +18,34 @@ interface ContractAdapterInstances {
17
18
  audit?: AuditPort;
18
19
  auditQuery?: AuditQueryPort;
19
20
  subscriptionContractRepository?: SubscriptionContractRepository;
21
+ /**
22
+ * Enables the subscriber scenarios: a customer number the database
23
+ * counts, one live subscriber per tenant however many callers create one
24
+ * at once, and a correction that records the values it replaced. A
25
+ * contract names its subscriber, so the contract scenarios write theirs
26
+ * through `seed.createSubscriber` rather than through this port.
27
+ */
28
+ subscriberRepository?: SubscriberRepository;
29
+ /**
30
+ * Enables the gateway event scenarios: an event is claimed once per
31
+ * account, a duplicate leaves the caller's transaction usable, and a claim
32
+ * rolled back with its transaction is free for the gateway's retry.
33
+ */
34
+ paymentEventLog?: PaymentEventLog;
35
+ /**
36
+ * Enables the payment method scenarios: one payment method in use per
37
+ * subscriber however many confirmations arrive at once, the one it
38
+ * replaced kept as history, and a confirmation recorded twice recognised.
39
+ * Its subscribers come from `seed.createSubscriber`.
40
+ */
41
+ subscriberPaymentMethodRepository?: SubscriberPaymentMethodRepository;
42
+ /**
43
+ * Enables the checkout offer scenarios: an offer is consumed once, and a
44
+ * consume on a transaction that rolls back leaves it open. Neither shipped
45
+ * adapter provides one; an application that implements the port wires it
46
+ * here.
47
+ */
48
+ checkoutOfferRepository?: CheckoutOfferRepository;
20
49
  /**
21
50
  * Enables the atomic plan-binding scenarios. Adapters should expose this
22
51
  * member only for a mode that promises to keep `plan`,
@@ -25,6 +54,14 @@ interface ContractAdapterInstances {
25
54
  tenantSubscriptionWrite?: TenantSubscriptionWritePort;
26
55
  /** Enables BundleVersion validity-window and auto-succession scenarios. */
27
56
  bundleRepository?: BundleRepository;
57
+ /**
58
+ * Enables the booking scenarios — the junction a tenant's bundles hang off.
59
+ *
60
+ * Separate from `bundleRepository`, which is the catalog: one answers what
61
+ * may be sold, the other what a tenant actually bought and for which
62
+ * period. `adapter-drizzle` has neither yet.
63
+ */
64
+ subscriptionBundleRepository?: SubscriptionBundleRepository;
28
65
  /** Enables PlanVersion lifecycle, identity and validity-window scenarios. */
29
66
  planRepository?: PlanRepository;
30
67
  /**
@@ -37,6 +74,16 @@ interface ContractAdapterInstances {
37
74
  * tenant, which is what makes selecting the wrong row possible at all.
38
75
  */
39
76
  promoSubscriptionLookup?: PromoSubscriptionLookup;
77
+ /**
78
+ * Enables the applied-settings scenarios: the one row an installation keeps
79
+ * about the configuration it runs on, and the changes noticed between
80
+ * starts. The singleton and the guard are the parts worth a contract — two
81
+ * adapters that upsert differently would leave one of them with two rows
82
+ * and a reader picking one at random, and one whose write ignores the
83
+ * fingerprint it was given lets every replica of a deployment record the
84
+ * same change.
85
+ */
86
+ appliedSettings?: AppliedSettingsPort;
40
87
  }
41
88
  /** Fixture writers — implemented per adapter against its own schema. */
42
89
  interface ContractSeed {
@@ -63,6 +110,34 @@ interface ContractSeed {
63
110
  }): Promise<{
64
111
  subscriptionId: string;
65
112
  }>;
113
+ /**
114
+ * A published BundleVersion to book against.
115
+ *
116
+ * A fixture writer rather than a call into `bundleRepository`: the catalog
117
+ * repository is a subject of the suite, not a tool for setting up someone
118
+ * else's scenario, and a booking scenario that failed because the catalog
119
+ * did would say the wrong thing.
120
+ *
121
+ * Optional because an adapter may not carry the catalog tables at all —
122
+ * `adapter-drizzle` does not. A required writer nobody can implement is a
123
+ * contract that lies about what conformance means; the booking scenarios
124
+ * gate on this and report the gap as a skip instead.
125
+ */
126
+ createBundleVersion?(input: {
127
+ bundleKey: string;
128
+ features: string[];
129
+ }): Promise<{
130
+ bundleVersionId: string;
131
+ }>;
132
+ /**
133
+ * Clears a booking's `canceledAt` while leaving `canceledEffectiveAt`.
134
+ *
135
+ * A shape no repository method produces, and one the nullable columns
136
+ * nonetheless permit — so the adapters have to agree on how to read it.
137
+ * Optional: a harness that cannot reach its store directly says so and the
138
+ * scenario skips, rather than the scenario inventing a way in.
139
+ */
140
+ clearBookingRequestDate?(subscriptionBundleId: string): Promise<void>;
66
141
  createPromoCode(input: {
67
142
  code: string;
68
143
  maxRedemptions: number | null;
@@ -70,6 +145,19 @@ interface ContractSeed {
70
145
  }): Promise<{
71
146
  promoCodeId: string;
72
147
  }>;
148
+ /**
149
+ * A subscriber row for a contract scenario to name, linked to no tenant.
150
+ *
151
+ * A fixture writer for the reason `createBundleVersion` is one: the
152
+ * subscriber repository is a subject of its own scenarios, and a contract
153
+ * scenario that failed because of it would say the wrong thing. Optional
154
+ * like the port it stands beside; the contract scenarios report it missing.
155
+ */
156
+ createSubscriber?(input: {
157
+ legalName: string;
158
+ }): Promise<{
159
+ subscriberId: string;
160
+ }>;
73
161
  }
74
162
  interface PersistenceContractHarness {
75
163
  adapter: ContractAdapterInstances;
@@ -79,13 +167,25 @@ interface PersistenceContractHarness {
79
167
  /** Teardown after the last scenario (close pools etc.). */
80
168
  close?(): Promise<void>;
81
169
  }
170
+ /**
171
+ * A part of the contract an adapter may deliberately not provide.
172
+ *
173
+ * Each names the members its scenarios need; `contract.ts` holds the list with
174
+ * what each one checks.
175
+ */
176
+ type ContractGap = 'atomicPlanBinding' | 'atomicOnboarding' | 'promoCodes' | 'promoCodeRedemptions' | 'promoSubscriptionLookup' | 'planRepository' | 'planLifecycle' | 'planRetirement' | 'planVersionReads' | 'planVersionRetirement' | 'bundleRepository' | 'bundleValidity' | 'bundleDraftDiscard' | 'bundleDraftPublish' | 'bundleRetirement' | 'bundleBookings' | 'halfCancelledBookingSeed' | 'countByPlanVersionId' | 'audit' | 'mfa' | 'subscriptionContracts' | 'subscribers' | 'paymentEventLog' | 'subscriberPaymentMethods' | 'checkoutOffers' | 'appliedSettings';
82
177
  interface PersistenceAdapterContractOptions {
83
178
  /** Display name in the test output, e.g. `'adapter-prisma @ postgres16'`. */
84
179
  name: string;
85
- /** Project identity used by catalog lifecycle scenarios. */
86
- projectKey: string;
87
180
  /** Builds the harness once for the whole suite. */
88
181
  create(): Promise<PersistenceContractHarness>;
182
+ /**
183
+ * The parts this adapter deliberately does not provide. Their scenarios
184
+ * report as skipped; a part missing without being named here fails its
185
+ * scenarios, and a part named here that the harness does provide fails
186
+ * the suite.
187
+ */
188
+ gaps?: readonly ContractGap[];
89
189
  }
90
190
 
91
191
  /**
@@ -94,11 +194,10 @@ interface PersistenceAdapterContractOptions {
94
194
  * ```ts
95
195
  * persistenceAdapterContract({
96
196
  * name: 'adapter-prisma @ postgres',
97
- * projectKey: 'my-app',
98
197
  * create: () => createPrismaHarness(),
99
198
  * });
100
199
  * ```
101
200
  */
102
201
  declare function persistenceAdapterContract(options: PersistenceAdapterContractOptions): void;
103
202
 
104
- export { type ContractAdapterInstances, type ContractSeed, type PersistenceAdapterContractOptions, type PersistenceContractHarness, persistenceAdapterContract };
203
+ export { type ContractAdapterInstances, type ContractGap, type ContractSeed, type PersistenceAdapterContractOptions, type PersistenceContractHarness, persistenceAdapterContract };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
- import { PersistenceCapabilities, TransactionRunner, SubscriptionRepository, PlanVersionRepository, PromoCodeRepository, PromoCodeRedemptionRepository, MfaPort, AuditPort, AuditQueryPort, SubscriptionContractRepository, TenantSubscriptionWritePort, BundleRepository, PlanRepository, PromoSubscriptionLookup } from '@saasicat/core';
1
+ import { PersistenceCapabilities, TransactionRunner, SubscriptionRepository, PlanVersionRepository, PromoCodeRepository, PromoCodeRedemptionRepository, MfaPort, AuditPort, AuditQueryPort, SubscriptionContractRepository, SubscriberRepository, PaymentEventLog, SubscriberPaymentMethodRepository, CheckoutOfferRepository, TenantSubscriptionWritePort, BundleRepository, SubscriptionBundleRepository, PlanRepository, PromoSubscriptionLookup, AppliedSettingsPort } from '@saasicat/core';
2
2
 
3
3
  /**
4
4
  * Port instances under test. Required members define the minimum an adapter
5
5
  * must ship to call itself a SaaSiCat persistence adapter; optional members
6
- * activate additional scenario groups (absent the group reports as
7
- * skipped, never silently).
6
+ * activate additional scenario groups. An absent member fails its scenarios
7
+ * unless the contract options declare it in `gaps`, in which case they report
8
+ * as skipped.
8
9
  */
9
10
  interface ContractAdapterInstances {
10
11
  capabilities: PersistenceCapabilities;
@@ -17,6 +18,34 @@ interface ContractAdapterInstances {
17
18
  audit?: AuditPort;
18
19
  auditQuery?: AuditQueryPort;
19
20
  subscriptionContractRepository?: SubscriptionContractRepository;
21
+ /**
22
+ * Enables the subscriber scenarios: a customer number the database
23
+ * counts, one live subscriber per tenant however many callers create one
24
+ * at once, and a correction that records the values it replaced. A
25
+ * contract names its subscriber, so the contract scenarios write theirs
26
+ * through `seed.createSubscriber` rather than through this port.
27
+ */
28
+ subscriberRepository?: SubscriberRepository;
29
+ /**
30
+ * Enables the gateway event scenarios: an event is claimed once per
31
+ * account, a duplicate leaves the caller's transaction usable, and a claim
32
+ * rolled back with its transaction is free for the gateway's retry.
33
+ */
34
+ paymentEventLog?: PaymentEventLog;
35
+ /**
36
+ * Enables the payment method scenarios: one payment method in use per
37
+ * subscriber however many confirmations arrive at once, the one it
38
+ * replaced kept as history, and a confirmation recorded twice recognised.
39
+ * Its subscribers come from `seed.createSubscriber`.
40
+ */
41
+ subscriberPaymentMethodRepository?: SubscriberPaymentMethodRepository;
42
+ /**
43
+ * Enables the checkout offer scenarios: an offer is consumed once, and a
44
+ * consume on a transaction that rolls back leaves it open. Neither shipped
45
+ * adapter provides one; an application that implements the port wires it
46
+ * here.
47
+ */
48
+ checkoutOfferRepository?: CheckoutOfferRepository;
20
49
  /**
21
50
  * Enables the atomic plan-binding scenarios. Adapters should expose this
22
51
  * member only for a mode that promises to keep `plan`,
@@ -25,6 +54,14 @@ interface ContractAdapterInstances {
25
54
  tenantSubscriptionWrite?: TenantSubscriptionWritePort;
26
55
  /** Enables BundleVersion validity-window and auto-succession scenarios. */
27
56
  bundleRepository?: BundleRepository;
57
+ /**
58
+ * Enables the booking scenarios — the junction a tenant's bundles hang off.
59
+ *
60
+ * Separate from `bundleRepository`, which is the catalog: one answers what
61
+ * may be sold, the other what a tenant actually bought and for which
62
+ * period. `adapter-drizzle` has neither yet.
63
+ */
64
+ subscriptionBundleRepository?: SubscriptionBundleRepository;
28
65
  /** Enables PlanVersion lifecycle, identity and validity-window scenarios. */
29
66
  planRepository?: PlanRepository;
30
67
  /**
@@ -37,6 +74,16 @@ interface ContractAdapterInstances {
37
74
  * tenant, which is what makes selecting the wrong row possible at all.
38
75
  */
39
76
  promoSubscriptionLookup?: PromoSubscriptionLookup;
77
+ /**
78
+ * Enables the applied-settings scenarios: the one row an installation keeps
79
+ * about the configuration it runs on, and the changes noticed between
80
+ * starts. The singleton and the guard are the parts worth a contract — two
81
+ * adapters that upsert differently would leave one of them with two rows
82
+ * and a reader picking one at random, and one whose write ignores the
83
+ * fingerprint it was given lets every replica of a deployment record the
84
+ * same change.
85
+ */
86
+ appliedSettings?: AppliedSettingsPort;
40
87
  }
41
88
  /** Fixture writers — implemented per adapter against its own schema. */
42
89
  interface ContractSeed {
@@ -63,6 +110,34 @@ interface ContractSeed {
63
110
  }): Promise<{
64
111
  subscriptionId: string;
65
112
  }>;
113
+ /**
114
+ * A published BundleVersion to book against.
115
+ *
116
+ * A fixture writer rather than a call into `bundleRepository`: the catalog
117
+ * repository is a subject of the suite, not a tool for setting up someone
118
+ * else's scenario, and a booking scenario that failed because the catalog
119
+ * did would say the wrong thing.
120
+ *
121
+ * Optional because an adapter may not carry the catalog tables at all —
122
+ * `adapter-drizzle` does not. A required writer nobody can implement is a
123
+ * contract that lies about what conformance means; the booking scenarios
124
+ * gate on this and report the gap as a skip instead.
125
+ */
126
+ createBundleVersion?(input: {
127
+ bundleKey: string;
128
+ features: string[];
129
+ }): Promise<{
130
+ bundleVersionId: string;
131
+ }>;
132
+ /**
133
+ * Clears a booking's `canceledAt` while leaving `canceledEffectiveAt`.
134
+ *
135
+ * A shape no repository method produces, and one the nullable columns
136
+ * nonetheless permit — so the adapters have to agree on how to read it.
137
+ * Optional: a harness that cannot reach its store directly says so and the
138
+ * scenario skips, rather than the scenario inventing a way in.
139
+ */
140
+ clearBookingRequestDate?(subscriptionBundleId: string): Promise<void>;
66
141
  createPromoCode(input: {
67
142
  code: string;
68
143
  maxRedemptions: number | null;
@@ -70,6 +145,19 @@ interface ContractSeed {
70
145
  }): Promise<{
71
146
  promoCodeId: string;
72
147
  }>;
148
+ /**
149
+ * A subscriber row for a contract scenario to name, linked to no tenant.
150
+ *
151
+ * A fixture writer for the reason `createBundleVersion` is one: the
152
+ * subscriber repository is a subject of its own scenarios, and a contract
153
+ * scenario that failed because of it would say the wrong thing. Optional
154
+ * like the port it stands beside; the contract scenarios report it missing.
155
+ */
156
+ createSubscriber?(input: {
157
+ legalName: string;
158
+ }): Promise<{
159
+ subscriberId: string;
160
+ }>;
73
161
  }
74
162
  interface PersistenceContractHarness {
75
163
  adapter: ContractAdapterInstances;
@@ -79,13 +167,25 @@ interface PersistenceContractHarness {
79
167
  /** Teardown after the last scenario (close pools etc.). */
80
168
  close?(): Promise<void>;
81
169
  }
170
+ /**
171
+ * A part of the contract an adapter may deliberately not provide.
172
+ *
173
+ * Each names the members its scenarios need; `contract.ts` holds the list with
174
+ * what each one checks.
175
+ */
176
+ type ContractGap = 'atomicPlanBinding' | 'atomicOnboarding' | 'promoCodes' | 'promoCodeRedemptions' | 'promoSubscriptionLookup' | 'planRepository' | 'planLifecycle' | 'planRetirement' | 'planVersionReads' | 'planVersionRetirement' | 'bundleRepository' | 'bundleValidity' | 'bundleDraftDiscard' | 'bundleDraftPublish' | 'bundleRetirement' | 'bundleBookings' | 'halfCancelledBookingSeed' | 'countByPlanVersionId' | 'audit' | 'mfa' | 'subscriptionContracts' | 'subscribers' | 'paymentEventLog' | 'subscriberPaymentMethods' | 'checkoutOffers' | 'appliedSettings';
82
177
  interface PersistenceAdapterContractOptions {
83
178
  /** Display name in the test output, e.g. `'adapter-prisma @ postgres16'`. */
84
179
  name: string;
85
- /** Project identity used by catalog lifecycle scenarios. */
86
- projectKey: string;
87
180
  /** Builds the harness once for the whole suite. */
88
181
  create(): Promise<PersistenceContractHarness>;
182
+ /**
183
+ * The parts this adapter deliberately does not provide. Their scenarios
184
+ * report as skipped; a part missing without being named here fails its
185
+ * scenarios, and a part named here that the harness does provide fails
186
+ * the suite.
187
+ */
188
+ gaps?: readonly ContractGap[];
89
189
  }
90
190
 
91
191
  /**
@@ -94,11 +194,10 @@ interface PersistenceAdapterContractOptions {
94
194
  * ```ts
95
195
  * persistenceAdapterContract({
96
196
  * name: 'adapter-prisma @ postgres',
97
- * projectKey: 'my-app',
98
197
  * create: () => createPrismaHarness(),
99
198
  * });
100
199
  * ```
101
200
  */
102
201
  declare function persistenceAdapterContract(options: PersistenceAdapterContractOptions): void;
103
202
 
104
- export { type ContractAdapterInstances, type ContractSeed, type PersistenceAdapterContractOptions, type PersistenceContractHarness, persistenceAdapterContract };
203
+ export { type ContractAdapterInstances, type ContractGap, type ContractSeed, type PersistenceAdapterContractOptions, type PersistenceContractHarness, persistenceAdapterContract };