@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,3942 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from3, except, desc) => {
14
+ if (from3 && typeof from3 === "object" || typeof from3 === "function") {
15
+ for (let key of __getOwnPropNames(from3))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from3[key], enumerable: !(desc = __getOwnPropDesc(from3, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/testing/index.ts
32
+ var testing_exports = {};
33
+ __export(testing_exports, {
34
+ FakeBundleRepository: () => FakeBundleRepository,
35
+ FakeBusinessTypeRepository: () => FakeBusinessTypeRepository,
36
+ FakeMarketingProjectionRepository: () => FakeMarketingProjectionRepository,
37
+ FakePlanRepository: () => FakePlanRepository,
38
+ FakePlanVersionRepository: () => FakePlanVersionRepository,
39
+ FakeSubscriptionBundleRepository: () => FakeSubscriptionBundleRepository,
40
+ FakeSubscriptionContractRepository: () => FakeSubscriptionContractRepository,
41
+ FakeSubscriptionRepository: () => FakeSubscriptionRepository,
42
+ FakeTransactionRunner: () => FakeTransactionRunner,
43
+ StubAuditPort: () => StubAuditPort,
44
+ StubMfaPort: () => StubMfaPort,
45
+ StubRlsBypassPort: () => StubRlsBypassPort,
46
+ createSaasPlatformTestModule: () => createSaasPlatformTestModule
47
+ });
48
+ module.exports = __toCommonJS(testing_exports);
49
+
50
+ // src/testing/fake-repositories.ts
51
+ var import_types = require("@saasicat/types");
52
+ var FakeSubscriptionRepository = class {
53
+ static {
54
+ __name(this, "FakeSubscriptionRepository");
55
+ }
56
+ byTenantId = /* @__PURE__ */ new Map();
57
+ /**
58
+ * Test stubs for the `countBy*` editability methods. Tests set them
59
+ * directly; default 0 (= "no subscription binds the version" →
60
+ * published-but-future versions are editable when they are latest-in-chain
61
+ * and validFrom lies in the future).
62
+ */
63
+ planVersionCounts = /* @__PURE__ */ new Map();
64
+ bundleVersionCounts = /* @__PURE__ */ new Map();
65
+ set(record) {
66
+ this.byTenantId.set(record.tenantId, record);
67
+ }
68
+ delete(tenantId) {
69
+ this.byTenantId.delete(tenantId);
70
+ }
71
+ clear() {
72
+ this.byTenantId.clear();
73
+ this.planVersionCounts.clear();
74
+ this.bundleVersionCounts.clear();
75
+ }
76
+ /** Test helper: force the number of subscriptions for a PlanVersion. */
77
+ setPlanVersionCount(planVersionId, count) {
78
+ this.planVersionCounts.set(planVersionId, count);
79
+ }
80
+ /** Test helper: force the number of subscriptions for a BundleVersion. */
81
+ setBundleVersionCount(bundleVersionId, count) {
82
+ this.bundleVersionCounts.set(bundleVersionId, count);
83
+ }
84
+ async findByTenantId(tenantId) {
85
+ return this.byTenantId.get(tenantId) ?? null;
86
+ }
87
+ async findByTenantIdLocked(tenantId, _tx) {
88
+ return this.findByTenantId(tenantId);
89
+ }
90
+ async countByPlanVersionId(planVersionId) {
91
+ return this.planVersionCounts.get(planVersionId) ?? 0;
92
+ }
93
+ async countByBundleVersionId(bundleVersionId) {
94
+ return this.bundleVersionCounts.get(bundleVersionId) ?? 0;
95
+ }
96
+ };
97
+ var FakeSubscriptionBundleRepository = class {
98
+ static {
99
+ __name(this, "FakeSubscriptionBundleRepository");
100
+ }
101
+ byId = /* @__PURE__ */ new Map();
102
+ nextId = 1;
103
+ now() {
104
+ return /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z");
105
+ }
106
+ clear() {
107
+ this.byId.clear();
108
+ }
109
+ async listBySubscription(subscriptionId) {
110
+ return [
111
+ ...this.byId.values()
112
+ ].filter((r) => r.subscriptionId === subscriptionId).sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
113
+ }
114
+ async findById(subscriptionBundleId) {
115
+ return this.byId.get(subscriptionBundleId) ?? null;
116
+ }
117
+ async listActiveBySubscription(subscriptionId, asOf = /* @__PURE__ */ new Date()) {
118
+ return [
119
+ ...this.byId.values()
120
+ ].filter((r) => r.subscriptionId === subscriptionId && (r.canceledAt === null || r.canceledEffectiveAt !== null && r.canceledEffectiveAt.getTime() > asOf.getTime()));
121
+ }
122
+ async add(data) {
123
+ const id = `sub-bundle-${this.nextId++}`;
124
+ const createdAt = this.now();
125
+ const row = {
126
+ id,
127
+ subscriptionId: data.subscriptionId,
128
+ bundleVersionId: data.bundleVersionId,
129
+ startedAt: data.startedAt,
130
+ minimumTermEndsAt: data.minimumTermEndsAt ?? null,
131
+ canceledAt: null,
132
+ canceledEffectiveAt: null,
133
+ createdAt,
134
+ updatedAt: createdAt
135
+ };
136
+ this.byId.set(id, row);
137
+ return row;
138
+ }
139
+ async cancel(subscriptionBundleId, data) {
140
+ const existing = this.byId.get(subscriptionBundleId);
141
+ if (!existing) {
142
+ throw new Error(`SubscriptionBundle '${subscriptionBundleId}' nicht gefunden`);
143
+ }
144
+ if (existing.canceledAt !== null) {
145
+ throw new Error(`SubscriptionBundle '${subscriptionBundleId}' ist bereits gek\xFCndigt`);
146
+ }
147
+ const updated = {
148
+ ...existing,
149
+ canceledAt: data.canceledAt,
150
+ canceledEffectiveAt: data.canceledEffectiveAt,
151
+ updatedAt: this.now()
152
+ };
153
+ this.byId.set(subscriptionBundleId, updated);
154
+ return updated;
155
+ }
156
+ async reactivate(subscriptionBundleId) {
157
+ const existing = this.byId.get(subscriptionBundleId);
158
+ if (!existing) {
159
+ throw new Error(`SubscriptionBundle '${subscriptionBundleId}' nicht gefunden`);
160
+ }
161
+ const updated = {
162
+ ...existing,
163
+ canceledAt: null,
164
+ canceledEffectiveAt: null,
165
+ updatedAt: this.now()
166
+ };
167
+ this.byId.set(subscriptionBundleId, updated);
168
+ return updated;
169
+ }
170
+ async countActiveByBundleVersionId(bundleVersionId, asOf = /* @__PURE__ */ new Date()) {
171
+ let n = 0;
172
+ for (const r of this.byId.values()) {
173
+ if (r.bundleVersionId !== bundleVersionId) continue;
174
+ if (r.canceledAt === null) n += 1;
175
+ else if (r.canceledEffectiveAt !== null && r.canceledEffectiveAt.getTime() > asOf.getTime()) {
176
+ n += 1;
177
+ }
178
+ }
179
+ return n;
180
+ }
181
+ };
182
+ var FakeSubscriptionContractRepository = class {
183
+ static {
184
+ __name(this, "FakeSubscriptionContractRepository");
185
+ }
186
+ byId = /* @__PURE__ */ new Map();
187
+ nextContractId = 1;
188
+ nextLineItemId = 1;
189
+ clear() {
190
+ this.byId.clear();
191
+ this.nextContractId = 1;
192
+ this.nextLineItemId = 1;
193
+ }
194
+ async list(filter) {
195
+ return [
196
+ ...this.byId.values()
197
+ ].filter((row) => !filter.projectKey || row.projectKey === filter.projectKey).filter((row) => !filter.tenantId || row.tenantId === filter.tenantId).filter((row) => !filter.status || row.status === filter.status).filter((row) => !filter.asOf || this.isActiveAt(row, filter.asOf)).sort((a, b) => b.effectiveFrom.getTime() - a.effectiveFrom.getTime()).map((row) => this.cloneRecord(row));
198
+ }
199
+ async findById(contractId) {
200
+ const row = this.byId.get(contractId);
201
+ return row ? this.cloneRecord(row) : null;
202
+ }
203
+ async findActiveByTenantId(tenantId, asOf = /* @__PURE__ */ new Date()) {
204
+ const active = [
205
+ ...this.byId.values()
206
+ ].filter((row) => row.tenantId === tenantId).filter((row) => this.isActiveAt(row, asOf)).sort((a, b) => b.effectiveFrom.getTime() - a.effectiveFrom.getTime())[0];
207
+ return active ? this.cloneRecord(active) : null;
208
+ }
209
+ async create(data) {
210
+ const now = /* @__PURE__ */ new Date();
211
+ const id = `contract-${this.nextContractId++}`;
212
+ const row = {
213
+ id,
214
+ projectKey: data.projectKey,
215
+ tenantId: data.tenantId,
216
+ status: data.status ?? "active",
217
+ effectiveFrom: new Date(data.effectiveFrom),
218
+ effectiveUntil: data.effectiveUntil ? new Date(data.effectiveUntil) : null,
219
+ originalOfferId: data.originalOfferId ?? null,
220
+ originalPlanVersionId: data.originalPlanVersionId ?? null,
221
+ originalBundleVersionIds: [
222
+ ...data.originalBundleVersionIds ?? []
223
+ ],
224
+ entitlementSnapshot: data.entitlementSnapshot ? {
225
+ plan: data.entitlementSnapshot.plan,
226
+ quotas: {
227
+ ...data.entitlementSnapshot.quotas
228
+ },
229
+ features: [
230
+ ...data.entitlementSnapshot.features
231
+ ]
232
+ } : null,
233
+ priceSnapshot: {
234
+ ...data.priceSnapshot
235
+ },
236
+ promotionSnapshots: [
237
+ ...data.promotionSnapshots ?? []
238
+ ],
239
+ promoCodeSnapshots: [
240
+ ...data.promoCodeSnapshots ?? []
241
+ ],
242
+ termsSnapshot: data.termsSnapshot ? {
243
+ ...data.termsSnapshot
244
+ } : null,
245
+ lineItems: data.lineItems.map((item) => ({
246
+ ...item,
247
+ id: `contract-line-${this.nextLineItemId++}`,
248
+ contractId: id,
249
+ minimumTermUntil: item.minimumTermUntil ? new Date(item.minimumTermUntil) : null,
250
+ featuresSnapshot: [
251
+ ...item.featuresSnapshot
252
+ ],
253
+ quotaEffectsSnapshot: {
254
+ ...item.quotaEffectsSnapshot
255
+ },
256
+ metadata: item.metadata ? {
257
+ ...item.metadata
258
+ } : null,
259
+ createdAt: now
260
+ })),
261
+ createdAt: now,
262
+ updatedAt: now
263
+ };
264
+ this.byId.set(id, row);
265
+ return this.cloneRecord(row);
266
+ }
267
+ async terminate(contractId, data) {
268
+ const existing = this.byId.get(contractId);
269
+ if (!existing) throw new Error(`SubscriptionContract '${contractId}' nicht gefunden`);
270
+ const updated = {
271
+ ...existing,
272
+ status: data.status,
273
+ effectiveUntil: new Date(data.effectiveUntil),
274
+ updatedAt: /* @__PURE__ */ new Date()
275
+ };
276
+ this.byId.set(contractId, updated);
277
+ return this.cloneRecord(updated);
278
+ }
279
+ isActiveAt(row, asOf) {
280
+ if (row.status !== "active" && row.status !== "scheduled") return false;
281
+ return row.effectiveFrom.getTime() <= asOf.getTime() && (row.effectiveUntil === null || row.effectiveUntil.getTime() > asOf.getTime());
282
+ }
283
+ cloneRecord(row) {
284
+ return {
285
+ ...row,
286
+ effectiveFrom: new Date(row.effectiveFrom),
287
+ effectiveUntil: row.effectiveUntil ? new Date(row.effectiveUntil) : null,
288
+ originalBundleVersionIds: [
289
+ ...row.originalBundleVersionIds
290
+ ],
291
+ entitlementSnapshot: row.entitlementSnapshot ? {
292
+ plan: row.entitlementSnapshot.plan,
293
+ quotas: {
294
+ ...row.entitlementSnapshot.quotas
295
+ },
296
+ features: [
297
+ ...row.entitlementSnapshot.features
298
+ ]
299
+ } : null,
300
+ priceSnapshot: {
301
+ ...row.priceSnapshot
302
+ },
303
+ promotionSnapshots: [
304
+ ...row.promotionSnapshots
305
+ ],
306
+ promoCodeSnapshots: [
307
+ ...row.promoCodeSnapshots
308
+ ],
309
+ termsSnapshot: row.termsSnapshot ? {
310
+ ...row.termsSnapshot
311
+ } : null,
312
+ lineItems: row.lineItems.map((item) => ({
313
+ ...item,
314
+ minimumTermUntil: item.minimumTermUntil ? new Date(item.minimumTermUntil) : null,
315
+ featuresSnapshot: [
316
+ ...item.featuresSnapshot
317
+ ],
318
+ quotaEffectsSnapshot: {
319
+ ...item.quotaEffectsSnapshot
320
+ },
321
+ metadata: item.metadata ? {
322
+ ...item.metadata
323
+ } : null,
324
+ createdAt: new Date(item.createdAt)
325
+ })),
326
+ createdAt: new Date(row.createdAt),
327
+ updatedAt: new Date(row.updatedAt)
328
+ };
329
+ }
330
+ };
331
+ var FakePlanVersionRepository = class {
332
+ static {
333
+ __name(this, "FakePlanVersionRepository");
334
+ }
335
+ byPlanId = /* @__PURE__ */ new Map();
336
+ set(record) {
337
+ this.byPlanId.set(record.planId, record);
338
+ }
339
+ clear() {
340
+ this.byPlanId.clear();
341
+ }
342
+ async findLatestLive(planId, _tx) {
343
+ return this.byPlanId.get(planId) ?? null;
344
+ }
345
+ // The fake keeps only one record per planId — `findActive` returns the
346
+ // same as `findLatestLive`. Consumers test real time-window semantics
347
+ // against the Prisma implementation or against `FakePlanRepository`
348
+ // (which supports multiple versions per planKey).
349
+ async findActive(planId, _asOf, _tx) {
350
+ return this.byPlanId.get(planId) ?? null;
351
+ }
352
+ };
353
+ var FakeTransactionRunner = class _FakeTransactionRunner {
354
+ static {
355
+ __name(this, "FakeTransactionRunner");
356
+ }
357
+ /** Sentinel value passed through as the TransactionContext. */
358
+ static TX_SENTINEL = /* @__PURE__ */ Symbol.for("FakeTransactionRunner.tx");
359
+ runCount = 0;
360
+ async run(fn) {
361
+ this.runCount += 1;
362
+ return fn(_FakeTransactionRunner.TX_SENTINEL);
363
+ }
364
+ };
365
+ var FakeBundleRepository = class {
366
+ static {
367
+ __name(this, "FakeBundleRepository");
368
+ }
369
+ bundles = /* @__PURE__ */ new Map();
370
+ versions = /* @__PURE__ */ new Map();
371
+ nextId = 1;
372
+ genId(prefix) {
373
+ const n = this.nextId++;
374
+ const suffix = n.toString(16).padStart(12, "0");
375
+ return `${prefix}aaaa-aaaa-aaaa-aaaa-${suffix}`;
376
+ }
377
+ nowIso() {
378
+ return (/* @__PURE__ */ new Date()).toISOString();
379
+ }
380
+ // Test helpers
381
+ seedBundle(row) {
382
+ this.bundles.set(row.id, row);
383
+ }
384
+ seedVersion(row) {
385
+ this.versions.set(row.id, row);
386
+ }
387
+ clear() {
388
+ this.bundles.clear();
389
+ this.versions.clear();
390
+ }
391
+ // ─── Master operations ───
392
+ async list(filter) {
393
+ const excludeDeleted = filter.excludeDeleted ?? true;
394
+ return [
395
+ ...this.bundles.values()
396
+ ].filter((b) => b.projectKey === filter.projectKey && (!excludeDeleted || b.deletedAt === null));
397
+ }
398
+ async findById(bundleId) {
399
+ return this.bundles.get(bundleId) ?? null;
400
+ }
401
+ async findByKey(projectKey, bundleKey) {
402
+ for (const b of this.bundles.values()) {
403
+ if (b.projectKey === projectKey && b.bundleKey === bundleKey && b.deletedAt === null) {
404
+ return b;
405
+ }
406
+ }
407
+ return null;
408
+ }
409
+ async create(data) {
410
+ const now = this.nowIso();
411
+ const row = {
412
+ id: this.genId("bbbb"),
413
+ projectKey: data.projectKey,
414
+ bundleKey: data.bundleKey,
415
+ label: data.label,
416
+ description: data.description ?? null,
417
+ icon: data.icon ?? null,
418
+ sortOrder: data.sortOrder ?? 0,
419
+ i18n: data.i18n ?? {},
420
+ createdAt: now,
421
+ updatedAt: now,
422
+ deletedAt: null
423
+ };
424
+ this.bundles.set(row.id, row);
425
+ return row;
426
+ }
427
+ async update(bundleId, data) {
428
+ const existing = this.bundles.get(bundleId);
429
+ if (!existing) throw new Error(`Bundle '${bundleId}' nicht gefunden`);
430
+ const updated = {
431
+ ...existing,
432
+ label: data.label ?? existing.label,
433
+ description: data.description !== void 0 ? data.description : existing.description,
434
+ icon: data.icon !== void 0 ? data.icon : existing.icon,
435
+ sortOrder: data.sortOrder ?? existing.sortOrder,
436
+ i18n: data.i18n ?? existing.i18n,
437
+ updatedAt: this.nowIso()
438
+ };
439
+ this.bundles.set(bundleId, updated);
440
+ return updated;
441
+ }
442
+ async softDelete(bundleId) {
443
+ const existing = this.bundles.get(bundleId);
444
+ if (!existing) throw new Error(`Bundle '${bundleId}' nicht gefunden`);
445
+ this.bundles.set(bundleId, {
446
+ ...existing,
447
+ deletedAt: this.nowIso()
448
+ });
449
+ }
450
+ // ─── Version operations ───
451
+ async listVersions(bundleId) {
452
+ return [
453
+ ...this.versions.values()
454
+ ].filter((v) => v.bundleId === bundleId).sort((a, b) => a.version - b.version);
455
+ }
456
+ async findVersionById(versionId) {
457
+ return this.versions.get(versionId) ?? null;
458
+ }
459
+ async findCurrentDraft(bundleId) {
460
+ for (const v of this.versions.values()) {
461
+ if (v.bundleId === bundleId && v.publishedAt === null) return v;
462
+ }
463
+ return null;
464
+ }
465
+ async findLatestLive(bundleId) {
466
+ for (const v of this.versions.values()) {
467
+ if (v.bundleId === bundleId && v.publishedAt !== null && v.supersededAt === null) {
468
+ return v;
469
+ }
470
+ }
471
+ return null;
472
+ }
473
+ async createDraft(data) {
474
+ const draft = await this.findCurrentDraft(data.bundleId);
475
+ if (draft) {
476
+ throw new Error(`Bundle '${data.bundleId}' hat bereits eine Draft-Version v${draft.version}`);
477
+ }
478
+ const all = await this.listVersions(data.bundleId);
479
+ const nextVersion = all.length === 0 ? 1 : Math.max(...all.map((v) => v.version)) + 1;
480
+ const now = this.nowIso();
481
+ const row = {
482
+ id: this.genId("vvvv"),
483
+ version: nextVersion,
484
+ baseVersionId: data.baseVersionId ?? null,
485
+ publishedAt: null,
486
+ supersededAt: null,
487
+ publishedChanges: null,
488
+ changeNote: data.changeNote ?? "",
489
+ nonRegressive: true,
490
+ createdByUserId: data.createdByUserId ?? null,
491
+ publishedByUserId: null,
492
+ validFrom: data.validFrom ?? null,
493
+ validUntil: data.validUntil ?? null,
494
+ createdAt: now,
495
+ updatedAt: now,
496
+ bundleId: data.bundleId,
497
+ bundleKey: this.bundles.get(data.bundleId)?.bundleKey ?? "",
498
+ label: this.bundles.get(data.bundleId)?.label ?? "",
499
+ features: data.features,
500
+ quotas: data.quotas ?? {},
501
+ compatibility: data.compatibility ?? {},
502
+ pricingOverrides: data.pricingOverrides ?? [],
503
+ monthlyNet: data.monthlyNet ?? null,
504
+ yearlyNet: data.yearlyNet ?? null,
505
+ marketed: data.marketed ?? true
506
+ };
507
+ this.versions.set(row.id, row);
508
+ return row;
509
+ }
510
+ async updateDraft(versionId, data) {
511
+ const existing = this.versions.get(versionId);
512
+ if (!existing) throw new Error(`BundleVersion '${versionId}' nicht gefunden`);
513
+ const updated = {
514
+ ...existing,
515
+ features: data.features ?? existing.features,
516
+ quotas: data.quotas ?? existing.quotas,
517
+ compatibility: data.compatibility ?? existing.compatibility,
518
+ pricingOverrides: data.pricingOverrides ?? existing.pricingOverrides,
519
+ monthlyNet: data.monthlyNet !== void 0 ? data.monthlyNet : existing.monthlyNet,
520
+ yearlyNet: data.yearlyNet !== void 0 ? data.yearlyNet : existing.yearlyNet,
521
+ marketed: data.marketed ?? existing.marketed,
522
+ changeNote: data.changeNote ?? existing.changeNote,
523
+ validFrom: data.validFrom !== void 0 ? data.validFrom : existing.validFrom,
524
+ validUntil: data.validUntil !== void 0 ? data.validUntil : existing.validUntil,
525
+ updatedAt: this.nowIso()
526
+ };
527
+ this.versions.set(versionId, updated);
528
+ return updated;
529
+ }
530
+ async deleteDraft(versionId) {
531
+ const existing = this.versions.get(versionId);
532
+ if (!existing) return;
533
+ if (existing.publishedAt !== null) {
534
+ throw new Error(`BundleVersion '${versionId}' ist bereits published`);
535
+ }
536
+ this.versions.delete(versionId);
537
+ }
538
+ async publishDraft(versionId, publishMeta) {
539
+ const draft = this.versions.get(versionId);
540
+ if (!draft) throw new Error(`BundleVersion '${versionId}' nicht gefunden`);
541
+ if (draft.publishedAt !== null) {
542
+ throw new Error(`BundleVersion '${versionId}' ist bereits published`);
543
+ }
544
+ const now = this.nowIso();
545
+ const validFromIso = publishMeta.validFrom.toISOString();
546
+ const validUntilIso = publishMeta.validUntil ? publishMeta.validUntil.toISOString() : null;
547
+ const previous = await this.findLatestLive(draft.bundleId);
548
+ if (previous) {
549
+ const dayMs = 24 * 60 * 60 * 1e3;
550
+ const inheritedValidUntil = new Date(publishMeta.validFrom.getTime() - dayMs).toISOString();
551
+ this.versions.set(previous.id, {
552
+ ...previous,
553
+ supersededAt: now,
554
+ validUntil: inheritedValidUntil,
555
+ updatedAt: now
556
+ });
557
+ }
558
+ const published = {
559
+ ...draft,
560
+ publishedAt: now,
561
+ publishedChanges: publishMeta.publishedChanges,
562
+ nonRegressive: publishMeta.nonRegressive,
563
+ publishedByUserId: publishMeta.publishedByUserId,
564
+ validFrom: validFromIso,
565
+ validUntil: validUntilIso,
566
+ updatedAt: now
567
+ };
568
+ this.versions.set(versionId, published);
569
+ return published;
570
+ }
571
+ };
572
+ var FakeBusinessTypeRepository = class {
573
+ static {
574
+ __name(this, "FakeBusinessTypeRepository");
575
+ }
576
+ types = /* @__PURE__ */ new Map();
577
+ versions = /* @__PURE__ */ new Map();
578
+ nextId = 1;
579
+ genId(prefix) {
580
+ const n = this.nextId++;
581
+ const suffix = n.toString(16).padStart(12, "0");
582
+ return `${prefix}aaaa-aaaa-aaaa-aaaa-${suffix}`;
583
+ }
584
+ nowIso() {
585
+ return (/* @__PURE__ */ new Date()).toISOString();
586
+ }
587
+ seedBusinessType(row) {
588
+ this.types.set(row.id, row);
589
+ }
590
+ seedVersion(row) {
591
+ this.versions.set(row.id, row);
592
+ }
593
+ clear() {
594
+ this.types.clear();
595
+ this.versions.clear();
596
+ }
597
+ async list(filter) {
598
+ const excludeDeleted = filter.excludeDeleted ?? true;
599
+ return [
600
+ ...this.types.values()
601
+ ].filter((t) => t.projectKey === filter.projectKey && (!excludeDeleted || t.deletedAt === null));
602
+ }
603
+ async findById(businessTypeId) {
604
+ return this.types.get(businessTypeId) ?? null;
605
+ }
606
+ async findByKey(projectKey, businessTypeKey) {
607
+ for (const t of this.types.values()) {
608
+ if (t.projectKey === projectKey && t.businessTypeKey === businessTypeKey && t.deletedAt === null) {
609
+ return t;
610
+ }
611
+ }
612
+ return null;
613
+ }
614
+ async create(data) {
615
+ const now = this.nowIso();
616
+ const row = {
617
+ id: this.genId("cccc"),
618
+ projectKey: data.projectKey,
619
+ businessTypeKey: data.businessTypeKey,
620
+ label: data.label,
621
+ description: data.description ?? null,
622
+ icon: data.icon ?? null,
623
+ sortOrder: data.sortOrder ?? 0,
624
+ createdAt: now,
625
+ updatedAt: now,
626
+ deletedAt: null
627
+ };
628
+ this.types.set(row.id, row);
629
+ return row;
630
+ }
631
+ async update(businessTypeId, data) {
632
+ const existing = this.types.get(businessTypeId);
633
+ if (!existing) throw new Error(`BusinessType '${businessTypeId}' nicht gefunden`);
634
+ const updated = {
635
+ ...existing,
636
+ label: data.label ?? existing.label,
637
+ description: data.description !== void 0 ? data.description : existing.description,
638
+ icon: data.icon !== void 0 ? data.icon : existing.icon,
639
+ sortOrder: data.sortOrder ?? existing.sortOrder,
640
+ updatedAt: this.nowIso()
641
+ };
642
+ this.types.set(businessTypeId, updated);
643
+ return updated;
644
+ }
645
+ async softDelete(businessTypeId) {
646
+ const existing = this.types.get(businessTypeId);
647
+ if (!existing) throw new Error(`BusinessType '${businessTypeId}' nicht gefunden`);
648
+ this.types.set(businessTypeId, {
649
+ ...existing,
650
+ deletedAt: this.nowIso()
651
+ });
652
+ }
653
+ async listVersions(businessTypeId) {
654
+ return [
655
+ ...this.versions.values()
656
+ ].filter((v) => v.businessTypeId === businessTypeId).sort((a, b) => a.version - b.version);
657
+ }
658
+ async findVersionById(versionId) {
659
+ return this.versions.get(versionId) ?? null;
660
+ }
661
+ async findCurrentDraft(businessTypeId) {
662
+ for (const v of this.versions.values()) {
663
+ if (v.businessTypeId === businessTypeId && v.publishedAt === null) return v;
664
+ }
665
+ return null;
666
+ }
667
+ async findLatestLive(businessTypeId) {
668
+ for (const v of this.versions.values()) {
669
+ if (v.businessTypeId === businessTypeId && v.publishedAt !== null && v.supersededAt === null) {
670
+ return v;
671
+ }
672
+ }
673
+ return null;
674
+ }
675
+ async createDraft(data) {
676
+ const draft = await this.findCurrentDraft(data.businessTypeId);
677
+ if (draft) {
678
+ throw new Error(`BusinessType '${data.businessTypeId}' hat bereits eine Draft-Version v${draft.version}`);
679
+ }
680
+ const all = await this.listVersions(data.businessTypeId);
681
+ const nextVersion = all.length === 0 ? 1 : Math.max(...all.map((v) => v.version)) + 1;
682
+ const now = this.nowIso();
683
+ const businessType = this.types.get(data.businessTypeId);
684
+ const bundles = data.bundles.map((b) => ({
685
+ bundleVersionId: b.bundleVersionId,
686
+ bundleKey: "",
687
+ bundleLabel: "",
688
+ bundleVersion: 0,
689
+ sortOrder: b.sortOrder ?? 0
690
+ }));
691
+ const row = {
692
+ id: this.genId("wwww"),
693
+ version: nextVersion,
694
+ baseVersionId: data.baseVersionId ?? null,
695
+ publishedAt: null,
696
+ supersededAt: null,
697
+ publishedChanges: null,
698
+ changeNote: data.changeNote ?? "",
699
+ nonRegressive: true,
700
+ createdByUserId: data.createdByUserId ?? null,
701
+ publishedByUserId: null,
702
+ validFrom: null,
703
+ validUntil: null,
704
+ createdAt: now,
705
+ updatedAt: now,
706
+ businessTypeId: data.businessTypeId,
707
+ businessTypeKey: businessType?.businessTypeKey ?? "",
708
+ label: businessType?.label ?? "",
709
+ quotaOverrides: data.quotaOverrides ?? {},
710
+ monthlyNet: data.monthlyNet ?? null,
711
+ yearlyNet: data.yearlyNet ?? null,
712
+ marketed: data.marketed ?? true,
713
+ bundles
714
+ };
715
+ this.versions.set(row.id, row);
716
+ return row;
717
+ }
718
+ async updateDraft(versionId, data) {
719
+ const existing = this.versions.get(versionId);
720
+ if (!existing) throw new Error(`BusinessTypeVersion '${versionId}' nicht gefunden`);
721
+ if (existing.publishedAt !== null) {
722
+ throw new Error(`BusinessTypeVersion '${versionId}' ist bereits published`);
723
+ }
724
+ const bundles = data.bundles ? data.bundles.map((b) => ({
725
+ bundleVersionId: b.bundleVersionId,
726
+ bundleKey: "",
727
+ bundleLabel: "",
728
+ bundleVersion: 0,
729
+ sortOrder: b.sortOrder ?? 0
730
+ })) : existing.bundles;
731
+ const updated = {
732
+ ...existing,
733
+ quotaOverrides: data.quotaOverrides ?? existing.quotaOverrides,
734
+ monthlyNet: data.monthlyNet !== void 0 ? data.monthlyNet : existing.monthlyNet,
735
+ yearlyNet: data.yearlyNet !== void 0 ? data.yearlyNet : existing.yearlyNet,
736
+ marketed: data.marketed ?? existing.marketed,
737
+ changeNote: data.changeNote ?? existing.changeNote,
738
+ bundles,
739
+ updatedAt: this.nowIso()
740
+ };
741
+ this.versions.set(versionId, updated);
742
+ return updated;
743
+ }
744
+ async publishDraft(versionId, publishMeta) {
745
+ const draft = this.versions.get(versionId);
746
+ if (!draft) throw new Error(`BusinessTypeVersion '${versionId}' nicht gefunden`);
747
+ if (draft.publishedAt !== null) {
748
+ throw new Error(`BusinessTypeVersion '${versionId}' ist bereits published`);
749
+ }
750
+ const now = this.nowIso();
751
+ const previous = await this.findLatestLive(draft.businessTypeId);
752
+ if (previous) {
753
+ this.versions.set(previous.id, {
754
+ ...previous,
755
+ supersededAt: now,
756
+ updatedAt: now
757
+ });
758
+ }
759
+ const published = {
760
+ ...draft,
761
+ publishedAt: now,
762
+ publishedChanges: publishMeta.publishedChanges,
763
+ nonRegressive: publishMeta.nonRegressive,
764
+ publishedByUserId: publishMeta.publishedByUserId,
765
+ updatedAt: now
766
+ };
767
+ this.versions.set(versionId, published);
768
+ return published;
769
+ }
770
+ };
771
+ var FakeMarketingProjectionRepository = class {
772
+ static {
773
+ __name(this, "FakeMarketingProjectionRepository");
774
+ }
775
+ rows = /* @__PURE__ */ new Map();
776
+ nextId = 1;
777
+ genId() {
778
+ const n = this.nextId++;
779
+ const suffix = n.toString(16).padStart(12, "0");
780
+ return `mmmmaaaa-aaaa-aaaa-aaaa-${suffix}`;
781
+ }
782
+ nowIso() {
783
+ return (/* @__PURE__ */ new Date()).toISOString();
784
+ }
785
+ seed(row) {
786
+ this.rows.set(row.id, row);
787
+ }
788
+ clear() {
789
+ this.rows.clear();
790
+ }
791
+ async list(filter) {
792
+ return [
793
+ ...this.rows.values()
794
+ ].filter((r) => {
795
+ if (r.projectKey !== filter.projectKey) return false;
796
+ if (filter.targetType && r.targetType !== filter.targetType) return false;
797
+ if (filter.targetVersionId && r.targetVersionId !== filter.targetVersionId) return false;
798
+ if (filter.locale && r.locale !== filter.locale) return false;
799
+ return true;
800
+ });
801
+ }
802
+ async findById(id) {
803
+ return this.rows.get(id) ?? null;
804
+ }
805
+ async findByTarget(targetType, targetVersionId, locale) {
806
+ for (const r of this.rows.values()) {
807
+ if (r.targetType === targetType && r.targetVersionId === targetVersionId && r.locale === locale) {
808
+ return r;
809
+ }
810
+ }
811
+ return null;
812
+ }
813
+ async create(data) {
814
+ const locale = data.locale ?? "de";
815
+ const existing = await this.findByTarget(data.targetType, data.targetVersionId, locale);
816
+ if (existing) {
817
+ throw new Error(`MarketingProjection f\xFCr ${data.targetType}/${data.targetVersionId}/${locale} existiert bereits`);
818
+ }
819
+ const now = this.nowIso();
820
+ const row = {
821
+ id: this.genId(),
822
+ projectKey: data.projectKey,
823
+ targetType: data.targetType,
824
+ targetVersionId: data.targetVersionId,
825
+ locale,
826
+ displayLabel: data.displayLabel,
827
+ description: data.description,
828
+ visible: data.visible ?? true,
829
+ badge: data.badge ?? "",
830
+ topFeatures: data.topFeatures ?? [],
831
+ trialEnabled: data.trialEnabled ?? false,
832
+ trialDays: data.trialDays ?? 30,
833
+ priceTag: data.priceTag ?? null,
834
+ ctaLabel: data.ctaLabel ?? null,
835
+ priority: data.priority ?? 0,
836
+ highlight: data.highlight ?? false,
837
+ createdAt: now,
838
+ updatedAt: now
839
+ };
840
+ this.rows.set(row.id, row);
841
+ return row;
842
+ }
843
+ async update(id, data) {
844
+ const existing = this.rows.get(id);
845
+ if (!existing) throw new Error(`MarketingProjection '${id}' nicht gefunden`);
846
+ const updated = {
847
+ ...existing,
848
+ displayLabel: data.displayLabel ?? existing.displayLabel,
849
+ description: data.description ?? existing.description,
850
+ visible: data.visible ?? existing.visible,
851
+ badge: data.badge ?? existing.badge,
852
+ topFeatures: data.topFeatures ?? existing.topFeatures,
853
+ trialEnabled: data.trialEnabled ?? existing.trialEnabled,
854
+ trialDays: data.trialDays ?? existing.trialDays,
855
+ priceTag: data.priceTag !== void 0 ? data.priceTag : existing.priceTag,
856
+ ctaLabel: data.ctaLabel !== void 0 ? data.ctaLabel : existing.ctaLabel,
857
+ priority: data.priority ?? existing.priority,
858
+ highlight: data.highlight ?? existing.highlight,
859
+ updatedAt: this.nowIso()
860
+ };
861
+ this.rows.set(id, updated);
862
+ return updated;
863
+ }
864
+ async delete(id) {
865
+ if (!this.rows.has(id)) throw new Error(`MarketingProjection '${id}' nicht gefunden`);
866
+ this.rows.delete(id);
867
+ }
868
+ };
869
+ var FakePlanRepository = class {
870
+ static {
871
+ __name(this, "FakePlanRepository");
872
+ }
873
+ plans = /* @__PURE__ */ new Map();
874
+ versions = /* @__PURE__ */ new Map();
875
+ nextId = 1;
876
+ nextVersionId = 1;
877
+ genId() {
878
+ const n = this.nextId++;
879
+ const suffix = n.toString(16).padStart(12, "0");
880
+ return `dddd-aaaa-aaaa-aaaa-aaaa-${suffix}`;
881
+ }
882
+ genVersionId() {
883
+ const n = this.nextVersionId++;
884
+ const suffix = n.toString(16).padStart(12, "0");
885
+ return `eeee-aaaa-aaaa-aaaa-aaaa-${suffix}`;
886
+ }
887
+ nowIso() {
888
+ return (/* @__PURE__ */ new Date()).toISOString();
889
+ }
890
+ seed(row) {
891
+ this.plans.set(row.id, row);
892
+ }
893
+ seedVersion(row) {
894
+ this.versions.set(row.id, row);
895
+ }
896
+ clear() {
897
+ this.plans.clear();
898
+ this.versions.clear();
899
+ }
900
+ async list(filter) {
901
+ const excludeDeleted = filter.excludeDeleted ?? true;
902
+ const liveKeys = filter.onlyPublished ? new Set([
903
+ ...this.versions.values()
904
+ ].filter((v) => v.publishedAt != null && v.supersededAt == null).map((v) => v.planId)) : null;
905
+ return [
906
+ ...this.plans.values()
907
+ ].filter((p) => p.projectKey === filter.projectKey && (!excludeDeleted || p.deletedAt === null) && (!liveKeys || liveKeys.has(p.planKey)));
908
+ }
909
+ async findById(planId) {
910
+ return this.plans.get(planId) ?? null;
911
+ }
912
+ async findByKey(projectKey, planKey) {
913
+ for (const p of this.plans.values()) {
914
+ if (p.projectKey === projectKey && p.planKey === planKey && p.deletedAt === null) {
915
+ return p;
916
+ }
917
+ }
918
+ return null;
919
+ }
920
+ async create(data) {
921
+ const now = this.nowIso();
922
+ const row = {
923
+ id: this.genId(),
924
+ projectKey: data.projectKey,
925
+ planKey: data.planKey,
926
+ label: data.label,
927
+ description: data.description ?? null,
928
+ icon: data.icon ?? null,
929
+ sortOrder: data.sortOrder ?? 0,
930
+ createdAt: now,
931
+ updatedAt: now,
932
+ deletedAt: null
933
+ };
934
+ this.plans.set(row.id, row);
935
+ return row;
936
+ }
937
+ async update(planId, data) {
938
+ const existing = this.plans.get(planId);
939
+ if (!existing) throw new Error(`Plan '${planId}' nicht gefunden`);
940
+ const updated = {
941
+ ...existing,
942
+ label: data.label ?? existing.label,
943
+ description: data.description ?? existing.description,
944
+ icon: data.icon ?? existing.icon,
945
+ sortOrder: data.sortOrder ?? existing.sortOrder,
946
+ updatedAt: this.nowIso()
947
+ };
948
+ this.plans.set(planId, updated);
949
+ return updated;
950
+ }
951
+ async softDelete(planId) {
952
+ const existing = this.plans.get(planId);
953
+ if (!existing) throw new Error(`Plan '${planId}' nicht gefunden`);
954
+ const updated = {
955
+ ...existing,
956
+ deletedAt: this.nowIso(),
957
+ updatedAt: this.nowIso()
958
+ };
959
+ this.plans.set(planId, updated);
960
+ }
961
+ async hardDelete(planId) {
962
+ const existing = this.plans.get(planId);
963
+ if (!existing) return;
964
+ this.plans.delete(planId);
965
+ for (const [id, v] of this.versions.entries()) {
966
+ if (v.planId === existing.planKey) this.versions.delete(id);
967
+ }
968
+ }
969
+ // ─── Lifecycle (SPEC_V2 §11.1 M6 Pack 2a) ───
970
+ // Binding Plan ↔ PlanVersion: PlanVersion.planId === Plan.planKey
971
+ // (soft string reference). Lifecycle methods take planKey
972
+ // as input — the service resolves the plan UUID beforehand.
973
+ async listVersions(planKey) {
974
+ return [
975
+ ...this.versions.values()
976
+ ].filter((v) => v.planId === planKey).sort((a, b) => a.version - b.version);
977
+ }
978
+ async findVersionById(versionId) {
979
+ return this.versions.get(versionId) ?? null;
980
+ }
981
+ async findCurrentDraft(planKey) {
982
+ for (const v of this.versions.values()) {
983
+ if (v.planId === planKey && v.publishedAt === null) return v;
984
+ }
985
+ return null;
986
+ }
987
+ async findLatestLivePlanVersion(planKey) {
988
+ const now = Date.now();
989
+ const live = [
990
+ ...this.versions.values()
991
+ ].filter((v) => {
992
+ if (v.planId !== planKey) return false;
993
+ if (v.publishedAt === null) return false;
994
+ if (v.supersededAt !== null) return false;
995
+ if (v.endsAt) {
996
+ const ends = new Date(v.endsAt).getTime();
997
+ if (!Number.isNaN(ends) && ends <= now) return false;
998
+ }
999
+ return true;
1000
+ });
1001
+ live.sort((a, b) => b.version - a.version);
1002
+ return live[0] ?? null;
1003
+ }
1004
+ async findActivePlanVersion(planKey, asOf = /* @__PURE__ */ new Date()) {
1005
+ const t = asOf.getTime();
1006
+ const dayStart = (0, import_types.startOfUtcDay)(asOf).getTime();
1007
+ const matches = [
1008
+ ...this.versions.values()
1009
+ ].filter((v) => {
1010
+ if (v.planId !== planKey) return false;
1011
+ if (v.publishedAt === null) return false;
1012
+ if (v.validFrom) {
1013
+ const from3 = new Date(v.validFrom).getTime();
1014
+ if (Number.isNaN(from3) || from3 > t) return false;
1015
+ }
1016
+ if (v.validUntil) {
1017
+ const until = new Date(v.validUntil).getTime();
1018
+ if (!Number.isNaN(until) && until < dayStart) return false;
1019
+ }
1020
+ if (v.endsAt) {
1021
+ const ends = new Date(v.endsAt).getTime();
1022
+ if (!Number.isNaN(ends) && ends <= t) return false;
1023
+ }
1024
+ return true;
1025
+ });
1026
+ matches.sort((a, b) => {
1027
+ const fa = a.validFrom ? new Date(a.validFrom).getTime() : 0;
1028
+ const fb = b.validFrom ? new Date(b.validFrom).getTime() : 0;
1029
+ if (fb !== fa) return fb - fa;
1030
+ return b.version - a.version;
1031
+ });
1032
+ return matches[0] ?? null;
1033
+ }
1034
+ async createPlanVersionDraft(data) {
1035
+ const planKey = data.planId;
1036
+ const versions = [
1037
+ ...this.versions.values()
1038
+ ].filter((v) => v.planId === planKey);
1039
+ const nextVersion = versions.reduce((max, v) => Math.max(max, v.version), 0) + 1;
1040
+ const now = this.nowIso();
1041
+ const row = {
1042
+ id: this.genVersionId(),
1043
+ planId: planKey,
1044
+ version: nextVersion,
1045
+ baseVersionId: data.baseVersionId ?? null,
1046
+ features: data.features,
1047
+ bundles: data.bundles ?? [],
1048
+ quotas: data.quotas,
1049
+ monthlyNet: data.monthlyNet,
1050
+ yearlyNet: data.yearlyNet,
1051
+ marketed: data.marketed ?? true,
1052
+ publishedAt: null,
1053
+ supersededAt: null,
1054
+ publishedChanges: null,
1055
+ changeNote: data.changeNote ?? "",
1056
+ nonRegressive: true,
1057
+ validFrom: data.validFrom ?? null,
1058
+ validUntil: data.validUntil ?? null,
1059
+ endsAt: null,
1060
+ createdByUserId: data.createdByUserId ?? null,
1061
+ publishedByUserId: null,
1062
+ createdAt: now,
1063
+ updatedAt: now
1064
+ };
1065
+ this.versions.set(row.id, row);
1066
+ return row;
1067
+ }
1068
+ async updatePlanVersionDraft(versionId, data) {
1069
+ const existing = this.versions.get(versionId);
1070
+ if (!existing) throw new Error(`PlanVersion '${versionId}' nicht gefunden`);
1071
+ const updated = {
1072
+ ...existing,
1073
+ features: data.features ?? existing.features,
1074
+ bundles: data.bundles ?? existing.bundles ?? [],
1075
+ quotas: data.quotas ?? existing.quotas,
1076
+ monthlyNet: data.monthlyNet ?? existing.monthlyNet,
1077
+ yearlyNet: data.yearlyNet ?? existing.yearlyNet,
1078
+ marketed: data.marketed ?? existing.marketed,
1079
+ changeNote: data.changeNote ?? existing.changeNote,
1080
+ updatedAt: this.nowIso()
1081
+ };
1082
+ this.versions.set(versionId, updated);
1083
+ return updated;
1084
+ }
1085
+ async publishPlanVersionDraft(versionId, publishMeta) {
1086
+ const draft = this.versions.get(versionId);
1087
+ if (!draft) throw new Error(`PlanVersion '${versionId}' nicht gefunden`);
1088
+ const planKey = draft.planId;
1089
+ const now = this.nowIso();
1090
+ const dayMs = 24 * 60 * 60 * 1e3;
1091
+ const inheritedValidUntil = new Date(publishMeta.validFrom.getTime() - dayMs).toISOString();
1092
+ for (const v of this.versions.values()) {
1093
+ if (v.planId === planKey && v.publishedAt !== null && v.supersededAt === null && v.id !== versionId) {
1094
+ this.versions.set(v.id, {
1095
+ ...v,
1096
+ supersededAt: now,
1097
+ validUntil: v.validUntil ?? inheritedValidUntil,
1098
+ updatedAt: now
1099
+ });
1100
+ }
1101
+ }
1102
+ const published = {
1103
+ ...draft,
1104
+ publishedAt: now,
1105
+ publishedChanges: publishMeta.publishedChanges,
1106
+ nonRegressive: publishMeta.nonRegressive,
1107
+ publishedByUserId: publishMeta.publishedByUserId,
1108
+ validFrom: publishMeta.validFrom.toISOString(),
1109
+ validUntil: publishMeta.validUntil ? publishMeta.validUntil.toISOString() : null,
1110
+ updatedAt: now
1111
+ };
1112
+ this.versions.set(versionId, published);
1113
+ return published;
1114
+ }
1115
+ async deletePlanVersionDraft(versionId) {
1116
+ const existing = this.versions.get(versionId);
1117
+ if (!existing) return;
1118
+ if (existing.publishedAt !== null) {
1119
+ throw new Error(`FakePlanRepository: PlanVersion '${versionId}' ist bereits published \u2014 Discard nicht erlaubt`);
1120
+ }
1121
+ this.versions.delete(versionId);
1122
+ }
1123
+ async terminate(versionId, endsAt) {
1124
+ const existing = this.versions.get(versionId);
1125
+ if (!existing) {
1126
+ throw new Error(`FakePlanRepository: PlanVersion '${versionId}' nicht gefunden`);
1127
+ }
1128
+ const updated = {
1129
+ ...existing,
1130
+ endsAt: endsAt.toISOString(),
1131
+ updatedAt: this.nowIso()
1132
+ };
1133
+ this.versions.set(versionId, updated);
1134
+ return updated;
1135
+ }
1136
+ };
1137
+
1138
+ // src/testing/create-saas-platform-test-module.ts
1139
+ var import_common23 = require("@nestjs/common");
1140
+
1141
+ // src/platform/saas-platform.module.ts
1142
+ var import_common22 = require("@nestjs/common");
1143
+ var import_core6 = require("@nestjs/core");
1144
+
1145
+ // src/core/di.ts
1146
+ function asProvider(token, impl) {
1147
+ if (typeof impl === "object" && impl !== null && "useFactory" in impl && typeof impl.useFactory === "function") {
1148
+ const factory = impl;
1149
+ return {
1150
+ provide: token,
1151
+ useFactory: factory.useFactory,
1152
+ inject: factory.inject
1153
+ };
1154
+ }
1155
+ return {
1156
+ provide: token,
1157
+ useValue: impl
1158
+ };
1159
+ }
1160
+ __name(asProvider, "asProvider");
1161
+
1162
+ // src/admin/module.ts
1163
+ var import_common6 = require("@nestjs/common");
1164
+
1165
+ // src/admin/admin-audit.service.ts
1166
+ var os = __toESM(require("os"), 1);
1167
+ var import_common = require("@nestjs/common");
1168
+
1169
+ // src/admin/tokens.ts
1170
+ var MFA_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/MfaPort");
1171
+ var AUDIT_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/AuditPort");
1172
+ var RLS_BYPASS_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/RlsBypassPort");
1173
+
1174
+ // src/admin/admin-audit.service.ts
1175
+ function _ts_decorate(decorators, target, key, desc) {
1176
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1177
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1178
+ 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;
1179
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1180
+ }
1181
+ __name(_ts_decorate, "_ts_decorate");
1182
+ function _ts_metadata(k, v) {
1183
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1184
+ }
1185
+ __name(_ts_metadata, "_ts_metadata");
1186
+ function _ts_param(paramIndex, decorator) {
1187
+ return function(target, key) {
1188
+ decorator(target, key, paramIndex);
1189
+ };
1190
+ }
1191
+ __name(_ts_param, "_ts_param");
1192
+ var AdminAuditService = class {
1193
+ static {
1194
+ __name(this, "AdminAuditService");
1195
+ }
1196
+ audit;
1197
+ constructor(audit) {
1198
+ this.audit = audit;
1199
+ }
1200
+ async log(input) {
1201
+ await this.audit.write({
1202
+ actor: input.actor,
1203
+ entity: input.entity,
1204
+ entityId: input.entityId,
1205
+ action: input.action,
1206
+ changes: {
1207
+ ...input.changes ?? {},
1208
+ actor: this.actorTag(input.actor)
1209
+ }
1210
+ });
1211
+ }
1212
+ /**
1213
+ * Standardized actor tag for the `changes.actor` column:
1214
+ * `<source>:<email>:<context>`.
1215
+ */
1216
+ actorTag(actor) {
1217
+ return `${actor.source}:${actor.email}:${actor.context}`;
1218
+ }
1219
+ /** Helper: build an AdminActor from an authenticated web request. */
1220
+ fromWebRequest(user, sessionId) {
1221
+ return {
1222
+ userId: user.id,
1223
+ email: user.email,
1224
+ source: "web",
1225
+ context: sessionId ?? "unknown"
1226
+ };
1227
+ }
1228
+ /** Helper: build an AdminActor from a CLI identity (hostname as context). */
1229
+ fromCli(user) {
1230
+ return {
1231
+ userId: user.id,
1232
+ email: user.email,
1233
+ source: "cli",
1234
+ context: os.hostname()
1235
+ };
1236
+ }
1237
+ };
1238
+ AdminAuditService = _ts_decorate([
1239
+ (0, import_common.Injectable)(),
1240
+ _ts_param(0, (0, import_common.Inject)(AUDIT_PORT_TOKEN)),
1241
+ _ts_metadata("design:type", Function),
1242
+ _ts_metadata("design:paramtypes", [
1243
+ typeof AuditPort === "undefined" ? Object : AuditPort
1244
+ ])
1245
+ ], AdminAuditService);
1246
+
1247
+ // src/admin/admin-bypass-rls.interceptor.ts
1248
+ var import_common2 = require("@nestjs/common");
1249
+ var import_rxjs = require("rxjs");
1250
+ var import_operators = require("rxjs/operators");
1251
+ function _ts_decorate2(decorators, target, key, desc) {
1252
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1253
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1254
+ 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;
1255
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1256
+ }
1257
+ __name(_ts_decorate2, "_ts_decorate");
1258
+ function _ts_metadata2(k, v) {
1259
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1260
+ }
1261
+ __name(_ts_metadata2, "_ts_metadata");
1262
+ function _ts_param2(paramIndex, decorator) {
1263
+ return function(target, key) {
1264
+ decorator(target, key, paramIndex);
1265
+ };
1266
+ }
1267
+ __name(_ts_param2, "_ts_param");
1268
+ var AdminBypassRlsInterceptor = class {
1269
+ static {
1270
+ __name(this, "AdminBypassRlsInterceptor");
1271
+ }
1272
+ port;
1273
+ constructor(port) {
1274
+ this.port = port;
1275
+ }
1276
+ intercept(_context, next) {
1277
+ return (0, import_rxjs.defer)(() => (0, import_rxjs.from)(this.port.runWithBypass(async () => {
1278
+ return next.handle();
1279
+ })).pipe((0, import_operators.switchAll)()));
1280
+ }
1281
+ };
1282
+ AdminBypassRlsInterceptor = _ts_decorate2([
1283
+ (0, import_common2.Injectable)(),
1284
+ _ts_param2(0, (0, import_common2.Inject)(RLS_BYPASS_PORT_TOKEN)),
1285
+ _ts_metadata2("design:type", Function),
1286
+ _ts_metadata2("design:paramtypes", [
1287
+ typeof RlsBypassPort === "undefined" ? Object : RlsBypassPort
1288
+ ])
1289
+ ], AdminBypassRlsInterceptor);
1290
+
1291
+ // src/admin/mfa.ts
1292
+ var import_common3 = require("@nestjs/common");
1293
+ var import_otplib = require("otplib");
1294
+ function _ts_decorate3(decorators, target, key, desc) {
1295
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1296
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1297
+ 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;
1298
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1299
+ }
1300
+ __name(_ts_decorate3, "_ts_decorate");
1301
+ function _ts_metadata3(k, v) {
1302
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1303
+ }
1304
+ __name(_ts_metadata3, "_ts_metadata");
1305
+ function _ts_param3(paramIndex, decorator) {
1306
+ return function(target, key) {
1307
+ decorator(target, key, paramIndex);
1308
+ };
1309
+ }
1310
+ __name(_ts_param3, "_ts_param");
1311
+ var CLOCK_DRIFT_TOLERANCE_SECONDS = 30;
1312
+ var OTPLIB_TOKEN_ERROR_NAMES = /* @__PURE__ */ new Set([
1313
+ "TokenError",
1314
+ "TokenFormatError",
1315
+ "TokenLengthError"
1316
+ ]);
1317
+ function isOtplibTokenError(error) {
1318
+ return error instanceof Error && OTPLIB_TOKEN_ERROR_NAMES.has(error.name);
1319
+ }
1320
+ __name(isOtplibTokenError, "isOtplibTokenError");
1321
+ var MfaService = class _MfaService {
1322
+ static {
1323
+ __name(this, "MfaService");
1324
+ }
1325
+ mfa;
1326
+ logger = new import_common3.Logger(_MfaService.name);
1327
+ constructor(mfa) {
1328
+ this.mfa = mfa;
1329
+ }
1330
+ /**
1331
+ * Generates a new TOTP secret, persists it via `MfaPort.setSecret`
1332
+ * and returns the secret + the otpauth URI for the QR code.
1333
+ *
1334
+ * `issuer` and `label` shape the display in the authenticator (e.g.
1335
+ * "DemoApp:taci@example.com").
1336
+ */
1337
+ async setup(userId, label, issuer) {
1338
+ const secret = (0, import_otplib.generateSecret)();
1339
+ const otpauthUri = (0, import_otplib.generateURI)({
1340
+ issuer,
1341
+ label,
1342
+ secret
1343
+ });
1344
+ await this.mfa.setSecret(userId, secret);
1345
+ return {
1346
+ secret,
1347
+ otpauthUri
1348
+ };
1349
+ }
1350
+ /**
1351
+ * Verifies the entered code against the stored secret,
1352
+ * clock-drift-tolerant by ±1 time step.
1353
+ */
1354
+ async verify(input) {
1355
+ const secret = await this.mfa.getSecret(input.userId);
1356
+ if (!secret) return false;
1357
+ try {
1358
+ const result = await (0, import_otplib.verify)({
1359
+ secret,
1360
+ token: input.code.trim(),
1361
+ epochTolerance: CLOCK_DRIFT_TOLERANCE_SECONDS
1362
+ });
1363
+ return result.valid;
1364
+ } catch (error) {
1365
+ if (isOtplibTokenError(error)) {
1366
+ return false;
1367
+ }
1368
+ const message = error instanceof Error ? error.message : String(error);
1369
+ this.logger.warn(`TOTP-Verify f\xFCr User ${input.userId} unerwartet fehlgeschlagen: ${message}`);
1370
+ return false;
1371
+ }
1372
+ }
1373
+ /**
1374
+ * Deletes a user's secret — used by `ahp admin mfa-reset`.
1375
+ */
1376
+ async disable(userId) {
1377
+ await this.mfa.setSecret(userId, null);
1378
+ }
1379
+ /** Delegates to `MfaPort.isEnabled`. */
1380
+ async isEnabled(userId) {
1381
+ return this.mfa.isEnabled(userId);
1382
+ }
1383
+ };
1384
+ MfaService = _ts_decorate3([
1385
+ (0, import_common3.Injectable)(),
1386
+ _ts_param3(0, (0, import_common3.Inject)(MFA_PORT_TOKEN)),
1387
+ _ts_metadata3("design:type", Function),
1388
+ _ts_metadata3("design:paramtypes", [
1389
+ typeof MfaPort === "undefined" ? Object : MfaPort
1390
+ ])
1391
+ ], MfaService);
1392
+
1393
+ // src/admin/mfa.guard.ts
1394
+ var import_common4 = require("@nestjs/common");
1395
+ var import_core = require("@nestjs/core");
1396
+ function _ts_decorate4(decorators, target, key, desc) {
1397
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1398
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1399
+ 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;
1400
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1401
+ }
1402
+ __name(_ts_decorate4, "_ts_decorate");
1403
+ function _ts_metadata4(k, v) {
1404
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1405
+ }
1406
+ __name(_ts_metadata4, "_ts_metadata");
1407
+ function _ts_param4(paramIndex, decorator) {
1408
+ return function(target, key) {
1409
+ decorator(target, key, paramIndex);
1410
+ };
1411
+ }
1412
+ __name(_ts_param4, "_ts_param");
1413
+ var REQUIRE_MFA_KEY = "saas-platform/require-mfa";
1414
+ var MfaGuard = class _MfaGuard {
1415
+ static {
1416
+ __name(this, "MfaGuard");
1417
+ }
1418
+ reflector;
1419
+ mfaService;
1420
+ logger = new import_common4.Logger(_MfaGuard.name);
1421
+ constructor(reflector, mfaService) {
1422
+ this.reflector = reflector;
1423
+ this.mfaService = mfaService;
1424
+ }
1425
+ async canActivate(context) {
1426
+ const required = this.reflector.getAllAndOverride(REQUIRE_MFA_KEY, [
1427
+ context.getHandler(),
1428
+ context.getClass()
1429
+ ]);
1430
+ if (!required) return true;
1431
+ if (process.env.SAAS_PLATFORM_SKIP_MFA === "1" && process.env.NODE_ENV !== "production") {
1432
+ return true;
1433
+ }
1434
+ const request = context.switchToHttp().getRequest();
1435
+ const user = request.user;
1436
+ if (!user) throw new import_common4.UnauthorizedException({
1437
+ reason: "NOT_AUTHENTICATED"
1438
+ });
1439
+ const enabled = await this.mfaService.isEnabled(user.id);
1440
+ if (!enabled) {
1441
+ throw new import_common4.UnauthorizedException({
1442
+ reason: "MFA_NOT_SET_UP",
1443
+ message: "Bitte zuerst MFA-Setup \xFCber das CLI ausf\xFChren."
1444
+ });
1445
+ }
1446
+ const headerVal = request.headers?.["x-mfa-code"];
1447
+ const code = Array.isArray(headerVal) ? headerVal[0] : headerVal;
1448
+ if (!code) {
1449
+ throw new import_common4.UnauthorizedException({
1450
+ reason: "MFA_REQUIRED",
1451
+ message: "TOTP-Code im Header X-Mfa-Code erforderlich."
1452
+ });
1453
+ }
1454
+ const valid = await this.mfaService.verify({
1455
+ userId: user.id,
1456
+ code: String(code)
1457
+ });
1458
+ if (!valid) {
1459
+ throw new import_common4.UnauthorizedException({
1460
+ reason: "MFA_FAILED",
1461
+ message: "TOTP-Code ung\xFCltig."
1462
+ });
1463
+ }
1464
+ return true;
1465
+ }
1466
+ };
1467
+ MfaGuard = _ts_decorate4([
1468
+ (0, import_common4.Injectable)(),
1469
+ _ts_param4(0, (0, import_common4.Inject)(import_core.Reflector)),
1470
+ _ts_metadata4("design:type", Function),
1471
+ _ts_metadata4("design:paramtypes", [
1472
+ typeof import_core.Reflector === "undefined" ? Object : import_core.Reflector,
1473
+ typeof MfaService === "undefined" ? Object : MfaService
1474
+ ])
1475
+ ], MfaGuard);
1476
+
1477
+ // src/admin/super-admin.guard.ts
1478
+ var import_common5 = require("@nestjs/common");
1479
+ function _ts_decorate5(decorators, target, key, desc) {
1480
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1481
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1482
+ 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;
1483
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1484
+ }
1485
+ __name(_ts_decorate5, "_ts_decorate");
1486
+ var SuperAdminGuard = class {
1487
+ static {
1488
+ __name(this, "SuperAdminGuard");
1489
+ }
1490
+ canActivate(context) {
1491
+ const request = context.switchToHttp().getRequest();
1492
+ if (!request.user) throw new import_common5.ForbiddenException("Nicht authentifiziert");
1493
+ if (request.user.role !== "SUPER_ADMIN") {
1494
+ throw new import_common5.ForbiddenException("Nur SUPER_ADMIN-Rolle erlaubt");
1495
+ }
1496
+ return true;
1497
+ }
1498
+ };
1499
+ SuperAdminGuard = _ts_decorate5([
1500
+ (0, import_common5.Injectable)()
1501
+ ], SuperAdminGuard);
1502
+
1503
+ // src/admin/module.ts
1504
+ function _ts_decorate6(decorators, target, key, desc) {
1505
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1506
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1507
+ 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;
1508
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1509
+ }
1510
+ __name(_ts_decorate6, "_ts_decorate");
1511
+ var AdminModule = class _AdminModule {
1512
+ static {
1513
+ __name(this, "AdminModule");
1514
+ }
1515
+ static forRoot(options) {
1516
+ const providers = [
1517
+ asProvider(MFA_PORT_TOKEN, options.mfaPort),
1518
+ asProvider(AUDIT_PORT_TOKEN, options.auditPort),
1519
+ asProvider(RLS_BYPASS_PORT_TOKEN, options.rlsBypassPort),
1520
+ MfaService,
1521
+ AdminAuditService,
1522
+ SuperAdminGuard,
1523
+ MfaGuard,
1524
+ AdminBypassRlsInterceptor
1525
+ ];
1526
+ return {
1527
+ module: _AdminModule,
1528
+ global: options.global ?? false,
1529
+ providers,
1530
+ exports: [
1531
+ MFA_PORT_TOKEN,
1532
+ AUDIT_PORT_TOKEN,
1533
+ RLS_BYPASS_PORT_TOKEN,
1534
+ MfaService,
1535
+ AdminAuditService,
1536
+ SuperAdminGuard,
1537
+ MfaGuard,
1538
+ AdminBypassRlsInterceptor
1539
+ ]
1540
+ };
1541
+ }
1542
+ };
1543
+ AdminModule = _ts_decorate6([
1544
+ (0, import_common6.Module)({})
1545
+ ], AdminModule);
1546
+
1547
+ // src/admin/admin-manifest.module.ts
1548
+ var import_common9 = require("@nestjs/common");
1549
+
1550
+ // src/admin/admin-manifest.config.ts
1551
+ var ADMIN_MANIFEST_CONFIG = /* @__PURE__ */ Symbol("ADMIN_MANIFEST_CONFIG");
1552
+
1553
+ // src/admin/admin-manifest.service.ts
1554
+ var import_common7 = require("@nestjs/common");
1555
+ var import_node_crypto = require("crypto");
1556
+ function _ts_decorate7(decorators, target, key, desc) {
1557
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1558
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1559
+ 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;
1560
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1561
+ }
1562
+ __name(_ts_decorate7, "_ts_decorate");
1563
+ function _ts_metadata5(k, v) {
1564
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1565
+ }
1566
+ __name(_ts_metadata5, "_ts_metadata");
1567
+ function _ts_param5(paramIndex, decorator) {
1568
+ return function(target, key) {
1569
+ decorator(target, key, paramIndex);
1570
+ };
1571
+ }
1572
+ __name(_ts_param5, "_ts_param");
1573
+ var PLATFORM_CORE_CONTRIBUTION_TOKEN = /* @__PURE__ */ Symbol("PLATFORM_CORE_CONTRIBUTION");
1574
+ var AdminManifestService = class _AdminManifestService {
1575
+ static {
1576
+ __name(this, "AdminManifestService");
1577
+ }
1578
+ config;
1579
+ logger = new import_common7.Logger(_AdminManifestService.name);
1580
+ contributions = [];
1581
+ cached = null;
1582
+ constructor(config, platformCore = null) {
1583
+ this.config = config;
1584
+ if (platformCore) {
1585
+ this.contributions.push(platformCore);
1586
+ }
1587
+ }
1588
+ register(contribution) {
1589
+ this.contributions.push(contribution);
1590
+ this.cached = null;
1591
+ }
1592
+ getManifest() {
1593
+ if (this.cached) return this.cached;
1594
+ this.cached = this.build();
1595
+ this.logger.log(`Manifest built: ${this.contributions.length} contribution(s), hash=${this.cached.build.manifestHash.slice(0, 24)}\u2026`);
1596
+ return this.cached;
1597
+ }
1598
+ rebuild() {
1599
+ this.cached = null;
1600
+ return this.getManifest();
1601
+ }
1602
+ build() {
1603
+ const merged = this.mergeContributions(this.contributions);
1604
+ const draft = {
1605
+ schemaVersion: 1,
1606
+ project: this.config.project,
1607
+ build: {
1608
+ ...this.config.build,
1609
+ manifestHash: "sha256-pending"
1610
+ },
1611
+ planCatalogSnapshot: this.config.planCatalogSnapshot,
1612
+ capabilities: merged.capabilities,
1613
+ navigation: merged.navigation,
1614
+ dashboard: merged.dashboard,
1615
+ tenants: merged.tenants,
1616
+ audit: merged.audit
1617
+ };
1618
+ return {
1619
+ ...draft,
1620
+ build: {
1621
+ ...draft.build,
1622
+ manifestHash: this.computeHash(draft)
1623
+ }
1624
+ };
1625
+ }
1626
+ mergeContributions(contributions) {
1627
+ const capabilities = {};
1628
+ const standardPages = {};
1629
+ const projectPages = [];
1630
+ const kpiCards = [];
1631
+ const tenantColumns = [];
1632
+ const tenantActions = [];
1633
+ const auditActions = [];
1634
+ for (const c of contributions) {
1635
+ if (c.capabilities) Object.assign(capabilities, c.capabilities);
1636
+ if (c.navigation?.standardPages) Object.assign(standardPages, c.navigation.standardPages);
1637
+ if (c.navigation?.projectPages) projectPages.push(...c.navigation.projectPages);
1638
+ if (c.dashboard?.kpiCards) kpiCards.push(...c.dashboard.kpiCards);
1639
+ if (c.tenants?.columns) tenantColumns.push(...c.tenants.columns);
1640
+ if (c.tenants?.actions) tenantActions.push(...c.tenants.actions);
1641
+ if (c.audit?.actions) auditActions.push(...c.audit.actions);
1642
+ }
1643
+ kpiCards.sort((a, b) => (b.slotPriority ?? 0) - (a.slotPriority ?? 0));
1644
+ return {
1645
+ capabilities,
1646
+ navigation: {
1647
+ standardPages,
1648
+ projectPages
1649
+ },
1650
+ dashboard: kpiCards.length > 0 ? {
1651
+ kpiCards
1652
+ } : void 0,
1653
+ tenants: tenantColumns.length > 0 || tenantActions.length > 0 ? {
1654
+ columns: tenantColumns,
1655
+ actions: tenantActions
1656
+ } : void 0,
1657
+ audit: auditActions.length > 0 ? {
1658
+ actions: auditActions
1659
+ } : void 0
1660
+ };
1661
+ }
1662
+ /**
1663
+ * Deterministic SHA-256 hash over the manifest. Uses the insertion
1664
+ * order of the object keys — so that semantically meaningful reorders (e.g.
1665
+ * the order of the sidebar items in `navigation.standardPages`) change the
1666
+ * hash and invalidate the browser ETag cache. The manifest's own
1667
+ * `manifestHash` field is hidden before serialization.
1668
+ */
1669
+ computeHash(manifest) {
1670
+ const canonical = stableStringify({
1671
+ ...manifest,
1672
+ build: {
1673
+ ...manifest.build,
1674
+ manifestHash: void 0
1675
+ }
1676
+ });
1677
+ const digest = (0, import_node_crypto.createHash)("sha256").update(canonical).digest("base64url");
1678
+ return `sha256-${digest}`;
1679
+ }
1680
+ };
1681
+ AdminManifestService = _ts_decorate7([
1682
+ (0, import_common7.Injectable)(),
1683
+ _ts_param5(0, (0, import_common7.Inject)(ADMIN_MANIFEST_CONFIG)),
1684
+ _ts_param5(1, (0, import_common7.Optional)()),
1685
+ _ts_param5(1, (0, import_common7.Inject)(PLATFORM_CORE_CONTRIBUTION_TOKEN)),
1686
+ _ts_metadata5("design:type", Function),
1687
+ _ts_metadata5("design:paramtypes", [
1688
+ typeof AdminManifestConfig === "undefined" ? Object : AdminManifestConfig,
1689
+ Object
1690
+ ])
1691
+ ], AdminManifestService);
1692
+ function stableStringify(value) {
1693
+ if (value === null) return "null";
1694
+ if (value === void 0) return "null";
1695
+ if (typeof value === "number") return Number.isFinite(value) ? String(value) : "null";
1696
+ if (typeof value === "boolean") return value ? "true" : "false";
1697
+ if (typeof value === "string") return JSON.stringify(value);
1698
+ if (Array.isArray(value)) {
1699
+ return "[" + value.map(stableStringify).join(",") + "]";
1700
+ }
1701
+ if (typeof value === "object") {
1702
+ const obj = value;
1703
+ const keys = Object.keys(obj).filter((k) => obj[k] !== void 0);
1704
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
1705
+ }
1706
+ return JSON.stringify(value);
1707
+ }
1708
+ __name(stableStringify, "stableStringify");
1709
+
1710
+ // src/admin/admin-public-boot.controller.ts
1711
+ var import_common8 = require("@nestjs/common");
1712
+ function _ts_decorate8(decorators, target, key, desc) {
1713
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1714
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1715
+ 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;
1716
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1717
+ }
1718
+ __name(_ts_decorate8, "_ts_decorate");
1719
+ function _ts_metadata6(k, v) {
1720
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1721
+ }
1722
+ __name(_ts_metadata6, "_ts_metadata");
1723
+ function _ts_param6(paramIndex, decorator) {
1724
+ return function(target, key) {
1725
+ decorator(target, key, paramIndex);
1726
+ };
1727
+ }
1728
+ __name(_ts_param6, "_ts_param");
1729
+ var AdminPublicBootController = class {
1730
+ static {
1731
+ __name(this, "AdminPublicBootController");
1732
+ }
1733
+ config;
1734
+ constructor(config) {
1735
+ this.config = config;
1736
+ }
1737
+ getBoot() {
1738
+ return {
1739
+ project: {
1740
+ key: this.config.project.key,
1741
+ displayName: this.config.project.displayName,
1742
+ label: this.config.project.label,
1743
+ icon: this.config.project.icon,
1744
+ logoUrl: this.config.project.logoUrl,
1745
+ environment: this.config.project.environment
1746
+ }
1747
+ };
1748
+ }
1749
+ };
1750
+ _ts_decorate8([
1751
+ (0, import_common8.Get)("boot"),
1752
+ _ts_metadata6("design:type", Function),
1753
+ _ts_metadata6("design:paramtypes", []),
1754
+ _ts_metadata6("design:returntype", typeof PublicBootResponse === "undefined" ? Object : PublicBootResponse)
1755
+ ], AdminPublicBootController.prototype, "getBoot", null);
1756
+ AdminPublicBootController = _ts_decorate8([
1757
+ (0, import_common8.Controller)("admin"),
1758
+ _ts_param6(0, (0, import_common8.Inject)(ADMIN_MANIFEST_CONFIG)),
1759
+ _ts_metadata6("design:type", Function),
1760
+ _ts_metadata6("design:paramtypes", [
1761
+ typeof AdminManifestConfig === "undefined" ? Object : AdminManifestConfig
1762
+ ])
1763
+ ], AdminPublicBootController);
1764
+
1765
+ // src/admin/manifest-core.ts
1766
+ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
1767
+ navigation: {
1768
+ // Key order is render order in the sidebar (within the
1769
+ // section, see `DEFAULT_NAV_SECTIONS` in the nav-builder). The order per
1770
+ // section is deliberately curated:
1771
+ // Overview: dashboard
1772
+ // Product catalog: discovery → bundles → businessTypes → plans →
1773
+ // planVersions → marketingCatalog → promoCodes
1774
+ // (promoCodes is appended by the app contributions
1775
+ // and therefore lands at the end of the section)
1776
+ // Customers: tenants → users → pilots
1777
+ // System: audit
1778
+ standardPages: {
1779
+ dashboard: {
1780
+ enabled: true,
1781
+ requiredCapability: "dashboard.read"
1782
+ },
1783
+ // SPEC_V2 §3.3 — discovery snapshot of the running backend.
1784
+ // Apps must set `discovery.read` in their manifest capabilities
1785
+ // to `true` so that the NavBuilder wires the page into the
1786
+ // sidebar; otherwise it is filtered out automatically.
1787
+ discovery: {
1788
+ enabled: true,
1789
+ requiredCapability: "discovery.read"
1790
+ },
1791
+ // SPEC_V2 §11.1 M3 — Bundle editor in SuperAdmin.
1792
+ // Apps with a DB-driven catalog set `bundles.read = true`.
1793
+ bundles: {
1794
+ enabled: true,
1795
+ requiredCapability: "bundles.read"
1796
+ },
1797
+ // SPEC_V2 §11.1 M3 — BusinessType editor in SuperAdmin.
1798
+ // Apps with domain verticals set `businessTypes.read = true`.
1799
+ businessTypes: {
1800
+ enabled: true,
1801
+ requiredCapability: "businessTypes.read"
1802
+ },
1803
+ plans: {
1804
+ enabled: true,
1805
+ requiredCapability: "plans.read"
1806
+ },
1807
+ planVersions: {
1808
+ enabled: true,
1809
+ requiredCapability: "plans.read"
1810
+ },
1811
+ // SPEC_V2 §11.1 M3 — marketing catalog (locale pivot).
1812
+ // Apps that market a public catalog set
1813
+ // `marketingProjections.read = true`.
1814
+ marketingCatalog: {
1815
+ enabled: true,
1816
+ requiredCapability: "marketingProjections.read"
1817
+ },
1818
+ tenants: {
1819
+ enabled: true,
1820
+ requiredCapability: "tenants.read"
1821
+ },
1822
+ users: {
1823
+ enabled: true,
1824
+ requiredCapability: "users.read"
1825
+ },
1826
+ pilots: {
1827
+ enabled: true,
1828
+ requiredCapability: "pilots.read"
1829
+ },
1830
+ // Audit log deliberately at the end — rarely needed, should
1831
+ // not obscure the productive pages.
1832
+ audit: {
1833
+ enabled: true,
1834
+ requiredCapability: "audit.read"
1835
+ },
1836
+ // Platform email sender (system/registration mails). Apps
1837
+ // set `platformEmail.read = true` so that the NavBuilder wires the page
1838
+ // in; otherwise it is filtered out automatically.
1839
+ platformEmail: {
1840
+ enabled: true,
1841
+ requiredCapability: "platformEmail.read"
1842
+ },
1843
+ // Platform email history (audit of the sent system mails). Apps
1844
+ // set `platformEmailHistory.read = true` so that the NavBuilder wires the
1845
+ // page in; otherwise it is filtered out automatically.
1846
+ platformEmailHistory: {
1847
+ enabled: true,
1848
+ requiredCapability: "platformEmailHistory.read"
1849
+ }
1850
+ }
1851
+ },
1852
+ tenants: {
1853
+ actions: [
1854
+ {
1855
+ id: "platform.tenants.suspend",
1856
+ label: "Mandant suspendieren",
1857
+ actionKey: "tenants.suspend",
1858
+ requiredCapability: "tenants.suspend",
1859
+ requiresMfa: true,
1860
+ confirmType: "typed-slug"
1861
+ },
1862
+ {
1863
+ id: "platform.tenants.reactivate",
1864
+ label: "Mandant reaktivieren",
1865
+ actionKey: "tenants.reactivate",
1866
+ requiredCapability: "tenants.reactivate",
1867
+ requiresMfa: true,
1868
+ confirmType: "simple"
1869
+ },
1870
+ {
1871
+ id: "platform.tenants.impersonate",
1872
+ label: "Impersonate",
1873
+ actionKey: "tenants.impersonate",
1874
+ requiredCapability: "tenants.impersonate",
1875
+ requiresMfa: true,
1876
+ confirmType: "simple"
1877
+ },
1878
+ {
1879
+ id: "platform.tenants.export",
1880
+ label: "DSGVO-Export",
1881
+ actionKey: "tenants.export",
1882
+ requiredCapability: "tenants.export",
1883
+ requiresMfa: true,
1884
+ confirmType: "simple"
1885
+ },
1886
+ {
1887
+ id: "platform.subscriptions.cancel",
1888
+ label: "Subscription k\xFCndigen",
1889
+ actionKey: "subscriptions.cancel",
1890
+ requiredCapability: "subscriptions.cancel",
1891
+ requiresMfa: true,
1892
+ confirmType: "typed-slug"
1893
+ },
1894
+ {
1895
+ id: "platform.pilots.grant",
1896
+ label: "Pilot-Status setzen",
1897
+ actionKey: "pilots.grant",
1898
+ requiredCapability: "pilots.grant",
1899
+ requiresMfa: true,
1900
+ confirmType: "simple"
1901
+ },
1902
+ {
1903
+ id: "platform.pilots.revoke",
1904
+ label: "Pilot-Status entziehen",
1905
+ actionKey: "pilots.revoke",
1906
+ requiredCapability: "pilots.revoke",
1907
+ requiresMfa: true,
1908
+ confirmType: "typed-slug"
1909
+ },
1910
+ {
1911
+ id: "platform.pilots.extend",
1912
+ label: "Pilot verl\xE4ngern",
1913
+ actionKey: "pilots.extend",
1914
+ requiredCapability: "pilots.extend",
1915
+ requiresMfa: true,
1916
+ confirmType: "date"
1917
+ }
1918
+ ]
1919
+ },
1920
+ audit: {
1921
+ actions: [
1922
+ {
1923
+ key: "TENANT_SUSPEND",
1924
+ label: "Mandant suspendiert",
1925
+ severity: "high"
1926
+ },
1927
+ {
1928
+ key: "TENANT_REACTIVATE",
1929
+ label: "Mandant reaktiviert",
1930
+ severity: "medium"
1931
+ },
1932
+ {
1933
+ key: "TENANT_IMPERSONATE",
1934
+ label: "Impersonation gestartet",
1935
+ severity: "high"
1936
+ },
1937
+ {
1938
+ key: "TENANT_EXPORT",
1939
+ label: "DSGVO-Export ausgel\xF6st",
1940
+ severity: "medium"
1941
+ },
1942
+ {
1943
+ key: "PILOT_CREATE",
1944
+ label: "Pilot-Tenant angelegt",
1945
+ severity: "medium"
1946
+ },
1947
+ {
1948
+ key: "PILOT_GRANT",
1949
+ label: "Pilot-Status gesetzt",
1950
+ severity: "medium"
1951
+ },
1952
+ {
1953
+ key: "PILOT_REVOKE",
1954
+ label: "Pilot-Status entzogen",
1955
+ severity: "medium"
1956
+ },
1957
+ {
1958
+ key: "PILOT_EXTEND",
1959
+ label: "Pilot verl\xE4ngert",
1960
+ severity: "low"
1961
+ },
1962
+ {
1963
+ key: "PLAN_VERSION_PUBLISH",
1964
+ label: "Plan-Version freigegeben",
1965
+ severity: "high"
1966
+ },
1967
+ {
1968
+ key: "PLAN_VERSION_TERMINATE",
1969
+ label: "Plan-Version terminiert",
1970
+ severity: "high"
1971
+ },
1972
+ {
1973
+ key: "SUBSCRIPTION_CANCEL",
1974
+ label: "Subscription gek\xFCndigt",
1975
+ severity: "high"
1976
+ },
1977
+ {
1978
+ key: "USER_RESET_PASSWORD",
1979
+ label: "Passwort-Reset ausgel\xF6st",
1980
+ severity: "medium"
1981
+ },
1982
+ {
1983
+ key: "USER_DEACTIVATE",
1984
+ label: "User deaktiviert",
1985
+ severity: "high"
1986
+ },
1987
+ {
1988
+ key: "USER_REASSIGN_ADMIN",
1989
+ label: "Admin-Rolle \xFCbertragen",
1990
+ severity: "high"
1991
+ },
1992
+ {
1993
+ key: "PLATFORM_EMAIL_PROVIDER_CREATE",
1994
+ label: "Plattform-Absender angelegt",
1995
+ severity: "medium"
1996
+ },
1997
+ {
1998
+ key: "PLATFORM_EMAIL_PROVIDER_UPDATE",
1999
+ label: "Plattform-Absender ge\xE4ndert",
2000
+ severity: "medium"
2001
+ },
2002
+ {
2003
+ key: "PLATFORM_EMAIL_PROVIDER_DELETE",
2004
+ label: "Plattform-Absender gel\xF6scht",
2005
+ severity: "medium"
2006
+ },
2007
+ {
2008
+ key: "PLATFORM_EMAIL_PROVIDER_TEST",
2009
+ label: "Plattform-Absender Test-Versand",
2010
+ severity: "low"
2011
+ },
2012
+ {
2013
+ key: "EMAIL_HISTORY_DELETE",
2014
+ label: "Plattform-E-Mail aus Verlauf entfernt",
2015
+ severity: "medium"
2016
+ },
2017
+ {
2018
+ key: "EMAIL_HISTORY_RESEND",
2019
+ label: "Plattform-E-Mail erneut versendet",
2020
+ severity: "low"
2021
+ }
2022
+ ]
2023
+ }
2024
+ };
2025
+
2026
+ // src/admin/admin-manifest.module.ts
2027
+ function _ts_decorate9(decorators, target, key, desc) {
2028
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2029
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2030
+ 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;
2031
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2032
+ }
2033
+ __name(_ts_decorate9, "_ts_decorate");
2034
+ function _ts_metadata7(k, v) {
2035
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
2036
+ }
2037
+ __name(_ts_metadata7, "_ts_metadata");
2038
+ function _ts_param7(paramIndex, decorator) {
2039
+ return function(target, key) {
2040
+ decorator(target, key, paramIndex);
2041
+ };
2042
+ }
2043
+ __name(_ts_param7, "_ts_param");
2044
+ function asConfigProvider(impl) {
2045
+ if (typeof impl === "object" && impl !== null && "useFactory" in impl && typeof impl.useFactory === "function") {
2046
+ return {
2047
+ provide: ADMIN_MANIFEST_CONFIG,
2048
+ useFactory: impl.useFactory,
2049
+ inject: impl.inject
2050
+ };
2051
+ }
2052
+ return {
2053
+ provide: ADMIN_MANIFEST_CONFIG,
2054
+ useValue: impl
2055
+ };
2056
+ }
2057
+ __name(asConfigProvider, "asConfigProvider");
2058
+ function buildManifestController(guards, reloadGuards) {
2059
+ const reloadChain = [
2060
+ ...guards,
2061
+ ...reloadGuards
2062
+ ];
2063
+ let GeneratedAdminManifestController = class GeneratedAdminManifestController {
2064
+ static {
2065
+ __name(this, "GeneratedAdminManifestController");
2066
+ }
2067
+ manifest;
2068
+ // Explicit @Inject instead of type reflection: tsup/esbuild do not emit
2069
+ // `design:paramtypes` metadata, so Nest cannot otherwise resolve the
2070
+ // service type at the constructor.
2071
+ constructor(manifest) {
2072
+ this.manifest = manifest;
2073
+ }
2074
+ getManifest(ifNoneMatch, res) {
2075
+ const m = this.manifest.getManifest();
2076
+ const etag = `"${m.build.manifestHash}"`;
2077
+ res.header("ETag", etag);
2078
+ if (ifNoneMatch && ifNoneMatch === etag) {
2079
+ res.status(import_common9.HttpStatus.NOT_MODIFIED);
2080
+ return null;
2081
+ }
2082
+ return m;
2083
+ }
2084
+ reload() {
2085
+ const m = this.manifest.rebuild();
2086
+ return {
2087
+ manifestHash: m.build.manifestHash,
2088
+ reloadedAt: (/* @__PURE__ */ new Date()).toISOString()
2089
+ };
2090
+ }
2091
+ };
2092
+ _ts_decorate9([
2093
+ (0, import_common9.Get)("manifest"),
2094
+ (0, import_common9.Header)("Cache-Control", "private, max-age=60, must-revalidate"),
2095
+ _ts_param7(0, (0, import_common9.Headers)("if-none-match")),
2096
+ _ts_param7(1, (0, import_common9.Res)({
2097
+ passthrough: true
2098
+ })),
2099
+ _ts_metadata7("design:type", Function),
2100
+ _ts_metadata7("design:paramtypes", [
2101
+ Object,
2102
+ typeof HttpResponseLike === "undefined" ? Object : HttpResponseLike
2103
+ ]),
2104
+ _ts_metadata7("design:returntype", void 0)
2105
+ ], GeneratedAdminManifestController.prototype, "getManifest", null);
2106
+ _ts_decorate9([
2107
+ (0, import_common9.Post)("manifest/reload"),
2108
+ (0, import_common9.UseGuards)(...reloadChain),
2109
+ (0, import_common9.HttpCode)(import_common9.HttpStatus.OK),
2110
+ _ts_metadata7("design:type", Function),
2111
+ _ts_metadata7("design:paramtypes", []),
2112
+ _ts_metadata7("design:returntype", void 0)
2113
+ ], GeneratedAdminManifestController.prototype, "reload", null);
2114
+ GeneratedAdminManifestController = _ts_decorate9([
2115
+ (0, import_common9.Controller)("admin"),
2116
+ (0, import_common9.UseGuards)(...guards),
2117
+ _ts_param7(0, (0, import_common9.Inject)(AdminManifestService)),
2118
+ _ts_metadata7("design:type", Function),
2119
+ _ts_metadata7("design:paramtypes", [
2120
+ typeof AdminManifestService === "undefined" ? Object : AdminManifestService
2121
+ ])
2122
+ ], GeneratedAdminManifestController);
2123
+ return GeneratedAdminManifestController;
2124
+ }
2125
+ __name(buildManifestController, "buildManifestController");
2126
+ var AdminManifestModule = class _AdminManifestModule {
2127
+ static {
2128
+ __name(this, "AdminManifestModule");
2129
+ }
2130
+ static forRoot(options) {
2131
+ const includeManifestController = options.includeManifestController ?? true;
2132
+ const registerPlatformCore = options.registerPlatformCore ?? true;
2133
+ const controllers = [
2134
+ AdminPublicBootController
2135
+ ];
2136
+ if (includeManifestController) {
2137
+ if (options.guards === void 0) {
2138
+ throw new Error("AdminManifestModule.forRoot: `guards` ist Pflicht, wenn `includeManifestController` aktiv ist (Default `true`). Setze `guards: [JwtAuthGuard, SuperAdminGuard]` (oder analog) \u2014 oder `guards: []`, um den Endpoint absichtlich auth-frei zu betreiben. Alternative: `includeManifestController: false` und einen eigenen Wrapping-Controller registrieren.");
2139
+ }
2140
+ controllers.push(buildManifestController(options.guards, options.reloadGuards ?? []));
2141
+ }
2142
+ const providers = [
2143
+ asConfigProvider(options.config),
2144
+ AdminManifestService,
2145
+ ...options.extraProviders ?? []
2146
+ ];
2147
+ if (registerPlatformCore) {
2148
+ providers.push({
2149
+ provide: PLATFORM_CORE_CONTRIBUTION_TOKEN,
2150
+ useValue: PLATFORM_CORE_MANIFEST_CONTRIBUTION
2151
+ });
2152
+ }
2153
+ return {
2154
+ module: _AdminManifestModule,
2155
+ global: options.global ?? false,
2156
+ imports: options.imports ?? [],
2157
+ controllers,
2158
+ providers,
2159
+ exports: [
2160
+ ADMIN_MANIFEST_CONFIG,
2161
+ AdminManifestService
2162
+ ]
2163
+ };
2164
+ }
2165
+ };
2166
+ AdminManifestModule = _ts_decorate9([
2167
+ (0, import_common9.Module)({})
2168
+ ], AdminManifestModule);
2169
+
2170
+ // src/billing/plan-catalog.module.ts
2171
+ var import_common10 = require("@nestjs/common");
2172
+
2173
+ // src/billing/plan-catalog-from-snapshot.ts
2174
+ function buildPlanCatalogFromSnapshot(settings, snapshot) {
2175
+ const liveByPlanKey = new Map(snapshot.livePlanVersions.map((v) => [
2176
+ v.planId,
2177
+ v
2178
+ ]));
2179
+ const plans = snapshot.plans.filter((p) => p.deletedAt === null).sort((a, b) => a.sortOrder - b.sortOrder).map((stem) => {
2180
+ const live = liveByPlanKey.get(stem.planKey);
2181
+ if (!live) {
2182
+ return {
2183
+ id: stem.planKey,
2184
+ name: stem.label,
2185
+ tagline: stem.description ?? void 0,
2186
+ marketed: false,
2187
+ monthlyNet: null,
2188
+ yearlyNet: null,
2189
+ features: [],
2190
+ quotas: {}
2191
+ };
2192
+ }
2193
+ return {
2194
+ id: stem.planKey,
2195
+ name: stem.label,
2196
+ tagline: stem.description ?? void 0,
2197
+ marketed: live.marketed,
2198
+ monthlyNet: parseFloat(live.monthlyNet),
2199
+ yearlyNet: parseFloat(live.yearlyNet),
2200
+ features: live.features,
2201
+ quotas: live.quotas ?? {}
2202
+ };
2203
+ });
2204
+ const features = snapshot.featureEntries.filter((f) => f.deletedAt === null).sort((a, b) => a.sortOrder - b.sortOrder).map((row) => ({
2205
+ key: row.featureKey,
2206
+ label: row.label,
2207
+ icon: row.icon ?? void 0,
2208
+ tier: row.tier ?? void 0,
2209
+ plannedOnly: row.plannedOnly
2210
+ }));
2211
+ return {
2212
+ schemaVersion: 1,
2213
+ projectKey: settings.projectKey,
2214
+ ...settings.app ? {
2215
+ app: settings.app
2216
+ } : {},
2217
+ currency: settings.currency,
2218
+ vatRate: settings.vatRate,
2219
+ ...settings.marketing ? {
2220
+ marketing: settings.marketing
2221
+ } : {},
2222
+ features,
2223
+ plans
2224
+ };
2225
+ }
2226
+ __name(buildPlanCatalogFromSnapshot, "buildPlanCatalogFromSnapshot");
2227
+
2228
+ // src/billing/plan-catalog.module.ts
2229
+ function _ts_decorate10(decorators, target, key, desc) {
2230
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2231
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2232
+ 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;
2233
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2234
+ }
2235
+ __name(_ts_decorate10, "_ts_decorate");
2236
+ var PLAN_CATALOG_TOKEN = /* @__PURE__ */ Symbol("PLAN_CATALOG");
2237
+ var PLAN_CATALOG_READ_SINK_TOKEN = /* @__PURE__ */ Symbol("PLAN_CATALOG_READ_SINK");
2238
+ var PlanCatalogModule = class _PlanCatalogModule {
2239
+ static {
2240
+ __name(this, "PlanCatalogModule");
2241
+ }
2242
+ static forRoot(options) {
2243
+ return {
2244
+ module: _PlanCatalogModule,
2245
+ global: options.global ?? true,
2246
+ imports: options.imports ?? [],
2247
+ providers: [
2248
+ ...options.extraProviders ?? [],
2249
+ asProvider(PLAN_CATALOG_READ_SINK_TOKEN, options.sink),
2250
+ {
2251
+ provide: PLAN_CATALOG_TOKEN,
2252
+ useFactory: /* @__PURE__ */ __name(async (sink) => {
2253
+ const snapshot = await sink.loadSnapshot(options.projectKey);
2254
+ return buildPlanCatalogFromSnapshot({
2255
+ projectKey: options.projectKey,
2256
+ app: options.app,
2257
+ currency: options.currency,
2258
+ vatRate: options.vatRate,
2259
+ marketing: options.marketing
2260
+ }, snapshot);
2261
+ }, "useFactory"),
2262
+ inject: [
2263
+ PLAN_CATALOG_READ_SINK_TOKEN
2264
+ ]
2265
+ }
2266
+ ],
2267
+ exports: [
2268
+ PLAN_CATALOG_TOKEN,
2269
+ PLAN_CATALOG_READ_SINK_TOKEN
2270
+ ]
2271
+ };
2272
+ }
2273
+ /**
2274
+ * Tests / in-memory setup: takes the catalog object directly, without a
2275
+ * sink/DB lookup. For unit tests + test bootstraps.
2276
+ */
2277
+ static forRootWithCatalog(catalog, opts = {}) {
2278
+ return {
2279
+ module: _PlanCatalogModule,
2280
+ providers: [
2281
+ {
2282
+ provide: PLAN_CATALOG_TOKEN,
2283
+ useValue: catalog
2284
+ }
2285
+ ],
2286
+ exports: [
2287
+ PLAN_CATALOG_TOKEN
2288
+ ],
2289
+ global: opts.global ?? true
2290
+ };
2291
+ }
2292
+ };
2293
+ PlanCatalogModule = _ts_decorate10([
2294
+ (0, import_common10.Module)({})
2295
+ ], PlanCatalogModule);
2296
+
2297
+ // src/discovery/discovery.module.ts
2298
+ var import_common13 = require("@nestjs/common");
2299
+ var import_core3 = require("@nestjs/core");
2300
+
2301
+ // src/discovery/discovery.controller.ts
2302
+ var import_common12 = require("@nestjs/common");
2303
+
2304
+ // src/discovery/discovery.scanner.ts
2305
+ var import_node_crypto2 = require("crypto");
2306
+ var import_node_fs = require("fs");
2307
+ var import_node_path = require("path");
2308
+ var import_common11 = require("@nestjs/common");
2309
+ var import_core2 = require("@nestjs/core");
2310
+
2311
+ // src/discovery/tokens.ts
2312
+ var IMPLEMENTS_CAPABILITY_KEY = "discovery:implements-capability";
2313
+ var DEFINES_QUOTA_KEY = "discovery:defines-quota";
2314
+ var ENFORCE_QUOTA_KEY = "discovery:enforce-quota";
2315
+ var DISCOVERY_SNAPSHOT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/DiscoverySnapshot");
2316
+
2317
+ // src/discovery/discovery.scanner.ts
2318
+ function _ts_decorate11(decorators, target, key, desc) {
2319
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2320
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2321
+ 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;
2322
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2323
+ }
2324
+ __name(_ts_decorate11, "_ts_decorate");
2325
+ function _ts_metadata8(k, v) {
2326
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
2327
+ }
2328
+ __name(_ts_metadata8, "_ts_metadata");
2329
+ function _ts_param8(paramIndex, decorator) {
2330
+ return function(target, key) {
2331
+ decorator(target, key, paramIndex);
2332
+ };
2333
+ }
2334
+ __name(_ts_param8, "_ts_param");
2335
+ var DEFAULT_APP_INFO = {
2336
+ key: "unknown",
2337
+ version: "0.0.0"
2338
+ };
2339
+ var DISCOVERY_APP_INFO_TOKEN = /* @__PURE__ */ Symbol("DISCOVERY_APP_INFO");
2340
+ var DISCOVERY_SNAPSHOT_PATH_TOKEN = /* @__PURE__ */ Symbol("DISCOVERY_SNAPSHOT_PATH");
2341
+ var DiscoveryScanner = class _DiscoveryScanner {
2342
+ static {
2343
+ __name(this, "DiscoveryScanner");
2344
+ }
2345
+ discoveryService;
2346
+ metadataScanner;
2347
+ reflector;
2348
+ appInfo;
2349
+ snapshotPath;
2350
+ logger = new import_common11.Logger(_DiscoveryScanner.name);
2351
+ snapshot = null;
2352
+ // Explicit @Inject instead of type reflection: in the platform package
2353
+ // build tsup/esbuild do not emit `design:paramtypes` metadata, so Nest
2354
+ // cannot otherwise resolve the DI targets at the constructor
2355
+ // (UndefinedDependencyException at the consumer's boot). Pattern
2356
+ // analogous to admin/admin-manifest.module.ts.
2357
+ constructor(discoveryService, metadataScanner, reflector, appInfo = DEFAULT_APP_INFO, snapshotPath = null) {
2358
+ this.discoveryService = discoveryService;
2359
+ this.metadataScanner = metadataScanner;
2360
+ this.reflector = reflector;
2361
+ this.appInfo = appInfo;
2362
+ this.snapshotPath = snapshotPath;
2363
+ }
2364
+ onApplicationBootstrap() {
2365
+ this.snapshot = this.buildSnapshot();
2366
+ this.logger.log(`Discovery snapshot built: ${this.snapshot.capabilities.length} capabilities, ${this.snapshot.features.length} features, ${this.snapshot.quotas.length} quotas (hash=${this.snapshot.hash.slice(0, 12)}\u2026)`);
2367
+ if (this.snapshotPath) {
2368
+ this.persistSnapshot(this.snapshotPath, this.snapshot);
2369
+ }
2370
+ }
2371
+ persistSnapshot(path, snapshot) {
2372
+ try {
2373
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), {
2374
+ recursive: true
2375
+ });
2376
+ (0, import_node_fs.writeFileSync)(path, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
2377
+ this.logger.log(`Discovery snapshot persisted to ${path}`);
2378
+ } catch (err) {
2379
+ this.logger.warn(`Discovery snapshot konnte nicht nach ${path} geschrieben werden: ${err.message}`);
2380
+ }
2381
+ }
2382
+ /**
2383
+ * Returns the boot-time snapshot. Before `onApplicationBootstrap` (e.g. in
2384
+ * tests) the snapshot is built lazily.
2385
+ */
2386
+ getSnapshot() {
2387
+ if (!this.snapshot) {
2388
+ this.snapshot = this.buildSnapshot();
2389
+ }
2390
+ return this.snapshot;
2391
+ }
2392
+ /**
2393
+ * Forces a re-scan (SuperAdmin "re-run discovery" or tests with
2394
+ * dynamically registered providers). `scannedAt` is set anew and the
2395
+ * snapshot — as at boot — is persisted to disk so that the last scan
2396
+ * actually performed is preserved.
2397
+ */
2398
+ rebuildSnapshot() {
2399
+ this.snapshot = this.buildSnapshot();
2400
+ if (this.snapshotPath) {
2401
+ this.persistSnapshot(this.snapshotPath, this.snapshot);
2402
+ }
2403
+ return this.snapshot;
2404
+ }
2405
+ buildSnapshot() {
2406
+ const capabilitiesByKey = /* @__PURE__ */ new Map();
2407
+ const quotasByKey = /* @__PURE__ */ new Map();
2408
+ const enforceRefs = [];
2409
+ const wrappers = [
2410
+ ...this.discoveryService.getProviders(),
2411
+ ...this.discoveryService.getControllers()
2412
+ ];
2413
+ for (const wrapper of wrappers) {
2414
+ const instance = wrapper.instance;
2415
+ if (!instance || typeof instance !== "object") continue;
2416
+ const ctor = instance.constructor;
2417
+ if (!ctor) continue;
2418
+ const quotaOpts = this.reflector.get(DEFINES_QUOTA_KEY, ctor);
2419
+ if (quotaOpts) {
2420
+ this.registerQuota(quotasByKey, quotaOpts, ctor.name);
2421
+ }
2422
+ const prototype = Object.getPrototypeOf(instance);
2423
+ const methodNames = this.metadataScanner.getAllMethodNames(prototype);
2424
+ for (const methodName of methodNames) {
2425
+ const methodRef = instance[methodName];
2426
+ if (typeof methodRef !== "function") continue;
2427
+ const capMeta = this.reflector.get(IMPLEMENTS_CAPABILITY_KEY, methodRef);
2428
+ if (capMeta) {
2429
+ this.registerCapability(capabilitiesByKey, capMeta, `${ctor.name}.${methodName}`);
2430
+ }
2431
+ const enforceMeta = this.reflector.get(ENFORCE_QUOTA_KEY, methodRef);
2432
+ if (enforceMeta && capMeta) {
2433
+ enforceRefs.push({
2434
+ capabilityKey: capMeta.capabilityKey,
2435
+ quotaKey: enforceMeta.quotaKey
2436
+ });
2437
+ }
2438
+ }
2439
+ }
2440
+ const capabilities = sortByKey([
2441
+ ...capabilitiesByKey.values()
2442
+ ], (c) => c.capabilityKey);
2443
+ const features = aggregateFeatures(capabilities);
2444
+ const quotas = sortByKey(decorateQuotasWithEnforcers([
2445
+ ...quotasByKey.values()
2446
+ ], enforceRefs), (q) => q.quotaKey);
2447
+ const snapshot = {
2448
+ schemaVersion: 1,
2449
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
2450
+ app: {
2451
+ ...this.appInfo
2452
+ },
2453
+ capabilities,
2454
+ features,
2455
+ quotas,
2456
+ hash: ""
2457
+ };
2458
+ snapshot.hash = computeSnapshotHash(snapshot);
2459
+ return snapshot;
2460
+ }
2461
+ registerCapability(target, meta, declaredAt) {
2462
+ const existing = target.get(meta.capabilityKey);
2463
+ if (existing) {
2464
+ this.logger.warn(`Capability '${meta.capabilityKey}' wird an mehreren Stellen deklariert (${existing.declaredAt} und ${declaredAt}); erste Deklaration gewinnt`);
2465
+ return;
2466
+ }
2467
+ target.set(meta.capabilityKey, {
2468
+ capabilityKey: meta.capabilityKey,
2469
+ label: meta.label ?? null,
2470
+ feature: meta.feature ?? null,
2471
+ status: meta.status ?? "active",
2472
+ kind: meta.kind ?? "endpoint",
2473
+ owner: meta.owner ?? null,
2474
+ replacementKey: meta.replacementKey ?? null,
2475
+ removalPlannedAt: meta.removalPlannedAt ?? null,
2476
+ reason: meta.reason ?? null,
2477
+ requires: normalizeKeyList(meta.requires),
2478
+ replaces: normalizeKeyList(meta.replaces),
2479
+ declaredAt
2480
+ });
2481
+ }
2482
+ registerQuota(target, opts, declaredAt) {
2483
+ const existing = target.get(opts.key);
2484
+ if (existing) {
2485
+ this.logger.warn(`Quota '${opts.key}' wird an mehreren Stellen definiert (${existing.declaredAt} und ${declaredAt}); erste Definition gewinnt`);
2486
+ return;
2487
+ }
2488
+ target.set(opts.key, {
2489
+ quotaKey: opts.key,
2490
+ label: opts.label,
2491
+ unit: opts.unit,
2492
+ policy: opts.policy,
2493
+ feature: opts.feature ?? null,
2494
+ replaces: normalizeKeyList(opts.replaces),
2495
+ declaredAt,
2496
+ enforcedBy: []
2497
+ });
2498
+ }
2499
+ };
2500
+ DiscoveryScanner = _ts_decorate11([
2501
+ (0, import_common11.Injectable)(),
2502
+ _ts_param8(0, (0, import_common11.Inject)(import_core2.DiscoveryService)),
2503
+ _ts_param8(1, (0, import_common11.Inject)(import_core2.MetadataScanner)),
2504
+ _ts_param8(2, (0, import_common11.Inject)(import_core2.Reflector)),
2505
+ _ts_param8(3, (0, import_common11.Optional)()),
2506
+ _ts_param8(3, (0, import_common11.Inject)(DISCOVERY_APP_INFO_TOKEN)),
2507
+ _ts_param8(4, (0, import_common11.Optional)()),
2508
+ _ts_param8(4, (0, import_common11.Inject)(DISCOVERY_SNAPSHOT_PATH_TOKEN)),
2509
+ _ts_metadata8("design:type", Function),
2510
+ _ts_metadata8("design:paramtypes", [
2511
+ typeof import_core2.DiscoveryService === "undefined" ? Object : import_core2.DiscoveryService,
2512
+ typeof import_core2.MetadataScanner === "undefined" ? Object : import_core2.MetadataScanner,
2513
+ typeof import_core2.Reflector === "undefined" ? Object : import_core2.Reflector,
2514
+ typeof DiscoveryAppInfo === "undefined" ? Object : DiscoveryAppInfo,
2515
+ Object
2516
+ ])
2517
+ ], DiscoveryScanner);
2518
+ function aggregateFeatures(capabilities) {
2519
+ const byFeature = /* @__PURE__ */ new Map();
2520
+ for (const cap of capabilities) {
2521
+ if (!cap.feature) continue;
2522
+ const acc = byFeature.get(cap.feature) ?? {
2523
+ capabilityKeys: [],
2524
+ requires: /* @__PURE__ */ new Set(),
2525
+ replaces: /* @__PURE__ */ new Set()
2526
+ };
2527
+ acc.capabilityKeys.push(cap.capabilityKey);
2528
+ for (const key of cap.requires ?? []) acc.requires.add(key);
2529
+ for (const key of cap.replaces ?? []) acc.replaces.add(key);
2530
+ byFeature.set(cap.feature, acc);
2531
+ }
2532
+ return sortByKey([
2533
+ ...byFeature.entries()
2534
+ ].map(([featureKey, acc]) => {
2535
+ acc.requires.delete(featureKey);
2536
+ acc.replaces.delete(featureKey);
2537
+ return {
2538
+ featureKey,
2539
+ capabilityKeys: [
2540
+ ...acc.capabilityKeys
2541
+ ].sort((a, b) => a.localeCompare(b)),
2542
+ requires: setToSortedListOrNull(acc.requires),
2543
+ replaces: setToSortedListOrNull(acc.replaces)
2544
+ };
2545
+ }), (f) => f.featureKey);
2546
+ }
2547
+ __name(aggregateFeatures, "aggregateFeatures");
2548
+ function normalizeKeyList(keys) {
2549
+ if (!keys || keys.length === 0) return null;
2550
+ return [
2551
+ ...new Set(keys)
2552
+ ].sort((a, b) => a.localeCompare(b));
2553
+ }
2554
+ __name(normalizeKeyList, "normalizeKeyList");
2555
+ function setToSortedListOrNull(keys) {
2556
+ if (keys.size === 0) return null;
2557
+ return [
2558
+ ...keys
2559
+ ].sort((a, b) => a.localeCompare(b));
2560
+ }
2561
+ __name(setToSortedListOrNull, "setToSortedListOrNull");
2562
+ function decorateQuotasWithEnforcers(quotas, enforceRefs) {
2563
+ const enforcedByQuota = /* @__PURE__ */ new Map();
2564
+ for (const ref of enforceRefs) {
2565
+ const set = enforcedByQuota.get(ref.quotaKey) ?? /* @__PURE__ */ new Set();
2566
+ set.add(ref.capabilityKey);
2567
+ enforcedByQuota.set(ref.quotaKey, set);
2568
+ }
2569
+ return quotas.map((q) => ({
2570
+ ...q,
2571
+ enforcedBy: [
2572
+ ...enforcedByQuota.get(q.quotaKey) ?? /* @__PURE__ */ new Set()
2573
+ ].sort((a, b) => a.localeCompare(b))
2574
+ }));
2575
+ }
2576
+ __name(decorateQuotasWithEnforcers, "decorateQuotasWithEnforcers");
2577
+ function sortByKey(items, keyFn) {
2578
+ return [
2579
+ ...items
2580
+ ].sort((a, b) => keyFn(a).localeCompare(keyFn(b)));
2581
+ }
2582
+ __name(sortByKey, "sortByKey");
2583
+ function computeSnapshotHash(snapshot) {
2584
+ const stableInput = {
2585
+ schemaVersion: snapshot.schemaVersion,
2586
+ appKey: snapshot.app.key,
2587
+ capabilities: snapshot.capabilities.map((c) => ({
2588
+ ...c
2589
+ })),
2590
+ features: snapshot.features,
2591
+ quotas: snapshot.quotas
2592
+ };
2593
+ const json = canonicalStringify(stableInput);
2594
+ return `sha256-${(0, import_node_crypto2.createHash)("sha256").update(json).digest("hex")}`;
2595
+ }
2596
+ __name(computeSnapshotHash, "computeSnapshotHash");
2597
+ function canonicalStringify(value) {
2598
+ return JSON.stringify(value, (_key, val) => {
2599
+ if (val && typeof val === "object" && !Array.isArray(val)) {
2600
+ const sorted = {};
2601
+ for (const k of Object.keys(val).sort()) {
2602
+ sorted[k] = val[k];
2603
+ }
2604
+ return sorted;
2605
+ }
2606
+ return val;
2607
+ });
2608
+ }
2609
+ __name(canonicalStringify, "canonicalStringify");
2610
+
2611
+ // src/discovery/discovery.controller.ts
2612
+ function _ts_decorate12(decorators, target, key, desc) {
2613
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2614
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2615
+ 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;
2616
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2617
+ }
2618
+ __name(_ts_decorate12, "_ts_decorate");
2619
+ function _ts_metadata9(k, v) {
2620
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
2621
+ }
2622
+ __name(_ts_metadata9, "_ts_metadata");
2623
+ function _ts_param9(paramIndex, decorator) {
2624
+ return function(target, key) {
2625
+ decorator(target, key, paramIndex);
2626
+ };
2627
+ }
2628
+ __name(_ts_param9, "_ts_param");
2629
+ function buildDiscoveryController(guards) {
2630
+ let GeneratedDiscoveryController = class GeneratedDiscoveryController {
2631
+ static {
2632
+ __name(this, "GeneratedDiscoveryController");
2633
+ }
2634
+ scanner;
2635
+ // Explicit @Inject instead of type reflection: tsup/esbuild do not emit
2636
+ // any `design:paramtypes` metadata, so otherwise Nest cannot resolve the
2637
+ // service type on the constructor.
2638
+ constructor(scanner) {
2639
+ this.scanner = scanner;
2640
+ }
2641
+ getDiscovery(ifNoneMatch, res) {
2642
+ const snapshot = this.scanner.getSnapshot();
2643
+ const etag = `"${snapshot.hash}-${snapshot.scannedAt}"`;
2644
+ res.header("ETag", etag);
2645
+ if (ifNoneMatch && ifNoneMatch === etag) {
2646
+ res.status(import_common12.HttpStatus.NOT_MODIFIED);
2647
+ return null;
2648
+ }
2649
+ return snapshot;
2650
+ }
2651
+ /**
2652
+ * Forces a fresh code scan and returns the new snapshot.
2653
+ * `scannedAt` is updated; the Capability/Feature/Quota data
2654
+ * are identical within the running process (code is stable at runtime).
2655
+ */
2656
+ rescanDiscovery() {
2657
+ return this.scanner.rebuildSnapshot();
2658
+ }
2659
+ };
2660
+ _ts_decorate12([
2661
+ (0, import_common12.Get)("discovery"),
2662
+ (0, import_common12.Header)("Cache-Control", "private, no-cache"),
2663
+ _ts_param9(0, (0, import_common12.Headers)("if-none-match")),
2664
+ _ts_param9(1, (0, import_common12.Res)({
2665
+ passthrough: true
2666
+ })),
2667
+ _ts_metadata9("design:type", Function),
2668
+ _ts_metadata9("design:paramtypes", [
2669
+ Object,
2670
+ typeof HttpResponseLike === "undefined" ? Object : HttpResponseLike
2671
+ ]),
2672
+ _ts_metadata9("design:returntype", Object)
2673
+ ], GeneratedDiscoveryController.prototype, "getDiscovery", null);
2674
+ _ts_decorate12([
2675
+ (0, import_common12.Post)("discovery/rescan"),
2676
+ _ts_metadata9("design:type", Function),
2677
+ _ts_metadata9("design:paramtypes", []),
2678
+ _ts_metadata9("design:returntype", typeof DiscoverySnapshot === "undefined" ? Object : DiscoverySnapshot)
2679
+ ], GeneratedDiscoveryController.prototype, "rescanDiscovery", null);
2680
+ GeneratedDiscoveryController = _ts_decorate12([
2681
+ (0, import_common12.Controller)("admin"),
2682
+ (0, import_common12.UseGuards)(...guards),
2683
+ _ts_param9(0, (0, import_common12.Inject)(DiscoveryScanner)),
2684
+ _ts_metadata9("design:type", Function),
2685
+ _ts_metadata9("design:paramtypes", [
2686
+ typeof DiscoveryScanner === "undefined" ? Object : DiscoveryScanner
2687
+ ])
2688
+ ], GeneratedDiscoveryController);
2689
+ return GeneratedDiscoveryController;
2690
+ }
2691
+ __name(buildDiscoveryController, "buildDiscoveryController");
2692
+
2693
+ // src/discovery/discovery.module.ts
2694
+ function _ts_decorate13(decorators, target, key, desc) {
2695
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2696
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2697
+ 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;
2698
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2699
+ }
2700
+ __name(_ts_decorate13, "_ts_decorate");
2701
+ var DiscoveryModule = class _DiscoveryModule {
2702
+ static {
2703
+ __name(this, "DiscoveryModule");
2704
+ }
2705
+ static forRoot(options) {
2706
+ const isGlobal = options.isGlobal ?? true;
2707
+ const controllers = [];
2708
+ if (options.controller) {
2709
+ controllers.push(buildDiscoveryController(options.controller.guards));
2710
+ }
2711
+ const providers = [
2712
+ {
2713
+ provide: DISCOVERY_APP_INFO_TOKEN,
2714
+ useValue: options.app
2715
+ },
2716
+ {
2717
+ provide: DISCOVERY_SNAPSHOT_PATH_TOKEN,
2718
+ useValue: options.snapshotPath ?? null
2719
+ },
2720
+ DiscoveryScanner,
2721
+ {
2722
+ provide: DISCOVERY_SNAPSHOT_TOKEN,
2723
+ useFactory: /* @__PURE__ */ __name((scanner) => scanner.getSnapshot(), "useFactory"),
2724
+ inject: [
2725
+ DiscoveryScanner
2726
+ ]
2727
+ }
2728
+ ];
2729
+ return {
2730
+ module: _DiscoveryModule,
2731
+ imports: [
2732
+ import_core3.DiscoveryModule,
2733
+ ...options.imports ?? []
2734
+ ],
2735
+ controllers,
2736
+ providers,
2737
+ exports: [
2738
+ DiscoveryScanner,
2739
+ DISCOVERY_SNAPSHOT_TOKEN
2740
+ ],
2741
+ global: isGlobal
2742
+ };
2743
+ }
2744
+ };
2745
+ DiscoveryModule = _ts_decorate13([
2746
+ (0, import_common13.Module)({})
2747
+ ], DiscoveryModule);
2748
+
2749
+ // src/entitlement/module.ts
2750
+ var import_common15 = require("@nestjs/common");
2751
+
2752
+ // src/entitlement/service.ts
2753
+ var import_common14 = require("@nestjs/common");
2754
+
2755
+ // src/catalog/tokens.ts
2756
+ var BUNDLE_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol("BUNDLE_REPOSITORY");
2757
+
2758
+ // src/billing/subscription-bundles.tokens.ts
2759
+ var SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/SubscriptionBundleRepository");
2760
+
2761
+ // src/subscription-contract/tokens.ts
2762
+ var SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/SubscriptionContractRepository");
2763
+
2764
+ // src/billing/plan-helpers.ts
2765
+ function isFeaturePlannedOnly(catalog, featureKey) {
2766
+ const def = (catalog.features ?? []).find((f) => f.key === featureKey);
2767
+ return def?.plannedOnly === true;
2768
+ }
2769
+ __name(isFeaturePlannedOnly, "isFeaturePlannedOnly");
2770
+
2771
+ // src/entitlement/aggregation.ts
2772
+ function aggregateBusinessTypeQuotas(snapshot) {
2773
+ const sums = {};
2774
+ for (const bundle of snapshot.bundles) {
2775
+ for (const [key, value] of Object.entries(bundle.quotas)) {
2776
+ if (sums[key] === -1 || value === -1) {
2777
+ sums[key] = -1;
2778
+ } else {
2779
+ sums[key] = (sums[key] ?? 0) + value;
2780
+ }
2781
+ }
2782
+ }
2783
+ for (const [key, value] of Object.entries(snapshot.quotaOverrides)) {
2784
+ if (value !== void 0) {
2785
+ sums[key] = value;
2786
+ }
2787
+ }
2788
+ return sums;
2789
+ }
2790
+ __name(aggregateBusinessTypeQuotas, "aggregateBusinessTypeQuotas");
2791
+ function filterActiveSubscriptionBundles(bundles, now) {
2792
+ return bundles.filter((b) => b.canceledEffectiveAt === null || b.canceledEffectiveAt > now);
2793
+ }
2794
+ __name(filterActiveSubscriptionBundles, "filterActiveSubscriptionBundles");
2795
+ function aggregateSubscriptionBundleQuotas(bundles) {
2796
+ const sums = {};
2797
+ for (const bundle of bundles) {
2798
+ for (const [key, value] of Object.entries(bundle.quotas)) {
2799
+ if (sums[key] === -1 || value === -1) {
2800
+ sums[key] = -1;
2801
+ } else {
2802
+ sums[key] = (sums[key] ?? 0) + value;
2803
+ }
2804
+ }
2805
+ }
2806
+ return sums;
2807
+ }
2808
+ __name(aggregateSubscriptionBundleQuotas, "aggregateSubscriptionBundleQuotas");
2809
+ function collectSubscriptionBundleFeatures(bundles) {
2810
+ const set = /* @__PURE__ */ new Set();
2811
+ for (const bundle of bundles) {
2812
+ for (const f of bundle.features) set.add(f);
2813
+ }
2814
+ return Array.from(set);
2815
+ }
2816
+ __name(collectSubscriptionBundleFeatures, "collectSubscriptionBundleFeatures");
2817
+ function aggregateContractLineItemEntitlements(lineItems, fallbackPlan = "UNKNOWN") {
2818
+ const planItem = lineItems.find((item) => item.kind === "plan");
2819
+ const plan = planItem?.sourceKey ?? fallbackPlan;
2820
+ const quotas = {};
2821
+ const features = /* @__PURE__ */ new Set();
2822
+ for (const item of lineItems) {
2823
+ for (const feature of item.featuresSnapshot) {
2824
+ features.add(feature);
2825
+ }
2826
+ for (const [key, value] of Object.entries(item.quotaEffectsSnapshot)) {
2827
+ if (quotas[key] === -1 || value === -1) {
2828
+ quotas[key] = -1;
2829
+ } else {
2830
+ quotas[key] = (quotas[key] ?? 0) + value;
2831
+ }
2832
+ }
2833
+ }
2834
+ return {
2835
+ plan,
2836
+ quotas,
2837
+ features
2838
+ };
2839
+ }
2840
+ __name(aggregateContractLineItemEntitlements, "aggregateContractLineItemEntitlements");
2841
+ function collectBusinessTypeFeatures(snapshot) {
2842
+ const set = /* @__PURE__ */ new Set();
2843
+ for (const bundle of snapshot.bundles) {
2844
+ for (const f of bundle.features) {
2845
+ set.add(f);
2846
+ }
2847
+ }
2848
+ return Array.from(set);
2849
+ }
2850
+ __name(collectBusinessTypeFeatures, "collectBusinessTypeFeatures");
2851
+ function filterPlannedOnlyFeatures(features, catalog) {
2852
+ const out = /* @__PURE__ */ new Set();
2853
+ for (const f of features) {
2854
+ if (!isFeaturePlannedOnly(catalog, f)) {
2855
+ out.add(f);
2856
+ }
2857
+ }
2858
+ return out;
2859
+ }
2860
+ __name(filterPlannedOnlyFeatures, "filterPlannedOnlyFeatures");
2861
+ function applyCustomLimits(base, custom) {
2862
+ if (!custom) return base;
2863
+ const quotas = {
2864
+ ...base.quotas
2865
+ };
2866
+ if (custom.quotas) {
2867
+ for (const [k, v] of Object.entries(custom.quotas)) {
2868
+ quotas[k] = v;
2869
+ }
2870
+ }
2871
+ const features = new Set(base.features);
2872
+ if (custom.features) {
2873
+ for (const f of custom.features) features.add(f);
2874
+ }
2875
+ return {
2876
+ plan: base.plan,
2877
+ quotas,
2878
+ features
2879
+ };
2880
+ }
2881
+ __name(applyCustomLimits, "applyCustomLimits");
2882
+ function aggregateLimits(input, catalog, now) {
2883
+ const businessTypeQuotas = input.businessTypeVersion ? aggregateBusinessTypeQuotas(input.businessTypeVersion) : {};
2884
+ const businessTypeFeatures = input.businessTypeVersion ? collectBusinessTypeFeatures(input.businessTypeVersion) : [];
2885
+ const activeBundles = filterActiveSubscriptionBundles(input.subscriptionBundles ?? [], now);
2886
+ const subBundleQuotas = aggregateSubscriptionBundleQuotas(activeBundles);
2887
+ const subBundleFeatures = collectSubscriptionBundleFeatures(activeBundles);
2888
+ const quotas = {};
2889
+ for (const [k, v] of Object.entries(input.planVersion.quotas)) {
2890
+ quotas[k] = v;
2891
+ }
2892
+ for (const [k, v] of Object.entries(businessTypeQuotas)) {
2893
+ if (quotas[k] === -1 || v === -1) {
2894
+ quotas[k] = -1;
2895
+ } else {
2896
+ quotas[k] = (quotas[k] ?? 0) + v;
2897
+ }
2898
+ }
2899
+ for (const [k, v] of Object.entries(subBundleQuotas)) {
2900
+ if (quotas[k] === -1 || v === -1) {
2901
+ quotas[k] = -1;
2902
+ } else {
2903
+ quotas[k] = (quotas[k] ?? 0) + v;
2904
+ }
2905
+ }
2906
+ const features = /* @__PURE__ */ new Set([
2907
+ ...input.planVersion.features,
2908
+ ...businessTypeFeatures,
2909
+ ...subBundleFeatures
2910
+ ]);
2911
+ const withCustom = applyCustomLimits({
2912
+ plan: input.plan,
2913
+ quotas,
2914
+ features
2915
+ }, input.customLimits);
2916
+ const filteredFeatures = filterPlannedOnlyFeatures(withCustom.features, catalog);
2917
+ return {
2918
+ plan: withCustom.plan,
2919
+ quotas: withCustom.quotas,
2920
+ features: filteredFeatures
2921
+ };
2922
+ }
2923
+ __name(aggregateLimits, "aggregateLimits");
2924
+
2925
+ // src/entitlement/feature-aliases.ts
2926
+ function buildReplacedByIndex(features) {
2927
+ const index = /* @__PURE__ */ new Map();
2928
+ for (const feature of features) {
2929
+ for (const oldKey of feature.replaces ?? []) {
2930
+ if (oldKey === feature.featureKey) continue;
2931
+ const list = index.get(oldKey) ?? [];
2932
+ list.push(feature.featureKey);
2933
+ index.set(oldKey, list);
2934
+ }
2935
+ }
2936
+ for (const list of index.values()) list.sort((a, b) => a.localeCompare(b));
2937
+ assertNoReplacesCycle(index);
2938
+ return index;
2939
+ }
2940
+ __name(buildReplacedByIndex, "buildReplacedByIndex");
2941
+ function expandReplacedFeatures(granted, index) {
2942
+ const expanded = new Set(granted);
2943
+ const worklist = [
2944
+ ...granted
2945
+ ];
2946
+ while (worklist.length > 0) {
2947
+ const key = worklist.pop();
2948
+ for (const successor of index.get(key) ?? []) {
2949
+ if (expanded.has(successor)) continue;
2950
+ expanded.add(successor);
2951
+ worklist.push(successor);
2952
+ }
2953
+ }
2954
+ return expanded;
2955
+ }
2956
+ __name(expandReplacedFeatures, "expandReplacedFeatures");
2957
+ function assertNoReplacesCycle(index) {
2958
+ const DONE = 2;
2959
+ const IN_PROGRESS = 1;
2960
+ const state = /* @__PURE__ */ new Map();
2961
+ const visit = /* @__PURE__ */ __name((key, path) => {
2962
+ const s = state.get(key);
2963
+ if (s === DONE) return;
2964
+ if (s === IN_PROGRESS) {
2965
+ const cycleStart = path.indexOf(key);
2966
+ const cycle = [
2967
+ ...path.slice(cycleStart),
2968
+ key
2969
+ ].join(" \u2192 ");
2970
+ throw new Error(`replaces-Zyklus in den Discovery-Metadaten: ${cycle}. Feature-Ersetzungen m\xFCssen azyklisch sein \u2014 replaces-Deklarationen im Code korrigieren (#39).`);
2971
+ }
2972
+ state.set(key, IN_PROGRESS);
2973
+ for (const successor of index.get(key) ?? []) {
2974
+ visit(successor, [
2975
+ ...path,
2976
+ key
2977
+ ]);
2978
+ }
2979
+ state.set(key, DONE);
2980
+ }, "visit");
2981
+ for (const key of index.keys()) visit(key, []);
2982
+ }
2983
+ __name(assertNoReplacesCycle, "assertNoReplacesCycle");
2984
+
2985
+ // src/entitlement/limit-exceeded-error.ts
2986
+ var LimitExceededError = class extends Error {
2987
+ static {
2988
+ __name(this, "LimitExceededError");
2989
+ }
2990
+ dimension;
2991
+ max;
2992
+ used;
2993
+ constructor(dimension, max, used) {
2994
+ super(`Limit f\xFCr ${dimension} erreicht: ${used}/${max}.`), this.dimension = dimension, this.max = max, this.used = used;
2995
+ this.name = "LimitExceededError";
2996
+ }
2997
+ };
2998
+
2999
+ // src/entitlement/plan-resolution.ts
3000
+ function resolveEntitlementPlan(input, config, now) {
3001
+ if (input.isPilot && config.pilotEntitlementPlan !== void 0) {
3002
+ return config.pilotEntitlementPlan;
3003
+ }
3004
+ if (input.status === "TRIAL") {
3005
+ return input.trialEntitlementPlan ?? config.defaultTrialEntitlementPlan ?? input.plan;
3006
+ }
3007
+ if (input.status === "PENDING_SALES" && config.pendingSalesEntitlementPlan !== void 0) {
3008
+ return config.pendingSalesEntitlementPlan;
3009
+ }
3010
+ if (input.pendingPlan && input.pendingEffectiveAt && input.pendingEffectiveAt <= now) {
3011
+ return input.pendingPlan;
3012
+ }
3013
+ return input.plan;
3014
+ }
3015
+ __name(resolveEntitlementPlan, "resolveEntitlementPlan");
3016
+
3017
+ // src/entitlement/tokens.ts
3018
+ var SUBSCRIPTION_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/SubscriptionRepository");
3019
+ var PLAN_VERSION_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/PlanVersionRepository");
3020
+ var TRANSACTION_RUNNER_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/TransactionRunner");
3021
+ var ENTITLEMENT_RESOLUTION_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/EntitlementResolutionConfig");
3022
+
3023
+ // src/entitlement/service.ts
3024
+ function _ts_decorate14(decorators, target, key, desc) {
3025
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3026
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3027
+ 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;
3028
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3029
+ }
3030
+ __name(_ts_decorate14, "_ts_decorate");
3031
+ function _ts_metadata10(k, v) {
3032
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
3033
+ }
3034
+ __name(_ts_metadata10, "_ts_metadata");
3035
+ function _ts_param10(paramIndex, decorator) {
3036
+ return function(target, key) {
3037
+ decorator(target, key, paramIndex);
3038
+ };
3039
+ }
3040
+ __name(_ts_param10, "_ts_param");
3041
+ var CACHE_TTL_MS = 6e4;
3042
+ var CACHE_MAX_ENTRIES = 1e3;
3043
+ var EntitlementService = class {
3044
+ static {
3045
+ __name(this, "EntitlementService");
3046
+ }
3047
+ catalog;
3048
+ subscriptions;
3049
+ planVersions;
3050
+ tx;
3051
+ resolutionConfig;
3052
+ subscriptionBundles;
3053
+ bundles;
3054
+ subscriptionContracts;
3055
+ discoverySnapshot;
3056
+ // Map preserves insert order — on a hit the entry is removed and
3057
+ // re-inserted (LRU). The TTL is a safety net for forgotten
3058
+ // invalidations — 60s is enough, because limits generally only change
3059
+ // through mutations, not through time (a pending plan's effective date
3060
+ // is day-granular).
3061
+ cache = /* @__PURE__ */ new Map();
3062
+ // #39 — replaces alias index, built lazily from the boot-static snapshot;
3063
+ // throws on replaces cycles (a clear error instead of an infinite loop).
3064
+ replacedByIndex = null;
3065
+ constructor(catalog, subscriptions, planVersions, tx, resolutionConfig = null, subscriptionBundles = null, bundles = null, subscriptionContracts = null, discoverySnapshot = null) {
3066
+ this.catalog = catalog;
3067
+ this.subscriptions = subscriptions;
3068
+ this.planVersions = planVersions;
3069
+ this.tx = tx;
3070
+ this.resolutionConfig = resolutionConfig;
3071
+ this.subscriptionBundles = subscriptionBundles;
3072
+ this.bundles = bundles;
3073
+ this.subscriptionContracts = subscriptionContracts;
3074
+ this.discoverySnapshot = discoverySnapshot;
3075
+ }
3076
+ // ---------------------------------------------------------------------
3077
+ // Read path — for FeatureGuard, sidebar hooks, GET /billing/entitlement
3078
+ // ---------------------------------------------------------------------
3079
+ async computeLimits(tenantId, now = /* @__PURE__ */ new Date()) {
3080
+ const cached = this.readCache(tenantId, now.getTime());
3081
+ if (cached) return cached;
3082
+ const sub = await this.subscriptions.findByTenantId(tenantId);
3083
+ if (!sub) {
3084
+ throw new import_common14.NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
3085
+ }
3086
+ const limits = await this.deriveLimits(sub, now);
3087
+ this.writeCache(tenantId, limits, now.getTime());
3088
+ return limits;
3089
+ }
3090
+ /**
3091
+ * Clears a tenant's cache entry. MUST be called as soon as a mutation
3092
+ * changes the subscription, a bundle booking, or a PlanVersion binding —
3093
+ * otherwise FeatureGuard and the sidebar stay on stale state for up to
3094
+ * 60s.
3095
+ */
3096
+ invalidateTenant(tenantId) {
3097
+ this.cache.delete(tenantId);
3098
+ }
3099
+ /** Clears the entire cache. Only for tests / bootstrap. */
3100
+ invalidateAll() {
3101
+ this.cache.clear();
3102
+ }
3103
+ /**
3104
+ * Aggregator path — also callable directly by consumers that already
3105
+ * have a `SubscriptionRecord` (e.g. within a transaction).
3106
+ */
3107
+ async deriveLimits(sub, now, tx) {
3108
+ const contractLimits = await this.deriveLimitsFromContract(sub.tenantId, now);
3109
+ if (contractLimits) return this.withReplacedFeatureAliases(contractLimits);
3110
+ const effectivePlan = resolveEntitlementPlan(sub, this.resolutionConfig ?? {}, now);
3111
+ const planVersion = effectivePlan === sub.plan ? sub.planVersion : await this.findActivePlanVersionOrFallback(effectivePlan, now, tx);
3112
+ const subscriptionBundles = await this.loadSubscriptionBundleSnapshots(sub.id, now);
3113
+ return this.withReplacedFeatureAliases(aggregateLimits({
3114
+ plan: effectivePlan,
3115
+ planVersion,
3116
+ subscriptionBundles,
3117
+ customLimits: sub.customLimits ?? null
3118
+ }, this.catalog, now));
3119
+ }
3120
+ /**
3121
+ * Grandfathering (#39): granted old feature keys transitively grant
3122
+ * their successors from the `replaces` chains of the discovery snapshot.
3123
+ * A no-op without a snapshot or without replaces declarations.
3124
+ */
3125
+ withReplacedFeatureAliases(limits) {
3126
+ if (!this.discoverySnapshot) return limits;
3127
+ if (!this.replacedByIndex) {
3128
+ this.replacedByIndex = buildReplacedByIndex(this.discoverySnapshot.features);
3129
+ }
3130
+ if (this.replacedByIndex.size === 0) return limits;
3131
+ return {
3132
+ ...limits,
3133
+ features: expandReplacedFeatures(limits.features, this.replacedByIndex)
3134
+ };
3135
+ }
3136
+ async deriveLimitsFromContract(tenantId, now) {
3137
+ if (!this.subscriptionContracts) return null;
3138
+ const contract = await this.subscriptionContracts.findActiveByTenantId(tenantId, now);
3139
+ if (!contract) return null;
3140
+ if (contract.entitlementSnapshot) {
3141
+ return {
3142
+ plan: contract.entitlementSnapshot.plan,
3143
+ quotas: {
3144
+ ...contract.entitlementSnapshot.quotas
3145
+ },
3146
+ features: new Set(contract.entitlementSnapshot.features)
3147
+ };
3148
+ }
3149
+ return aggregateContractLineItemEntitlements(contract.lineItems);
3150
+ }
3151
+ /**
3152
+ * Loads a subscription's active bundle bookings and resolves the
3153
+ * `BundleVersion` features/quotas per entry. Without a registered
3154
+ * `SubscriptionBundleRepository` or `BundleRepository` the method
3155
+ * returns an empty list — apps without a bundle schema stay unchanged
3156
+ * (plan-only aggregation).
3157
+ */
3158
+ async loadSubscriptionBundleSnapshots(subscriptionId, now) {
3159
+ if (!this.subscriptionBundles || !this.bundles) return [];
3160
+ const active = await this.subscriptionBundles.listActiveBySubscription(subscriptionId, now);
3161
+ if (active.length === 0) return [];
3162
+ return Promise.all(active.map(async (booking) => {
3163
+ const bv = await this.bundles.findVersionById(booking.bundleVersionId);
3164
+ if (!bv) {
3165
+ throw new Error(`BundleVersion '${booking.bundleVersionId}' aus aktiver SubscriptionBundle nicht gefunden`);
3166
+ }
3167
+ return {
3168
+ bundleKey: bv.bundleKey,
3169
+ features: bv.features,
3170
+ quotas: bv.quotas,
3171
+ canceledEffectiveAt: booking.canceledEffectiveAt
3172
+ };
3173
+ }));
3174
+ }
3175
+ // ---------------------------------------------------------------------
3176
+ // Write/consumption path — row lock + count + insert in the same
3177
+ // transaction. Protects against race conditions on concurrent creations
3178
+ // for the same tenant.
3179
+ // ---------------------------------------------------------------------
3180
+ async enforceLimit(input) {
3181
+ const now = input.now ?? /* @__PURE__ */ new Date();
3182
+ const delta = input.delta ?? 1;
3183
+ return this.tx.run(async (tx) => {
3184
+ const sub = await this.subscriptions.findByTenantIdLocked(input.tenantId, tx);
3185
+ if (!sub) {
3186
+ throw new import_common14.NotFoundException(`Keine Subscription f\xFCr Tenant ${input.tenantId}`);
3187
+ }
3188
+ const limits = await this.deriveLimits(sub, now, tx);
3189
+ const max = limits.quotas[input.dimension];
3190
+ if (max === void 0) {
3191
+ throw new Error(`Quota-Dimension "${input.dimension}" nicht im PlanCatalog (${this.catalog.projectKey}).`);
3192
+ }
3193
+ const used = await input.currentUsage(tx, input.tenantId);
3194
+ if (max !== -1 && used + delta > max) {
3195
+ throw new LimitExceededError(input.dimension, max, used);
3196
+ }
3197
+ return input.insert(tx);
3198
+ });
3199
+ }
3200
+ // ---------------------------------------------------------------------
3201
+ // Helpers
3202
+ // ---------------------------------------------------------------------
3203
+ /**
3204
+ * Plan fallback for TRIAL/PENDING_SALES: returns the PlanVersion active
3205
+ * as of `asOf` (SPEC_V2 §4.2). If the repo does not implement
3206
+ * `findActive`, we fall back to `findLatestLive` (backward compat for
3207
+ * adapters without `validFrom` columns).
3208
+ */
3209
+ async findActivePlanVersionOrFallback(planId, asOf, tx) {
3210
+ const v = this.planVersions.findActive ? await this.planVersions.findActive(planId, asOf, tx) : await this.planVersions.findLatestLive(planId, tx);
3211
+ if (!v) {
3212
+ throw new import_common14.NotFoundException(`Keine zu ${asOf.toISOString().slice(0, 10)} aktive PlanVersion f\xFCr ${planId} \u2014 weder validFrom-window erf\xFCllt noch latest-live verf\xFCgbar.`);
3213
+ }
3214
+ return v;
3215
+ }
3216
+ readCache(tenantId, nowMs) {
3217
+ const entry = this.cache.get(tenantId);
3218
+ if (!entry) return null;
3219
+ if (entry.expiresAt < nowMs) {
3220
+ this.cache.delete(tenantId);
3221
+ return null;
3222
+ }
3223
+ this.cache.delete(tenantId);
3224
+ this.cache.set(tenantId, entry);
3225
+ return entry.value;
3226
+ }
3227
+ writeCache(tenantId, value, nowMs) {
3228
+ if (this.cache.has(tenantId)) this.cache.delete(tenantId);
3229
+ this.cache.set(tenantId, {
3230
+ value,
3231
+ expiresAt: nowMs + CACHE_TTL_MS
3232
+ });
3233
+ while (this.cache.size > CACHE_MAX_ENTRIES) {
3234
+ const oldest = this.cache.keys().next().value;
3235
+ if (oldest === void 0) break;
3236
+ this.cache.delete(oldest);
3237
+ }
3238
+ }
3239
+ };
3240
+ EntitlementService = _ts_decorate14([
3241
+ (0, import_common14.Injectable)(),
3242
+ _ts_param10(0, (0, import_common14.Inject)(PLAN_CATALOG_TOKEN)),
3243
+ _ts_param10(1, (0, import_common14.Inject)(SUBSCRIPTION_REPOSITORY_TOKEN)),
3244
+ _ts_param10(2, (0, import_common14.Inject)(PLAN_VERSION_REPOSITORY_TOKEN)),
3245
+ _ts_param10(3, (0, import_common14.Inject)(TRANSACTION_RUNNER_TOKEN)),
3246
+ _ts_param10(4, (0, import_common14.Optional)()),
3247
+ _ts_param10(4, (0, import_common14.Inject)(ENTITLEMENT_RESOLUTION_CONFIG_TOKEN)),
3248
+ _ts_param10(5, (0, import_common14.Optional)()),
3249
+ _ts_param10(5, (0, import_common14.Inject)(SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN)),
3250
+ _ts_param10(6, (0, import_common14.Optional)()),
3251
+ _ts_param10(6, (0, import_common14.Inject)(BUNDLE_REPOSITORY_TOKEN)),
3252
+ _ts_param10(7, (0, import_common14.Optional)()),
3253
+ _ts_param10(7, (0, import_common14.Inject)(SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN)),
3254
+ _ts_param10(8, (0, import_common14.Optional)()),
3255
+ _ts_param10(8, (0, import_common14.Inject)(DISCOVERY_SNAPSHOT_TOKEN)),
3256
+ _ts_metadata10("design:type", Function),
3257
+ _ts_metadata10("design:paramtypes", [
3258
+ typeof PlanCatalog === "undefined" ? Object : PlanCatalog,
3259
+ typeof SubscriptionRepository === "undefined" ? Object : SubscriptionRepository,
3260
+ typeof PlanVersionRepository === "undefined" ? Object : PlanVersionRepository,
3261
+ typeof TransactionRunner === "undefined" ? Object : TransactionRunner,
3262
+ Object,
3263
+ Object,
3264
+ Object,
3265
+ Object,
3266
+ Object
3267
+ ])
3268
+ ], EntitlementService);
3269
+
3270
+ // src/entitlement/module.ts
3271
+ function _ts_decorate15(decorators, target, key, desc) {
3272
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3273
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3274
+ 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;
3275
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3276
+ }
3277
+ __name(_ts_decorate15, "_ts_decorate");
3278
+ var EntitlementModule = class _EntitlementModule {
3279
+ static {
3280
+ __name(this, "EntitlementModule");
3281
+ }
3282
+ static forRoot(options) {
3283
+ const providers = [
3284
+ asProvider(SUBSCRIPTION_REPOSITORY_TOKEN, options.subscriptionRepository),
3285
+ asProvider(PLAN_VERSION_REPOSITORY_TOKEN, options.planVersionRepository),
3286
+ asProvider(TRANSACTION_RUNNER_TOKEN, options.transactionRunner),
3287
+ EntitlementService
3288
+ ];
3289
+ if (options.resolutionConfig) {
3290
+ providers.push({
3291
+ provide: ENTITLEMENT_RESOLUTION_CONFIG_TOKEN,
3292
+ useValue: options.resolutionConfig
3293
+ });
3294
+ }
3295
+ if (options.subscriptionContractRepository) {
3296
+ providers.push(asProvider(SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN, options.subscriptionContractRepository));
3297
+ }
3298
+ if (options.subscriptionBundleRepository) {
3299
+ providers.push(asProvider(SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN, options.subscriptionBundleRepository));
3300
+ }
3301
+ if (options.bundleRepository) {
3302
+ providers.push(asProvider(BUNDLE_REPOSITORY_TOKEN, options.bundleRepository));
3303
+ }
3304
+ return {
3305
+ module: _EntitlementModule,
3306
+ global: options.global ?? false,
3307
+ providers,
3308
+ exports: [
3309
+ EntitlementService,
3310
+ SUBSCRIPTION_REPOSITORY_TOKEN
3311
+ ]
3312
+ };
3313
+ }
3314
+ };
3315
+ EntitlementModule = _ts_decorate15([
3316
+ (0, import_common15.Module)({})
3317
+ ], EntitlementModule);
3318
+
3319
+ // src/platform/plan-resolver.port.ts
3320
+ var PLAN_RESOLVER_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-nest/PlanResolverPort");
3321
+ var StaticPlanResolver = class {
3322
+ static {
3323
+ __name(this, "StaticPlanResolver");
3324
+ }
3325
+ planId;
3326
+ constructor(planId) {
3327
+ this.planId = planId;
3328
+ }
3329
+ async getPlanIdForTenant() {
3330
+ return this.planId;
3331
+ }
3332
+ };
3333
+
3334
+ // src/platform/static-entitlement.service.ts
3335
+ var import_common16 = require("@nestjs/common");
3336
+ function _ts_decorate16(decorators, target, key, desc) {
3337
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3338
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3339
+ 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;
3340
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3341
+ }
3342
+ __name(_ts_decorate16, "_ts_decorate");
3343
+ function _ts_metadata11(k, v) {
3344
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
3345
+ }
3346
+ __name(_ts_metadata11, "_ts_metadata");
3347
+ function _ts_param11(paramIndex, decorator) {
3348
+ return function(target, key) {
3349
+ decorator(target, key, paramIndex);
3350
+ };
3351
+ }
3352
+ __name(_ts_param11, "_ts_param");
3353
+ var StaticEntitlementService = class {
3354
+ static {
3355
+ __name(this, "StaticEntitlementService");
3356
+ }
3357
+ catalog;
3358
+ resolver;
3359
+ constructor(catalog, resolver) {
3360
+ this.catalog = catalog;
3361
+ this.resolver = resolver;
3362
+ }
3363
+ /**
3364
+ * Returns the effective features + quotas of a tenant from the
3365
+ * plan catalog. When the plan is missing: an empty snapshot (no feature,
3366
+ * all quotas at 0).
3367
+ */
3368
+ async snapshot(tenantId) {
3369
+ const planId = await this.resolver.getPlanIdForTenant(tenantId);
3370
+ if (!planId) {
3371
+ return {
3372
+ planId: null,
3373
+ features: [],
3374
+ quotas: {}
3375
+ };
3376
+ }
3377
+ const plan = this.findPlan(planId);
3378
+ if (!plan) {
3379
+ return {
3380
+ planId,
3381
+ features: [],
3382
+ quotas: {}
3383
+ };
3384
+ }
3385
+ return {
3386
+ planId,
3387
+ features: plan.features ?? [],
3388
+ quotas: plan.quotas ?? {}
3389
+ };
3390
+ }
3391
+ /** Convenience helper for tests / synchronous checks in the UI manifest. */
3392
+ async hasFeature(tenantId, featureKey) {
3393
+ const snap = await this.snapshot(tenantId);
3394
+ return snap.features.includes(featureKey);
3395
+ }
3396
+ async quotaLimit(tenantId, quotaKey) {
3397
+ const snap = await this.snapshot(tenantId);
3398
+ const value = snap.quotas[quotaKey];
3399
+ return value === void 0 ? null : value;
3400
+ }
3401
+ findPlan(planId) {
3402
+ return (this.catalog.plans ?? []).find((p) => p.id === planId);
3403
+ }
3404
+ };
3405
+ StaticEntitlementService = _ts_decorate16([
3406
+ (0, import_common16.Injectable)(),
3407
+ _ts_param11(0, (0, import_common16.Inject)(PLAN_CATALOG_TOKEN)),
3408
+ _ts_param11(1, (0, import_common16.Inject)(PLAN_RESOLVER_PORT_TOKEN)),
3409
+ _ts_metadata11("design:type", Function),
3410
+ _ts_metadata11("design:paramtypes", [
3411
+ typeof PlanCatalog === "undefined" ? Object : PlanCatalog,
3412
+ typeof PlanResolverPort === "undefined" ? Object : PlanResolverPort
3413
+ ])
3414
+ ], StaticEntitlementService);
3415
+
3416
+ // src/platform/static-feature.guard.ts
3417
+ var import_common18 = require("@nestjs/common");
3418
+ var import_core4 = require("@nestjs/core");
3419
+
3420
+ // src/billing/require-feature.decorator.ts
3421
+ var import_common17 = require("@nestjs/common");
3422
+ var REQUIRE_FEATURE_KEY = "require-feature";
3423
+
3424
+ // src/platform/static-feature.guard.ts
3425
+ function _ts_decorate17(decorators, target, key, desc) {
3426
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3427
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3428
+ 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;
3429
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3430
+ }
3431
+ __name(_ts_decorate17, "_ts_decorate");
3432
+ function _ts_metadata12(k, v) {
3433
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
3434
+ }
3435
+ __name(_ts_metadata12, "_ts_metadata");
3436
+ function _ts_param12(paramIndex, decorator) {
3437
+ return function(target, key) {
3438
+ decorator(target, key, paramIndex);
3439
+ };
3440
+ }
3441
+ __name(_ts_param12, "_ts_param");
3442
+ var STATIC_FEATURE_GUARD_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-nest/StaticFeatureGuardConfig");
3443
+ var StaticFeatureGuard = class {
3444
+ static {
3445
+ __name(this, "StaticFeatureGuard");
3446
+ }
3447
+ reflector;
3448
+ entitlements;
3449
+ config;
3450
+ constructor(reflector, entitlements, config = null) {
3451
+ this.reflector = reflector;
3452
+ this.entitlements = entitlements;
3453
+ this.config = config;
3454
+ }
3455
+ async canActivate(context) {
3456
+ const required = this.reflector.getAllAndOverride(REQUIRE_FEATURE_KEY, [
3457
+ context.getHandler(),
3458
+ context.getClass()
3459
+ ]);
3460
+ if (!required || required.length === 0) return true;
3461
+ const request = context.switchToHttp().getRequest();
3462
+ const user = request.user;
3463
+ if (!user) throw new import_common18.ForbiddenException("Nicht authentifiziert");
3464
+ const role = this.config?.userRoleResolver ? this.config.userRoleResolver(user) : user.role ?? user.platformRole;
3465
+ if (role === "SUPER_ADMIN") return true;
3466
+ const tenantId = this.config?.tenantIdResolver ? this.config.tenantIdResolver(request) : request.tenantId ?? user.tenantId;
3467
+ if (!tenantId) throw new import_common18.ForbiddenException("Kein Mandant zugeordnet");
3468
+ const snap = await this.entitlements.snapshot(tenantId);
3469
+ const allowed = required.some((f) => snap.features.includes(f));
3470
+ if (!allowed) {
3471
+ throw new import_common18.ForbiddenException(`Feature ${required.join(" / ")} nicht im aktuellen Plan enthalten.`);
3472
+ }
3473
+ return true;
3474
+ }
3475
+ };
3476
+ StaticFeatureGuard = _ts_decorate17([
3477
+ (0, import_common18.Injectable)(),
3478
+ _ts_param12(0, (0, import_common18.Inject)(import_core4.Reflector)),
3479
+ _ts_param12(1, (0, import_common18.Inject)(StaticEntitlementService)),
3480
+ _ts_param12(2, (0, import_common18.Optional)()),
3481
+ _ts_param12(2, (0, import_common18.Inject)(STATIC_FEATURE_GUARD_CONFIG_TOKEN)),
3482
+ _ts_metadata12("design:type", Function),
3483
+ _ts_metadata12("design:paramtypes", [
3484
+ typeof import_core4.Reflector === "undefined" ? Object : import_core4.Reflector,
3485
+ typeof StaticEntitlementService === "undefined" ? Object : StaticEntitlementService,
3486
+ Object
3487
+ ])
3488
+ ], StaticFeatureGuard);
3489
+
3490
+ // src/platform/enforce-quota.interceptor.ts
3491
+ var import_common19 = require("@nestjs/common");
3492
+ var import_core5 = require("@nestjs/core");
3493
+ var import_rxjs2 = require("rxjs");
3494
+ var import_operators2 = require("rxjs/operators");
3495
+ function _ts_decorate18(decorators, target, key, desc) {
3496
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3497
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3498
+ 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;
3499
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3500
+ }
3501
+ __name(_ts_decorate18, "_ts_decorate");
3502
+ function _ts_metadata13(k, v) {
3503
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
3504
+ }
3505
+ __name(_ts_metadata13, "_ts_metadata");
3506
+ function _ts_param13(paramIndex, decorator) {
3507
+ return function(target, key) {
3508
+ decorator(target, key, paramIndex);
3509
+ };
3510
+ }
3511
+ __name(_ts_param13, "_ts_param");
3512
+ var QUOTA_PROVIDERS_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-nest/QuotaProviders");
3513
+ var EnforceQuotaInterceptor = class {
3514
+ static {
3515
+ __name(this, "EnforceQuotaInterceptor");
3516
+ }
3517
+ reflector;
3518
+ entitlements;
3519
+ providers;
3520
+ constructor(reflector, entitlements, providers = null) {
3521
+ this.reflector = reflector;
3522
+ this.entitlements = entitlements;
3523
+ this.providers = providers;
3524
+ }
3525
+ intercept(context, next) {
3526
+ const meta = this.reflector.getAllAndOverride(ENFORCE_QUOTA_KEY, [
3527
+ context.getHandler(),
3528
+ context.getClass()
3529
+ ]);
3530
+ if (!meta?.quotaKey) return next.handle();
3531
+ return (0, import_rxjs2.from)(this.check(context, meta)).pipe((0, import_operators2.switchMap)(() => next.handle()));
3532
+ }
3533
+ async check(context, meta) {
3534
+ const request = context.switchToHttp().getRequest();
3535
+ const user = request.user;
3536
+ if (!user) return;
3537
+ const role = user.role ?? user.platformRole;
3538
+ if (role === "SUPER_ADMIN") return;
3539
+ const tenantId = request.tenantId ?? user.tenantId;
3540
+ if (!tenantId) return;
3541
+ const provider = (this.providers ?? []).find((p) => p.key === meta.quotaKey);
3542
+ if (!provider) {
3543
+ return;
3544
+ }
3545
+ const limit = await this.entitlements.quotaLimit(tenantId, meta.quotaKey);
3546
+ if (limit === null || limit === -1) return;
3547
+ const current = await provider.count(tenantId);
3548
+ const delta = meta.incrementBy ?? 1;
3549
+ if (current + delta > limit) {
3550
+ throw new LimitExceededError(meta.quotaKey, limit, current);
3551
+ }
3552
+ }
3553
+ };
3554
+ EnforceQuotaInterceptor = _ts_decorate18([
3555
+ (0, import_common19.Injectable)(),
3556
+ _ts_param13(0, (0, import_common19.Inject)(import_core5.Reflector)),
3557
+ _ts_param13(1, (0, import_common19.Inject)(StaticEntitlementService)),
3558
+ _ts_param13(2, (0, import_common19.Optional)()),
3559
+ _ts_param13(2, (0, import_common19.Inject)(QUOTA_PROVIDERS_TOKEN)),
3560
+ _ts_metadata13("design:type", Function),
3561
+ _ts_metadata13("design:paramtypes", [
3562
+ typeof import_core5.Reflector === "undefined" ? Object : import_core5.Reflector,
3563
+ typeof StaticEntitlementService === "undefined" ? Object : StaticEntitlementService,
3564
+ Object
3565
+ ])
3566
+ ], EnforceQuotaInterceptor);
3567
+
3568
+ // src/platform/tenant-manifest.service.ts
3569
+ var import_common20 = require("@nestjs/common");
3570
+ function _ts_decorate19(decorators, target, key, desc) {
3571
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3572
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3573
+ 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;
3574
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3575
+ }
3576
+ __name(_ts_decorate19, "_ts_decorate");
3577
+ function _ts_metadata14(k, v) {
3578
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
3579
+ }
3580
+ __name(_ts_metadata14, "_ts_metadata");
3581
+ var TenantManifestService = class {
3582
+ static {
3583
+ __name(this, "TenantManifestService");
3584
+ }
3585
+ entitlements;
3586
+ navItems = [];
3587
+ constructor(entitlements) {
3588
+ this.entitlements = entitlements;
3589
+ }
3590
+ /**
3591
+ * Call in the app's `OnModuleInit`: registers app navigation for the
3592
+ * Tenant UI. Idempotent: the same `id` overwrites.
3593
+ */
3594
+ registerNavItem(item) {
3595
+ const existing = this.navItems.findIndex((i) => i.id === item.id);
3596
+ if (existing >= 0) this.navItems[existing] = item;
3597
+ else this.navItems.push(item);
3598
+ }
3599
+ /** Bulk variant. */
3600
+ registerNavItems(items) {
3601
+ for (const item of items) this.registerNavItem(item);
3602
+ }
3603
+ /**
3604
+ * Returns the filtered manifest for a Tenant. NavItems with
3605
+ * `requiresFeature` are filtered out server-side as soon as none of the
3606
+ * required features are present in the entitlement snapshot.
3607
+ */
3608
+ async getManifest(tenantId) {
3609
+ const snap = await this.entitlements.snapshot(tenantId);
3610
+ const featureSet = new Set(snap.features);
3611
+ const visible = this.navItems.filter((item) => isVisible(item, featureSet)).map((item) => ({
3612
+ ...item
3613
+ })).sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
3614
+ return {
3615
+ schemaVersion: 1,
3616
+ tenant: {
3617
+ id: tenantId
3618
+ },
3619
+ planId: snap.planId,
3620
+ features: snap.features,
3621
+ quotas: snap.quotas,
3622
+ navigation: visible
3623
+ };
3624
+ }
3625
+ };
3626
+ TenantManifestService = _ts_decorate19([
3627
+ (0, import_common20.Injectable)(),
3628
+ _ts_metadata14("design:type", Function),
3629
+ _ts_metadata14("design:paramtypes", [
3630
+ typeof StaticEntitlementService === "undefined" ? Object : StaticEntitlementService
3631
+ ])
3632
+ ], TenantManifestService);
3633
+ function isVisible(item, features) {
3634
+ if (!item.requiresFeature) return true;
3635
+ const required = Array.isArray(item.requiresFeature) ? item.requiresFeature : [
3636
+ item.requiresFeature
3637
+ ];
3638
+ return required.some((f) => features.has(f));
3639
+ }
3640
+ __name(isVisible, "isVisible");
3641
+
3642
+ // src/platform/tenant-manifest.controller.ts
3643
+ var import_common21 = require("@nestjs/common");
3644
+ function _ts_decorate20(decorators, target, key, desc) {
3645
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3646
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3647
+ 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;
3648
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3649
+ }
3650
+ __name(_ts_decorate20, "_ts_decorate");
3651
+ function _ts_metadata15(k, v) {
3652
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
3653
+ }
3654
+ __name(_ts_metadata15, "_ts_metadata");
3655
+ function _ts_param14(paramIndex, decorator) {
3656
+ return function(target, key) {
3657
+ decorator(target, key, paramIndex);
3658
+ };
3659
+ }
3660
+ __name(_ts_param14, "_ts_param");
3661
+ function buildTenantManifestController(options) {
3662
+ const path = options.path ?? "tenant/manifest";
3663
+ let GeneratedTenantManifestController = class GeneratedTenantManifestController {
3664
+ static {
3665
+ __name(this, "GeneratedTenantManifestController");
3666
+ }
3667
+ svc;
3668
+ constructor(svc) {
3669
+ this.svc = svc;
3670
+ }
3671
+ async get(req) {
3672
+ const tenantId = req.user?.tenantId ?? req.tenantId;
3673
+ if (!tenantId) {
3674
+ throw new Error("Kein Mandant im Request \u2014 TenantManifest braucht user.tenantId.");
3675
+ }
3676
+ return this.svc.getManifest(tenantId);
3677
+ }
3678
+ };
3679
+ _ts_decorate20([
3680
+ (0, import_common21.Get)(),
3681
+ _ts_param14(0, (0, import_common21.Req)()),
3682
+ _ts_metadata15("design:type", Function),
3683
+ _ts_metadata15("design:paramtypes", [
3684
+ typeof RequestWithUser === "undefined" ? Object : RequestWithUser
3685
+ ]),
3686
+ _ts_metadata15("design:returntype", Promise)
3687
+ ], GeneratedTenantManifestController.prototype, "get", null);
3688
+ GeneratedTenantManifestController = _ts_decorate20([
3689
+ (0, import_common21.Controller)(path),
3690
+ (0, import_common21.UseGuards)(...options.guards),
3691
+ _ts_param14(0, (0, import_common21.Inject)(TenantManifestService)),
3692
+ _ts_metadata15("design:type", Function),
3693
+ _ts_metadata15("design:paramtypes", [
3694
+ typeof TenantManifestService === "undefined" ? Object : TenantManifestService
3695
+ ])
3696
+ ], GeneratedTenantManifestController);
3697
+ return GeneratedTenantManifestController;
3698
+ }
3699
+ __name(buildTenantManifestController, "buildTenantManifestController");
3700
+
3701
+ // src/platform/saas-platform.module.ts
3702
+ function _ts_decorate21(decorators, target, key, desc) {
3703
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3704
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3705
+ 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;
3706
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3707
+ }
3708
+ __name(_ts_decorate21, "_ts_decorate");
3709
+ function buildMinimalManifestConfig() {
3710
+ return {
3711
+ useFactory: /* @__PURE__ */ __name((catalog) => ({
3712
+ project: {
3713
+ key: catalog.projectKey,
3714
+ displayName: catalog.app?.name ?? catalog.projectKey,
3715
+ label: catalog.app?.label,
3716
+ icon: catalog.app?.icon,
3717
+ logoUrl: catalog.app?.logoUrl,
3718
+ environment: process.env.NODE_ENV === "production" ? "production" : "development",
3719
+ availableLocales: catalog.marketing?.availableLocales,
3720
+ defaultLocale: catalog.marketing?.availableLocales?.[0]
3721
+ },
3722
+ build: {
3723
+ platformPackageVersion: "0.0.0",
3724
+ appVersion: "0.0.0"
3725
+ },
3726
+ planCatalogSnapshot: {
3727
+ source: "saas-platform-module",
3728
+ hash: "sha256-quickstart",
3729
+ currency: catalog.currency,
3730
+ vatRate: catalog.vatRate,
3731
+ plans: catalog.plans ?? [],
3732
+ features: catalog.features ?? []
3733
+ }
3734
+ }), "useFactory"),
3735
+ inject: [
3736
+ PLAN_CATALOG_TOKEN
3737
+ ]
3738
+ };
3739
+ }
3740
+ __name(buildMinimalManifestConfig, "buildMinimalManifestConfig");
3741
+ var SaasPlatformModule = class _SaasPlatformModule {
3742
+ static {
3743
+ __name(this, "SaasPlatformModule");
3744
+ }
3745
+ static forRoot(options) {
3746
+ if (!options.planCatalog && !options.adapters.planCatalogReadSink) {
3747
+ throw new Error("SaasPlatformModule.forRoot: entweder `planCatalog` (Quickstart) oder `adapters.planCatalogReadSink` (DB-Hydration) muss gesetzt sein.");
3748
+ }
3749
+ if (options.entitlement) {
3750
+ const missing = [];
3751
+ if (!options.adapters.subscriptionRepository) missing.push("subscriptionRepository");
3752
+ if (!options.adapters.planVersionRepository) missing.push("planVersionRepository");
3753
+ if (!options.adapters.transactionRunner) missing.push("transactionRunner");
3754
+ if (missing.length) {
3755
+ throw new Error(`SaasPlatformModule.forRoot: entitlement aktiv, aber Adapter fehlen: ${missing.join(", ")}`);
3756
+ }
3757
+ }
3758
+ const planCatalogModule = options.planCatalog ? PlanCatalogModule.forRootWithCatalog(options.planCatalog, {
3759
+ global: true
3760
+ }) : PlanCatalogModule.forRoot({
3761
+ projectKey: "",
3762
+ currency: "",
3763
+ vatRate: 0,
3764
+ sink: options.adapters.planCatalogReadSink,
3765
+ imports: options.imports
3766
+ });
3767
+ const appInfo = options.app ?? {
3768
+ key: options.planCatalog?.projectKey ?? "app",
3769
+ version: options.planCatalog?.app?.version ?? "0.0.0"
3770
+ };
3771
+ const imports = [
3772
+ planCatalogModule,
3773
+ DiscoveryModule.forRoot({
3774
+ app: appInfo,
3775
+ controller: {
3776
+ guards: options.controller.guards
3777
+ },
3778
+ imports: options.imports,
3779
+ snapshotPath: options.discoverySnapshotPath === void 0 ? "var/discovery-snapshot.json" : options.discoverySnapshotPath
3780
+ }),
3781
+ AdminModule.forRoot({
3782
+ mfaPort: options.adapters.mfa,
3783
+ auditPort: options.adapters.audit,
3784
+ rlsBypassPort: options.adapters.rlsBypass,
3785
+ global: true
3786
+ }),
3787
+ AdminManifestModule.forRoot({
3788
+ config: options.adminManifestConfig ?? buildMinimalManifestConfig(),
3789
+ guards: options.controller.guards,
3790
+ reloadGuards: options.reloadGuards
3791
+ })
3792
+ ];
3793
+ if (options.entitlement) {
3794
+ imports.push(EntitlementModule.forRoot({
3795
+ subscriptionRepository: options.adapters.subscriptionRepository,
3796
+ planVersionRepository: options.adapters.planVersionRepository,
3797
+ transactionRunner: options.adapters.transactionRunner,
3798
+ resolutionConfig: options.entitlement.resolutionConfig
3799
+ }));
3800
+ }
3801
+ const lightweightProviders = [];
3802
+ const lightweightExports = [];
3803
+ const hasResolver = !!options.adapters.planResolver;
3804
+ const hasFallback = !!options.defaultPlanId;
3805
+ if (hasResolver || hasFallback) {
3806
+ lightweightProviders.push(hasResolver ? asProvider(PLAN_RESOLVER_PORT_TOKEN, options.adapters.planResolver) : {
3807
+ provide: PLAN_RESOLVER_PORT_TOKEN,
3808
+ useValue: new StaticPlanResolver(options.defaultPlanId)
3809
+ }, StaticEntitlementService, StaticFeatureGuard, EnforceQuotaInterceptor, ...options.quotaProviders ?? [], {
3810
+ provide: QUOTA_PROVIDERS_TOKEN,
3811
+ useFactory: /* @__PURE__ */ __name((...providers) => providers, "useFactory"),
3812
+ inject: options.quotaProviders ?? []
3813
+ }, {
3814
+ provide: import_core6.APP_GUARD,
3815
+ useExisting: StaticFeatureGuard
3816
+ }, {
3817
+ provide: import_core6.APP_INTERCEPTOR,
3818
+ useExisting: EnforceQuotaInterceptor
3819
+ });
3820
+ lightweightExports.push(PLAN_RESOLVER_PORT_TOKEN, StaticEntitlementService, StaticFeatureGuard, EnforceQuotaInterceptor, QUOTA_PROVIDERS_TOKEN);
3821
+ }
3822
+ const tenantControllers = [];
3823
+ if (options.tenantManifest) {
3824
+ if (!hasResolver && !hasFallback) {
3825
+ throw new Error("SaasPlatformModule.forRoot: tenantManifest erfordert defaultPlanId oder adapters.planResolver, sonst kann das Manifest keine Features aufl\xF6sen.");
3826
+ }
3827
+ lightweightProviders.push(TenantManifestService);
3828
+ lightweightExports.push(TenantManifestService);
3829
+ tenantControllers.push(buildTenantManifestController(options.tenantManifest));
3830
+ }
3831
+ return {
3832
+ module: _SaasPlatformModule,
3833
+ imports,
3834
+ providers: lightweightProviders,
3835
+ controllers: tenantControllers,
3836
+ exports: [
3837
+ PlanCatalogModule,
3838
+ DiscoveryModule,
3839
+ AdminModule,
3840
+ AdminManifestModule,
3841
+ ...options.entitlement ? [
3842
+ EntitlementModule
3843
+ ] : [],
3844
+ ADMIN_MANIFEST_CONFIG,
3845
+ ...lightweightExports
3846
+ ],
3847
+ global: true
3848
+ };
3849
+ }
3850
+ };
3851
+ SaasPlatformModule = _ts_decorate21([
3852
+ (0, import_common22.Module)({})
3853
+ ], SaasPlatformModule);
3854
+
3855
+ // src/testing/create-saas-platform-test-module.ts
3856
+ function _ts_decorate22(decorators, target, key, desc) {
3857
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3858
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3859
+ 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;
3860
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3861
+ }
3862
+ __name(_ts_decorate22, "_ts_decorate");
3863
+ var StubMfaPort = class {
3864
+ static {
3865
+ __name(this, "StubMfaPort");
3866
+ }
3867
+ secrets = /* @__PURE__ */ new Map();
3868
+ async getSecret(userId) {
3869
+ return this.secrets.get(userId) ?? null;
3870
+ }
3871
+ async setSecret(userId, secret) {
3872
+ if (secret === null) this.secrets.delete(userId);
3873
+ else this.secrets.set(userId, secret);
3874
+ }
3875
+ async isEnabled(userId) {
3876
+ return this.secrets.has(userId);
3877
+ }
3878
+ };
3879
+ var StubAuditPort = class {
3880
+ static {
3881
+ __name(this, "StubAuditPort");
3882
+ }
3883
+ calls = [];
3884
+ async write(input) {
3885
+ this.calls.push(input);
3886
+ }
3887
+ };
3888
+ var StubRlsBypassPort = class {
3889
+ static {
3890
+ __name(this, "StubRlsBypassPort");
3891
+ }
3892
+ async runWithBypass(fn) {
3893
+ return fn();
3894
+ }
3895
+ };
3896
+ function createSaasPlatformTestModule(options) {
3897
+ const defaultPlanId = options.defaultPlanId ?? options.planCatalog.plans?.[0]?.id;
3898
+ let SaasPlatformTestHost = class SaasPlatformTestHost {
3899
+ static {
3900
+ __name(this, "SaasPlatformTestHost");
3901
+ }
3902
+ };
3903
+ SaasPlatformTestHost = _ts_decorate22([
3904
+ (0, import_common23.Module)({
3905
+ imports: [
3906
+ SaasPlatformModule.forRoot({
3907
+ planCatalog: options.planCatalog,
3908
+ controller: {
3909
+ guards: []
3910
+ },
3911
+ adapters: {
3912
+ mfa: options.overrides?.mfa ?? new StubMfaPort(),
3913
+ audit: options.overrides?.audit ?? new StubAuditPort(),
3914
+ rlsBypass: options.overrides?.rlsBypass ?? new StubRlsBypassPort()
3915
+ },
3916
+ defaultPlanId,
3917
+ quotaProviders: options.quotaProviders
3918
+ })
3919
+ ]
3920
+ })
3921
+ ], SaasPlatformTestHost);
3922
+ return {
3923
+ module: SaasPlatformTestHost
3924
+ };
3925
+ }
3926
+ __name(createSaasPlatformTestModule, "createSaasPlatformTestModule");
3927
+ // Annotate the CommonJS export names for ESM import in node:
3928
+ 0 && (module.exports = {
3929
+ FakeBundleRepository,
3930
+ FakeBusinessTypeRepository,
3931
+ FakeMarketingProjectionRepository,
3932
+ FakePlanRepository,
3933
+ FakePlanVersionRepository,
3934
+ FakeSubscriptionBundleRepository,
3935
+ FakeSubscriptionContractRepository,
3936
+ FakeSubscriptionRepository,
3937
+ FakeTransactionRunner,
3938
+ StubAuditPort,
3939
+ StubMfaPort,
3940
+ StubRlsBypassPort,
3941
+ createSaasPlatformTestModule
3942
+ });