@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,614 @@
1
+ import {
2
+ PLAN_CATALOG_TOKEN,
3
+ isFeaturePlannedOnly
4
+ } from "./chunk-MV6LW5UB.js";
5
+ import {
6
+ SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN
7
+ } from "./chunk-NYLON2VC.js";
8
+ import {
9
+ ENTITLEMENT_RESOLUTION_CONFIG_TOKEN,
10
+ PLAN_VERSION_REPOSITORY_TOKEN,
11
+ SUBSCRIPTION_REPOSITORY_TOKEN,
12
+ TRANSACTION_RUNNER_TOKEN
13
+ } from "./chunk-7MLGHWKO.js";
14
+ import {
15
+ BUNDLE_REPOSITORY_TOKEN
16
+ } from "./chunk-XCZ3TTFK.js";
17
+ import {
18
+ asProvider
19
+ } from "./chunk-SABTXESR.js";
20
+ import {
21
+ DISCOVERY_SNAPSHOT_TOKEN
22
+ } from "./chunk-L4SUWH5B.js";
23
+ import {
24
+ __name
25
+ } from "./chunk-SHUYVCID.js";
26
+
27
+ // src/billing/subscription-bundles.tokens.ts
28
+ var SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/SubscriptionBundleRepository");
29
+ var SUBSCRIPTION_BUNDLE_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/SubscriptionBundleConfig");
30
+
31
+ // src/entitlement/aggregation.ts
32
+ function aggregateBusinessTypeQuotas(snapshot) {
33
+ const sums = {};
34
+ for (const bundle of snapshot.bundles) {
35
+ for (const [key, value] of Object.entries(bundle.quotas)) {
36
+ if (sums[key] === -1 || value === -1) {
37
+ sums[key] = -1;
38
+ } else {
39
+ sums[key] = (sums[key] ?? 0) + value;
40
+ }
41
+ }
42
+ }
43
+ for (const [key, value] of Object.entries(snapshot.quotaOverrides)) {
44
+ if (value !== void 0) {
45
+ sums[key] = value;
46
+ }
47
+ }
48
+ return sums;
49
+ }
50
+ __name(aggregateBusinessTypeQuotas, "aggregateBusinessTypeQuotas");
51
+ function filterActiveSubscriptionBundles(bundles, now) {
52
+ return bundles.filter((b) => b.canceledEffectiveAt === null || b.canceledEffectiveAt > now);
53
+ }
54
+ __name(filterActiveSubscriptionBundles, "filterActiveSubscriptionBundles");
55
+ function aggregateSubscriptionBundleQuotas(bundles) {
56
+ const sums = {};
57
+ for (const bundle of bundles) {
58
+ for (const [key, value] of Object.entries(bundle.quotas)) {
59
+ if (sums[key] === -1 || value === -1) {
60
+ sums[key] = -1;
61
+ } else {
62
+ sums[key] = (sums[key] ?? 0) + value;
63
+ }
64
+ }
65
+ }
66
+ return sums;
67
+ }
68
+ __name(aggregateSubscriptionBundleQuotas, "aggregateSubscriptionBundleQuotas");
69
+ function collectSubscriptionBundleFeatures(bundles) {
70
+ const set = /* @__PURE__ */ new Set();
71
+ for (const bundle of bundles) {
72
+ for (const f of bundle.features) set.add(f);
73
+ }
74
+ return Array.from(set);
75
+ }
76
+ __name(collectSubscriptionBundleFeatures, "collectSubscriptionBundleFeatures");
77
+ function aggregateContractLineItemEntitlements(lineItems, fallbackPlan = "UNKNOWN") {
78
+ const planItem = lineItems.find((item) => item.kind === "plan");
79
+ const plan = planItem?.sourceKey ?? fallbackPlan;
80
+ const quotas = {};
81
+ const features = /* @__PURE__ */ new Set();
82
+ for (const item of lineItems) {
83
+ for (const feature of item.featuresSnapshot) {
84
+ features.add(feature);
85
+ }
86
+ for (const [key, value] of Object.entries(item.quotaEffectsSnapshot)) {
87
+ if (quotas[key] === -1 || value === -1) {
88
+ quotas[key] = -1;
89
+ } else {
90
+ quotas[key] = (quotas[key] ?? 0) + value;
91
+ }
92
+ }
93
+ }
94
+ return {
95
+ plan,
96
+ quotas,
97
+ features
98
+ };
99
+ }
100
+ __name(aggregateContractLineItemEntitlements, "aggregateContractLineItemEntitlements");
101
+ function collectBusinessTypeFeatures(snapshot) {
102
+ const set = /* @__PURE__ */ new Set();
103
+ for (const bundle of snapshot.bundles) {
104
+ for (const f of bundle.features) {
105
+ set.add(f);
106
+ }
107
+ }
108
+ return Array.from(set);
109
+ }
110
+ __name(collectBusinessTypeFeatures, "collectBusinessTypeFeatures");
111
+ function filterPlannedOnlyFeatures(features, catalog) {
112
+ const out = /* @__PURE__ */ new Set();
113
+ for (const f of features) {
114
+ if (!isFeaturePlannedOnly(catalog, f)) {
115
+ out.add(f);
116
+ }
117
+ }
118
+ return out;
119
+ }
120
+ __name(filterPlannedOnlyFeatures, "filterPlannedOnlyFeatures");
121
+ function applyCustomLimits(base, custom) {
122
+ if (!custom) return base;
123
+ const quotas = {
124
+ ...base.quotas
125
+ };
126
+ if (custom.quotas) {
127
+ for (const [k, v] of Object.entries(custom.quotas)) {
128
+ quotas[k] = v;
129
+ }
130
+ }
131
+ const features = new Set(base.features);
132
+ if (custom.features) {
133
+ for (const f of custom.features) features.add(f);
134
+ }
135
+ return {
136
+ plan: base.plan,
137
+ quotas,
138
+ features
139
+ };
140
+ }
141
+ __name(applyCustomLimits, "applyCustomLimits");
142
+ function aggregateLimits(input, catalog, now) {
143
+ const businessTypeQuotas = input.businessTypeVersion ? aggregateBusinessTypeQuotas(input.businessTypeVersion) : {};
144
+ const businessTypeFeatures = input.businessTypeVersion ? collectBusinessTypeFeatures(input.businessTypeVersion) : [];
145
+ const activeBundles = filterActiveSubscriptionBundles(input.subscriptionBundles ?? [], now);
146
+ const subBundleQuotas = aggregateSubscriptionBundleQuotas(activeBundles);
147
+ const subBundleFeatures = collectSubscriptionBundleFeatures(activeBundles);
148
+ const quotas = {};
149
+ for (const [k, v] of Object.entries(input.planVersion.quotas)) {
150
+ quotas[k] = v;
151
+ }
152
+ for (const [k, v] of Object.entries(businessTypeQuotas)) {
153
+ if (quotas[k] === -1 || v === -1) {
154
+ quotas[k] = -1;
155
+ } else {
156
+ quotas[k] = (quotas[k] ?? 0) + v;
157
+ }
158
+ }
159
+ for (const [k, v] of Object.entries(subBundleQuotas)) {
160
+ if (quotas[k] === -1 || v === -1) {
161
+ quotas[k] = -1;
162
+ } else {
163
+ quotas[k] = (quotas[k] ?? 0) + v;
164
+ }
165
+ }
166
+ const features = /* @__PURE__ */ new Set([
167
+ ...input.planVersion.features,
168
+ ...businessTypeFeatures,
169
+ ...subBundleFeatures
170
+ ]);
171
+ const withCustom = applyCustomLimits({
172
+ plan: input.plan,
173
+ quotas,
174
+ features
175
+ }, input.customLimits);
176
+ const filteredFeatures = filterPlannedOnlyFeatures(withCustom.features, catalog);
177
+ return {
178
+ plan: withCustom.plan,
179
+ quotas: withCustom.quotas,
180
+ features: filteredFeatures
181
+ };
182
+ }
183
+ __name(aggregateLimits, "aggregateLimits");
184
+ function hasFeature(limits, feature) {
185
+ return limits.features.has(feature);
186
+ }
187
+ __name(hasFeature, "hasFeature");
188
+ function hasAnyFeature(limits, features) {
189
+ return features.some((f) => limits.features.has(f));
190
+ }
191
+ __name(hasAnyFeature, "hasAnyFeature");
192
+ function toEffectiveLimitsSnapshot(limits) {
193
+ return {
194
+ plan: limits.plan,
195
+ quotas: {
196
+ ...limits.quotas
197
+ },
198
+ features: Array.from(limits.features).sort()
199
+ };
200
+ }
201
+ __name(toEffectiveLimitsSnapshot, "toEffectiveLimitsSnapshot");
202
+
203
+ // src/entitlement/feature-aliases.ts
204
+ function buildReplacedByIndex(features) {
205
+ const index = /* @__PURE__ */ new Map();
206
+ for (const feature of features) {
207
+ for (const oldKey of feature.replaces ?? []) {
208
+ if (oldKey === feature.featureKey) continue;
209
+ const list = index.get(oldKey) ?? [];
210
+ list.push(feature.featureKey);
211
+ index.set(oldKey, list);
212
+ }
213
+ }
214
+ for (const list of index.values()) list.sort((a, b) => a.localeCompare(b));
215
+ assertNoReplacesCycle(index);
216
+ return index;
217
+ }
218
+ __name(buildReplacedByIndex, "buildReplacedByIndex");
219
+ function expandReplacedFeatures(granted, index) {
220
+ const expanded = new Set(granted);
221
+ const worklist = [
222
+ ...granted
223
+ ];
224
+ while (worklist.length > 0) {
225
+ const key = worklist.pop();
226
+ for (const successor of index.get(key) ?? []) {
227
+ if (expanded.has(successor)) continue;
228
+ expanded.add(successor);
229
+ worklist.push(successor);
230
+ }
231
+ }
232
+ return expanded;
233
+ }
234
+ __name(expandReplacedFeatures, "expandReplacedFeatures");
235
+ function assertNoReplacesCycle(index) {
236
+ const DONE = 2;
237
+ const IN_PROGRESS = 1;
238
+ const state = /* @__PURE__ */ new Map();
239
+ const visit = /* @__PURE__ */ __name((key, path) => {
240
+ const s = state.get(key);
241
+ if (s === DONE) return;
242
+ if (s === IN_PROGRESS) {
243
+ const cycleStart = path.indexOf(key);
244
+ const cycle = [
245
+ ...path.slice(cycleStart),
246
+ key
247
+ ].join(" \u2192 ");
248
+ throw new Error(`replaces-Zyklus in den Discovery-Metadaten: ${cycle}. Feature-Ersetzungen m\xFCssen azyklisch sein \u2014 replaces-Deklarationen im Code korrigieren (#39).`);
249
+ }
250
+ state.set(key, IN_PROGRESS);
251
+ for (const successor of index.get(key) ?? []) {
252
+ visit(successor, [
253
+ ...path,
254
+ key
255
+ ]);
256
+ }
257
+ state.set(key, DONE);
258
+ }, "visit");
259
+ for (const key of index.keys()) visit(key, []);
260
+ }
261
+ __name(assertNoReplacesCycle, "assertNoReplacesCycle");
262
+
263
+ // src/entitlement/limit-exceeded-error.ts
264
+ var LimitExceededError = class extends Error {
265
+ static {
266
+ __name(this, "LimitExceededError");
267
+ }
268
+ dimension;
269
+ max;
270
+ used;
271
+ constructor(dimension, max, used) {
272
+ super(`Limit f\xFCr ${dimension} erreicht: ${used}/${max}.`), this.dimension = dimension, this.max = max, this.used = used;
273
+ this.name = "LimitExceededError";
274
+ }
275
+ };
276
+
277
+ // src/entitlement/plan-resolution.ts
278
+ function resolveEntitlementPlan(input, config, now) {
279
+ if (input.isPilot && config.pilotEntitlementPlan !== void 0) {
280
+ return config.pilotEntitlementPlan;
281
+ }
282
+ if (input.status === "TRIAL") {
283
+ return input.trialEntitlementPlan ?? config.defaultTrialEntitlementPlan ?? input.plan;
284
+ }
285
+ if (input.status === "PENDING_SALES" && config.pendingSalesEntitlementPlan !== void 0) {
286
+ return config.pendingSalesEntitlementPlan;
287
+ }
288
+ if (input.pendingPlan && input.pendingEffectiveAt && input.pendingEffectiveAt <= now) {
289
+ return input.pendingPlan;
290
+ }
291
+ return input.plan;
292
+ }
293
+ __name(resolveEntitlementPlan, "resolveEntitlementPlan");
294
+
295
+ // src/entitlement/service.ts
296
+ import { Inject, Injectable, NotFoundException, Optional } from "@nestjs/common";
297
+ function _ts_decorate(decorators, target, key, desc) {
298
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
299
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
300
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
301
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
302
+ }
303
+ __name(_ts_decorate, "_ts_decorate");
304
+ function _ts_metadata(k, v) {
305
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
306
+ }
307
+ __name(_ts_metadata, "_ts_metadata");
308
+ function _ts_param(paramIndex, decorator) {
309
+ return function(target, key) {
310
+ decorator(target, key, paramIndex);
311
+ };
312
+ }
313
+ __name(_ts_param, "_ts_param");
314
+ var CACHE_TTL_MS = 6e4;
315
+ var CACHE_MAX_ENTRIES = 1e3;
316
+ var EntitlementService = class {
317
+ static {
318
+ __name(this, "EntitlementService");
319
+ }
320
+ catalog;
321
+ subscriptions;
322
+ planVersions;
323
+ tx;
324
+ resolutionConfig;
325
+ subscriptionBundles;
326
+ bundles;
327
+ subscriptionContracts;
328
+ discoverySnapshot;
329
+ // Map preserves insert order — on a hit the entry is removed and
330
+ // re-inserted (LRU). The TTL is a safety net for forgotten
331
+ // invalidations — 60s is enough, because limits generally only change
332
+ // through mutations, not through time (a pending plan's effective date
333
+ // is day-granular).
334
+ cache = /* @__PURE__ */ new Map();
335
+ // #39 — replaces alias index, built lazily from the boot-static snapshot;
336
+ // throws on replaces cycles (a clear error instead of an infinite loop).
337
+ replacedByIndex = null;
338
+ constructor(catalog, subscriptions, planVersions, tx, resolutionConfig = null, subscriptionBundles = null, bundles = null, subscriptionContracts = null, discoverySnapshot = null) {
339
+ this.catalog = catalog;
340
+ this.subscriptions = subscriptions;
341
+ this.planVersions = planVersions;
342
+ this.tx = tx;
343
+ this.resolutionConfig = resolutionConfig;
344
+ this.subscriptionBundles = subscriptionBundles;
345
+ this.bundles = bundles;
346
+ this.subscriptionContracts = subscriptionContracts;
347
+ this.discoverySnapshot = discoverySnapshot;
348
+ }
349
+ // ---------------------------------------------------------------------
350
+ // Read path — for FeatureGuard, sidebar hooks, GET /billing/entitlement
351
+ // ---------------------------------------------------------------------
352
+ async computeLimits(tenantId, now = /* @__PURE__ */ new Date()) {
353
+ const cached = this.readCache(tenantId, now.getTime());
354
+ if (cached) return cached;
355
+ const sub = await this.subscriptions.findByTenantId(tenantId);
356
+ if (!sub) {
357
+ throw new NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
358
+ }
359
+ const limits = await this.deriveLimits(sub, now);
360
+ this.writeCache(tenantId, limits, now.getTime());
361
+ return limits;
362
+ }
363
+ /**
364
+ * Clears a tenant's cache entry. MUST be called as soon as a mutation
365
+ * changes the subscription, a bundle booking, or a PlanVersion binding —
366
+ * otherwise FeatureGuard and the sidebar stay on stale state for up to
367
+ * 60s.
368
+ */
369
+ invalidateTenant(tenantId) {
370
+ this.cache.delete(tenantId);
371
+ }
372
+ /** Clears the entire cache. Only for tests / bootstrap. */
373
+ invalidateAll() {
374
+ this.cache.clear();
375
+ }
376
+ /**
377
+ * Aggregator path — also callable directly by consumers that already
378
+ * have a `SubscriptionRecord` (e.g. within a transaction).
379
+ */
380
+ async deriveLimits(sub, now, tx) {
381
+ const contractLimits = await this.deriveLimitsFromContract(sub.tenantId, now);
382
+ if (contractLimits) return this.withReplacedFeatureAliases(contractLimits);
383
+ const effectivePlan = resolveEntitlementPlan(sub, this.resolutionConfig ?? {}, now);
384
+ const planVersion = effectivePlan === sub.plan ? sub.planVersion : await this.findActivePlanVersionOrFallback(effectivePlan, now, tx);
385
+ const subscriptionBundles = await this.loadSubscriptionBundleSnapshots(sub.id, now);
386
+ return this.withReplacedFeatureAliases(aggregateLimits({
387
+ plan: effectivePlan,
388
+ planVersion,
389
+ subscriptionBundles,
390
+ customLimits: sub.customLimits ?? null
391
+ }, this.catalog, now));
392
+ }
393
+ /**
394
+ * Grandfathering (#39): granted old feature keys transitively grant
395
+ * their successors from the `replaces` chains of the discovery snapshot.
396
+ * A no-op without a snapshot or without replaces declarations.
397
+ */
398
+ withReplacedFeatureAliases(limits) {
399
+ if (!this.discoverySnapshot) return limits;
400
+ if (!this.replacedByIndex) {
401
+ this.replacedByIndex = buildReplacedByIndex(this.discoverySnapshot.features);
402
+ }
403
+ if (this.replacedByIndex.size === 0) return limits;
404
+ return {
405
+ ...limits,
406
+ features: expandReplacedFeatures(limits.features, this.replacedByIndex)
407
+ };
408
+ }
409
+ async deriveLimitsFromContract(tenantId, now) {
410
+ if (!this.subscriptionContracts) return null;
411
+ const contract = await this.subscriptionContracts.findActiveByTenantId(tenantId, now);
412
+ if (!contract) return null;
413
+ if (contract.entitlementSnapshot) {
414
+ return {
415
+ plan: contract.entitlementSnapshot.plan,
416
+ quotas: {
417
+ ...contract.entitlementSnapshot.quotas
418
+ },
419
+ features: new Set(contract.entitlementSnapshot.features)
420
+ };
421
+ }
422
+ return aggregateContractLineItemEntitlements(contract.lineItems);
423
+ }
424
+ /**
425
+ * Loads a subscription's active bundle bookings and resolves the
426
+ * `BundleVersion` features/quotas per entry. Without a registered
427
+ * `SubscriptionBundleRepository` or `BundleRepository` the method
428
+ * returns an empty list — apps without a bundle schema stay unchanged
429
+ * (plan-only aggregation).
430
+ */
431
+ async loadSubscriptionBundleSnapshots(subscriptionId, now) {
432
+ if (!this.subscriptionBundles || !this.bundles) return [];
433
+ const active = await this.subscriptionBundles.listActiveBySubscription(subscriptionId, now);
434
+ if (active.length === 0) return [];
435
+ return Promise.all(active.map(async (booking) => {
436
+ const bv = await this.bundles.findVersionById(booking.bundleVersionId);
437
+ if (!bv) {
438
+ throw new Error(`BundleVersion '${booking.bundleVersionId}' aus aktiver SubscriptionBundle nicht gefunden`);
439
+ }
440
+ return {
441
+ bundleKey: bv.bundleKey,
442
+ features: bv.features,
443
+ quotas: bv.quotas,
444
+ canceledEffectiveAt: booking.canceledEffectiveAt
445
+ };
446
+ }));
447
+ }
448
+ // ---------------------------------------------------------------------
449
+ // Write/consumption path — row lock + count + insert in the same
450
+ // transaction. Protects against race conditions on concurrent creations
451
+ // for the same tenant.
452
+ // ---------------------------------------------------------------------
453
+ async enforceLimit(input) {
454
+ const now = input.now ?? /* @__PURE__ */ new Date();
455
+ const delta = input.delta ?? 1;
456
+ return this.tx.run(async (tx) => {
457
+ const sub = await this.subscriptions.findByTenantIdLocked(input.tenantId, tx);
458
+ if (!sub) {
459
+ throw new NotFoundException(`Keine Subscription f\xFCr Tenant ${input.tenantId}`);
460
+ }
461
+ const limits = await this.deriveLimits(sub, now, tx);
462
+ const max = limits.quotas[input.dimension];
463
+ if (max === void 0) {
464
+ throw new Error(`Quota-Dimension "${input.dimension}" nicht im PlanCatalog (${this.catalog.projectKey}).`);
465
+ }
466
+ const used = await input.currentUsage(tx, input.tenantId);
467
+ if (max !== -1 && used + delta > max) {
468
+ throw new LimitExceededError(input.dimension, max, used);
469
+ }
470
+ return input.insert(tx);
471
+ });
472
+ }
473
+ // ---------------------------------------------------------------------
474
+ // Helpers
475
+ // ---------------------------------------------------------------------
476
+ /**
477
+ * Plan fallback for TRIAL/PENDING_SALES: returns the PlanVersion active
478
+ * as of `asOf` (SPEC_V2 §4.2). If the repo does not implement
479
+ * `findActive`, we fall back to `findLatestLive` (backward compat for
480
+ * adapters without `validFrom` columns).
481
+ */
482
+ async findActivePlanVersionOrFallback(planId, asOf, tx) {
483
+ const v = this.planVersions.findActive ? await this.planVersions.findActive(planId, asOf, tx) : await this.planVersions.findLatestLive(planId, tx);
484
+ if (!v) {
485
+ throw new NotFoundException(`Keine zu ${asOf.toISOString().slice(0, 10)} aktive PlanVersion f\xFCr ${planId} \u2014 weder validFrom-window erf\xFCllt noch latest-live verf\xFCgbar.`);
486
+ }
487
+ return v;
488
+ }
489
+ readCache(tenantId, nowMs) {
490
+ const entry = this.cache.get(tenantId);
491
+ if (!entry) return null;
492
+ if (entry.expiresAt < nowMs) {
493
+ this.cache.delete(tenantId);
494
+ return null;
495
+ }
496
+ this.cache.delete(tenantId);
497
+ this.cache.set(tenantId, entry);
498
+ return entry.value;
499
+ }
500
+ writeCache(tenantId, value, nowMs) {
501
+ if (this.cache.has(tenantId)) this.cache.delete(tenantId);
502
+ this.cache.set(tenantId, {
503
+ value,
504
+ expiresAt: nowMs + CACHE_TTL_MS
505
+ });
506
+ while (this.cache.size > CACHE_MAX_ENTRIES) {
507
+ const oldest = this.cache.keys().next().value;
508
+ if (oldest === void 0) break;
509
+ this.cache.delete(oldest);
510
+ }
511
+ }
512
+ };
513
+ EntitlementService = _ts_decorate([
514
+ Injectable(),
515
+ _ts_param(0, Inject(PLAN_CATALOG_TOKEN)),
516
+ _ts_param(1, Inject(SUBSCRIPTION_REPOSITORY_TOKEN)),
517
+ _ts_param(2, Inject(PLAN_VERSION_REPOSITORY_TOKEN)),
518
+ _ts_param(3, Inject(TRANSACTION_RUNNER_TOKEN)),
519
+ _ts_param(4, Optional()),
520
+ _ts_param(4, Inject(ENTITLEMENT_RESOLUTION_CONFIG_TOKEN)),
521
+ _ts_param(5, Optional()),
522
+ _ts_param(5, Inject(SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN)),
523
+ _ts_param(6, Optional()),
524
+ _ts_param(6, Inject(BUNDLE_REPOSITORY_TOKEN)),
525
+ _ts_param(7, Optional()),
526
+ _ts_param(7, Inject(SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN)),
527
+ _ts_param(8, Optional()),
528
+ _ts_param(8, Inject(DISCOVERY_SNAPSHOT_TOKEN)),
529
+ _ts_metadata("design:type", Function),
530
+ _ts_metadata("design:paramtypes", [
531
+ typeof PlanCatalog === "undefined" ? Object : PlanCatalog,
532
+ typeof SubscriptionRepository === "undefined" ? Object : SubscriptionRepository,
533
+ typeof PlanVersionRepository === "undefined" ? Object : PlanVersionRepository,
534
+ typeof TransactionRunner === "undefined" ? Object : TransactionRunner,
535
+ Object,
536
+ Object,
537
+ Object,
538
+ Object,
539
+ Object
540
+ ])
541
+ ], EntitlementService);
542
+
543
+ // src/entitlement/module.ts
544
+ import { Module } from "@nestjs/common";
545
+ function _ts_decorate2(decorators, target, key, desc) {
546
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
547
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
548
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
549
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
550
+ }
551
+ __name(_ts_decorate2, "_ts_decorate");
552
+ var EntitlementModule = class _EntitlementModule {
553
+ static {
554
+ __name(this, "EntitlementModule");
555
+ }
556
+ static forRoot(options) {
557
+ const providers = [
558
+ asProvider(SUBSCRIPTION_REPOSITORY_TOKEN, options.subscriptionRepository),
559
+ asProvider(PLAN_VERSION_REPOSITORY_TOKEN, options.planVersionRepository),
560
+ asProvider(TRANSACTION_RUNNER_TOKEN, options.transactionRunner),
561
+ EntitlementService
562
+ ];
563
+ if (options.resolutionConfig) {
564
+ providers.push({
565
+ provide: ENTITLEMENT_RESOLUTION_CONFIG_TOKEN,
566
+ useValue: options.resolutionConfig
567
+ });
568
+ }
569
+ if (options.subscriptionContractRepository) {
570
+ providers.push(asProvider(SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN, options.subscriptionContractRepository));
571
+ }
572
+ if (options.subscriptionBundleRepository) {
573
+ providers.push(asProvider(SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN, options.subscriptionBundleRepository));
574
+ }
575
+ if (options.bundleRepository) {
576
+ providers.push(asProvider(BUNDLE_REPOSITORY_TOKEN, options.bundleRepository));
577
+ }
578
+ return {
579
+ module: _EntitlementModule,
580
+ global: options.global ?? false,
581
+ providers,
582
+ exports: [
583
+ EntitlementService,
584
+ SUBSCRIPTION_REPOSITORY_TOKEN
585
+ ]
586
+ };
587
+ }
588
+ };
589
+ EntitlementModule = _ts_decorate2([
590
+ Module({})
591
+ ], EntitlementModule);
592
+
593
+ export {
594
+ SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN,
595
+ SUBSCRIPTION_BUNDLE_CONFIG_TOKEN,
596
+ aggregateBusinessTypeQuotas,
597
+ filterActiveSubscriptionBundles,
598
+ aggregateSubscriptionBundleQuotas,
599
+ collectSubscriptionBundleFeatures,
600
+ aggregateContractLineItemEntitlements,
601
+ collectBusinessTypeFeatures,
602
+ filterPlannedOnlyFeatures,
603
+ applyCustomLimits,
604
+ aggregateLimits,
605
+ hasFeature,
606
+ hasAnyFeature,
607
+ toEffectiveLimitsSnapshot,
608
+ buildReplacedByIndex,
609
+ expandReplacedFeatures,
610
+ LimitExceededError,
611
+ resolveEntitlementPlan,
612
+ EntitlementService,
613
+ EntitlementModule
614
+ };