@saasicat/nest 0.2.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/LICENSE +202 -0
- package/README.md +53 -0
- package/dist/admin/index.cjs +1283 -0
- package/dist/admin/index.d.cts +225 -0
- package/dist/admin/index.d.ts +225 -0
- package/dist/admin/index.js +58 -0
- package/dist/admin-audit.service-9IqXMlZm.d.cts +30 -0
- package/dist/admin-audit.service-9IqXMlZm.d.ts +30 -0
- package/dist/admin-manifest.config-DyrQNT7M.d.cts +10 -0
- package/dist/admin-manifest.config-DyrQNT7M.d.ts +10 -0
- package/dist/aggregation-CJ3qQf92.d.cts +288 -0
- package/dist/aggregation-Dvz9e8X2.d.ts +288 -0
- package/dist/billing/index.cjs +5205 -0
- package/dist/billing/index.d.cts +1326 -0
- package/dist/billing/index.d.ts +1326 -0
- package/dist/billing/index.js +197 -0
- package/dist/catalog/index.cjs +6449 -0
- package/dist/catalog/index.d.cts +1073 -0
- package/dist/catalog/index.d.ts +1073 -0
- package/dist/catalog/index.js +158 -0
- package/dist/checkout-offer/index.cjs +807 -0
- package/dist/checkout-offer/index.d.cts +120 -0
- package/dist/checkout-offer/index.d.ts +120 -0
- package/dist/checkout-offer/index.js +20 -0
- package/dist/chunk-2FR6ZL7R.js +0 -0
- package/dist/chunk-2PNX2QL2.js +347 -0
- package/dist/chunk-2VNZYRDV.js +326 -0
- package/dist/chunk-7MLGHWKO.js +12 -0
- package/dist/chunk-ABB2CRAS.js +31 -0
- package/dist/chunk-AJ3EPELS.js +157 -0
- package/dist/chunk-E56W4U2P.js +90 -0
- package/dist/chunk-HRU3CP57.js +71 -0
- package/dist/chunk-L4SUWH5B.js +14 -0
- package/dist/chunk-M6CZQYPY.js +314 -0
- package/dist/chunk-MDIZUVIK.js +87 -0
- package/dist/chunk-MV6LW5UB.js +217 -0
- package/dist/chunk-NYLON2VC.js +6 -0
- package/dist/chunk-P6MYZMXQ.js +951 -0
- package/dist/chunk-Q53N43LQ.js +13 -0
- package/dist/chunk-QJVPRD3R.js +573 -0
- package/dist/chunk-RIGLQ4BV.js +3367 -0
- package/dist/chunk-SABTXESR.js +24 -0
- package/dist/chunk-SHUYVCID.js +6 -0
- package/dist/chunk-SYEVGPFC.js +686 -0
- package/dist/chunk-V7HUZ6UQ.js +5923 -0
- package/dist/chunk-WPUVJRIN.js +29 -0
- package/dist/chunk-XCZ3TTFK.js +22 -0
- package/dist/chunk-XSGU2Y2O.js +1611 -0
- package/dist/chunk-YNEW3L63.js +190 -0
- package/dist/chunk-YXGHPILG.js +614 -0
- package/dist/chunk-ZL4LLE2Z.js +465 -0
- package/dist/di-CcNeq9v-.d.cts +16 -0
- package/dist/di-CcNeq9v-.d.ts +16 -0
- package/dist/discovery/index.cjs +588 -0
- package/dist/discovery/index.d.cts +299 -0
- package/dist/discovery/index.d.ts +299 -0
- package/dist/discovery/index.js +49 -0
- package/dist/discovery.scanner-CUYLKlYT.d.cts +59 -0
- package/dist/discovery.scanner-CUYLKlYT.d.ts +59 -0
- package/dist/entitlement/index.cjs +805 -0
- package/dist/entitlement/index.d.cts +60 -0
- package/dist/entitlement/index.d.ts +60 -0
- package/dist/entitlement/index.js +57 -0
- package/dist/index.cjs +15918 -0
- package/dist/index.d.cts +85 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +956 -0
- package/dist/plan-resolution-CFCoUkrE.d.cts +41 -0
- package/dist/plan-resolution-CFCoUkrE.d.ts +41 -0
- package/dist/platform/index.cjs +2772 -0
- package/dist/platform/index.d.cts +118 -0
- package/dist/platform/index.d.ts +118 -0
- package/dist/platform/index.js +37 -0
- package/dist/promo/index.cjs +995 -0
- package/dist/promo/index.d.cts +121 -0
- package/dist/promo/index.d.ts +121 -0
- package/dist/promo/index.js +58 -0
- package/dist/registration/index.cjs +1687 -0
- package/dist/registration/index.d.cts +444 -0
- package/dist/registration/index.d.ts +444 -0
- package/dist/registration/index.js +82 -0
- package/dist/saas-platform.module-CgZ2omgg.d.cts +180 -0
- package/dist/saas-platform.module-DT9TnCzk.d.ts +180 -0
- package/dist/service-DX8KbGXl.d.cts +109 -0
- package/dist/service-DX8KbGXl.d.ts +109 -0
- package/dist/subscription-contract/index.cjs +471 -0
- package/dist/subscription-contract/index.d.cts +17 -0
- package/dist/subscription-contract/index.d.ts +17 -0
- package/dist/subscription-contract/index.js +21 -0
- package/dist/subscription-contract.service--cm47ZJJ.d.cts +39 -0
- package/dist/subscription-contract.service--cm47ZJJ.d.ts +39 -0
- package/dist/testing/index.cjs +3942 -0
- package/dist/testing/index.d.cts +279 -0
- package/dist/testing/index.d.ts +279 -0
- package/dist/testing/index.js +1195 -0
- package/package.json +185 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { PlanId, QuotaKey, FeatureKey, PlanCatalog, SubscriptionRepository, PlanVersionRepository, TransactionRunner, SubscriptionBundleRepository, BundleRepository, SubscriptionContractRepository, DiscoverySnapshot, SubscriptionRecord, TransactionContext, ContractLineItemRecord } from '@saasicat/types';
|
|
2
|
+
import { E as EntitlementResolutionConfig } from './plan-resolution-CFCoUkrE.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Snapshot of the binding `PlanVersion` of a subscription. Quotas are
|
|
6
|
+
* delivered as a record of `quotaKey → number`; concrete keys are declared
|
|
7
|
+
* by the code via `@DefinesQuota`.
|
|
8
|
+
*
|
|
9
|
+
* `-1` is the catalog convention for "unlimited" — consumers must map that
|
|
10
|
+
* to `Number.POSITIVE_INFINITY` themselves if they want to compute with it.
|
|
11
|
+
*/
|
|
12
|
+
interface PlanVersionSnapshot {
|
|
13
|
+
planId: PlanId;
|
|
14
|
+
quotas: Record<QuotaKey, number>;
|
|
15
|
+
features: FeatureKey[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Snapshot of a published BundleVersion (for BusinessType aggregation).
|
|
19
|
+
* The consumer resolves the bundle composition via repository lookup before
|
|
20
|
+
* the `aggregateLimits()` call.
|
|
21
|
+
*/
|
|
22
|
+
interface BundleVersionSnapshot {
|
|
23
|
+
bundleKey: string;
|
|
24
|
+
quotas: Record<QuotaKey, number>;
|
|
25
|
+
features: FeatureKey[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Snapshot of the binding `BusinessTypeVersion` of a subscription
|
|
29
|
+
* (SPEC_V2 §11.1 M5). Contains the resolved bundle snapshots in sortOrder
|
|
30
|
+
* order, plus the quota overrides of the BusinessTypeVersion.
|
|
31
|
+
*
|
|
32
|
+
* Aggregation (see GESCHAEFTSTYP_SPEC §6.2):
|
|
33
|
+
* - Quotas: Σ(bundle quotas) per QuotaKey, then override via
|
|
34
|
+
* `quotaOverrides[k]` if set
|
|
35
|
+
* - Features: ⋃ of all bundle features (set union)
|
|
36
|
+
*/
|
|
37
|
+
interface BusinessTypeVersionSnapshot {
|
|
38
|
+
businessTypeKey: string;
|
|
39
|
+
/** Bundles in sortOrder order. */
|
|
40
|
+
bundles: BundleVersionSnapshot[];
|
|
41
|
+
/** Override per QuotaKey. Missing key → Σ(bundle quotas) is used. */
|
|
42
|
+
quotaOverrides: Partial<Record<QuotaKey, number>>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Snapshot of an active SubscriptionBundle booking (P11.7.3 +
|
|
46
|
+
* SPEC_V2 §11.1 M6 Pack 2e). Resolved by the EntitlementService from the
|
|
47
|
+
* `subscription_bundles` junction + BundleRepository.findVersionById and
|
|
48
|
+
* passed to `aggregateLimits`.
|
|
49
|
+
*
|
|
50
|
+
* Aggregation: quotas additive with -1 dominance, features ⋃ set union.
|
|
51
|
+
* The filter (`canceledEffectiveAt > now`) lives in the aggregator
|
|
52
|
+
* (`filterActiveSubscriptionBundles`), so the caller can simply pass in all
|
|
53
|
+
* bookings.
|
|
54
|
+
*/
|
|
55
|
+
interface SubscriptionBundleSnapshot {
|
|
56
|
+
bundleKey: string;
|
|
57
|
+
features: FeatureKey[];
|
|
58
|
+
quotas: Record<QuotaKey, number>;
|
|
59
|
+
/**
|
|
60
|
+
* Date up to which the booking is effectively active (= NULL for
|
|
61
|
+
* non-canceled bookings). The aggregator filters
|
|
62
|
+
* `canceledEffectiveAt > now` as active.
|
|
63
|
+
*/
|
|
64
|
+
canceledEffectiveAt: Date | null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Consumer override from `Subscription.customLimits` (e.g. an ENTERPRISE
|
|
68
|
+
* special contract or pilot). Field-wise — unset quotas/features fall back
|
|
69
|
+
* to the plan default.
|
|
70
|
+
*/
|
|
71
|
+
interface CustomLimitsShape {
|
|
72
|
+
quotas?: Record<QuotaKey, number>;
|
|
73
|
+
features?: FeatureKey[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Input for `aggregateLimits` — the platform expects the consumer to have
|
|
77
|
+
* already performed the plan resolution (trial/pilot/pending). `plan` and
|
|
78
|
+
* `planVersion` are the result of that resolution; see
|
|
79
|
+
* `resolveEntitlementPlan` for a configurable default strategy.
|
|
80
|
+
*
|
|
81
|
+
* `businessTypeVersion` is optional (SPEC_V2 §11.1 M5). When set, the
|
|
82
|
+
* BusinessType composition is included in the aggregation in addition to
|
|
83
|
+
* plan + add-ons — see GESCHAEFTSTYP_SPEC §6.
|
|
84
|
+
*/
|
|
85
|
+
interface SubscriptionLimitsInput {
|
|
86
|
+
plan: PlanId;
|
|
87
|
+
planVersion: PlanVersionSnapshot;
|
|
88
|
+
businessTypeVersion?: BusinessTypeVersionSnapshot | null;
|
|
89
|
+
/**
|
|
90
|
+
* Active bundle bookings (P11.7.3). The aggregator filters by
|
|
91
|
+
* `canceledEffectiveAt > now` and sums quotas + collects features into
|
|
92
|
+
* the effective limits.
|
|
93
|
+
*/
|
|
94
|
+
subscriptionBundles?: SubscriptionBundleSnapshot[];
|
|
95
|
+
customLimits?: CustomLimitsShape | null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Effective limits of a tenant: plan ID + aggregated quotas + features.
|
|
99
|
+
*/
|
|
100
|
+
interface EffectiveLimits {
|
|
101
|
+
plan: PlanId;
|
|
102
|
+
quotas: Record<QuotaKey, number>;
|
|
103
|
+
features: Set<FeatureKey>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Serializable form for snapshot fields (e.g. `Invoice.entitlementSnapshot`).
|
|
107
|
+
* `features` is a sorted array instead of a Set for stable JSON serialization.
|
|
108
|
+
*/
|
|
109
|
+
interface EffectiveLimitsSnapshot {
|
|
110
|
+
plan: PlanId;
|
|
111
|
+
quotas: Record<QuotaKey, number>;
|
|
112
|
+
features: FeatureKey[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface EnforceLimitInput<T> {
|
|
116
|
+
/** Tenant whose subscription is checked. */
|
|
117
|
+
tenantId: string;
|
|
118
|
+
/** Quota key from saas.yaml (`users`, `vehicles`, `storageGb`, …). */
|
|
119
|
+
dimension: string;
|
|
120
|
+
/**
|
|
121
|
+
* Counter for the units currently consumed within the transaction.
|
|
122
|
+
* The consumer counts e.g. `users WHERE tenantId AND deletedAt IS NULL`.
|
|
123
|
+
*/
|
|
124
|
+
currentUsage: (tx: TransactionContext, tenantId: string) => Promise<number>;
|
|
125
|
+
/**
|
|
126
|
+
* Inserts the new row(s). Only executed when the limit is not exceeded
|
|
127
|
+
* after the increment.
|
|
128
|
+
*/
|
|
129
|
+
insert: (tx: TransactionContext) => Promise<T>;
|
|
130
|
+
/**
|
|
131
|
+
* Increment that `insert` adds to the usage. Default 1
|
|
132
|
+
* (USERS/VEHICLES — one row per insert). For STORAGE inserts the caller
|
|
133
|
+
* can pass variable GB values through, so that even a 10 GB file blocks
|
|
134
|
+
* against a 1 GB limit.
|
|
135
|
+
*/
|
|
136
|
+
delta?: number;
|
|
137
|
+
/** Override for tests; default `new Date()`. */
|
|
138
|
+
now?: Date;
|
|
139
|
+
}
|
|
140
|
+
declare class EntitlementService {
|
|
141
|
+
private readonly catalog;
|
|
142
|
+
private readonly subscriptions;
|
|
143
|
+
private readonly planVersions;
|
|
144
|
+
private readonly tx;
|
|
145
|
+
private readonly resolutionConfig;
|
|
146
|
+
private readonly subscriptionBundles;
|
|
147
|
+
private readonly bundles;
|
|
148
|
+
private readonly subscriptionContracts;
|
|
149
|
+
private readonly discoverySnapshot;
|
|
150
|
+
private readonly cache;
|
|
151
|
+
private replacedByIndex;
|
|
152
|
+
constructor(catalog: PlanCatalog, subscriptions: SubscriptionRepository, planVersions: PlanVersionRepository, tx: TransactionRunner, resolutionConfig?: EntitlementResolutionConfig | null, subscriptionBundles?: SubscriptionBundleRepository | null, bundles?: BundleRepository | null, subscriptionContracts?: SubscriptionContractRepository | null, discoverySnapshot?: DiscoverySnapshot | null);
|
|
153
|
+
computeLimits(tenantId: string, now?: Date): Promise<EffectiveLimits>;
|
|
154
|
+
/**
|
|
155
|
+
* Clears a tenant's cache entry. MUST be called as soon as a mutation
|
|
156
|
+
* changes the subscription, a bundle booking, or a PlanVersion binding —
|
|
157
|
+
* otherwise FeatureGuard and the sidebar stay on stale state for up to
|
|
158
|
+
* 60s.
|
|
159
|
+
*/
|
|
160
|
+
invalidateTenant(tenantId: string): void;
|
|
161
|
+
/** Clears the entire cache. Only for tests / bootstrap. */
|
|
162
|
+
invalidateAll(): void;
|
|
163
|
+
/**
|
|
164
|
+
* Aggregator path — also callable directly by consumers that already
|
|
165
|
+
* have a `SubscriptionRecord` (e.g. within a transaction).
|
|
166
|
+
*/
|
|
167
|
+
deriveLimits(sub: SubscriptionRecord, now: Date, tx?: TransactionContext): Promise<EffectiveLimits>;
|
|
168
|
+
/**
|
|
169
|
+
* Grandfathering (#39): granted old feature keys transitively grant
|
|
170
|
+
* their successors from the `replaces` chains of the discovery snapshot.
|
|
171
|
+
* A no-op without a snapshot or without replaces declarations.
|
|
172
|
+
*/
|
|
173
|
+
private withReplacedFeatureAliases;
|
|
174
|
+
private deriveLimitsFromContract;
|
|
175
|
+
/**
|
|
176
|
+
* Loads a subscription's active bundle bookings and resolves the
|
|
177
|
+
* `BundleVersion` features/quotas per entry. Without a registered
|
|
178
|
+
* `SubscriptionBundleRepository` or `BundleRepository` the method
|
|
179
|
+
* returns an empty list — apps without a bundle schema stay unchanged
|
|
180
|
+
* (plan-only aggregation).
|
|
181
|
+
*/
|
|
182
|
+
private loadSubscriptionBundleSnapshots;
|
|
183
|
+
enforceLimit<T>(input: EnforceLimitInput<T>): Promise<T>;
|
|
184
|
+
/**
|
|
185
|
+
* Plan fallback for TRIAL/PENDING_SALES: returns the PlanVersion active
|
|
186
|
+
* as of `asOf` (SPEC_V2 §4.2). If the repo does not implement
|
|
187
|
+
* `findActive`, we fall back to `findLatestLive` (backward compat for
|
|
188
|
+
* adapters without `validFrom` columns).
|
|
189
|
+
*/
|
|
190
|
+
private findActivePlanVersionOrFallback;
|
|
191
|
+
private readCache;
|
|
192
|
+
private writeCache;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
declare class LimitExceededError extends Error {
|
|
196
|
+
readonly dimension: string;
|
|
197
|
+
readonly max: number;
|
|
198
|
+
readonly used: number;
|
|
199
|
+
constructor(dimension: string, max: number, used: number);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Aggregates the quotas of a BusinessTypeVersion: Σ(bundle quotas) per
|
|
204
|
+
* QuotaKey, then override via `quotaOverrides[k]` if set.
|
|
205
|
+
*
|
|
206
|
+
* `-1` (unlimited) is handled as follows:
|
|
207
|
+
* - In Σ summation: -1 dominates (if a bundle has -1 for a key,
|
|
208
|
+
* the Σ is likewise -1 for that key — regardless of the other values).
|
|
209
|
+
* - In overrides: if the override is -1, it replaces the sum entirely.
|
|
210
|
+
*
|
|
211
|
+
* Spec: GESCHAEFTSTYP_SPEC §6.2.
|
|
212
|
+
*/
|
|
213
|
+
declare function aggregateBusinessTypeQuotas(snapshot: BusinessTypeVersionSnapshot): Record<QuotaKey, number>;
|
|
214
|
+
/**
|
|
215
|
+
* Filters SubscriptionBundle bookings down to those active at the given
|
|
216
|
+
* point in time. Active = `canceledEffectiveAt === null` or
|
|
217
|
+
* `canceledEffectiveAt > now`.
|
|
218
|
+
*
|
|
219
|
+
* Spec: SPEC_V2 §11.1 M6 Pack 2e + P11.7.3.
|
|
220
|
+
*/
|
|
221
|
+
declare function filterActiveSubscriptionBundles(bundles: readonly SubscriptionBundleSnapshot[], now: Date): SubscriptionBundleSnapshot[];
|
|
222
|
+
/**
|
|
223
|
+
* Aggregates the quotas of all active SubscriptionBundle bookings:
|
|
224
|
+
* Σ per QuotaKey, `-1` (unlimited) dominates. Bundle features +
|
|
225
|
+
* bundle quotas are **additive** to the PlanVersion (not replacing).
|
|
226
|
+
*/
|
|
227
|
+
declare function aggregateSubscriptionBundleQuotas(bundles: readonly SubscriptionBundleSnapshot[]): Record<QuotaKey, number>;
|
|
228
|
+
/**
|
|
229
|
+
* Collects all feature keys from active SubscriptionBundle bookings
|
|
230
|
+
* (set union; duplicate features are included once).
|
|
231
|
+
*/
|
|
232
|
+
declare function collectSubscriptionBundleFeatures(bundles: readonly SubscriptionBundleSnapshot[]): FeatureKey[];
|
|
233
|
+
/**
|
|
234
|
+
* V3 aggregation directly from frozen ContractLineItems. This function
|
|
235
|
+
* uses no catalog lookups: `featuresSnapshot` and
|
|
236
|
+
* `quotaEffectsSnapshot` are the contractual truth.
|
|
237
|
+
*/
|
|
238
|
+
declare function aggregateContractLineItemEntitlements(lineItems: readonly Pick<ContractLineItemRecord, 'kind' | 'sourceKey' | 'featuresSnapshot' | 'quotaEffectsSnapshot'>[], fallbackPlan?: string): EffectiveLimits;
|
|
239
|
+
/**
|
|
240
|
+
* Collects all feature keys from all bundles of a BusinessTypeVersion
|
|
241
|
+
* (set union; duplicate features are included once).
|
|
242
|
+
*
|
|
243
|
+
* Spec: GESCHAEFTSTYP_SPEC §6.3.
|
|
244
|
+
*/
|
|
245
|
+
declare function collectBusinessTypeFeatures(snapshot: BusinessTypeVersionSnapshot): FeatureKey[];
|
|
246
|
+
/**
|
|
247
|
+
* Consistently filters out `plannedOnly` features — regardless of whether they
|
|
248
|
+
* come from the plan, a bundle or customLimits. `plannedOnly` means: the feature
|
|
249
|
+
* is declared in the catalog, but not yet rolled out to production.
|
|
250
|
+
*/
|
|
251
|
+
declare function filterPlannedOnlyFeatures(features: ReadonlySet<FeatureKey>, catalog: PlanCatalog): Set<FeatureKey>;
|
|
252
|
+
/**
|
|
253
|
+
* Applies `customLimits` (ENTERPRISE special contract, pilot) field-by-field to
|
|
254
|
+
* the plan default limits. `quotas[k]` overrides; `features[]` adds.
|
|
255
|
+
* Fields not set in the override fall back to the default.
|
|
256
|
+
*/
|
|
257
|
+
declare function applyCustomLimits(base: EffectiveLimits, custom: CustomLimitsShape | null | undefined): EffectiveLimits;
|
|
258
|
+
/**
|
|
259
|
+
* Main aggregator — yields the effective limits from PlanVersion + optional
|
|
260
|
+
* BusinessTypeVersion + active bundle bookings + catalog + optional
|
|
261
|
+
* CustomLimits override.
|
|
262
|
+
*
|
|
263
|
+
* Order (SPEC_V2 §11.1 M5 + GESCHAEFTSTYP_SPEC §6):
|
|
264
|
+
* 1. Filter active bundle bookings (canceledEffectiveAt).
|
|
265
|
+
* 2. Sum plan quotas + (BusinessType quotas with override logic) + bundle quotas
|
|
266
|
+
* per `quotaKey`. -1 (unlimited) dominates.
|
|
267
|
+
* 3. Collect plan features ∪ BusinessType features ∪ bundle features
|
|
268
|
+
* (set union, deduplicated).
|
|
269
|
+
* 4. Apply CustomLimits (quotas override, features add).
|
|
270
|
+
* 5. Consistently hide plannedOnly features.
|
|
271
|
+
*/
|
|
272
|
+
declare function aggregateLimits(input: SubscriptionLimitsInput, catalog: PlanCatalog, now: Date): EffectiveLimits;
|
|
273
|
+
/**
|
|
274
|
+
* Checks whether a feature is contained in the effective limits.
|
|
275
|
+
*/
|
|
276
|
+
declare function hasFeature(limits: EffectiveLimits, feature: FeatureKey): boolean;
|
|
277
|
+
/**
|
|
278
|
+
* Checks whether at least one of the given features is contained in the
|
|
279
|
+
* effective limits. An empty list yields `false`.
|
|
280
|
+
*/
|
|
281
|
+
declare function hasAnyFeature(limits: EffectiveLimits, features: readonly FeatureKey[]): boolean;
|
|
282
|
+
/**
|
|
283
|
+
* Converts EffectiveLimits into a JSON-serializable shape (Set → sorted
|
|
284
|
+
* array). Used e.g. in `Invoice.entitlementSnapshot`.
|
|
285
|
+
*/
|
|
286
|
+
declare function toEffectiveLimitsSnapshot(limits: EffectiveLimits): EffectiveLimitsSnapshot;
|
|
287
|
+
|
|
288
|
+
export { type BundleVersionSnapshot as B, type CustomLimitsShape as C, type EffectiveLimits as E, LimitExceededError as L, type PlanVersionSnapshot as P, type SubscriptionBundleSnapshot as S, type BusinessTypeVersionSnapshot as a, type EffectiveLimitsSnapshot as b, type EnforceLimitInput as c, EntitlementService as d, type SubscriptionLimitsInput as e, aggregateBusinessTypeQuotas as f, aggregateContractLineItemEntitlements as g, aggregateLimits as h, aggregateSubscriptionBundleQuotas as i, applyCustomLimits as j, collectBusinessTypeFeatures as k, collectSubscriptionBundleFeatures as l, filterActiveSubscriptionBundles as m, filterPlannedOnlyFeatures as n, hasAnyFeature as o, hasFeature as p, toEffectiveLimitsSnapshot as t };
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { PlanId, QuotaKey, FeatureKey, PlanCatalog, SubscriptionRepository, PlanVersionRepository, TransactionRunner, SubscriptionBundleRepository, BundleRepository, SubscriptionContractRepository, DiscoverySnapshot, SubscriptionRecord, TransactionContext, ContractLineItemRecord } from '@saasicat/types';
|
|
2
|
+
import { E as EntitlementResolutionConfig } from './plan-resolution-CFCoUkrE.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Snapshot of the binding `PlanVersion` of a subscription. Quotas are
|
|
6
|
+
* delivered as a record of `quotaKey → number`; concrete keys are declared
|
|
7
|
+
* by the code via `@DefinesQuota`.
|
|
8
|
+
*
|
|
9
|
+
* `-1` is the catalog convention for "unlimited" — consumers must map that
|
|
10
|
+
* to `Number.POSITIVE_INFINITY` themselves if they want to compute with it.
|
|
11
|
+
*/
|
|
12
|
+
interface PlanVersionSnapshot {
|
|
13
|
+
planId: PlanId;
|
|
14
|
+
quotas: Record<QuotaKey, number>;
|
|
15
|
+
features: FeatureKey[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Snapshot of a published BundleVersion (for BusinessType aggregation).
|
|
19
|
+
* The consumer resolves the bundle composition via repository lookup before
|
|
20
|
+
* the `aggregateLimits()` call.
|
|
21
|
+
*/
|
|
22
|
+
interface BundleVersionSnapshot {
|
|
23
|
+
bundleKey: string;
|
|
24
|
+
quotas: Record<QuotaKey, number>;
|
|
25
|
+
features: FeatureKey[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Snapshot of the binding `BusinessTypeVersion` of a subscription
|
|
29
|
+
* (SPEC_V2 §11.1 M5). Contains the resolved bundle snapshots in sortOrder
|
|
30
|
+
* order, plus the quota overrides of the BusinessTypeVersion.
|
|
31
|
+
*
|
|
32
|
+
* Aggregation (see GESCHAEFTSTYP_SPEC §6.2):
|
|
33
|
+
* - Quotas: Σ(bundle quotas) per QuotaKey, then override via
|
|
34
|
+
* `quotaOverrides[k]` if set
|
|
35
|
+
* - Features: ⋃ of all bundle features (set union)
|
|
36
|
+
*/
|
|
37
|
+
interface BusinessTypeVersionSnapshot {
|
|
38
|
+
businessTypeKey: string;
|
|
39
|
+
/** Bundles in sortOrder order. */
|
|
40
|
+
bundles: BundleVersionSnapshot[];
|
|
41
|
+
/** Override per QuotaKey. Missing key → Σ(bundle quotas) is used. */
|
|
42
|
+
quotaOverrides: Partial<Record<QuotaKey, number>>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Snapshot of an active SubscriptionBundle booking (P11.7.3 +
|
|
46
|
+
* SPEC_V2 §11.1 M6 Pack 2e). Resolved by the EntitlementService from the
|
|
47
|
+
* `subscription_bundles` junction + BundleRepository.findVersionById and
|
|
48
|
+
* passed to `aggregateLimits`.
|
|
49
|
+
*
|
|
50
|
+
* Aggregation: quotas additive with -1 dominance, features ⋃ set union.
|
|
51
|
+
* The filter (`canceledEffectiveAt > now`) lives in the aggregator
|
|
52
|
+
* (`filterActiveSubscriptionBundles`), so the caller can simply pass in all
|
|
53
|
+
* bookings.
|
|
54
|
+
*/
|
|
55
|
+
interface SubscriptionBundleSnapshot {
|
|
56
|
+
bundleKey: string;
|
|
57
|
+
features: FeatureKey[];
|
|
58
|
+
quotas: Record<QuotaKey, number>;
|
|
59
|
+
/**
|
|
60
|
+
* Date up to which the booking is effectively active (= NULL for
|
|
61
|
+
* non-canceled bookings). The aggregator filters
|
|
62
|
+
* `canceledEffectiveAt > now` as active.
|
|
63
|
+
*/
|
|
64
|
+
canceledEffectiveAt: Date | null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Consumer override from `Subscription.customLimits` (e.g. an ENTERPRISE
|
|
68
|
+
* special contract or pilot). Field-wise — unset quotas/features fall back
|
|
69
|
+
* to the plan default.
|
|
70
|
+
*/
|
|
71
|
+
interface CustomLimitsShape {
|
|
72
|
+
quotas?: Record<QuotaKey, number>;
|
|
73
|
+
features?: FeatureKey[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Input for `aggregateLimits` — the platform expects the consumer to have
|
|
77
|
+
* already performed the plan resolution (trial/pilot/pending). `plan` and
|
|
78
|
+
* `planVersion` are the result of that resolution; see
|
|
79
|
+
* `resolveEntitlementPlan` for a configurable default strategy.
|
|
80
|
+
*
|
|
81
|
+
* `businessTypeVersion` is optional (SPEC_V2 §11.1 M5). When set, the
|
|
82
|
+
* BusinessType composition is included in the aggregation in addition to
|
|
83
|
+
* plan + add-ons — see GESCHAEFTSTYP_SPEC §6.
|
|
84
|
+
*/
|
|
85
|
+
interface SubscriptionLimitsInput {
|
|
86
|
+
plan: PlanId;
|
|
87
|
+
planVersion: PlanVersionSnapshot;
|
|
88
|
+
businessTypeVersion?: BusinessTypeVersionSnapshot | null;
|
|
89
|
+
/**
|
|
90
|
+
* Active bundle bookings (P11.7.3). The aggregator filters by
|
|
91
|
+
* `canceledEffectiveAt > now` and sums quotas + collects features into
|
|
92
|
+
* the effective limits.
|
|
93
|
+
*/
|
|
94
|
+
subscriptionBundles?: SubscriptionBundleSnapshot[];
|
|
95
|
+
customLimits?: CustomLimitsShape | null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Effective limits of a tenant: plan ID + aggregated quotas + features.
|
|
99
|
+
*/
|
|
100
|
+
interface EffectiveLimits {
|
|
101
|
+
plan: PlanId;
|
|
102
|
+
quotas: Record<QuotaKey, number>;
|
|
103
|
+
features: Set<FeatureKey>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Serializable form for snapshot fields (e.g. `Invoice.entitlementSnapshot`).
|
|
107
|
+
* `features` is a sorted array instead of a Set for stable JSON serialization.
|
|
108
|
+
*/
|
|
109
|
+
interface EffectiveLimitsSnapshot {
|
|
110
|
+
plan: PlanId;
|
|
111
|
+
quotas: Record<QuotaKey, number>;
|
|
112
|
+
features: FeatureKey[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface EnforceLimitInput<T> {
|
|
116
|
+
/** Tenant whose subscription is checked. */
|
|
117
|
+
tenantId: string;
|
|
118
|
+
/** Quota key from saas.yaml (`users`, `vehicles`, `storageGb`, …). */
|
|
119
|
+
dimension: string;
|
|
120
|
+
/**
|
|
121
|
+
* Counter for the units currently consumed within the transaction.
|
|
122
|
+
* The consumer counts e.g. `users WHERE tenantId AND deletedAt IS NULL`.
|
|
123
|
+
*/
|
|
124
|
+
currentUsage: (tx: TransactionContext, tenantId: string) => Promise<number>;
|
|
125
|
+
/**
|
|
126
|
+
* Inserts the new row(s). Only executed when the limit is not exceeded
|
|
127
|
+
* after the increment.
|
|
128
|
+
*/
|
|
129
|
+
insert: (tx: TransactionContext) => Promise<T>;
|
|
130
|
+
/**
|
|
131
|
+
* Increment that `insert` adds to the usage. Default 1
|
|
132
|
+
* (USERS/VEHICLES — one row per insert). For STORAGE inserts the caller
|
|
133
|
+
* can pass variable GB values through, so that even a 10 GB file blocks
|
|
134
|
+
* against a 1 GB limit.
|
|
135
|
+
*/
|
|
136
|
+
delta?: number;
|
|
137
|
+
/** Override for tests; default `new Date()`. */
|
|
138
|
+
now?: Date;
|
|
139
|
+
}
|
|
140
|
+
declare class EntitlementService {
|
|
141
|
+
private readonly catalog;
|
|
142
|
+
private readonly subscriptions;
|
|
143
|
+
private readonly planVersions;
|
|
144
|
+
private readonly tx;
|
|
145
|
+
private readonly resolutionConfig;
|
|
146
|
+
private readonly subscriptionBundles;
|
|
147
|
+
private readonly bundles;
|
|
148
|
+
private readonly subscriptionContracts;
|
|
149
|
+
private readonly discoverySnapshot;
|
|
150
|
+
private readonly cache;
|
|
151
|
+
private replacedByIndex;
|
|
152
|
+
constructor(catalog: PlanCatalog, subscriptions: SubscriptionRepository, planVersions: PlanVersionRepository, tx: TransactionRunner, resolutionConfig?: EntitlementResolutionConfig | null, subscriptionBundles?: SubscriptionBundleRepository | null, bundles?: BundleRepository | null, subscriptionContracts?: SubscriptionContractRepository | null, discoverySnapshot?: DiscoverySnapshot | null);
|
|
153
|
+
computeLimits(tenantId: string, now?: Date): Promise<EffectiveLimits>;
|
|
154
|
+
/**
|
|
155
|
+
* Clears a tenant's cache entry. MUST be called as soon as a mutation
|
|
156
|
+
* changes the subscription, a bundle booking, or a PlanVersion binding —
|
|
157
|
+
* otherwise FeatureGuard and the sidebar stay on stale state for up to
|
|
158
|
+
* 60s.
|
|
159
|
+
*/
|
|
160
|
+
invalidateTenant(tenantId: string): void;
|
|
161
|
+
/** Clears the entire cache. Only for tests / bootstrap. */
|
|
162
|
+
invalidateAll(): void;
|
|
163
|
+
/**
|
|
164
|
+
* Aggregator path — also callable directly by consumers that already
|
|
165
|
+
* have a `SubscriptionRecord` (e.g. within a transaction).
|
|
166
|
+
*/
|
|
167
|
+
deriveLimits(sub: SubscriptionRecord, now: Date, tx?: TransactionContext): Promise<EffectiveLimits>;
|
|
168
|
+
/**
|
|
169
|
+
* Grandfathering (#39): granted old feature keys transitively grant
|
|
170
|
+
* their successors from the `replaces` chains of the discovery snapshot.
|
|
171
|
+
* A no-op without a snapshot or without replaces declarations.
|
|
172
|
+
*/
|
|
173
|
+
private withReplacedFeatureAliases;
|
|
174
|
+
private deriveLimitsFromContract;
|
|
175
|
+
/**
|
|
176
|
+
* Loads a subscription's active bundle bookings and resolves the
|
|
177
|
+
* `BundleVersion` features/quotas per entry. Without a registered
|
|
178
|
+
* `SubscriptionBundleRepository` or `BundleRepository` the method
|
|
179
|
+
* returns an empty list — apps without a bundle schema stay unchanged
|
|
180
|
+
* (plan-only aggregation).
|
|
181
|
+
*/
|
|
182
|
+
private loadSubscriptionBundleSnapshots;
|
|
183
|
+
enforceLimit<T>(input: EnforceLimitInput<T>): Promise<T>;
|
|
184
|
+
/**
|
|
185
|
+
* Plan fallback for TRIAL/PENDING_SALES: returns the PlanVersion active
|
|
186
|
+
* as of `asOf` (SPEC_V2 §4.2). If the repo does not implement
|
|
187
|
+
* `findActive`, we fall back to `findLatestLive` (backward compat for
|
|
188
|
+
* adapters without `validFrom` columns).
|
|
189
|
+
*/
|
|
190
|
+
private findActivePlanVersionOrFallback;
|
|
191
|
+
private readCache;
|
|
192
|
+
private writeCache;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
declare class LimitExceededError extends Error {
|
|
196
|
+
readonly dimension: string;
|
|
197
|
+
readonly max: number;
|
|
198
|
+
readonly used: number;
|
|
199
|
+
constructor(dimension: string, max: number, used: number);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Aggregates the quotas of a BusinessTypeVersion: Σ(bundle quotas) per
|
|
204
|
+
* QuotaKey, then override via `quotaOverrides[k]` if set.
|
|
205
|
+
*
|
|
206
|
+
* `-1` (unlimited) is handled as follows:
|
|
207
|
+
* - In Σ summation: -1 dominates (if a bundle has -1 for a key,
|
|
208
|
+
* the Σ is likewise -1 for that key — regardless of the other values).
|
|
209
|
+
* - In overrides: if the override is -1, it replaces the sum entirely.
|
|
210
|
+
*
|
|
211
|
+
* Spec: GESCHAEFTSTYP_SPEC §6.2.
|
|
212
|
+
*/
|
|
213
|
+
declare function aggregateBusinessTypeQuotas(snapshot: BusinessTypeVersionSnapshot): Record<QuotaKey, number>;
|
|
214
|
+
/**
|
|
215
|
+
* Filters SubscriptionBundle bookings down to those active at the given
|
|
216
|
+
* point in time. Active = `canceledEffectiveAt === null` or
|
|
217
|
+
* `canceledEffectiveAt > now`.
|
|
218
|
+
*
|
|
219
|
+
* Spec: SPEC_V2 §11.1 M6 Pack 2e + P11.7.3.
|
|
220
|
+
*/
|
|
221
|
+
declare function filterActiveSubscriptionBundles(bundles: readonly SubscriptionBundleSnapshot[], now: Date): SubscriptionBundleSnapshot[];
|
|
222
|
+
/**
|
|
223
|
+
* Aggregates the quotas of all active SubscriptionBundle bookings:
|
|
224
|
+
* Σ per QuotaKey, `-1` (unlimited) dominates. Bundle features +
|
|
225
|
+
* bundle quotas are **additive** to the PlanVersion (not replacing).
|
|
226
|
+
*/
|
|
227
|
+
declare function aggregateSubscriptionBundleQuotas(bundles: readonly SubscriptionBundleSnapshot[]): Record<QuotaKey, number>;
|
|
228
|
+
/**
|
|
229
|
+
* Collects all feature keys from active SubscriptionBundle bookings
|
|
230
|
+
* (set union; duplicate features are included once).
|
|
231
|
+
*/
|
|
232
|
+
declare function collectSubscriptionBundleFeatures(bundles: readonly SubscriptionBundleSnapshot[]): FeatureKey[];
|
|
233
|
+
/**
|
|
234
|
+
* V3 aggregation directly from frozen ContractLineItems. This function
|
|
235
|
+
* uses no catalog lookups: `featuresSnapshot` and
|
|
236
|
+
* `quotaEffectsSnapshot` are the contractual truth.
|
|
237
|
+
*/
|
|
238
|
+
declare function aggregateContractLineItemEntitlements(lineItems: readonly Pick<ContractLineItemRecord, 'kind' | 'sourceKey' | 'featuresSnapshot' | 'quotaEffectsSnapshot'>[], fallbackPlan?: string): EffectiveLimits;
|
|
239
|
+
/**
|
|
240
|
+
* Collects all feature keys from all bundles of a BusinessTypeVersion
|
|
241
|
+
* (set union; duplicate features are included once).
|
|
242
|
+
*
|
|
243
|
+
* Spec: GESCHAEFTSTYP_SPEC §6.3.
|
|
244
|
+
*/
|
|
245
|
+
declare function collectBusinessTypeFeatures(snapshot: BusinessTypeVersionSnapshot): FeatureKey[];
|
|
246
|
+
/**
|
|
247
|
+
* Consistently filters out `plannedOnly` features — regardless of whether they
|
|
248
|
+
* come from the plan, a bundle or customLimits. `plannedOnly` means: the feature
|
|
249
|
+
* is declared in the catalog, but not yet rolled out to production.
|
|
250
|
+
*/
|
|
251
|
+
declare function filterPlannedOnlyFeatures(features: ReadonlySet<FeatureKey>, catalog: PlanCatalog): Set<FeatureKey>;
|
|
252
|
+
/**
|
|
253
|
+
* Applies `customLimits` (ENTERPRISE special contract, pilot) field-by-field to
|
|
254
|
+
* the plan default limits. `quotas[k]` overrides; `features[]` adds.
|
|
255
|
+
* Fields not set in the override fall back to the default.
|
|
256
|
+
*/
|
|
257
|
+
declare function applyCustomLimits(base: EffectiveLimits, custom: CustomLimitsShape | null | undefined): EffectiveLimits;
|
|
258
|
+
/**
|
|
259
|
+
* Main aggregator — yields the effective limits from PlanVersion + optional
|
|
260
|
+
* BusinessTypeVersion + active bundle bookings + catalog + optional
|
|
261
|
+
* CustomLimits override.
|
|
262
|
+
*
|
|
263
|
+
* Order (SPEC_V2 §11.1 M5 + GESCHAEFTSTYP_SPEC §6):
|
|
264
|
+
* 1. Filter active bundle bookings (canceledEffectiveAt).
|
|
265
|
+
* 2. Sum plan quotas + (BusinessType quotas with override logic) + bundle quotas
|
|
266
|
+
* per `quotaKey`. -1 (unlimited) dominates.
|
|
267
|
+
* 3. Collect plan features ∪ BusinessType features ∪ bundle features
|
|
268
|
+
* (set union, deduplicated).
|
|
269
|
+
* 4. Apply CustomLimits (quotas override, features add).
|
|
270
|
+
* 5. Consistently hide plannedOnly features.
|
|
271
|
+
*/
|
|
272
|
+
declare function aggregateLimits(input: SubscriptionLimitsInput, catalog: PlanCatalog, now: Date): EffectiveLimits;
|
|
273
|
+
/**
|
|
274
|
+
* Checks whether a feature is contained in the effective limits.
|
|
275
|
+
*/
|
|
276
|
+
declare function hasFeature(limits: EffectiveLimits, feature: FeatureKey): boolean;
|
|
277
|
+
/**
|
|
278
|
+
* Checks whether at least one of the given features is contained in the
|
|
279
|
+
* effective limits. An empty list yields `false`.
|
|
280
|
+
*/
|
|
281
|
+
declare function hasAnyFeature(limits: EffectiveLimits, features: readonly FeatureKey[]): boolean;
|
|
282
|
+
/**
|
|
283
|
+
* Converts EffectiveLimits into a JSON-serializable shape (Set → sorted
|
|
284
|
+
* array). Used e.g. in `Invoice.entitlementSnapshot`.
|
|
285
|
+
*/
|
|
286
|
+
declare function toEffectiveLimitsSnapshot(limits: EffectiveLimits): EffectiveLimitsSnapshot;
|
|
287
|
+
|
|
288
|
+
export { type BundleVersionSnapshot as B, type CustomLimitsShape as C, type EffectiveLimits as E, LimitExceededError as L, type PlanVersionSnapshot as P, type SubscriptionBundleSnapshot as S, type BusinessTypeVersionSnapshot as a, type EffectiveLimitsSnapshot as b, type EnforceLimitInput as c, EntitlementService as d, type SubscriptionLimitsInput as e, aggregateBusinessTypeQuotas as f, aggregateContractLineItemEntitlements as g, aggregateLimits as h, aggregateSubscriptionBundleQuotas as i, applyCustomLimits as j, collectBusinessTypeFeatures as k, collectSubscriptionBundleFeatures as l, filterActiveSubscriptionBundles as m, filterPlannedOnlyFeatures as n, hasAnyFeature as o, hasFeature as p, toEffectiveLimitsSnapshot as t };
|