@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.
Files changed (96) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +53 -0
  3. package/dist/admin/index.cjs +1283 -0
  4. package/dist/admin/index.d.cts +225 -0
  5. package/dist/admin/index.d.ts +225 -0
  6. package/dist/admin/index.js +58 -0
  7. package/dist/admin-audit.service-9IqXMlZm.d.cts +30 -0
  8. package/dist/admin-audit.service-9IqXMlZm.d.ts +30 -0
  9. package/dist/admin-manifest.config-DyrQNT7M.d.cts +10 -0
  10. package/dist/admin-manifest.config-DyrQNT7M.d.ts +10 -0
  11. package/dist/aggregation-CJ3qQf92.d.cts +288 -0
  12. package/dist/aggregation-Dvz9e8X2.d.ts +288 -0
  13. package/dist/billing/index.cjs +5205 -0
  14. package/dist/billing/index.d.cts +1326 -0
  15. package/dist/billing/index.d.ts +1326 -0
  16. package/dist/billing/index.js +197 -0
  17. package/dist/catalog/index.cjs +6449 -0
  18. package/dist/catalog/index.d.cts +1073 -0
  19. package/dist/catalog/index.d.ts +1073 -0
  20. package/dist/catalog/index.js +158 -0
  21. package/dist/checkout-offer/index.cjs +807 -0
  22. package/dist/checkout-offer/index.d.cts +120 -0
  23. package/dist/checkout-offer/index.d.ts +120 -0
  24. package/dist/checkout-offer/index.js +20 -0
  25. package/dist/chunk-2FR6ZL7R.js +0 -0
  26. package/dist/chunk-2PNX2QL2.js +347 -0
  27. package/dist/chunk-2VNZYRDV.js +326 -0
  28. package/dist/chunk-7MLGHWKO.js +12 -0
  29. package/dist/chunk-ABB2CRAS.js +31 -0
  30. package/dist/chunk-AJ3EPELS.js +157 -0
  31. package/dist/chunk-E56W4U2P.js +90 -0
  32. package/dist/chunk-HRU3CP57.js +71 -0
  33. package/dist/chunk-L4SUWH5B.js +14 -0
  34. package/dist/chunk-M6CZQYPY.js +314 -0
  35. package/dist/chunk-MDIZUVIK.js +87 -0
  36. package/dist/chunk-MV6LW5UB.js +217 -0
  37. package/dist/chunk-NYLON2VC.js +6 -0
  38. package/dist/chunk-P6MYZMXQ.js +951 -0
  39. package/dist/chunk-Q53N43LQ.js +13 -0
  40. package/dist/chunk-QJVPRD3R.js +573 -0
  41. package/dist/chunk-RIGLQ4BV.js +3367 -0
  42. package/dist/chunk-SABTXESR.js +24 -0
  43. package/dist/chunk-SHUYVCID.js +6 -0
  44. package/dist/chunk-SYEVGPFC.js +686 -0
  45. package/dist/chunk-V7HUZ6UQ.js +5923 -0
  46. package/dist/chunk-WPUVJRIN.js +29 -0
  47. package/dist/chunk-XCZ3TTFK.js +22 -0
  48. package/dist/chunk-XSGU2Y2O.js +1611 -0
  49. package/dist/chunk-YNEW3L63.js +190 -0
  50. package/dist/chunk-YXGHPILG.js +614 -0
  51. package/dist/chunk-ZL4LLE2Z.js +465 -0
  52. package/dist/di-CcNeq9v-.d.cts +16 -0
  53. package/dist/di-CcNeq9v-.d.ts +16 -0
  54. package/dist/discovery/index.cjs +588 -0
  55. package/dist/discovery/index.d.cts +299 -0
  56. package/dist/discovery/index.d.ts +299 -0
  57. package/dist/discovery/index.js +49 -0
  58. package/dist/discovery.scanner-CUYLKlYT.d.cts +59 -0
  59. package/dist/discovery.scanner-CUYLKlYT.d.ts +59 -0
  60. package/dist/entitlement/index.cjs +805 -0
  61. package/dist/entitlement/index.d.cts +60 -0
  62. package/dist/entitlement/index.d.ts +60 -0
  63. package/dist/entitlement/index.js +57 -0
  64. package/dist/index.cjs +15918 -0
  65. package/dist/index.d.cts +85 -0
  66. package/dist/index.d.ts +85 -0
  67. package/dist/index.js +956 -0
  68. package/dist/plan-resolution-CFCoUkrE.d.cts +41 -0
  69. package/dist/plan-resolution-CFCoUkrE.d.ts +41 -0
  70. package/dist/platform/index.cjs +2772 -0
  71. package/dist/platform/index.d.cts +118 -0
  72. package/dist/platform/index.d.ts +118 -0
  73. package/dist/platform/index.js +37 -0
  74. package/dist/promo/index.cjs +995 -0
  75. package/dist/promo/index.d.cts +121 -0
  76. package/dist/promo/index.d.ts +121 -0
  77. package/dist/promo/index.js +58 -0
  78. package/dist/registration/index.cjs +1687 -0
  79. package/dist/registration/index.d.cts +444 -0
  80. package/dist/registration/index.d.ts +444 -0
  81. package/dist/registration/index.js +82 -0
  82. package/dist/saas-platform.module-CgZ2omgg.d.cts +180 -0
  83. package/dist/saas-platform.module-DT9TnCzk.d.ts +180 -0
  84. package/dist/service-DX8KbGXl.d.cts +109 -0
  85. package/dist/service-DX8KbGXl.d.ts +109 -0
  86. package/dist/subscription-contract/index.cjs +471 -0
  87. package/dist/subscription-contract/index.d.cts +17 -0
  88. package/dist/subscription-contract/index.d.ts +17 -0
  89. package/dist/subscription-contract/index.js +21 -0
  90. package/dist/subscription-contract.service--cm47ZJJ.d.cts +39 -0
  91. package/dist/subscription-contract.service--cm47ZJJ.d.ts +39 -0
  92. package/dist/testing/index.cjs +3942 -0
  93. package/dist/testing/index.d.cts +279 -0
  94. package/dist/testing/index.d.ts +279 -0
  95. package/dist/testing/index.js +1195 -0
  96. 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 };