@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,3367 @@
1
+ import {
2
+ PromoCodesService
3
+ } from "./chunk-ZL4LLE2Z.js";
4
+ import {
5
+ SubscriptionContractService
6
+ } from "./chunk-2PNX2QL2.js";
7
+ import {
8
+ AUDIT_CONTEXT_RESOLVER_TOKEN,
9
+ PENDING_PLAN_QUERY_PORT_TOKEN,
10
+ SUBSCRIPTION_USAGE_PORT_TOKEN,
11
+ SUBSCRIPTION_WRITE_PORT_TOKEN,
12
+ TENANT_AUTH_GUARDS_TOKEN,
13
+ TENANT_ID_RESOLVER_TOKEN,
14
+ TRIAL_PROJECTION_PORT_TOKEN,
15
+ USAGE_SNAPSHOT_PORT_TOKEN,
16
+ USER_EMAIL_RESOLVER_TOKEN,
17
+ USER_ID_RESOLVER_TOKEN
18
+ } from "./chunk-WPUVJRIN.js";
19
+ import {
20
+ REQUIRE_FEATURE_KEY
21
+ } from "./chunk-Q53N43LQ.js";
22
+ import {
23
+ AdminAuditService
24
+ } from "./chunk-E56W4U2P.js";
25
+ import {
26
+ EntitlementService,
27
+ LimitExceededError,
28
+ SUBSCRIPTION_BUNDLE_CONFIG_TOKEN,
29
+ SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN,
30
+ toEffectiveLimitsSnapshot
31
+ } from "./chunk-YXGHPILG.js";
32
+ import {
33
+ PLAN_CATALOG_TOKEN,
34
+ findPlan,
35
+ getMarketedPlans,
36
+ getPlanPriceNet
37
+ } from "./chunk-MV6LW5UB.js";
38
+ import {
39
+ SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN
40
+ } from "./chunk-NYLON2VC.js";
41
+ import {
42
+ BUNDLE_REPOSITORY_TOKEN,
43
+ CATALOG_ENTRY_REPOSITORY_TOKEN,
44
+ PLAN_REPOSITORY_TOKEN
45
+ } from "./chunk-XCZ3TTFK.js";
46
+ import {
47
+ asProvider
48
+ } from "./chunk-SABTXESR.js";
49
+ import {
50
+ __name
51
+ } from "./chunk-SHUYVCID.js";
52
+
53
+ // src/billing/billing-period.ts
54
+ function advanceOneCycle(d, cycle) {
55
+ const out = new Date(d);
56
+ if (cycle === "YEARLY") {
57
+ out.setUTCFullYear(out.getUTCFullYear() + 1);
58
+ } else {
59
+ out.setUTCMonth(out.getUTCMonth() + 1);
60
+ }
61
+ return out;
62
+ }
63
+ __name(advanceOneCycle, "advanceOneCycle");
64
+ function periodEndAfter(startedAt, cycle, after) {
65
+ let candidate = new Date(startedAt ?? after);
66
+ while (candidate <= after) {
67
+ candidate = advanceOneCycle(candidate, cycle);
68
+ }
69
+ return candidate;
70
+ }
71
+ __name(periodEndAfter, "periodEndAfter");
72
+ function initialPeriodWindow(startedAt, cycle) {
73
+ return {
74
+ start: startedAt,
75
+ end: advanceOneCycle(startedAt, cycle)
76
+ };
77
+ }
78
+ __name(initialPeriodWindow, "initialPeriodWindow");
79
+ function periodEndWithMinLead(startedAt, cycle, now, minLeadDays = 42) {
80
+ const minLeadMs = minLeadDays * 864e5;
81
+ let candidate = periodEndAfter(startedAt, cycle, now);
82
+ while (candidate.getTime() - now.getTime() < minLeadMs) {
83
+ candidate = advanceOneCycle(candidate, cycle);
84
+ }
85
+ return candidate;
86
+ }
87
+ __name(periodEndWithMinLead, "periodEndWithMinLead");
88
+
89
+ // src/billing/version-publish.ts
90
+ var PublishValidationError = class extends Error {
91
+ static {
92
+ __name(this, "PublishValidationError");
93
+ }
94
+ code;
95
+ constructor(code, message) {
96
+ super(message), this.code = code;
97
+ this.name = "PublishValidationError";
98
+ }
99
+ };
100
+ function assertChangeNote(changeNote) {
101
+ const trimmed = (changeNote ?? "").trim();
102
+ if (trimmed.length === 0) {
103
+ throw new PublishValidationError("CHANGE_NOTE_REQUIRED", "changeNote ist Pflicht beim Ver\xF6ffentlichen einer Version.");
104
+ }
105
+ return trimmed;
106
+ }
107
+ __name(assertChangeNote, "assertChangeNote");
108
+ function assertDraftPublishable(draft, draftId) {
109
+ if (!draft) {
110
+ throw new PublishValidationError("NOT_FOUND", `PlanVersion ${draftId} nicht gefunden`);
111
+ }
112
+ if (draft.publishedAt !== null) {
113
+ throw new PublishValidationError("ALREADY_PUBLISHED", `PlanVersion ${draftId} ist bereits ver\xF6ffentlicht.`);
114
+ }
115
+ if (!draft.baseVersionId) {
116
+ throw new PublishValidationError("NO_BASE_VERSION", `Draft ${draftId} hat keine baseVersion \u2014 wahrscheinlich manuell angelegtes v1, das nur per Migration ver\xF6ffentlicht werden darf.`);
117
+ }
118
+ }
119
+ __name(assertDraftPublishable, "assertDraftPublishable");
120
+ function assertBaseVersionFresh(base, baseId, planContext) {
121
+ if (!base) {
122
+ throw new PublishValidationError("BASE_NOT_FOUND", `baseVersion ${baseId} nicht mehr vorhanden.`);
123
+ }
124
+ if (base.supersededAt !== null) {
125
+ throw new PublishValidationError("BASE_SUPERSEDED", `Draft basiert auf einer abgel\xF6sten Version (${planContext}). Bitte den Draft gegen die aktuelle Live-Version rebasen oder neu anlegen.`);
126
+ }
127
+ }
128
+ __name(assertBaseVersionFresh, "assertBaseVersionFresh");
129
+ function assertOptimisticLockHeld(updateCount, draftId) {
130
+ if (updateCount !== 1) {
131
+ throw new PublishValidationError("OPTIMISTIC_LOCK_CONFLICT", `Draft ${draftId} wurde zwischenzeitlich von einem anderen Admin ver\xF6ffentlicht.`);
132
+ }
133
+ }
134
+ __name(assertOptimisticLockHeld, "assertOptimisticLockHeld");
135
+
136
+ // src/billing/version-renewal.ts
137
+ function decideRenewal(sub, now) {
138
+ if (!sub.pendingPlanVersionId || !sub.pendingPlanVersionEffectiveAt) return "SKIP";
139
+ if (sub.pendingPlanVersionEffectiveAt > now) return "SKIP";
140
+ if (sub.pendingPlanVersionNonRegressive || sub.pendingPlanVersionAccepted) {
141
+ return "ROLL_FORWARD";
142
+ }
143
+ return "CLEAR_PENDING";
144
+ }
145
+ __name(decideRenewal, "decideRenewal");
146
+ function clearPendingPlanVersionFields() {
147
+ return {
148
+ pendingPlanVersionId: null,
149
+ pendingPlanVersionEffectiveAt: null,
150
+ pendingPlanVersionAccepted: false,
151
+ pendingPlanVersionAcceptedAt: null,
152
+ pendingPlanVersionAcceptedByUserId: null,
153
+ pendingPlanVersionNotifiedAt: null,
154
+ pendingPlanVersionReminderSentAt: null
155
+ };
156
+ }
157
+ __name(clearPendingPlanVersionFields, "clearPendingPlanVersionFields");
158
+ function computeNextPeriod(sub, now) {
159
+ if (sub.canceledAt !== null) return null;
160
+ if (sub.currentPeriodEnd === null) return null;
161
+ if (sub.currentPeriodEnd > now) return null;
162
+ const newStart = sub.currentPeriodEnd;
163
+ const newEnd = periodEndAfter(newStart, sub.billingCycle, now);
164
+ return {
165
+ currentPeriodStart: newStart,
166
+ currentPeriodEnd: newEnd
167
+ };
168
+ }
169
+ __name(computeNextPeriod, "computeNextPeriod");
170
+
171
+ // src/billing/feature-guard.tokens.ts
172
+ var FEATURE_GUARD_CONFIG_TOKEN = /* @__PURE__ */ Symbol("FEATURE_GUARD_CONFIG");
173
+
174
+ // src/billing/feature.guard.ts
175
+ import { ForbiddenException, Inject, Injectable, Logger, Optional } from "@nestjs/common";
176
+ import { Reflector } from "@nestjs/core";
177
+ import { FEATURE_NOT_LICENSED } from "@saasicat/types";
178
+
179
+ // src/billing/upsell.tokens.ts
180
+ var UPSELL_OFFER_RESOLVER_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/UpsellOfferResolver");
181
+ var UPSELL_OFFER_CURRENCY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/UpsellOfferCurrency");
182
+
183
+ // src/billing/feature.guard.ts
184
+ function _ts_decorate(decorators, target, key, desc) {
185
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
186
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
187
+ 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;
188
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
189
+ }
190
+ __name(_ts_decorate, "_ts_decorate");
191
+ function _ts_metadata(k, v) {
192
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
193
+ }
194
+ __name(_ts_metadata, "_ts_metadata");
195
+ function _ts_param(paramIndex, decorator) {
196
+ return function(target, key) {
197
+ decorator(target, key, paramIndex);
198
+ };
199
+ }
200
+ __name(_ts_param, "_ts_param");
201
+ var FeatureGuard = class _FeatureGuard {
202
+ static {
203
+ __name(this, "FeatureGuard");
204
+ }
205
+ reflector;
206
+ entitlements;
207
+ config;
208
+ upsellResolver;
209
+ logger = new Logger(_FeatureGuard.name);
210
+ constructor(reflector, entitlements, config = null, upsellResolver = null) {
211
+ this.reflector = reflector;
212
+ this.entitlements = entitlements;
213
+ this.config = config;
214
+ this.upsellResolver = upsellResolver;
215
+ }
216
+ async canActivate(context) {
217
+ const required = this.reflector.getAllAndOverride(REQUIRE_FEATURE_KEY, [
218
+ context.getHandler(),
219
+ context.getClass()
220
+ ]);
221
+ if (!required || required.length === 0) return true;
222
+ const request = context.switchToHttp().getRequest();
223
+ const user = request.user;
224
+ if (!user) throw new ForbiddenException("Nicht authentifiziert");
225
+ const role = this.config?.userRoleResolver ? this.config.userRoleResolver(user) : user.role ?? user.platformRole;
226
+ if (role === "SUPER_ADMIN") return true;
227
+ const tenantId = this.config?.tenantIdResolver ? this.config.tenantIdResolver(request) : request.tenantId ?? user.tenantId;
228
+ if (!tenantId) throw new ForbiddenException("Kein Mandant zugeordnet");
229
+ const compute = /* @__PURE__ */ __name(() => this.entitlements.computeLimits(tenantId), "compute");
230
+ const limits = this.config?.tenantContextRunner ? await this.config.tenantContextRunner(tenantId, compute) : await compute();
231
+ const allowed = required.some((f) => limits.features.has(f));
232
+ if (!allowed) {
233
+ throw await this.buildNotLicensedException(required, tenantId);
234
+ }
235
+ return true;
236
+ }
237
+ /**
238
+ * Upsell response (#36): with a registered `UpsellOfferResolver` the 403
239
+ * becomes machine-readable (`FeatureNotLicensedBody`), so consumer UIs can
240
+ * render a purchase offer. Without a resolver the previous plain 403
241
+ * remains — no breaking change.
242
+ *
243
+ * Deliberately 403 + `code` field instead of 402 — rationale in
244
+ * `@saasicat/types` upsell.types.ts (402 is reserved / inconsistently
245
+ * supported; SPA interceptors must not treat the 403 as an auth error, the
246
+ * distinction runs through `code`).
247
+ */
248
+ async buildNotLicensedException(required, tenantId) {
249
+ const message = `Feature ${required.join(" / ")} nicht im aktuellen Paket enthalten.`;
250
+ if (!this.upsellResolver) return new ForbiddenException(message);
251
+ const body = {
252
+ code: FEATURE_NOT_LICENSED,
253
+ featureKey: required[0],
254
+ featureKeys: required,
255
+ offers: await this.resolveOffersSafe(required, tenantId),
256
+ message
257
+ };
258
+ return new ForbiddenException(body);
259
+ }
260
+ /**
261
+ * A resolver error must not turn the semantically correct 403 into a 500 —
262
+ * degrade to empty offers and log.
263
+ */
264
+ async resolveOffersSafe(required, tenantId) {
265
+ try {
266
+ return await this.upsellResolver.resolveOffers(required, tenantId);
267
+ } catch (error) {
268
+ this.logger.warn(`UpsellOfferResolver fehlgeschlagen f\xFCr [${required.join(", ")}]: ${String(error)}`);
269
+ return [];
270
+ }
271
+ }
272
+ };
273
+ FeatureGuard = _ts_decorate([
274
+ Injectable(),
275
+ _ts_param(0, Inject(Reflector)),
276
+ _ts_param(1, Inject(EntitlementService)),
277
+ _ts_param(2, Optional()),
278
+ _ts_param(2, Inject(FEATURE_GUARD_CONFIG_TOKEN)),
279
+ _ts_param(3, Optional()),
280
+ _ts_param(3, Inject(UPSELL_OFFER_RESOLVER_TOKEN)),
281
+ _ts_metadata("design:type", Function),
282
+ _ts_metadata("design:paramtypes", [
283
+ typeof Reflector === "undefined" ? Object : Reflector,
284
+ typeof EntitlementService === "undefined" ? Object : EntitlementService,
285
+ Object,
286
+ Object
287
+ ])
288
+ ], FeatureGuard);
289
+
290
+ // src/billing/catalog-bundle-upsell-resolver.ts
291
+ import { Inject as Inject2, Injectable as Injectable2, Optional as Optional2 } from "@nestjs/common";
292
+ import { buildFeatureRequiresIndex, collectUnsatisfiedRequires } from "@saasicat/types";
293
+
294
+ // src/billing/public-catalog.tokens.ts
295
+ var PUBLIC_CATALOG_PROJECT_KEY_TOKEN = /* @__PURE__ */ Symbol("PUBLIC_CATALOG_PROJECT_KEY");
296
+ var PUBLIC_CATALOG_BUNDLE_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol("PUBLIC_CATALOG_BUNDLE_REPOSITORY");
297
+ var PUBLIC_CATALOG_BUSINESS_TYPE_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol("PUBLIC_CATALOG_BUSINESS_TYPE_REPOSITORY");
298
+ var PUBLIC_CATALOG_MARKETING_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol("PUBLIC_CATALOG_MARKETING_REPOSITORY");
299
+ var PUBLIC_CATALOG_CATALOG_ENTRY_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol("PUBLIC_CATALOG_CATALOG_ENTRY_REPOSITORY");
300
+
301
+ // src/billing/catalog-bundle-upsell-resolver.ts
302
+ function _ts_decorate2(decorators, target, key, desc) {
303
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
304
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
305
+ 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;
306
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
307
+ }
308
+ __name(_ts_decorate2, "_ts_decorate");
309
+ function _ts_metadata2(k, v) {
310
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
311
+ }
312
+ __name(_ts_metadata2, "_ts_metadata");
313
+ function _ts_param2(paramIndex, decorator) {
314
+ return function(target, key) {
315
+ decorator(target, key, paramIndex);
316
+ };
317
+ }
318
+ __name(_ts_param2, "_ts_param");
319
+ var DEFAULT_CURRENCY = "EUR";
320
+ var CatalogBundleUpsellResolver = class {
321
+ static {
322
+ __name(this, "CatalogBundleUpsellResolver");
323
+ }
324
+ bundleRepo;
325
+ projectKey;
326
+ catalogEntryRepo;
327
+ currency;
328
+ constructor(bundleRepo, projectKey, catalogEntryRepo = null, currency = null) {
329
+ this.bundleRepo = bundleRepo;
330
+ this.projectKey = projectKey;
331
+ this.catalogEntryRepo = catalogEntryRepo;
332
+ this.currency = currency;
333
+ }
334
+ async resolveOffers(featureKeys, tenantId) {
335
+ void tenantId;
336
+ if (featureKeys.length === 0) return [];
337
+ const missing = new Set(featureKeys);
338
+ const wanted = /* @__PURE__ */ new Set([
339
+ ...featureKeys,
340
+ ...await this.lookupUnmetRequires(featureKeys)
341
+ ]);
342
+ const ranked = [];
343
+ for (const live of await this.listLiveMarketedBundles()) {
344
+ const features = live.features;
345
+ if (!features.some((key) => missing.has(key))) continue;
346
+ ranked.push({
347
+ offer: this.toOffer(live),
348
+ coverage: features.filter((key) => wanted.has(key)).length
349
+ });
350
+ }
351
+ return ranked.sort(compareRankedOffers).map((r) => r.offer);
352
+ }
353
+ /**
354
+ * Uncovered dependencies of the missing features (#35) — feed into the
355
+ * coverage ranking. Without a CatalogEntryRepository: empty (ranking
356
+ * degrades to price).
357
+ */
358
+ async lookupUnmetRequires(featureKeys) {
359
+ if (!this.catalogEntryRepo) return [];
360
+ const rows = await this.catalogEntryRepo.listFeatures({
361
+ projectKey: this.projectKey
362
+ });
363
+ return collectUnsatisfiedRequires(featureKeys, buildFeatureRequiresIndex(rows));
364
+ }
365
+ /** Only published-and-live AND marketed — non-marketed bundles are not an offer. */
366
+ async listLiveMarketedBundles() {
367
+ const stems = await this.bundleRepo.list({
368
+ projectKey: this.projectKey,
369
+ excludeDeleted: true
370
+ });
371
+ const out = [];
372
+ for (const stem of stems) {
373
+ const live = await this.bundleRepo.findLatestLive(stem.id);
374
+ if (live?.marketed) out.push(live);
375
+ }
376
+ return out;
377
+ }
378
+ toOffer(live) {
379
+ return {
380
+ bundleKey: live.bundleKey,
381
+ bundleVersionId: live.id,
382
+ priceMonthlyNet: parsePrice(live.monthlyNet),
383
+ currency: this.currency ?? DEFAULT_CURRENCY,
384
+ label: live.label
385
+ };
386
+ }
387
+ };
388
+ CatalogBundleUpsellResolver = _ts_decorate2([
389
+ Injectable2(),
390
+ _ts_param2(0, Inject2(PUBLIC_CATALOG_BUNDLE_REPOSITORY_TOKEN)),
391
+ _ts_param2(1, Inject2(PUBLIC_CATALOG_PROJECT_KEY_TOKEN)),
392
+ _ts_param2(2, Optional2()),
393
+ _ts_param2(2, Inject2(PUBLIC_CATALOG_CATALOG_ENTRY_REPOSITORY_TOKEN)),
394
+ _ts_param2(3, Optional2()),
395
+ _ts_param2(3, Inject2(UPSELL_OFFER_CURRENCY_TOKEN)),
396
+ _ts_metadata2("design:type", Function),
397
+ _ts_metadata2("design:paramtypes", [
398
+ typeof BundleRepository === "undefined" ? Object : BundleRepository,
399
+ String,
400
+ Object,
401
+ Object
402
+ ])
403
+ ], CatalogBundleUpsellResolver);
404
+ function parsePrice(decimal) {
405
+ if (decimal === null) return null;
406
+ const n = Number.parseFloat(decimal);
407
+ return Number.isFinite(n) ? n : null;
408
+ }
409
+ __name(parsePrice, "parsePrice");
410
+ function compareRankedOffers(a, b) {
411
+ if (a.coverage !== b.coverage) return b.coverage - a.coverage;
412
+ const priceA = a.offer.priceMonthlyNet ?? Number.POSITIVE_INFINITY;
413
+ const priceB = b.offer.priceMonthlyNet ?? Number.POSITIVE_INFINITY;
414
+ if (priceA !== priceB) return priceA - priceB;
415
+ return a.offer.bundleKey.localeCompare(b.offer.bundleKey);
416
+ }
417
+ __name(compareRankedOffers, "compareRankedOffers");
418
+
419
+ // src/billing/limit-exceeded.filter.ts
420
+ import { Catch, HttpStatus, Logger as Logger2 } from "@nestjs/common";
421
+ function _ts_decorate3(decorators, target, key, desc) {
422
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
423
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
424
+ 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;
425
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
426
+ }
427
+ __name(_ts_decorate3, "_ts_decorate");
428
+ var LimitExceededFilter = class _LimitExceededFilter {
429
+ static {
430
+ __name(this, "LimitExceededFilter");
431
+ }
432
+ logger = new Logger2(_LimitExceededFilter.name);
433
+ catch(exception, host) {
434
+ const ctx = host.switchToHttp();
435
+ const response = ctx.getResponse();
436
+ const request = ctx.getRequest();
437
+ this.logger.warn(`Limit ${exception.dimension} \xFCberschritten: ${exception.used}/${exception.max} bei ${request.method ?? "?"} ${request.url ?? ""}`);
438
+ response.status(HttpStatus.PAYMENT_REQUIRED).send({
439
+ statusCode: HttpStatus.PAYMENT_REQUIRED,
440
+ error: "PaymentRequired",
441
+ reason: "LIMIT_EXCEEDED",
442
+ dimension: exception.dimension,
443
+ used: exception.used,
444
+ max: exception.max,
445
+ message: exception.message
446
+ });
447
+ }
448
+ };
449
+ LimitExceededFilter = _ts_decorate3([
450
+ Catch(LimitExceededError)
451
+ ], LimitExceededFilter);
452
+
453
+ // src/billing/plan-catalog-loader.ts
454
+ import { readFileSync } from "fs";
455
+ import { resolve as resolvePath } from "path";
456
+ import yaml from "js-yaml";
457
+ import Ajv2020 from "ajv/dist/2020.js";
458
+ import addFormats from "ajv-formats";
459
+ import { planCatalogSchema } from "@saasicat/spec";
460
+ var PlanCatalogValidationError = class extends Error {
461
+ static {
462
+ __name(this, "PlanCatalogValidationError");
463
+ }
464
+ source;
465
+ errors;
466
+ constructor(source, errors) {
467
+ const messages = errors.map((e) => `${e.instancePath || "/"}: ${e.message ?? "unbekannt"}`).join("\n ");
468
+ super(`Plan-Catalog-Validierung fehlgeschlagen f\xFCr ${source}:
469
+ ${messages}`), this.source = source, this.errors = errors;
470
+ this.name = "PlanCatalogValidationError";
471
+ }
472
+ };
473
+ function loadPlanCatalogFromFile(opts) {
474
+ const absolutePath = resolvePath(opts.path);
475
+ const raw = readFileSync(absolutePath, "utf-8");
476
+ return loadPlanCatalogFromString(raw, {
477
+ source: absolutePath,
478
+ crossFieldChecks: opts.crossFieldChecks ?? true
479
+ });
480
+ }
481
+ __name(loadPlanCatalogFromFile, "loadPlanCatalogFromFile");
482
+ function loadPlanCatalogFromString(yamlContent, opts) {
483
+ const parsed = yaml.load(yamlContent);
484
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
485
+ throw new Error(`YAML-Inhalt von ${opts.source} ist kein Objekt`);
486
+ }
487
+ const ajv = new Ajv2020({
488
+ strict: false,
489
+ allErrors: true
490
+ });
491
+ addFormats.default(ajv);
492
+ const validate = ajv.compile(planCatalogSchema);
493
+ if (!validate(parsed)) {
494
+ throw new PlanCatalogValidationError(opts.source, validate.errors ?? []);
495
+ }
496
+ const catalog = parsed;
497
+ if (opts.crossFieldChecks ?? true) {
498
+ validateConsistency(catalog, opts.source);
499
+ }
500
+ return catalog;
501
+ }
502
+ __name(loadPlanCatalogFromString, "loadPlanCatalogFromString");
503
+ function validateConsistency(catalog, source) {
504
+ const errors = [];
505
+ const declaredFeatureKeys = new Set((catalog.features ?? []).map((f) => f.key));
506
+ const planIds = /* @__PURE__ */ new Set();
507
+ for (const plan of catalog.plans ?? []) {
508
+ if (planIds.has(plan.id)) {
509
+ errors.push(`plans[].id: Doppelte Plan-ID "${plan.id}"`);
510
+ }
511
+ planIds.add(plan.id);
512
+ if (catalog.features) {
513
+ for (const fk of plan.features) {
514
+ if (!declaredFeatureKeys.has(fk)) {
515
+ errors.push(`plans[id=${plan.id}].features: Unbekannter featureKey "${fk}" \u2014 nicht in catalog.features deklariert`);
516
+ }
517
+ }
518
+ }
519
+ }
520
+ if (errors.length > 0) {
521
+ const err = new Error(`Plan-Catalog-Konsistenz-Fehler in ${source}:
522
+ ${errors.join("\n ")}`);
523
+ err.name = "PlanCatalogConsistencyError";
524
+ throw err;
525
+ }
526
+ }
527
+ __name(validateConsistency, "validateConsistency");
528
+
529
+ // src/billing/plan-catalog-importer.service.ts
530
+ import { Inject as Inject3, Injectable as Injectable3 } from "@nestjs/common";
531
+ function _ts_decorate4(decorators, target, key, desc) {
532
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
533
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
534
+ 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;
535
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
536
+ }
537
+ __name(_ts_decorate4, "_ts_decorate");
538
+ function _ts_metadata3(k, v) {
539
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
540
+ }
541
+ __name(_ts_metadata3, "_ts_metadata");
542
+ function _ts_param3(paramIndex, decorator) {
543
+ return function(target, key) {
544
+ decorator(target, key, paramIndex);
545
+ };
546
+ }
547
+ __name(_ts_param3, "_ts_param");
548
+ var PLAN_CATALOG_IMPORT_SINK_TOKEN = /* @__PURE__ */ Symbol("PLAN_CATALOG_IMPORT_SINK");
549
+ var IMPORT_CHANGE_NOTE = "Initial import from saas.yaml (SPEC_V2 \xA711.1 M6 Pack 2c)";
550
+ var PlanCatalogImporterService = class {
551
+ static {
552
+ __name(this, "PlanCatalogImporterService");
553
+ }
554
+ sink;
555
+ constructor(sink) {
556
+ this.sink = sink;
557
+ }
558
+ /**
559
+ * Import directly from a YAML string. Apps pass the contents of the
560
+ * `saas.yaml` through via the HTTP body; CLI tools can read the file
561
+ * contents themselves beforehand.
562
+ *
563
+ * @throws on a schema failure of the YAML (validated first via the loader).
564
+ */
565
+ async importFromYaml(yamlContent, opts = {}) {
566
+ const catalog = loadPlanCatalogFromString(yamlContent, {
567
+ source: opts.source ?? "http-import",
568
+ crossFieldChecks: opts.crossFieldChecks ?? true
569
+ });
570
+ return this.importCatalog(catalog);
571
+ }
572
+ async importCatalog(catalog) {
573
+ const report = {
574
+ plansCreated: 0,
575
+ plansSkipped: 0,
576
+ planVersionsCreated: 0,
577
+ planVersionsSkipped: 0,
578
+ featureEntriesCreated: 0,
579
+ featureEntriesSkipped: 0,
580
+ warnings: []
581
+ };
582
+ for (const feature of catalog.features ?? []) {
583
+ const result = await this.sink.upsertFeatureCatalogEntry({
584
+ projectKey: catalog.projectKey,
585
+ featureKey: feature.key,
586
+ label: feature.label,
587
+ icon: feature.icon,
588
+ tier: feature.tier,
589
+ plannedOnly: feature.plannedOnly
590
+ });
591
+ if (result.created) report.featureEntriesCreated++;
592
+ else report.featureEntriesSkipped++;
593
+ }
594
+ for (const plan of catalog.plans ?? []) {
595
+ const planResult = await this.sink.upsertPlan({
596
+ projectKey: catalog.projectKey,
597
+ planKey: plan.id,
598
+ label: plan.name ?? plan.id,
599
+ description: plan.tagline ?? null
600
+ });
601
+ if (planResult.created) report.plansCreated++;
602
+ else report.plansSkipped++;
603
+ if (plan.monthlyNet === null || plan.monthlyNet === void 0) {
604
+ report.warnings.push(`Plan '${plan.id}' hat keinen monthlyNet \u2014 \xFCbersprungen (auf-Anfrage-Plans brauchen manuelle PlanVersion).`);
605
+ continue;
606
+ }
607
+ const versionResult = await this.sink.upsertPlanVersion({
608
+ planKey: plan.id,
609
+ version: 1,
610
+ features: plan.features,
611
+ quotas: plan.quotas,
612
+ monthlyNet: plan.monthlyNet.toFixed(2),
613
+ yearlyNet: (plan.yearlyNet ?? plan.monthlyNet * 10).toFixed(2),
614
+ marketed: plan.marketed ?? true,
615
+ publish: true,
616
+ changeNote: IMPORT_CHANGE_NOTE
617
+ });
618
+ if (versionResult.created) report.planVersionsCreated++;
619
+ else report.planVersionsSkipped++;
620
+ }
621
+ return report;
622
+ }
623
+ };
624
+ PlanCatalogImporterService = _ts_decorate4([
625
+ Injectable3(),
626
+ _ts_param3(0, Inject3(PLAN_CATALOG_IMPORT_SINK_TOKEN)),
627
+ _ts_metadata3("design:type", Function),
628
+ _ts_metadata3("design:paramtypes", [
629
+ typeof PlanCatalogImportSink === "undefined" ? Object : PlanCatalogImportSink
630
+ ])
631
+ ], PlanCatalogImporterService);
632
+
633
+ // src/billing/plan-catalog-importer.module.ts
634
+ import { Module } from "@nestjs/common";
635
+
636
+ // src/billing/plan-catalog-importer.controller.ts
637
+ import { Body, Controller, Inject as Inject4, Post, UseGuards } from "@nestjs/common";
638
+ import { IsBoolean, IsOptional, IsString, MinLength } from "class-validator";
639
+ function _ts_decorate5(decorators, target, key, desc) {
640
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
641
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
642
+ 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;
643
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
644
+ }
645
+ __name(_ts_decorate5, "_ts_decorate");
646
+ function _ts_metadata4(k, v) {
647
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
648
+ }
649
+ __name(_ts_metadata4, "_ts_metadata");
650
+ function _ts_param4(paramIndex, decorator) {
651
+ return function(target, key) {
652
+ decorator(target, key, paramIndex);
653
+ };
654
+ }
655
+ __name(_ts_param4, "_ts_param");
656
+ var PlanCatalogImportDto = class {
657
+ static {
658
+ __name(this, "PlanCatalogImportDto");
659
+ }
660
+ yamlContent;
661
+ crossFieldChecks;
662
+ };
663
+ _ts_decorate5([
664
+ IsString(),
665
+ MinLength(1),
666
+ _ts_metadata4("design:type", String)
667
+ ], PlanCatalogImportDto.prototype, "yamlContent", void 0);
668
+ _ts_decorate5([
669
+ IsOptional(),
670
+ IsBoolean(),
671
+ _ts_metadata4("design:type", Boolean)
672
+ ], PlanCatalogImportDto.prototype, "crossFieldChecks", void 0);
673
+ function buildPlanCatalogImporterController(guards) {
674
+ let GeneratedPlanCatalogImporterController = class GeneratedPlanCatalogImporterController {
675
+ static {
676
+ __name(this, "GeneratedPlanCatalogImporterController");
677
+ }
678
+ service;
679
+ constructor(service) {
680
+ this.service = service;
681
+ }
682
+ async import(dto) {
683
+ return this.service.importFromYaml(dto.yamlContent, {
684
+ crossFieldChecks: dto.crossFieldChecks
685
+ });
686
+ }
687
+ };
688
+ _ts_decorate5([
689
+ Post("import"),
690
+ _ts_param4(0, Body()),
691
+ _ts_metadata4("design:type", Function),
692
+ _ts_metadata4("design:paramtypes", [
693
+ typeof PlanCatalogImportDto === "undefined" ? Object : PlanCatalogImportDto
694
+ ]),
695
+ _ts_metadata4("design:returntype", Promise)
696
+ ], GeneratedPlanCatalogImporterController.prototype, "import", null);
697
+ GeneratedPlanCatalogImporterController = _ts_decorate5([
698
+ Controller("admin/billing/plan-catalog"),
699
+ UseGuards(...guards),
700
+ _ts_param4(0, Inject4(PlanCatalogImporterService)),
701
+ _ts_metadata4("design:type", Function),
702
+ _ts_metadata4("design:paramtypes", [
703
+ typeof PlanCatalogImporterService === "undefined" ? Object : PlanCatalogImporterService
704
+ ])
705
+ ], GeneratedPlanCatalogImporterController);
706
+ return GeneratedPlanCatalogImporterController;
707
+ }
708
+ __name(buildPlanCatalogImporterController, "buildPlanCatalogImporterController");
709
+
710
+ // src/billing/plan-catalog-importer.module.ts
711
+ function _ts_decorate6(decorators, target, key, desc) {
712
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
713
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
714
+ 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;
715
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
716
+ }
717
+ __name(_ts_decorate6, "_ts_decorate");
718
+ var PlanCatalogImporterModule = class _PlanCatalogImporterModule {
719
+ static {
720
+ __name(this, "PlanCatalogImporterModule");
721
+ }
722
+ static forRoot(options) {
723
+ const providers = [
724
+ ...options.extraProviders ?? [],
725
+ asProvider(PLAN_CATALOG_IMPORT_SINK_TOKEN, options.sink),
726
+ PlanCatalogImporterService
727
+ ];
728
+ const controllers = [];
729
+ if (options.controller) {
730
+ controllers.push(buildPlanCatalogImporterController(options.controller.guards));
731
+ }
732
+ return {
733
+ module: _PlanCatalogImporterModule,
734
+ global: options.global ?? false,
735
+ imports: options.imports ?? [],
736
+ controllers,
737
+ providers,
738
+ exports: [
739
+ PlanCatalogImporterService
740
+ ]
741
+ };
742
+ }
743
+ };
744
+ PlanCatalogImporterModule = _ts_decorate6([
745
+ Module({})
746
+ ], PlanCatalogImporterModule);
747
+
748
+ // src/billing/feature-ui-registry.tokens.ts
749
+ var FEATURE_UI_REGISTRY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/FeatureUiRegistry");
750
+
751
+ // src/billing/public-catalog.controller.ts
752
+ import { Controller as Controller2, Get, Inject as Inject5, Optional as Optional3, Query } from "@nestjs/common";
753
+ import { buildFeatureRequiresIndex as buildFeatureRequiresIndex2, collectUnsatisfiedRequires as collectUnsatisfiedRequires2 } from "@saasicat/types";
754
+ function _ts_decorate7(decorators, target, key, desc) {
755
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
756
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
757
+ 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;
758
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
759
+ }
760
+ __name(_ts_decorate7, "_ts_decorate");
761
+ function _ts_metadata5(k, v) {
762
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
763
+ }
764
+ __name(_ts_metadata5, "_ts_metadata");
765
+ function _ts_param5(paramIndex, decorator) {
766
+ return function(target, key) {
767
+ decorator(target, key, paramIndex);
768
+ };
769
+ }
770
+ __name(_ts_param5, "_ts_param");
771
+ var PublicCatalogController = class {
772
+ static {
773
+ __name(this, "PublicCatalogController");
774
+ }
775
+ planCatalog;
776
+ featureRegistry;
777
+ projectKey;
778
+ marketingRepo;
779
+ bundleRepo;
780
+ businessTypeRepo;
781
+ catalogEntryRepo;
782
+ constructor(planCatalog, featureRegistry, projectKey = null, marketingRepo = null, bundleRepo = null, businessTypeRepo = null, catalogEntryRepo = null) {
783
+ this.planCatalog = planCatalog;
784
+ this.featureRegistry = featureRegistry;
785
+ this.projectKey = projectKey;
786
+ this.marketingRepo = marketingRepo;
787
+ this.bundleRepo = bundleRepo;
788
+ this.businessTypeRepo = businessTypeRepo;
789
+ this.catalogEntryRepo = catalogEntryRepo;
790
+ }
791
+ async listPlans(lang, localeParam = "de") {
792
+ const locale = lang || localeParam;
793
+ const plans = getMarketedPlans(this.planCatalog).map((plan) => ({
794
+ id: plan.id,
795
+ name: plan.name ?? plan.id,
796
+ tagline: plan.tagline ?? "",
797
+ monthlyNet: plan.monthlyNet ?? null,
798
+ yearlyNet: plan.yearlyNet ?? null,
799
+ popular: plan.popular ?? false,
800
+ quotas: plan.quotas,
801
+ features: plan.features
802
+ }));
803
+ void locale;
804
+ return plans;
805
+ }
806
+ async listFeatureRegistry() {
807
+ if (!this.catalogEntryRepo || !this.projectKey) return this.featureRegistry;
808
+ let rows;
809
+ try {
810
+ rows = await this.catalogEntryRepo.listFeatures({
811
+ projectKey: this.projectKey
812
+ });
813
+ } catch {
814
+ return this.featureRegistry;
815
+ }
816
+ const byKey = new Map(rows.map((r) => [
817
+ r.featureKey,
818
+ r
819
+ ]));
820
+ const merged = {};
821
+ for (const [key, meta] of Object.entries(this.featureRegistry)) {
822
+ const dbIcon = byKey.get(key)?.icon;
823
+ merged[key] = dbIcon ? {
824
+ ...meta,
825
+ icon: dbIcon
826
+ } : meta;
827
+ }
828
+ for (const row of rows) {
829
+ if (merged[row.featureKey]) continue;
830
+ merged[row.featureKey] = {
831
+ label: row.label || row.featureKey,
832
+ description: row.description ?? "",
833
+ icon: row.icon ?? "",
834
+ ...row.plannedOnly ? {
835
+ plannedOnly: true
836
+ } : {}
837
+ };
838
+ }
839
+ return merged;
840
+ }
841
+ /**
842
+ * SPEC_V2 §11.1 M6 Pack 2c — public catalog endpoint for bundles
843
+ * (stem list with live versions). If a `MarketingProjection` with
844
+ * `targetType=BUNDLE` + matching locale exists, it is merged in.
845
+ */
846
+ async listBundles(lang, localeParam = "de") {
847
+ const locale = lang || localeParam;
848
+ if (!this.bundleRepo || !this.projectKey) return [];
849
+ const stems = await this.bundleRepo.list({
850
+ projectKey: this.projectKey
851
+ });
852
+ const requiresIndex = await this.loadFeatureRequiresIndex();
853
+ const out = [];
854
+ for (const stem of stems) {
855
+ const versions = await this.bundleRepo.listVersions(stem.id);
856
+ const live = versions.find((v) => v.publishedAt !== null && v.supersededAt === null);
857
+ if (!live) continue;
858
+ const marketing = await this.lookupMarketing("BUNDLE", live.id, locale);
859
+ out.push(this.toBundleEntry(stem.bundleKey, live, marketing, requiresIndex));
860
+ }
861
+ return out;
862
+ }
863
+ /**
864
+ * `requires` data for `requiresFeatures` (#35) — comes from the
865
+ * FeatureCatalogEntries. Without a CatalogEntryRepository or on DB errors,
866
+ * an empty index: the auth-free endpoint must not break on this,
867
+ * `requiresFeatures` then stays conservatively empty.
868
+ */
869
+ async loadFeatureRequiresIndex() {
870
+ if (!this.catalogEntryRepo || !this.projectKey) return /* @__PURE__ */ new Map();
871
+ try {
872
+ const rows = await this.catalogEntryRepo.listFeatures({
873
+ projectKey: this.projectKey
874
+ });
875
+ return buildFeatureRequiresIndex2(rows);
876
+ } catch {
877
+ return /* @__PURE__ */ new Map();
878
+ }
879
+ }
880
+ /**
881
+ * SPEC_V2 §11.1 M6 Pack 2c — public catalog endpoint for BusinessTypes
882
+ * (stem list with live versions + bundle composition + marketing).
883
+ */
884
+ async listBusinessTypes(lang, localeParam = "de") {
885
+ const locale = lang || localeParam;
886
+ if (!this.businessTypeRepo || !this.bundleRepo || !this.projectKey) return [];
887
+ const stems = await this.businessTypeRepo.list({
888
+ projectKey: this.projectKey
889
+ });
890
+ const out = [];
891
+ for (const stem of stems) {
892
+ const versions = await this.businessTypeRepo.listVersions(stem.id);
893
+ const live = versions.find((v) => v.publishedAt !== null && v.supersededAt === null);
894
+ if (!live) continue;
895
+ const bundleKeys = [];
896
+ for (const ref of live.bundles) {
897
+ const bv = await this.bundleRepo.findVersionById(ref.bundleVersionId);
898
+ if (bv) bundleKeys.push(bv.bundleKey);
899
+ }
900
+ const marketing = await this.lookupMarketing("BUSINESS_TYPE", live.id, locale);
901
+ out.push({
902
+ businessTypeVersionId: live.id,
903
+ businessTypeKey: stem.businessTypeKey,
904
+ label: stem.label,
905
+ description: stem.description ?? null,
906
+ monthlyNet: live.monthlyNet,
907
+ yearlyNet: live.yearlyNet,
908
+ bundleKeys,
909
+ quotaOverrides: live.quotaOverrides,
910
+ marketing
911
+ });
912
+ }
913
+ return out;
914
+ }
915
+ async lookupMarketing(targetType, targetVersionId, locale) {
916
+ if (!this.marketingRepo || !this.projectKey) return void 0;
917
+ const rows = await this.marketingRepo.list({
918
+ projectKey: this.projectKey,
919
+ targetType,
920
+ targetVersionId,
921
+ locale
922
+ });
923
+ const row = rows[0];
924
+ if (!row) return void 0;
925
+ return {
926
+ displayLabel: row.displayLabel,
927
+ description: row.description,
928
+ visible: row.visible,
929
+ badge: row.badge,
930
+ topFeatures: row.topFeatures,
931
+ trialEnabled: row.trialEnabled,
932
+ trialDays: row.trialDays,
933
+ priceTag: row.priceTag,
934
+ ctaLabel: row.ctaLabel,
935
+ highlight: row.highlight,
936
+ priority: row.priority
937
+ };
938
+ }
939
+ toBundleEntry(bundleKey, live, marketing, requiresIndex) {
940
+ const features = live.features;
941
+ return {
942
+ bundleVersionId: live.id,
943
+ bundleKey,
944
+ label: live.label,
945
+ description: null,
946
+ features,
947
+ quotas: live.quotas,
948
+ monthlyNet: live.monthlyNet,
949
+ yearlyNet: live.yearlyNet,
950
+ requiresFeatures: collectUnsatisfiedRequires2(features, requiresIndex),
951
+ marketing
952
+ };
953
+ }
954
+ };
955
+ _ts_decorate7([
956
+ Get("plans"),
957
+ _ts_param5(0, Query("lang")),
958
+ _ts_param5(1, Query("locale")),
959
+ _ts_metadata5("design:type", Function),
960
+ _ts_metadata5("design:paramtypes", [
961
+ String,
962
+ void 0
963
+ ]),
964
+ _ts_metadata5("design:returntype", Promise)
965
+ ], PublicCatalogController.prototype, "listPlans", null);
966
+ _ts_decorate7([
967
+ Get("feature-registry"),
968
+ _ts_metadata5("design:type", Function),
969
+ _ts_metadata5("design:paramtypes", []),
970
+ _ts_metadata5("design:returntype", Promise)
971
+ ], PublicCatalogController.prototype, "listFeatureRegistry", null);
972
+ _ts_decorate7([
973
+ Get("bundles"),
974
+ _ts_param5(0, Query("lang")),
975
+ _ts_param5(1, Query("locale")),
976
+ _ts_metadata5("design:type", Function),
977
+ _ts_metadata5("design:paramtypes", [
978
+ String,
979
+ void 0
980
+ ]),
981
+ _ts_metadata5("design:returntype", Promise)
982
+ ], PublicCatalogController.prototype, "listBundles", null);
983
+ _ts_decorate7([
984
+ Get("business-types"),
985
+ _ts_param5(0, Query("lang")),
986
+ _ts_param5(1, Query("locale")),
987
+ _ts_metadata5("design:type", Function),
988
+ _ts_metadata5("design:paramtypes", [
989
+ String,
990
+ void 0
991
+ ]),
992
+ _ts_metadata5("design:returntype", Promise)
993
+ ], PublicCatalogController.prototype, "listBusinessTypes", null);
994
+ PublicCatalogController = _ts_decorate7([
995
+ Controller2("billing"),
996
+ _ts_param5(0, Inject5(PLAN_CATALOG_TOKEN)),
997
+ _ts_param5(1, Inject5(FEATURE_UI_REGISTRY_TOKEN)),
998
+ _ts_param5(2, Optional3()),
999
+ _ts_param5(2, Inject5(PUBLIC_CATALOG_PROJECT_KEY_TOKEN)),
1000
+ _ts_param5(3, Optional3()),
1001
+ _ts_param5(3, Inject5(PUBLIC_CATALOG_MARKETING_REPOSITORY_TOKEN)),
1002
+ _ts_param5(4, Optional3()),
1003
+ _ts_param5(4, Inject5(PUBLIC_CATALOG_BUNDLE_REPOSITORY_TOKEN)),
1004
+ _ts_param5(5, Optional3()),
1005
+ _ts_param5(5, Inject5(PUBLIC_CATALOG_BUSINESS_TYPE_REPOSITORY_TOKEN)),
1006
+ _ts_param5(6, Optional3()),
1007
+ _ts_param5(6, Inject5(PUBLIC_CATALOG_CATALOG_ENTRY_REPOSITORY_TOKEN)),
1008
+ _ts_metadata5("design:type", Function),
1009
+ _ts_metadata5("design:paramtypes", [
1010
+ typeof PlanCatalog === "undefined" ? Object : PlanCatalog,
1011
+ typeof FeatureUiRegistry === "undefined" ? Object : FeatureUiRegistry,
1012
+ Object,
1013
+ Object,
1014
+ Object,
1015
+ Object,
1016
+ Object
1017
+ ])
1018
+ ], PublicCatalogController);
1019
+
1020
+ // src/billing/public-catalog.module.ts
1021
+ import { Module as Module2 } from "@nestjs/common";
1022
+ function _ts_decorate8(decorators, target, key, desc) {
1023
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1024
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1025
+ 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;
1026
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1027
+ }
1028
+ __name(_ts_decorate8, "_ts_decorate");
1029
+ var PublicCatalogModule = class _PublicCatalogModule {
1030
+ static {
1031
+ __name(this, "PublicCatalogModule");
1032
+ }
1033
+ static forRoot(options) {
1034
+ const providers = [
1035
+ ...options.extraProviders ?? [],
1036
+ {
1037
+ provide: FEATURE_UI_REGISTRY_TOKEN,
1038
+ useValue: options.featureUiRegistry
1039
+ }
1040
+ ];
1041
+ if (options.projectKey !== void 0) {
1042
+ providers.push({
1043
+ provide: PUBLIC_CATALOG_PROJECT_KEY_TOKEN,
1044
+ useValue: options.projectKey
1045
+ });
1046
+ }
1047
+ if (options.bundleRepository) {
1048
+ providers.push(asProvider(PUBLIC_CATALOG_BUNDLE_REPOSITORY_TOKEN, options.bundleRepository));
1049
+ }
1050
+ if (options.businessTypeRepository) {
1051
+ providers.push(asProvider(PUBLIC_CATALOG_BUSINESS_TYPE_REPOSITORY_TOKEN, options.businessTypeRepository));
1052
+ }
1053
+ if (options.marketingRepository) {
1054
+ providers.push(asProvider(PUBLIC_CATALOG_MARKETING_REPOSITORY_TOKEN, options.marketingRepository));
1055
+ }
1056
+ if (options.catalogEntryRepository) {
1057
+ providers.push(asProvider(PUBLIC_CATALOG_CATALOG_ENTRY_REPOSITORY_TOKEN, options.catalogEntryRepository));
1058
+ }
1059
+ return {
1060
+ module: _PublicCatalogModule,
1061
+ imports: options.imports ?? [],
1062
+ controllers: [
1063
+ PublicCatalogController
1064
+ ],
1065
+ providers,
1066
+ exports: [
1067
+ FEATURE_UI_REGISTRY_TOKEN
1068
+ ]
1069
+ };
1070
+ }
1071
+ };
1072
+ PublicCatalogModule = _ts_decorate8([
1073
+ Module2({})
1074
+ ], PublicCatalogModule);
1075
+
1076
+ // src/billing/composed-tenant-auth.guard.ts
1077
+ import { ForbiddenException as ForbiddenException2, Inject as Inject6, Injectable as Injectable4, Optional as Optional4 } from "@nestjs/common";
1078
+ function _ts_decorate9(decorators, target, key, desc) {
1079
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1080
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1081
+ 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;
1082
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1083
+ }
1084
+ __name(_ts_decorate9, "_ts_decorate");
1085
+ function _ts_metadata6(k, v) {
1086
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1087
+ }
1088
+ __name(_ts_metadata6, "_ts_metadata");
1089
+ function _ts_param6(paramIndex, decorator) {
1090
+ return function(target, key) {
1091
+ decorator(target, key, paramIndex);
1092
+ };
1093
+ }
1094
+ __name(_ts_param6, "_ts_param");
1095
+ var ComposedTenantAuthGuard = class {
1096
+ static {
1097
+ __name(this, "ComposedTenantAuthGuard");
1098
+ }
1099
+ guards;
1100
+ constructor(guards = null) {
1101
+ this.guards = guards;
1102
+ }
1103
+ async canActivate(context) {
1104
+ if (!this.guards || this.guards.length === 0) {
1105
+ throw new ForbiddenException2("TenantBillingModule.forRoot.authGuards ist nicht konfiguriert.");
1106
+ }
1107
+ for (const guard of this.guards) {
1108
+ const result = await Promise.resolve(guard.canActivate(context));
1109
+ if (!result) return false;
1110
+ }
1111
+ return true;
1112
+ }
1113
+ };
1114
+ ComposedTenantAuthGuard = _ts_decorate9([
1115
+ Injectable4(),
1116
+ _ts_param6(0, Optional4()),
1117
+ _ts_param6(0, Inject6(TENANT_AUTH_GUARDS_TOKEN)),
1118
+ _ts_metadata6("design:type", Function),
1119
+ _ts_metadata6("design:paramtypes", [
1120
+ Object
1121
+ ])
1122
+ ], ComposedTenantAuthGuard);
1123
+
1124
+ // src/billing/tenant-admin.guard.ts
1125
+ import { ForbiddenException as ForbiddenException3, Injectable as Injectable5 } from "@nestjs/common";
1126
+ function _ts_decorate10(decorators, target, key, desc) {
1127
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1128
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1129
+ 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;
1130
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1131
+ }
1132
+ __name(_ts_decorate10, "_ts_decorate");
1133
+ var ADMIN_ROLES = /* @__PURE__ */ new Set([
1134
+ "TENANT_ADMIN",
1135
+ "SUPER_ADMIN"
1136
+ ]);
1137
+ var TenantAdminGuard = class {
1138
+ static {
1139
+ __name(this, "TenantAdminGuard");
1140
+ }
1141
+ canActivate(context) {
1142
+ const request = context.switchToHttp().getRequest();
1143
+ if (!request.user) {
1144
+ throw new ForbiddenException3("Nicht authentifiziert");
1145
+ }
1146
+ const role = request.user.platformRole ?? request.user.role;
1147
+ if (!role || !ADMIN_ROLES.has(role)) {
1148
+ throw new ForbiddenException3("Diese Aktion erfordert die TENANT_ADMIN-Rolle.");
1149
+ }
1150
+ return true;
1151
+ }
1152
+ };
1153
+ TenantAdminGuard = _ts_decorate10([
1154
+ Injectable5()
1155
+ ], TenantAdminGuard);
1156
+
1157
+ // src/billing/self-service-policy.ts
1158
+ var SELF_SERVICE_BLOCKED_PLANS_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/SelfServiceBlockedPlans");
1159
+ var SELF_SERVICE_BLOCKED_BUNDLES_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/SelfServiceBlockedBundles");
1160
+
1161
+ // src/billing/proration.ts
1162
+ var DAY_MS = 864e5;
1163
+ function computeProration(input) {
1164
+ const { periodStart, periodEnd, now, currentPriceNet, targetPriceNet } = input;
1165
+ const daysInPeriod = Math.max(1, Math.round((periodEnd.getTime() - periodStart.getTime()) / DAY_MS));
1166
+ const daysRemaining = Math.max(0, Math.min(daysInPeriod, Math.round((periodEnd.getTime() - now.getTime()) / DAY_MS)));
1167
+ const prorataDeltaNet = round2((targetPriceNet - currentPriceNet) * daysRemaining / daysInPeriod);
1168
+ return {
1169
+ daysRemainingInPeriod: daysRemaining,
1170
+ daysInPeriod,
1171
+ periodStart,
1172
+ periodEnd,
1173
+ currentPriceNet,
1174
+ targetPriceNet,
1175
+ prorataDeltaNet
1176
+ };
1177
+ }
1178
+ __name(computeProration, "computeProration");
1179
+ function round2(n) {
1180
+ return Math.round(n * 100) / 100;
1181
+ }
1182
+ __name(round2, "round2");
1183
+
1184
+ // src/billing/plan-change-preview.service.ts
1185
+ import { Inject as Inject7, Injectable as Injectable6, NotFoundException, Optional as Optional5 } from "@nestjs/common";
1186
+ function _ts_decorate11(decorators, target, key, desc) {
1187
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1188
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1189
+ 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;
1190
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1191
+ }
1192
+ __name(_ts_decorate11, "_ts_decorate");
1193
+ function _ts_metadata7(k, v) {
1194
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1195
+ }
1196
+ __name(_ts_metadata7, "_ts_metadata");
1197
+ function _ts_param7(paramIndex, decorator) {
1198
+ return function(target, key) {
1199
+ decorator(target, key, paramIndex);
1200
+ };
1201
+ }
1202
+ __name(_ts_param7, "_ts_param");
1203
+ var PlanChangePreviewService = class {
1204
+ static {
1205
+ __name(this, "PlanChangePreviewService");
1206
+ }
1207
+ catalog;
1208
+ entitlements;
1209
+ subscriptions;
1210
+ usageSnapshot;
1211
+ blockedPlans;
1212
+ trialProjection;
1213
+ constructor(catalog, entitlements, subscriptions, usageSnapshot, blockedPlans = null, trialProjection = null) {
1214
+ this.catalog = catalog;
1215
+ this.entitlements = entitlements;
1216
+ this.subscriptions = subscriptions;
1217
+ this.usageSnapshot = usageSnapshot;
1218
+ this.blockedPlans = blockedPlans;
1219
+ this.trialProjection = trialProjection;
1220
+ }
1221
+ async preview(tenantId, targetPlan, targetCycle, now = /* @__PURE__ */ new Date()) {
1222
+ const sub = await this.subscriptions.findForTenant(tenantId);
1223
+ if (!sub) {
1224
+ throw new NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
1225
+ }
1226
+ const targetPlanDef = findPlan(this.catalog, targetPlan);
1227
+ if (!targetPlanDef) {
1228
+ throw new NotFoundException(`Plan "${targetPlan}" nicht im Catalog (${this.catalog.projectKey})`);
1229
+ }
1230
+ const ctx = {
1231
+ currentPeriodStart: sub.currentPeriodStart,
1232
+ currentPeriodEnd: sub.currentPeriodEnd,
1233
+ trialEndsAt: sub.trialEndsAt,
1234
+ status: sub.status,
1235
+ currentBillingCycle: sub.billingCycle,
1236
+ currentPlan: sub.plan,
1237
+ startedAt: sub.startedAt
1238
+ };
1239
+ const [currentLimits, usage] = await Promise.all([
1240
+ this.entitlements.computeLimits(tenantId, now),
1241
+ this.usageSnapshot.snapshot(tenantId)
1242
+ ]);
1243
+ const currentPlanDef = findPlan(this.catalog, currentLimits.plan);
1244
+ const currentSnap = {
1245
+ id: currentLimits.plan,
1246
+ name: currentPlanDef?.name ?? currentLimits.plan,
1247
+ monthlyNet: getPlanPriceNet(this.catalog, currentLimits.plan, "MONTHLY"),
1248
+ yearlyNet: getPlanPriceNet(this.catalog, currentLimits.plan, "YEARLY"),
1249
+ quotas: currentLimits.quotas,
1250
+ features: Array.from(currentLimits.features).sort()
1251
+ };
1252
+ const targetSnap = {
1253
+ id: targetPlanDef.id,
1254
+ name: targetPlanDef.name ?? targetPlanDef.id,
1255
+ monthlyNet: getPlanPriceNet(this.catalog, targetPlan, "MONTHLY"),
1256
+ yearlyNet: getPlanPriceNet(this.catalog, targetPlan, "YEARLY"),
1257
+ quotas: targetPlanDef.quotas,
1258
+ features: targetPlanDef.features.slice().sort()
1259
+ };
1260
+ const changeType = this.classify(sub.plan, sub.billingCycle, targetPlan, targetCycle);
1261
+ const limitsCheck = {};
1262
+ const quotaKeys = /* @__PURE__ */ new Set([
1263
+ ...Object.keys(currentLimits.quotas),
1264
+ ...Object.keys(targetSnap.quotas),
1265
+ ...Object.keys(usage)
1266
+ ]);
1267
+ for (const key of quotaKeys) {
1268
+ const used = usage[key] ?? 0;
1269
+ const currentMax = currentLimits.quotas[key] ?? 0;
1270
+ const targetMax = targetSnap.quotas[key] ?? 0;
1271
+ const exceeded = targetMax !== -1 && used > targetMax;
1272
+ limitsCheck[key] = {
1273
+ used,
1274
+ currentMax,
1275
+ targetMax,
1276
+ exceeded
1277
+ };
1278
+ }
1279
+ const currentFeatureSet = currentLimits.features;
1280
+ const targetFeatureSet = new Set(targetSnap.features);
1281
+ const featuresLost = Array.from(currentFeatureSet).filter((f) => !targetFeatureSet.has(f)).sort();
1282
+ const featuresGained = targetSnap.features.filter((f) => !currentFeatureSet.has(f));
1283
+ const isImmediate = changeType === "UPGRADE";
1284
+ const effectiveAt = isImmediate ? null : this.resolveEffectiveAt(ctx, now);
1285
+ const proration = isImmediate && ctx.status !== "TRIAL" ? this.computeProration(ctx, now, currentSnap, targetSnap, sub.billingCycle, targetCycle) : null;
1286
+ const blockers = [];
1287
+ const warnings = [];
1288
+ const blockedTargets = this.blockedPlans?.asTarget ?? [];
1289
+ const blockedSources = this.blockedPlans?.asSource ?? [];
1290
+ if (blockedTargets.includes(targetPlan)) {
1291
+ blockers.push({
1292
+ code: `${targetPlan}_NOT_SELF_SERVICE`,
1293
+ message: `${targetSnap.name} wird nur per Sondervertrag aktiviert. Bitte den Vertragsbetreuer kontaktieren.`
1294
+ });
1295
+ }
1296
+ if (blockedSources.includes(sub.plan)) {
1297
+ blockers.push({
1298
+ code: `${sub.plan}_LOCKED`,
1299
+ message: `Aktiver ${currentSnap.name}-Sondervertrag \u2014 bitte den Vertragsbetreuer f\xFCr Plan-Wechsel kontaktieren.`
1300
+ });
1301
+ }
1302
+ for (const [key, row] of Object.entries(limitsCheck)) {
1303
+ if (!row.exceeded) continue;
1304
+ const usedDisplay = isFloatQuota(key) ? row.used.toFixed(1) : row.used.toString();
1305
+ blockers.push({
1306
+ code: `${key.toUpperCase()}_OVER_TARGET`,
1307
+ message: `Aktueller Verbrauch ${usedDisplay} \xFCberschreitet das Ziel-Limit ${row.targetMax} (${key}) im ${targetSnap.name}-Paket. Bitte Verbrauch reduzieren.`
1308
+ });
1309
+ }
1310
+ if (featuresLost.length > 0) {
1311
+ warnings.push({
1312
+ code: "FEATURES_LOST",
1313
+ message: `Mit dem Wechsel verlierst du Zugriff auf ${featuresLost.length} Feature${featuresLost.length === 1 ? "" : "s"}. Bestehende Daten bleiben erhalten und werden nicht gel\xF6scht \u2014 du kannst sie wieder freischalten, wenn du erneut upgradest.`
1314
+ });
1315
+ }
1316
+ if (changeType === "NOOP") {
1317
+ warnings.push({
1318
+ code: "NO_CHANGE",
1319
+ message: "Ziel-Paket und Abrechnungszyklus stimmen mit dem aktuellen Stand \xFCberein."
1320
+ });
1321
+ }
1322
+ const projectedTrialEndsAt = this.trialProjection && sub.status === "TRIAL" ? await this.trialProjection.projectTrialEndsAt({
1323
+ currentPlan: sub.plan,
1324
+ targetPlan,
1325
+ currentTrialEndsAt: sub.trialEndsAt,
1326
+ status: sub.status,
1327
+ now
1328
+ }) : null;
1329
+ return {
1330
+ changeType,
1331
+ current: {
1332
+ plan: currentSnap,
1333
+ billingCycle: sub.billingCycle
1334
+ },
1335
+ target: {
1336
+ plan: targetSnap,
1337
+ billingCycle: targetCycle
1338
+ },
1339
+ effectiveAt,
1340
+ isImmediate,
1341
+ projectedTrialEndsAt,
1342
+ proration,
1343
+ limitsCheck,
1344
+ featuresLost,
1345
+ featuresGained,
1346
+ blockers,
1347
+ warnings
1348
+ };
1349
+ }
1350
+ /** Like `preview`, but only the blocker list — for a server-side
1351
+ * pre-check before the `changePlan` mutation (defense-in-depth). */
1352
+ async assertChangeAllowed(tenantId, targetPlan, targetCycle, now = /* @__PURE__ */ new Date()) {
1353
+ const dto = await this.preview(tenantId, targetPlan, targetCycle, now);
1354
+ return dto.blockers;
1355
+ }
1356
+ classify(currentPlan, currentCycle, targetPlan, targetCycle) {
1357
+ if (currentPlan === targetPlan && currentCycle === targetCycle) return "NOOP";
1358
+ if (currentPlan === targetPlan) return "CYCLE_CHANGE";
1359
+ const currentRank = this.planRank(currentPlan);
1360
+ const targetRank = this.planRank(targetPlan);
1361
+ return targetRank > currentRank ? "UPGRADE" : "DOWNGRADE";
1362
+ }
1363
+ /** Catalog order = rank. Non-marketed plans go to the end. */
1364
+ planRank(planId) {
1365
+ const plans = this.catalog.plans ?? [];
1366
+ const idx = plans.findIndex((p) => p.id === planId);
1367
+ if (idx === -1) return Number.POSITIVE_INFINITY;
1368
+ const plan = plans[idx];
1369
+ if (plan.marketed === false) return Number.POSITIVE_INFINITY - plans.length + idx;
1370
+ return idx;
1371
+ }
1372
+ resolveEffectiveAt(ctx, now) {
1373
+ if (ctx.status === "TRIAL" && ctx.trialEndsAt) return ctx.trialEndsAt;
1374
+ if (ctx.currentPeriodEnd) return ctx.currentPeriodEnd;
1375
+ return periodEndAfter(ctx.startedAt, ctx.currentBillingCycle, now);
1376
+ }
1377
+ computeProration(ctx, now, current, target, currentCycle, targetCycle) {
1378
+ const periodStart = ctx.currentPeriodStart ?? ctx.startedAt ?? now;
1379
+ const periodEnd = ctx.currentPeriodEnd ?? periodEndAfter(ctx.startedAt, ctx.currentBillingCycle, now);
1380
+ return computeProration({
1381
+ periodStart,
1382
+ periodEnd,
1383
+ now,
1384
+ currentPriceNet: priceForCycle(current, currentCycle) ?? 0,
1385
+ targetPriceNet: priceForCycle(target, targetCycle) ?? 0
1386
+ });
1387
+ }
1388
+ };
1389
+ PlanChangePreviewService = _ts_decorate11([
1390
+ Injectable6(),
1391
+ _ts_param7(0, Inject7(PLAN_CATALOG_TOKEN)),
1392
+ _ts_param7(1, Inject7(EntitlementService)),
1393
+ _ts_param7(2, Inject7(SUBSCRIPTION_USAGE_PORT_TOKEN)),
1394
+ _ts_param7(3, Inject7(USAGE_SNAPSHOT_PORT_TOKEN)),
1395
+ _ts_param7(4, Optional5()),
1396
+ _ts_param7(4, Inject7(SELF_SERVICE_BLOCKED_PLANS_TOKEN)),
1397
+ _ts_param7(5, Optional5()),
1398
+ _ts_param7(5, Inject7(TRIAL_PROJECTION_PORT_TOKEN)),
1399
+ _ts_metadata7("design:type", Function),
1400
+ _ts_metadata7("design:paramtypes", [
1401
+ typeof PlanCatalog === "undefined" ? Object : PlanCatalog,
1402
+ typeof EntitlementService === "undefined" ? Object : EntitlementService,
1403
+ typeof SubscriptionUsagePort === "undefined" ? Object : SubscriptionUsagePort,
1404
+ typeof UsageSnapshotPort === "undefined" ? Object : UsageSnapshotPort,
1405
+ Object,
1406
+ Object
1407
+ ])
1408
+ ], PlanChangePreviewService);
1409
+ function priceForCycle(snap, cycle) {
1410
+ return cycle === "YEARLY" ? snap.yearlyNet : snap.monthlyNet;
1411
+ }
1412
+ __name(priceForCycle, "priceForCycle");
1413
+ function isFloatQuota(key) {
1414
+ return key.toLowerCase().includes("storage");
1415
+ }
1416
+ __name(isFloatQuota, "isFloatQuota");
1417
+
1418
+ // src/billing/pending-plan-materialization.service.ts
1419
+ import { Inject as Inject8, Injectable as Injectable7, Logger as Logger3, Optional as Optional6 } from "@nestjs/common";
1420
+
1421
+ // src/billing/contract-freeze.tokens.ts
1422
+ var CONTRACT_FREEZE_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/ContractFreezePort");
1423
+ var CONTRACT_FREEZE_SOURCE_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/ContractFreezeSourcePort");
1424
+ var CONTRACT_FREEZE_PROJECT_KEY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/ContractFreezeProjectKey");
1425
+
1426
+ // src/billing/pending-plan-materialization.service.ts
1427
+ function _ts_decorate12(decorators, target, key, desc) {
1428
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1429
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1430
+ 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;
1431
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1432
+ }
1433
+ __name(_ts_decorate12, "_ts_decorate");
1434
+ function _ts_metadata8(k, v) {
1435
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1436
+ }
1437
+ __name(_ts_metadata8, "_ts_metadata");
1438
+ function _ts_param8(paramIndex, decorator) {
1439
+ return function(target, key) {
1440
+ decorator(target, key, paramIndex);
1441
+ };
1442
+ }
1443
+ __name(_ts_param8, "_ts_param");
1444
+ var PendingPlanMaterializationService = class _PendingPlanMaterializationService {
1445
+ static {
1446
+ __name(this, "PendingPlanMaterializationService");
1447
+ }
1448
+ query;
1449
+ subscriptionWrite;
1450
+ entitlements;
1451
+ contractFreeze;
1452
+ logger = new Logger3(_PendingPlanMaterializationService.name);
1453
+ constructor(query, subscriptionWrite, entitlements, contractFreeze = null) {
1454
+ this.query = query;
1455
+ this.subscriptionWrite = subscriptionWrite;
1456
+ this.entitlements = entitlements;
1457
+ this.contractFreeze = contractFreeze;
1458
+ }
1459
+ async materializeDuePlanChanges(now = /* @__PURE__ */ new Date()) {
1460
+ const due = await this.query.findDuePendingPlanChanges(now);
1461
+ let applied = 0;
1462
+ for (const change of due) {
1463
+ const cycle = change.pendingBillingCycle ?? "MONTHLY";
1464
+ const period = initialPeriodWindow(now, cycle);
1465
+ try {
1466
+ await this.subscriptionWrite.changePlanImmediate(change.tenantId, {
1467
+ planId: change.pendingPlan,
1468
+ cycle,
1469
+ periodStart: period.start,
1470
+ periodEnd: period.end,
1471
+ // Status stays (ACTIVE/PAST_DUE etc.) — only the plan is materialized.
1472
+ nextStatus: null
1473
+ });
1474
+ this.entitlements.invalidateTenant(change.tenantId);
1475
+ applied += 1;
1476
+ } catch (err) {
1477
+ this.logger.error(`Pending-Plan-Materialisierung fehlgeschlagen (tenant ${change.tenantId}): ${String(err)}`);
1478
+ continue;
1479
+ }
1480
+ await this.tryFreeze(change.tenantId, change.pendingPlan, cycle, now);
1481
+ }
1482
+ if (applied > 0) {
1483
+ this.logger.log(`Pending-Plan-Materialisierung: ${applied} geplante(r) Plan-Wechsel angewandt.`);
1484
+ }
1485
+ return {
1486
+ applied
1487
+ };
1488
+ }
1489
+ async tryFreeze(tenantId, plan, cycle, now) {
1490
+ if (!this.contractFreeze) return;
1491
+ try {
1492
+ await this.contractFreeze.freezeOnPlanChange(tenantId, plan, cycle, now);
1493
+ } catch (err) {
1494
+ this.logger.error(`Contract-Freeze nach Materialisierung fehlgeschlagen (tenant ${tenantId}): ${String(err)}`);
1495
+ }
1496
+ }
1497
+ };
1498
+ PendingPlanMaterializationService = _ts_decorate12([
1499
+ Injectable7(),
1500
+ _ts_param8(0, Inject8(PENDING_PLAN_QUERY_PORT_TOKEN)),
1501
+ _ts_param8(1, Inject8(SUBSCRIPTION_WRITE_PORT_TOKEN)),
1502
+ _ts_param8(3, Optional6()),
1503
+ _ts_param8(3, Inject8(CONTRACT_FREEZE_PORT_TOKEN)),
1504
+ _ts_metadata8("design:type", Function),
1505
+ _ts_metadata8("design:paramtypes", [
1506
+ typeof PendingPlanQueryPort === "undefined" ? Object : PendingPlanQueryPort,
1507
+ typeof TenantSubscriptionWritePort === "undefined" ? Object : TenantSubscriptionWritePort,
1508
+ typeof EntitlementService === "undefined" ? Object : EntitlementService,
1509
+ Object
1510
+ ])
1511
+ ], PendingPlanMaterializationService);
1512
+
1513
+ // src/billing/subscription-contract-freeze.service.ts
1514
+ import { Inject as Inject9, Injectable as Injectable8 } from "@nestjs/common";
1515
+ function _ts_decorate13(decorators, target, key, desc) {
1516
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1517
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1518
+ 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;
1519
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1520
+ }
1521
+ __name(_ts_decorate13, "_ts_decorate");
1522
+ function _ts_metadata9(k, v) {
1523
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1524
+ }
1525
+ __name(_ts_metadata9, "_ts_metadata");
1526
+ function _ts_param9(paramIndex, decorator) {
1527
+ return function(target, key) {
1528
+ decorator(target, key, paramIndex);
1529
+ };
1530
+ }
1531
+ __name(_ts_param9, "_ts_param");
1532
+ var SubscriptionContractFreezeService = class {
1533
+ static {
1534
+ __name(this, "SubscriptionContractFreezeService");
1535
+ }
1536
+ catalog;
1537
+ entitlements;
1538
+ contracts;
1539
+ projectKey;
1540
+ source;
1541
+ constructor(catalog, entitlements, contracts, projectKey, source) {
1542
+ this.catalog = catalog;
1543
+ this.entitlements = entitlements;
1544
+ this.contracts = contracts;
1545
+ this.projectKey = projectKey;
1546
+ this.source = source;
1547
+ }
1548
+ async freezeOnPlanChange(tenantId, newPlan, billingCycle, effectiveFrom) {
1549
+ const cycle = billingCycle === "YEARLY" ? "yearly" : "monthly";
1550
+ const vatRate = this.catalog.vatRate;
1551
+ const bundles = await this.source.loadBookedBundles(tenantId, cycle, vatRate);
1552
+ const livePlanVersionId = await this.source.findLivePlanVersionId(newPlan);
1553
+ const previous = await this.contracts.findActiveByTenantId(tenantId, effectiveFrom);
1554
+ if (previous) {
1555
+ await this.contracts.terminate(previous.id, {
1556
+ effectiveUntil: effectiveFrom,
1557
+ status: "superseded"
1558
+ });
1559
+ }
1560
+ this.entitlements.invalidateTenant(tenantId);
1561
+ const limits = await this.entitlements.computeLimits(tenantId, effectiveFrom);
1562
+ const planDef = findPlan(this.catalog, newPlan);
1563
+ const planPriceNet = getPlanPriceNet(this.catalog, newPlan, billingCycle) ?? 0;
1564
+ const planLineItem = {
1565
+ kind: "plan",
1566
+ sourceKey: newPlan,
1567
+ sourceVersionId: livePlanVersionId,
1568
+ titleSnapshot: planDef?.name ?? newPlan,
1569
+ descriptionSnapshot: planDef?.tagline ?? null,
1570
+ quantity: 1,
1571
+ unit: null,
1572
+ priceNet: planPriceNet,
1573
+ priceGross: round22(planPriceNet * (1 + vatRate / 100)),
1574
+ billingCycle: cycle,
1575
+ minimumTermUntil: null,
1576
+ featuresSnapshot: planDef?.features ?? [],
1577
+ quotaEffectsSnapshot: planDef?.quotas ?? {},
1578
+ metadata: null
1579
+ };
1580
+ const lineItems = [
1581
+ planLineItem,
1582
+ ...bundles.lineItems
1583
+ ];
1584
+ const subtotalNet = round22(lineItems.reduce((sum, li) => sum + li.priceNet, 0));
1585
+ const data = {
1586
+ projectKey: this.projectKey,
1587
+ tenantId,
1588
+ status: "active",
1589
+ effectiveFrom,
1590
+ effectiveUntil: null,
1591
+ originalPlanVersionId: livePlanVersionId,
1592
+ originalBundleVersionIds: bundles.bundleVersionIds,
1593
+ entitlementSnapshot: {
1594
+ plan: limits.plan,
1595
+ quotas: {
1596
+ ...limits.quotas
1597
+ },
1598
+ features: [
1599
+ ...limits.features
1600
+ ]
1601
+ },
1602
+ priceSnapshot: {
1603
+ currency: this.catalog.currency,
1604
+ billingCycle: cycle,
1605
+ subtotalNet,
1606
+ discountNet: 0,
1607
+ totalNet: subtotalNet,
1608
+ vatRate,
1609
+ totalGross: round22(subtotalNet * (1 + vatRate / 100))
1610
+ },
1611
+ lineItems
1612
+ };
1613
+ await this.contracts.create(data);
1614
+ this.entitlements.invalidateTenant(tenantId);
1615
+ }
1616
+ };
1617
+ SubscriptionContractFreezeService = _ts_decorate13([
1618
+ Injectable8(),
1619
+ _ts_param9(0, Inject9(PLAN_CATALOG_TOKEN)),
1620
+ _ts_param9(1, Inject9(EntitlementService)),
1621
+ _ts_param9(2, Inject9(SubscriptionContractService)),
1622
+ _ts_param9(3, Inject9(CONTRACT_FREEZE_PROJECT_KEY_TOKEN)),
1623
+ _ts_param9(4, Inject9(CONTRACT_FREEZE_SOURCE_PORT_TOKEN)),
1624
+ _ts_metadata9("design:type", Function),
1625
+ _ts_metadata9("design:paramtypes", [
1626
+ typeof PlanCatalog === "undefined" ? Object : PlanCatalog,
1627
+ typeof EntitlementService === "undefined" ? Object : EntitlementService,
1628
+ typeof SubscriptionContractService === "undefined" ? Object : SubscriptionContractService,
1629
+ String,
1630
+ typeof ContractFreezeSourcePort === "undefined" ? Object : ContractFreezeSourcePort
1631
+ ])
1632
+ ], SubscriptionContractFreezeService);
1633
+ function round22(value) {
1634
+ return Math.round(value * 100) / 100;
1635
+ }
1636
+ __name(round22, "round2");
1637
+
1638
+ // src/billing/trial-carryover.ts
1639
+ var MS_PER_DAY = 864e5;
1640
+ function computeCarriedTrialEndsAt(currentTrialDays, newTrialDays, currentTrialEndsAt, now) {
1641
+ const daysRemaining = Math.max(0, Math.ceil((currentTrialEndsAt.getTime() - now.getTime()) / MS_PER_DAY));
1642
+ const usedDays = Math.max(0, currentTrialDays - daysRemaining);
1643
+ const newDays = Math.max(0, newTrialDays - usedDays);
1644
+ return new Date(now.getTime() + newDays * MS_PER_DAY);
1645
+ }
1646
+ __name(computeCarriedTrialEndsAt, "computeCarriedTrialEndsAt");
1647
+
1648
+ // src/billing/dto/tenant-billing.dto.ts
1649
+ import { IsBoolean as IsBoolean2, IsOptional as IsOptional2, IsString as IsString2, Matches } from "class-validator";
1650
+ function _ts_decorate14(decorators, target, key, desc) {
1651
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1652
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1653
+ 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;
1654
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1655
+ }
1656
+ __name(_ts_decorate14, "_ts_decorate");
1657
+ function _ts_metadata10(k, v) {
1658
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1659
+ }
1660
+ __name(_ts_metadata10, "_ts_metadata");
1661
+ var PLAN_OR_CYCLE_PATTERN = /^[A-Z][A-Z0-9_]*$/;
1662
+ var PreviewPlanChangeDto = class {
1663
+ static {
1664
+ __name(this, "PreviewPlanChangeDto");
1665
+ }
1666
+ plan;
1667
+ billingCycle;
1668
+ };
1669
+ _ts_decorate14([
1670
+ IsString2(),
1671
+ Matches(PLAN_OR_CYCLE_PATTERN, {
1672
+ message: "plan muss SCREAMING_SNAKE_CASE sein"
1673
+ }),
1674
+ _ts_metadata10("design:type", String)
1675
+ ], PreviewPlanChangeDto.prototype, "plan", void 0);
1676
+ _ts_decorate14([
1677
+ IsString2(),
1678
+ Matches(PLAN_OR_CYCLE_PATTERN, {
1679
+ message: "billingCycle muss SCREAMING_SNAKE_CASE sein (z. B. MONTHLY, YEARLY)"
1680
+ }),
1681
+ _ts_metadata10("design:type", String)
1682
+ ], PreviewPlanChangeDto.prototype, "billingCycle", void 0);
1683
+ var ChangePlanDto = class extends PreviewPlanChangeDto {
1684
+ static {
1685
+ __name(this, "ChangePlanDto");
1686
+ }
1687
+ /** Immediate change (true) vs. change at period end (false/undefined). */
1688
+ effectiveImmediately;
1689
+ };
1690
+ _ts_decorate14([
1691
+ IsOptional2(),
1692
+ IsBoolean2(),
1693
+ _ts_metadata10("design:type", Boolean)
1694
+ ], ChangePlanDto.prototype, "effectiveImmediately", void 0);
1695
+ var CancelSubscriptionDto = class {
1696
+ static {
1697
+ __name(this, "CancelSubscriptionDto");
1698
+ }
1699
+ /** true = cancel immediately (status CANCELED). false/undefined = at period end. */
1700
+ immediately;
1701
+ };
1702
+ _ts_decorate14([
1703
+ IsOptional2(),
1704
+ IsBoolean2(),
1705
+ _ts_metadata10("design:type", Boolean)
1706
+ ], CancelSubscriptionDto.prototype, "immediately", void 0);
1707
+
1708
+ // src/billing/dto/onboarding-subscription.dto.ts
1709
+ import { ArrayMaxSize, IsArray, IsOptional as IsOptional3, IsString as IsString3, IsUUID, Matches as Matches2 } from "class-validator";
1710
+ function _ts_decorate15(decorators, target, key, desc) {
1711
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1712
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1713
+ 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;
1714
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1715
+ }
1716
+ __name(_ts_decorate15, "_ts_decorate");
1717
+ function _ts_metadata11(k, v) {
1718
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1719
+ }
1720
+ __name(_ts_metadata11, "_ts_metadata");
1721
+ var PLAN_OR_CYCLE_PATTERN2 = /^[A-Z][A-Z0-9_]*$/;
1722
+ var PROMO_CODE_PATTERN = /^[A-Z0-9_-]{4,32}$/i;
1723
+ var CompleteOnboardingSubscriptionDto = class {
1724
+ static {
1725
+ __name(this, "CompleteOnboardingSubscriptionDto");
1726
+ }
1727
+ plan;
1728
+ billingCycle;
1729
+ promoCode;
1730
+ /**
1731
+ * Optional — UUIDs of the BundleVersions that should be booked
1732
+ * together with the plan (P11.7.3). Per bundle, the platform
1733
+ * default minimum term (12 months) is set. Bundles are added
1734
+ * best-effort **after** the plan change — an error on an individual
1735
+ * bundle (e.g. incompatible with the chosen plan) lands as a
1736
+ * warning in the response, without rolling back the plan change.
1737
+ */
1738
+ bundleVersionIds;
1739
+ };
1740
+ _ts_decorate15([
1741
+ IsString3(),
1742
+ Matches2(PLAN_OR_CYCLE_PATTERN2, {
1743
+ message: "plan muss SCREAMING_SNAKE_CASE sein"
1744
+ }),
1745
+ _ts_metadata11("design:type", String)
1746
+ ], CompleteOnboardingSubscriptionDto.prototype, "plan", void 0);
1747
+ _ts_decorate15([
1748
+ IsString3(),
1749
+ Matches2(PLAN_OR_CYCLE_PATTERN2, {
1750
+ message: "billingCycle muss SCREAMING_SNAKE_CASE sein (z. B. MONTHLY, YEARLY)"
1751
+ }),
1752
+ _ts_metadata11("design:type", String)
1753
+ ], CompleteOnboardingSubscriptionDto.prototype, "billingCycle", void 0);
1754
+ _ts_decorate15([
1755
+ IsOptional3(),
1756
+ IsString3(),
1757
+ Matches2(PROMO_CODE_PATTERN, {
1758
+ message: 'promoCode muss aus A\u2013Z, 0\u20139, "-", "_" bestehen (4\u201332 Zeichen)'
1759
+ }),
1760
+ _ts_metadata11("design:type", String)
1761
+ ], CompleteOnboardingSubscriptionDto.prototype, "promoCode", void 0);
1762
+ _ts_decorate15([
1763
+ IsOptional3(),
1764
+ IsArray(),
1765
+ ArrayMaxSize(10),
1766
+ IsUUID("all", {
1767
+ each: true
1768
+ }),
1769
+ _ts_metadata11("design:type", Array)
1770
+ ], CompleteOnboardingSubscriptionDto.prototype, "bundleVersionIds", void 0);
1771
+
1772
+ // src/billing/tenant-billing.controller.ts
1773
+ import { BadRequestException, Body as Body2, Controller as Controller3, ForbiddenException as ForbiddenException4, Get as Get2, HttpCode, HttpStatus as HttpStatus2, Inject as Inject11, Logger as Logger4, NotFoundException as NotFoundException3, Optional as Optional8, Post as Post2, Req, UseGuards as UseGuards2 } from "@nestjs/common";
1774
+
1775
+ // src/billing/subscription-bundles.service.ts
1776
+ import { Inject as Inject10, Injectable as Injectable9, NotFoundException as NotFoundException2, Optional as Optional7, UnprocessableEntityException } from "@nestjs/common";
1777
+ function _ts_decorate16(decorators, target, key, desc) {
1778
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1779
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1780
+ 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;
1781
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1782
+ }
1783
+ __name(_ts_decorate16, "_ts_decorate");
1784
+ function _ts_metadata12(k, v) {
1785
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1786
+ }
1787
+ __name(_ts_metadata12, "_ts_metadata");
1788
+ function _ts_param10(paramIndex, decorator) {
1789
+ return function(target, key) {
1790
+ decorator(target, key, paramIndex);
1791
+ };
1792
+ }
1793
+ __name(_ts_param10, "_ts_param");
1794
+ var SubscriptionBundlesService = class {
1795
+ static {
1796
+ __name(this, "SubscriptionBundlesService");
1797
+ }
1798
+ repo;
1799
+ bundles;
1800
+ blockedBundles;
1801
+ defaultMinTermMonths;
1802
+ constructor(repo, bundles, config = {}, blockedBundles = null) {
1803
+ this.repo = repo;
1804
+ this.bundles = bundles;
1805
+ this.blockedBundles = blockedBundles;
1806
+ this.defaultMinTermMonths = config.defaultMinimumTermMonths ?? 12;
1807
+ }
1808
+ /** All bundle bookings of a subscription (for the "My Bundles" page). */
1809
+ async listForSubscription(subscriptionId) {
1810
+ const records = await this.repo.listBySubscription(subscriptionId);
1811
+ return Promise.all(records.map(async (r) => {
1812
+ const bv = await this.bundles.findVersionById(r.bundleVersionId);
1813
+ return {
1814
+ ...r,
1815
+ bundleKey: bv?.bundleKey ?? null,
1816
+ label: bv?.label ?? null,
1817
+ monthlyNet: bv?.monthlyNet ?? null
1818
+ };
1819
+ }));
1820
+ }
1821
+ async addBundleToSubscription(input) {
1822
+ const bundleVersion = await this.bundles.findVersionById(input.bundleVersionId);
1823
+ if (!bundleVersion) {
1824
+ throw new NotFoundException2(`BundleVersion '${input.bundleVersionId}' nicht gefunden`);
1825
+ }
1826
+ if (bundleVersion.publishedAt === null) {
1827
+ throw new UnprocessableEntityException({
1828
+ code: "BUNDLE_VERSION_NOT_PUBLISHED",
1829
+ message: `BundleVersion '${input.bundleVersionId}' ist nicht published und kann nicht gebucht werden.`
1830
+ });
1831
+ }
1832
+ if (bundleVersion.supersededAt !== null) {
1833
+ throw new UnprocessableEntityException({
1834
+ code: "BUNDLE_VERSION_SUPERSEDED",
1835
+ message: `BundleVersion '${input.bundleVersionId}' wurde durch eine Nachfolge-Version abgel\xF6st.`
1836
+ });
1837
+ }
1838
+ if (this.blockedBundles?.bundleKeys?.includes(bundleVersion.bundleKey)) {
1839
+ throw new UnprocessableEntityException({
1840
+ code: "BUNDLE_NOT_SELF_SERVICE",
1841
+ message: `Bundle '${bundleVersion.bundleKey}' wird nur per Sondervertrag aktiviert. Bitte den Vertragsbetreuer kontaktieren.`
1842
+ });
1843
+ }
1844
+ const planIds = bundleVersion.compatibility?.planIds ?? [];
1845
+ if (planIds.length > 0 && !planIds.includes(input.currentPlanKey)) {
1846
+ throw new UnprocessableEntityException({
1847
+ code: "BUNDLE_INCOMPATIBLE_WITH_PLAN",
1848
+ message: `BundleVersion '${input.bundleVersionId}' ist nicht mit Plan '${input.currentPlanKey}' kompatibel. Erlaubt: [${planIds.join(", ")}].`
1849
+ });
1850
+ }
1851
+ const active = await this.repo.listActiveBySubscription(input.subscriptionId);
1852
+ if (active.some((b) => b.bundleVersionId === input.bundleVersionId)) {
1853
+ throw new UnprocessableEntityException({
1854
+ code: "BUNDLE_ALREADY_SUBSCRIBED",
1855
+ message: `Subscription '${input.subscriptionId}' hat dieses Bundle bereits aktiv gebucht.`
1856
+ });
1857
+ }
1858
+ const startedAt = input.startedAt ?? /* @__PURE__ */ new Date();
1859
+ const minimumTermMonths = input.minimumTermMonths ?? this.defaultMinTermMonths;
1860
+ const minimumTermEndsAt = minimumTermMonths > 0 ? addMonths(startedAt, minimumTermMonths) : null;
1861
+ return this.repo.add({
1862
+ subscriptionId: input.subscriptionId,
1863
+ bundleVersionId: input.bundleVersionId,
1864
+ startedAt,
1865
+ minimumTermEndsAt
1866
+ });
1867
+ }
1868
+ async cancelBundleFromSubscription(input) {
1869
+ const existing = await this.repo.findById(input.subscriptionBundleId);
1870
+ if (!existing) {
1871
+ throw new NotFoundException2(`SubscriptionBundle '${input.subscriptionBundleId}' nicht gefunden`);
1872
+ }
1873
+ if (existing.canceledAt !== null) {
1874
+ throw new UnprocessableEntityException({
1875
+ code: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELED",
1876
+ message: `SubscriptionBundle '${input.subscriptionBundleId}' ist bereits gek\xFCndigt.`
1877
+ });
1878
+ }
1879
+ const canceledAt = input.canceledAt ?? /* @__PURE__ */ new Date();
1880
+ const canceledEffectiveAt = resolveBundleCancelEffectiveAt({
1881
+ canceledAt,
1882
+ currentPeriodEnd: input.currentPeriodEnd ?? null,
1883
+ minimumTermEndsAt: existing.minimumTermEndsAt
1884
+ });
1885
+ return this.repo.cancel(input.subscriptionBundleId, {
1886
+ canceledAt,
1887
+ canceledEffectiveAt
1888
+ });
1889
+ }
1890
+ /**
1891
+ * "Undo cancellation" — only as long as the cancellation is not yet effective
1892
+ * (the bundle runs until `canceledEffectiveAt`). After that, re-booking is the way.
1893
+ */
1894
+ async reactivateBundle(subscriptionBundleId) {
1895
+ const existing = await this.repo.findById(subscriptionBundleId);
1896
+ if (!existing) {
1897
+ throw new NotFoundException2(`SubscriptionBundle '${subscriptionBundleId}' nicht gefunden`);
1898
+ }
1899
+ if (existing.canceledAt === null) {
1900
+ throw new UnprocessableEntityException({
1901
+ code: "SUBSCRIPTION_BUNDLE_NOT_CANCELED",
1902
+ message: `SubscriptionBundle '${subscriptionBundleId}' ist nicht gek\xFCndigt.`
1903
+ });
1904
+ }
1905
+ if (existing.canceledEffectiveAt !== null && existing.canceledEffectiveAt <= /* @__PURE__ */ new Date()) {
1906
+ throw new UnprocessableEntityException({
1907
+ code: "SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE",
1908
+ message: "K\xFCndigung bereits wirksam \u2014 bitte das Bundle neu buchen."
1909
+ });
1910
+ }
1911
+ return this.repo.reactivate(subscriptionBundleId);
1912
+ }
1913
+ };
1914
+ SubscriptionBundlesService = _ts_decorate16([
1915
+ Injectable9(),
1916
+ _ts_param10(0, Inject10(SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN)),
1917
+ _ts_param10(1, Inject10(BUNDLE_REPOSITORY_TOKEN)),
1918
+ _ts_param10(2, Optional7()),
1919
+ _ts_param10(2, Inject10(SUBSCRIPTION_BUNDLE_CONFIG_TOKEN)),
1920
+ _ts_param10(3, Optional7()),
1921
+ _ts_param10(3, Inject10(SELF_SERVICE_BLOCKED_BUNDLES_TOKEN)),
1922
+ _ts_metadata12("design:type", Function),
1923
+ _ts_metadata12("design:paramtypes", [
1924
+ typeof SubscriptionBundleRepository === "undefined" ? Object : SubscriptionBundleRepository,
1925
+ typeof BundleRepository === "undefined" ? Object : BundleRepository,
1926
+ typeof SubscriptionBundleConfig === "undefined" ? Object : SubscriptionBundleConfig,
1927
+ Object
1928
+ ])
1929
+ ], SubscriptionBundlesService);
1930
+ function resolveBundleCancelEffectiveAt(input) {
1931
+ const periodEnd = input.currentPeriodEnd ?? input.canceledAt;
1932
+ const minTermEnd = input.minimumTermEndsAt ?? input.canceledAt;
1933
+ return periodEnd.getTime() >= minTermEnd.getTime() ? periodEnd : minTermEnd;
1934
+ }
1935
+ __name(resolveBundleCancelEffectiveAt, "resolveBundleCancelEffectiveAt");
1936
+ function addMonths(date, months) {
1937
+ const out = new Date(date.getTime());
1938
+ out.setUTCMonth(out.getUTCMonth() + months);
1939
+ return out;
1940
+ }
1941
+ __name(addMonths, "addMonths");
1942
+
1943
+ // src/billing/tenant-billing.controller.ts
1944
+ function _ts_decorate17(decorators, target, key, desc) {
1945
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1946
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1947
+ 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;
1948
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1949
+ }
1950
+ __name(_ts_decorate17, "_ts_decorate");
1951
+ function _ts_metadata13(k, v) {
1952
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1953
+ }
1954
+ __name(_ts_metadata13, "_ts_metadata");
1955
+ function _ts_param11(paramIndex, decorator) {
1956
+ return function(target, key) {
1957
+ decorator(target, key, paramIndex);
1958
+ };
1959
+ }
1960
+ __name(_ts_param11, "_ts_param");
1961
+ var TenantBillingController = class _TenantBillingController {
1962
+ static {
1963
+ __name(this, "TenantBillingController");
1964
+ }
1965
+ entitlements;
1966
+ planPreview;
1967
+ subscriptionUsage;
1968
+ usageSnapshot;
1969
+ subscriptionWrite;
1970
+ tenantIdResolver;
1971
+ userIdResolver;
1972
+ blockedPlans;
1973
+ promoCodes;
1974
+ auditService;
1975
+ userEmailResolver;
1976
+ auditContextResolver;
1977
+ subscriptionBundles;
1978
+ contractFreeze;
1979
+ trialProjection;
1980
+ constructor(entitlements, planPreview, subscriptionUsage, usageSnapshot, subscriptionWrite, tenantIdResolver = null, userIdResolver = null, blockedPlans = null, promoCodes = null, auditService = null, userEmailResolver = null, auditContextResolver = null, subscriptionBundles = null, contractFreeze = null, trialProjection = null) {
1981
+ this.entitlements = entitlements;
1982
+ this.planPreview = planPreview;
1983
+ this.subscriptionUsage = subscriptionUsage;
1984
+ this.usageSnapshot = usageSnapshot;
1985
+ this.subscriptionWrite = subscriptionWrite;
1986
+ this.tenantIdResolver = tenantIdResolver;
1987
+ this.userIdResolver = userIdResolver;
1988
+ this.blockedPlans = blockedPlans;
1989
+ this.promoCodes = promoCodes;
1990
+ this.auditService = auditService;
1991
+ this.userEmailResolver = userEmailResolver;
1992
+ this.auditContextResolver = auditContextResolver;
1993
+ this.subscriptionBundles = subscriptionBundles;
1994
+ this.contractFreeze = contractFreeze;
1995
+ this.trialProjection = trialProjection;
1996
+ }
1997
+ logger = new Logger4(_TenantBillingController.name);
1998
+ // ---------------------------------------------------------------------
1999
+ // Reads (Phase B) — all authenticated tenant users
2000
+ // ---------------------------------------------------------------------
2001
+ async getEntitlement(req) {
2002
+ const tenantId = this.requireTenantId(req);
2003
+ const limits = await this.entitlements.computeLimits(tenantId);
2004
+ return toEffectiveLimitsSnapshot(limits);
2005
+ }
2006
+ async getUsage(req) {
2007
+ const tenantId = this.requireTenantId(req);
2008
+ const [sub, limits, usageRaw] = await Promise.all([
2009
+ this.subscriptionUsage.findForTenant(tenantId),
2010
+ this.entitlements.computeLimits(tenantId),
2011
+ this.usageSnapshot.snapshot(tenantId)
2012
+ ]);
2013
+ if (!sub) {
2014
+ throw new NotFoundException3(`Keine Subscription f\xFCr Tenant ${tenantId}`);
2015
+ }
2016
+ const usage = {};
2017
+ for (const key of /* @__PURE__ */ new Set([
2018
+ ...Object.keys(limits.quotas),
2019
+ ...Object.keys(usageRaw)
2020
+ ])) {
2021
+ usage[key] = usageRaw[key] ?? 0;
2022
+ }
2023
+ return {
2024
+ plan: sub.plan,
2025
+ effectivePlan: limits.plan,
2026
+ billingCycle: sub.billingCycle,
2027
+ status: sub.status,
2028
+ isPilot: sub.isPilot,
2029
+ pilotEndsAt: sub.pilotEndsAt,
2030
+ trialEndsAt: sub.trialEndsAt,
2031
+ startedAt: sub.startedAt,
2032
+ currentPeriodStart: sub.currentPeriodStart,
2033
+ currentPeriodEnd: sub.currentPeriodEnd,
2034
+ pendingPlan: sub.pendingPlan,
2035
+ pendingBillingCycle: sub.pendingBillingCycle,
2036
+ pendingEffectiveAt: sub.pendingEffectiveAt,
2037
+ planVersion: sub.planVersion,
2038
+ pendingPlanVersion: sub.pendingPlanVersion,
2039
+ pendingPlanVersionEffectiveAt: sub.pendingPlanVersionEffectiveAt,
2040
+ pendingPlanVersionAccepted: sub.pendingPlanVersionAccepted,
2041
+ pendingPlanVersionAcceptedAt: sub.pendingPlanVersionAcceptedAt,
2042
+ limits: toEffectiveLimitsSnapshot(limits),
2043
+ usage,
2044
+ packageSnapshot: sub.packageSnapshot ?? null,
2045
+ checkoutOfferId: sub.checkoutOfferId ?? null
2046
+ };
2047
+ }
2048
+ // ---------------------------------------------------------------------
2049
+ // Plan preview (Phase C, read-only) — TENANT_ADMIN
2050
+ // ---------------------------------------------------------------------
2051
+ async previewPlanChange(req, dto) {
2052
+ const tenantId = this.requireTenantId(req);
2053
+ return this.planPreview.preview(tenantId, dto.plan, dto.billingCycle);
2054
+ }
2055
+ // ---------------------------------------------------------------------
2056
+ // Plan change — TENANT_ADMIN (cost-relevant)
2057
+ // ---------------------------------------------------------------------
2058
+ async changePlan(req, dto) {
2059
+ const tenantId = this.requireTenantId(req);
2060
+ const userId = this.requireUserId(req);
2061
+ const blockedTargets = this.blockedPlans?.asTarget ?? [];
2062
+ if (blockedTargets.includes(dto.plan)) {
2063
+ throw new ForbiddenException4(`${dto.plan} wird nicht per Self-Service aktiviert.`);
2064
+ }
2065
+ const sub = await this.subscriptionUsage.findForTenant(tenantId);
2066
+ if (!sub) throw new NotFoundException3(`Keine Subscription f\xFCr Tenant ${tenantId}`);
2067
+ const blockers = await this.planPreview.assertChangeAllowed(tenantId, dto.plan, dto.billingCycle);
2068
+ if (blockers.length > 0) {
2069
+ throw new BadRequestException({
2070
+ message: "Plan-Wechsel blockiert.",
2071
+ blockers
2072
+ });
2073
+ }
2074
+ if (dto.effectiveImmediately) {
2075
+ const wasTrial = sub.status === "TRIAL";
2076
+ const period = wasTrial ? null : initialPeriodWindow(/* @__PURE__ */ new Date(), dto.billingCycle);
2077
+ const trialEndsAt = wasTrial && this.trialProjection ? await this.trialProjection.projectTrialEndsAt({
2078
+ currentPlan: sub.plan,
2079
+ targetPlan: dto.plan,
2080
+ currentTrialEndsAt: sub.trialEndsAt,
2081
+ status: sub.status,
2082
+ now: /* @__PURE__ */ new Date()
2083
+ }) : void 0;
2084
+ const result = await this.subscriptionWrite.changePlanImmediate(tenantId, {
2085
+ planId: dto.plan,
2086
+ cycle: dto.billingCycle,
2087
+ periodStart: period?.start ?? null,
2088
+ periodEnd: period?.end ?? null,
2089
+ nextStatus: wasTrial ? null : "ACTIVE",
2090
+ trialEndsAt
2091
+ });
2092
+ this.entitlements.invalidateTenant(tenantId);
2093
+ await this.tryFreezeOnPlanChange(tenantId, dto.plan, dto.billingCycle, wasTrial);
2094
+ await this.auditLog(req, userId, "Subscription", tenantId, "CHANGE_PLAN", {
2095
+ fromPlan: sub.plan,
2096
+ fromCycle: sub.billingCycle,
2097
+ toPlan: result.plan,
2098
+ toCycle: result.billingCycle,
2099
+ immediate: true
2100
+ });
2101
+ return {
2102
+ plan: result.plan,
2103
+ billingCycle: result.billingCycle,
2104
+ immediate: true
2105
+ };
2106
+ }
2107
+ const effectiveAt = sub.status === "TRIAL" && sub.trialEndsAt ? sub.trialEndsAt : sub.currentPeriodEnd ?? /* @__PURE__ */ new Date();
2108
+ await this.subscriptionWrite.schedulePlanChange(tenantId, {
2109
+ pendingPlan: dto.plan,
2110
+ pendingBillingCycle: dto.billingCycle,
2111
+ pendingEffectiveAt: effectiveAt
2112
+ });
2113
+ this.entitlements.invalidateTenant(tenantId);
2114
+ await this.auditLog(req, userId, "Subscription", tenantId, "SCHEDULE_PLAN_CHANGE", {
2115
+ fromPlan: sub.plan,
2116
+ fromCycle: sub.billingCycle,
2117
+ pendingPlan: dto.plan,
2118
+ pendingCycle: dto.billingCycle,
2119
+ effectiveAt: effectiveAt.toISOString()
2120
+ });
2121
+ return {
2122
+ plan: sub.plan,
2123
+ billingCycle: sub.billingCycle,
2124
+ pendingPlan: dto.plan,
2125
+ pendingBillingCycle: dto.billingCycle,
2126
+ pendingEffectiveAt: effectiveAt,
2127
+ immediate: false
2128
+ };
2129
+ }
2130
+ // ---------------------------------------------------------------------
2131
+ // Onboarding — initial subscription from the configurator step
2132
+ // ---------------------------------------------------------------------
2133
+ async completeOnboardingSubscription(req, dto) {
2134
+ const tenantId = this.requireTenantId(req);
2135
+ const userId = this.requireUserId(req);
2136
+ const userEmail = this.resolveUserEmail(req);
2137
+ const warnings = [];
2138
+ const blockedTargets = this.blockedPlans?.asTarget ?? [];
2139
+ if (blockedTargets.includes(dto.plan)) {
2140
+ throw new ForbiddenException4(`${dto.plan} wird nicht per Self-Service aktiviert.`);
2141
+ }
2142
+ const sub = await this.subscriptionUsage.findForTenant(tenantId);
2143
+ if (!sub) throw new NotFoundException3(`Keine Subscription f\xFCr Tenant ${tenantId}`);
2144
+ const blockers = await this.planPreview.assertChangeAllowed(tenantId, dto.plan, dto.billingCycle);
2145
+ if (blockers.length > 0) {
2146
+ throw new BadRequestException({
2147
+ message: "Plan-Wechsel im Onboarding blockiert.",
2148
+ blockers
2149
+ });
2150
+ }
2151
+ const wasTrial = sub.status === "TRIAL";
2152
+ const period = wasTrial ? null : initialPeriodWindow(/* @__PURE__ */ new Date(), dto.billingCycle);
2153
+ const canRedeem = !!dto.promoCode && !!this.promoCodes && !!sub.id;
2154
+ const redeemPromoCallback = canRedeem ? async (tx, subscriptionId) => this.promoCodes.redeemInTransaction({
2155
+ code: dto.promoCode,
2156
+ subscriptionId,
2157
+ tenantId,
2158
+ email: userEmail ?? void 0
2159
+ }, tx) : null;
2160
+ let planResult;
2161
+ let promoRedemption = null;
2162
+ if (this.subscriptionWrite.applyOnboardingSelection) {
2163
+ try {
2164
+ const result = await this.subscriptionWrite.applyOnboardingSelection(tenantId, {
2165
+ planId: dto.plan,
2166
+ cycle: dto.billingCycle,
2167
+ periodStart: period?.start ?? null,
2168
+ periodEnd: period?.end ?? null,
2169
+ nextStatus: wasTrial ? null : "ACTIVE"
2170
+ }, redeemPromoCallback);
2171
+ planResult = {
2172
+ plan: result.plan,
2173
+ billingCycle: result.billingCycle
2174
+ };
2175
+ if (result.promoRedemption) {
2176
+ promoRedemption = this.toResponseRedemption(result.promoRedemption, dto.promoCode);
2177
+ } else if (dto.promoCode) {
2178
+ warnings.push(...this.collectPromoSkipReasons(dto.promoCode, sub));
2179
+ }
2180
+ } catch (err) {
2181
+ throw new BadRequestException({
2182
+ message: err instanceof Error ? `Onboarding-Anlage fehlgeschlagen: ${err.message}` : "Onboarding-Anlage fehlgeschlagen"
2183
+ });
2184
+ }
2185
+ } else {
2186
+ planResult = await this.subscriptionWrite.changePlanImmediate(tenantId, {
2187
+ planId: dto.plan,
2188
+ cycle: dto.billingCycle,
2189
+ periodStart: period?.start ?? null,
2190
+ periodEnd: period?.end ?? null,
2191
+ nextStatus: wasTrial ? null : "ACTIVE"
2192
+ });
2193
+ await this.tryFreezeOnPlanChange(tenantId, dto.plan, dto.billingCycle, wasTrial);
2194
+ if (dto.promoCode) {
2195
+ if (!this.promoCodes) {
2196
+ warnings.push("Promo-Code wurde gesendet, aber PromoCodesModule ist nicht geladen \u2014 der Code wurde NICHT eingel\xF6st.");
2197
+ } else if (!sub.id) {
2198
+ warnings.push("Promo-Code wurde gesendet, aber der Adapter liefert keine SubscriptionUsageRecord.id \u2014 der Code wurde NICHT eingel\xF6st.");
2199
+ } else {
2200
+ try {
2201
+ const redemption = await this.promoCodes.redeem({
2202
+ code: dto.promoCode,
2203
+ subscriptionId: sub.id,
2204
+ tenantId,
2205
+ email: userEmail ?? void 0
2206
+ });
2207
+ promoRedemption = this.toResponseRedemption(redemption, dto.promoCode);
2208
+ } catch (err) {
2209
+ const message = err instanceof Error ? err.message : "Unbekannter Fehler";
2210
+ warnings.push(`Promo-Code konnte nicht eingel\xF6st werden: ${message}`);
2211
+ }
2212
+ }
2213
+ }
2214
+ }
2215
+ this.entitlements.invalidateTenant(tenantId);
2216
+ await this.auditLog(req, userId, "Subscription", tenantId, "COMPLETE_ONBOARDING_SUBSCRIPTION", {
2217
+ plan: planResult.plan,
2218
+ billingCycle: planResult.billingCycle,
2219
+ promoCode: dto.promoCode ?? null,
2220
+ promoRedeemed: promoRedemption != null,
2221
+ warnings
2222
+ });
2223
+ const bundleVersionIds = dto.bundleVersionIds ?? [];
2224
+ let bundlesAdded = 0;
2225
+ if (bundleVersionIds.length > 0 && this.subscriptionBundles && sub.id) {
2226
+ for (const bundleVersionId of bundleVersionIds) {
2227
+ try {
2228
+ await this.subscriptionBundles.addBundleToSubscription({
2229
+ subscriptionId: sub.id,
2230
+ bundleVersionId,
2231
+ currentPlanKey: planResult.plan
2232
+ });
2233
+ bundlesAdded += 1;
2234
+ } catch (err) {
2235
+ warnings.push(`Bundle '${bundleVersionId}' konnte nicht gebucht werden: ` + (err instanceof Error ? err.message : String(err)));
2236
+ }
2237
+ }
2238
+ } else if (bundleVersionIds.length > 0 && !this.subscriptionBundles) {
2239
+ warnings.push("Bundle-Buchungen im Onboarding angefordert, aber SubscriptionBundleModule ist nicht im Konsumenten registriert. Bundles wurden nicht angelegt.");
2240
+ }
2241
+ return {
2242
+ plan: planResult.plan,
2243
+ // The adapter returns `string`; the UI expects `'MONTHLY' | 'YEARLY'`.
2244
+ // DTO pre-validation + the server-side PlanCatalog check
2245
+ // guarantee that only valid values reach this point.
2246
+ billingCycle: planResult.billingCycle,
2247
+ bundlesAdded,
2248
+ promoRedemption,
2249
+ warnings
2250
+ };
2251
+ }
2252
+ // ---------------------------------------------------------------------
2253
+ // Accept pending plan version — TENANT_ADMIN
2254
+ // ---------------------------------------------------------------------
2255
+ async acceptPendingPlanVersion(req) {
2256
+ const tenantId = this.requireTenantId(req);
2257
+ const userId = this.requireUserId(req);
2258
+ const sub = await this.subscriptionUsage.findForTenant(tenantId);
2259
+ if (!sub) throw new NotFoundException3(`Keine Subscription f\xFCr Tenant ${tenantId}`);
2260
+ if (!sub.pendingPlanVersion) {
2261
+ throw new BadRequestException("Es liegt aktuell keine Pending-Plan-Version zur Best\xE4tigung vor.");
2262
+ }
2263
+ const result = await this.subscriptionWrite.acceptPendingPlanVersion(tenantId, userId, /* @__PURE__ */ new Date());
2264
+ if (!result.alreadyAccepted) {
2265
+ this.entitlements.invalidateTenant(tenantId);
2266
+ await this.auditLog(req, userId, "Subscription", tenantId, "ACCEPT_PENDING_PLAN_VERSION", {
2267
+ pendingPlanVersionId: sub.pendingPlanVersion.id,
2268
+ pendingPlanId: sub.pendingPlanVersion.planId,
2269
+ pendingPlanVersion: sub.pendingPlanVersion.version,
2270
+ effectiveAt: result.effectiveAt?.toISOString() ?? null
2271
+ });
2272
+ }
2273
+ return {
2274
+ accepted: true,
2275
+ acceptedAt: result.acceptedAt,
2276
+ effectiveAt: result.effectiveAt,
2277
+ idempotent: result.alreadyAccepted
2278
+ };
2279
+ }
2280
+ // ---------------------------------------------------------------------
2281
+ // Cancel — TENANT_ADMIN
2282
+ // ---------------------------------------------------------------------
2283
+ async cancelSubscription(req, dto) {
2284
+ const tenantId = this.requireTenantId(req);
2285
+ const userId = this.requireUserId(req);
2286
+ const result = await this.subscriptionWrite.cancelSubscription(tenantId, !!dto.immediately, /* @__PURE__ */ new Date());
2287
+ this.entitlements.invalidateTenant(tenantId);
2288
+ await this.auditLog(req, userId, "Subscription", tenantId, "CANCEL_SUBSCRIPTION", {
2289
+ immediate: !!dto.immediately,
2290
+ canceledAt: result.canceledAt?.toISOString() ?? null,
2291
+ status: result.status
2292
+ });
2293
+ return {
2294
+ canceledAt: result.canceledAt,
2295
+ status: result.status,
2296
+ immediate: !!dto.immediately
2297
+ };
2298
+ }
2299
+ // ---------------------------------------------------------------------
2300
+ // Helpers
2301
+ // ---------------------------------------------------------------------
2302
+ requireTenantId(req) {
2303
+ const resolver = this.tenantIdResolver ?? ((r) => r.user?.tenantId ?? null);
2304
+ const tenantId = resolver(req);
2305
+ if (!tenantId) {
2306
+ throw new NotFoundException3("Tenant-ID nicht im Request gefunden");
2307
+ }
2308
+ return tenantId;
2309
+ }
2310
+ requireUserId(req) {
2311
+ const resolver = this.userIdResolver ?? ((r) => r.user?.sub ?? r.user?.id ?? null);
2312
+ const userId = resolver(req);
2313
+ if (!userId) {
2314
+ throw new NotFoundException3("User-ID nicht im Request gefunden");
2315
+ }
2316
+ return userId;
2317
+ }
2318
+ buildActor(req, userId) {
2319
+ const email = this.resolveUserEmail(req) ?? "unknown";
2320
+ const contextResolver = this.auditContextResolver ?? ((r) => {
2321
+ const headers = r.headers;
2322
+ const sid = headers?.["x-session-id"];
2323
+ if (Array.isArray(sid)) return sid[0] ?? null;
2324
+ return sid ?? null;
2325
+ });
2326
+ const context = contextResolver(req) ?? "tenant-self-service";
2327
+ return {
2328
+ userId,
2329
+ email,
2330
+ source: "web",
2331
+ context
2332
+ };
2333
+ }
2334
+ resolveUserEmail(req) {
2335
+ const emailResolver = this.userEmailResolver ?? ((r) => r.user?.email ?? null);
2336
+ return emailResolver(req) ?? null;
2337
+ }
2338
+ toResponseRedemption(redemption, code) {
2339
+ return {
2340
+ code: code.toUpperCase(),
2341
+ discount: {
2342
+ valueType: redemption.appliedValueType,
2343
+ value: String(redemption.appliedValue),
2344
+ durationType: redemption.appliedDurationType,
2345
+ durationValue: redemption.appliedDurationValue
2346
+ },
2347
+ startsAt: redemption.startsAt.toISOString(),
2348
+ endsAt: redemption.endsAt ? redemption.endsAt.toISOString() : null
2349
+ };
2350
+ }
2351
+ collectPromoSkipReasons(promoCode, sub) {
2352
+ const reasons = [];
2353
+ if (!this.promoCodes) {
2354
+ reasons.push(`Promo-Code "${promoCode}" wurde gesendet, aber PromoCodesModule ist nicht geladen \u2014 der Code wurde NICHT eingel\xF6st.`);
2355
+ }
2356
+ if (!sub.id) {
2357
+ reasons.push(`Promo-Code "${promoCode}" wurde gesendet, aber der Adapter liefert keine SubscriptionUsageRecord.id \u2014 der Code wurde NICHT eingel\xF6st.`);
2358
+ }
2359
+ return reasons;
2360
+ }
2361
+ /**
2362
+ * #18: contract freeze after the plan change — non-fatal, only outside of
2363
+ * the trial (during a trial the trial entitlements apply, not the booked plan;
2364
+ * the freeze happens on the transition to ACTIVE, i.e. on materialization).
2365
+ * Without a configured `contractFreeze` hook the call is a no-op.
2366
+ */
2367
+ async tryFreezeOnPlanChange(tenantId, plan, cycle, wasTrial) {
2368
+ if (wasTrial || !this.contractFreeze) return;
2369
+ try {
2370
+ await this.contractFreeze.freezeOnPlanChange(tenantId, plan, cycle, /* @__PURE__ */ new Date());
2371
+ } catch (err) {
2372
+ this.logger.error(`Contract-Freeze nach Plan-Wechsel fehlgeschlagen (tenant ${tenantId}): ${String(err)}`);
2373
+ }
2374
+ }
2375
+ /**
2376
+ * Audit-log helper — writes best-effort, does not block the response path.
2377
+ * If `AdminAuditService` is not injected (e.g. a minimal deploy without
2378
+ * AdminModule), the call is silently discarded.
2379
+ */
2380
+ async auditLog(req, userId, entity, entityId, action, changes) {
2381
+ if (!this.auditService) return;
2382
+ try {
2383
+ await this.auditService.log({
2384
+ actor: this.buildActor(req, userId),
2385
+ entity,
2386
+ entityId,
2387
+ action,
2388
+ changes
2389
+ });
2390
+ } catch {
2391
+ }
2392
+ }
2393
+ };
2394
+ _ts_decorate17([
2395
+ Get2("entitlement"),
2396
+ _ts_param11(0, Req()),
2397
+ _ts_metadata13("design:type", Function),
2398
+ _ts_metadata13("design:paramtypes", [
2399
+ typeof RequestLike === "undefined" ? Object : RequestLike
2400
+ ]),
2401
+ _ts_metadata13("design:returntype", Promise)
2402
+ ], TenantBillingController.prototype, "getEntitlement", null);
2403
+ _ts_decorate17([
2404
+ Get2("usage"),
2405
+ _ts_param11(0, Req()),
2406
+ _ts_metadata13("design:type", Function),
2407
+ _ts_metadata13("design:paramtypes", [
2408
+ typeof RequestLike === "undefined" ? Object : RequestLike
2409
+ ]),
2410
+ _ts_metadata13("design:returntype", Promise)
2411
+ ], TenantBillingController.prototype, "getUsage", null);
2412
+ _ts_decorate17([
2413
+ Post2("plan/preview"),
2414
+ HttpCode(HttpStatus2.OK),
2415
+ UseGuards2(TenantAdminGuard),
2416
+ _ts_param11(0, Req()),
2417
+ _ts_param11(1, Body2()),
2418
+ _ts_metadata13("design:type", Function),
2419
+ _ts_metadata13("design:paramtypes", [
2420
+ typeof RequestLike === "undefined" ? Object : RequestLike,
2421
+ typeof PreviewPlanChangeDto === "undefined" ? Object : PreviewPlanChangeDto
2422
+ ]),
2423
+ _ts_metadata13("design:returntype", Promise)
2424
+ ], TenantBillingController.prototype, "previewPlanChange", null);
2425
+ _ts_decorate17([
2426
+ Post2("plan"),
2427
+ UseGuards2(TenantAdminGuard),
2428
+ _ts_param11(0, Req()),
2429
+ _ts_param11(1, Body2()),
2430
+ _ts_metadata13("design:type", Function),
2431
+ _ts_metadata13("design:paramtypes", [
2432
+ typeof RequestLike === "undefined" ? Object : RequestLike,
2433
+ typeof ChangePlanDto === "undefined" ? Object : ChangePlanDto
2434
+ ]),
2435
+ _ts_metadata13("design:returntype", Promise)
2436
+ ], TenantBillingController.prototype, "changePlan", null);
2437
+ _ts_decorate17([
2438
+ Post2("onboarding/initial-subscription"),
2439
+ UseGuards2(TenantAdminGuard),
2440
+ _ts_param11(0, Req()),
2441
+ _ts_param11(1, Body2()),
2442
+ _ts_metadata13("design:type", Function),
2443
+ _ts_metadata13("design:paramtypes", [
2444
+ typeof RequestLike === "undefined" ? Object : RequestLike,
2445
+ typeof CompleteOnboardingSubscriptionDto === "undefined" ? Object : CompleteOnboardingSubscriptionDto
2446
+ ]),
2447
+ _ts_metadata13("design:returntype", Promise)
2448
+ ], TenantBillingController.prototype, "completeOnboardingSubscription", null);
2449
+ _ts_decorate17([
2450
+ Post2("subscription/accept-pending-version"),
2451
+ UseGuards2(TenantAdminGuard),
2452
+ _ts_param11(0, Req()),
2453
+ _ts_metadata13("design:type", Function),
2454
+ _ts_metadata13("design:paramtypes", [
2455
+ typeof RequestLike === "undefined" ? Object : RequestLike
2456
+ ]),
2457
+ _ts_metadata13("design:returntype", Promise)
2458
+ ], TenantBillingController.prototype, "acceptPendingPlanVersion", null);
2459
+ _ts_decorate17([
2460
+ Post2("cancel"),
2461
+ UseGuards2(TenantAdminGuard),
2462
+ _ts_param11(0, Req()),
2463
+ _ts_param11(1, Body2()),
2464
+ _ts_metadata13("design:type", Function),
2465
+ _ts_metadata13("design:paramtypes", [
2466
+ typeof RequestLike === "undefined" ? Object : RequestLike,
2467
+ typeof CancelSubscriptionDto === "undefined" ? Object : CancelSubscriptionDto
2468
+ ]),
2469
+ _ts_metadata13("design:returntype", Promise)
2470
+ ], TenantBillingController.prototype, "cancelSubscription", null);
2471
+ TenantBillingController = _ts_decorate17([
2472
+ Controller3("billing"),
2473
+ UseGuards2(ComposedTenantAuthGuard),
2474
+ _ts_param11(0, Inject11(EntitlementService)),
2475
+ _ts_param11(1, Inject11(PlanChangePreviewService)),
2476
+ _ts_param11(2, Inject11(SUBSCRIPTION_USAGE_PORT_TOKEN)),
2477
+ _ts_param11(3, Inject11(USAGE_SNAPSHOT_PORT_TOKEN)),
2478
+ _ts_param11(4, Inject11(SUBSCRIPTION_WRITE_PORT_TOKEN)),
2479
+ _ts_param11(5, Optional8()),
2480
+ _ts_param11(5, Inject11(TENANT_ID_RESOLVER_TOKEN)),
2481
+ _ts_param11(6, Optional8()),
2482
+ _ts_param11(6, Inject11(USER_ID_RESOLVER_TOKEN)),
2483
+ _ts_param11(7, Optional8()),
2484
+ _ts_param11(7, Inject11(SELF_SERVICE_BLOCKED_PLANS_TOKEN)),
2485
+ _ts_param11(8, Optional8()),
2486
+ _ts_param11(8, Inject11(PromoCodesService)),
2487
+ _ts_param11(9, Optional8()),
2488
+ _ts_param11(9, Inject11(AdminAuditService)),
2489
+ _ts_param11(10, Optional8()),
2490
+ _ts_param11(10, Inject11(USER_EMAIL_RESOLVER_TOKEN)),
2491
+ _ts_param11(11, Optional8()),
2492
+ _ts_param11(11, Inject11(AUDIT_CONTEXT_RESOLVER_TOKEN)),
2493
+ _ts_param11(12, Optional8()),
2494
+ _ts_param11(12, Inject11(SubscriptionBundlesService)),
2495
+ _ts_param11(13, Optional8()),
2496
+ _ts_param11(13, Inject11(CONTRACT_FREEZE_PORT_TOKEN)),
2497
+ _ts_param11(14, Optional8()),
2498
+ _ts_param11(14, Inject11(TRIAL_PROJECTION_PORT_TOKEN)),
2499
+ _ts_metadata13("design:type", Function),
2500
+ _ts_metadata13("design:paramtypes", [
2501
+ typeof EntitlementService === "undefined" ? Object : EntitlementService,
2502
+ typeof PlanChangePreviewService === "undefined" ? Object : PlanChangePreviewService,
2503
+ typeof SubscriptionUsagePort === "undefined" ? Object : SubscriptionUsagePort,
2504
+ typeof UsageSnapshotPort === "undefined" ? Object : UsageSnapshotPort,
2505
+ typeof TenantSubscriptionWritePort === "undefined" ? Object : TenantSubscriptionWritePort,
2506
+ Object,
2507
+ Object,
2508
+ Object,
2509
+ Object,
2510
+ Object,
2511
+ Object,
2512
+ Object,
2513
+ Object,
2514
+ Object,
2515
+ Object
2516
+ ])
2517
+ ], TenantBillingController);
2518
+
2519
+ // src/billing/tenant-billing.module.ts
2520
+ import { Module as Module3 } from "@nestjs/common";
2521
+ function _ts_decorate18(decorators, target, key, desc) {
2522
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2523
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2524
+ 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;
2525
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2526
+ }
2527
+ __name(_ts_decorate18, "_ts_decorate");
2528
+ var TenantBillingModule = class _TenantBillingModule {
2529
+ static {
2530
+ __name(this, "TenantBillingModule");
2531
+ }
2532
+ static forRoot(options) {
2533
+ const providers = [
2534
+ asProvider(TENANT_AUTH_GUARDS_TOKEN, options.authGuards),
2535
+ asProvider(SUBSCRIPTION_USAGE_PORT_TOKEN, options.subscriptionUsagePort),
2536
+ asProvider(USAGE_SNAPSHOT_PORT_TOKEN, options.usageSnapshotPort),
2537
+ asProvider(SUBSCRIPTION_WRITE_PORT_TOKEN, options.subscriptionWritePort),
2538
+ ComposedTenantAuthGuard,
2539
+ TenantAdminGuard,
2540
+ PlanChangePreviewService
2541
+ ];
2542
+ if (options.selfServiceBlockedPlans) {
2543
+ providers.push({
2544
+ provide: SELF_SERVICE_BLOCKED_PLANS_TOKEN,
2545
+ useValue: options.selfServiceBlockedPlans
2546
+ });
2547
+ }
2548
+ if (options.trialProjectionPort) {
2549
+ providers.push(asProvider(TRIAL_PROJECTION_PORT_TOKEN, options.trialProjectionPort));
2550
+ }
2551
+ const hasPendingPlanQueryPort = Boolean(options.pendingPlanQueryPort);
2552
+ if (options.pendingPlanQueryPort) {
2553
+ providers.push(asProvider(PENDING_PLAN_QUERY_PORT_TOKEN, options.pendingPlanQueryPort), PendingPlanMaterializationService);
2554
+ }
2555
+ const hasContractFreeze = Boolean(options.contractFreeze);
2556
+ if (options.contractFreeze) {
2557
+ providers.push({
2558
+ provide: CONTRACT_FREEZE_PROJECT_KEY_TOKEN,
2559
+ useValue: options.contractFreeze.projectKey
2560
+ }, asProvider(CONTRACT_FREEZE_SOURCE_PORT_TOKEN, options.contractFreeze.sourcePort), asProvider(SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN, options.contractFreeze.subscriptionContractRepository), SubscriptionContractService, {
2561
+ provide: CONTRACT_FREEZE_PORT_TOKEN,
2562
+ useClass: SubscriptionContractFreezeService
2563
+ });
2564
+ }
2565
+ if (options.tenantIdResolver) {
2566
+ providers.push({
2567
+ provide: TENANT_ID_RESOLVER_TOKEN,
2568
+ useValue: options.tenantIdResolver
2569
+ });
2570
+ }
2571
+ if (options.userIdResolver) {
2572
+ providers.push({
2573
+ provide: USER_ID_RESOLVER_TOKEN,
2574
+ useValue: options.userIdResolver
2575
+ });
2576
+ }
2577
+ if (options.userEmailResolver) {
2578
+ providers.push({
2579
+ provide: USER_EMAIL_RESOLVER_TOKEN,
2580
+ useValue: options.userEmailResolver
2581
+ });
2582
+ }
2583
+ if (options.auditContextResolver) {
2584
+ providers.push({
2585
+ provide: AUDIT_CONTEXT_RESOLVER_TOKEN,
2586
+ useValue: options.auditContextResolver
2587
+ });
2588
+ }
2589
+ if (options.extraProviders) providers.push(...options.extraProviders);
2590
+ return {
2591
+ module: _TenantBillingModule,
2592
+ global: options.global ?? false,
2593
+ imports: options.imports ?? [],
2594
+ controllers: [
2595
+ TenantBillingController
2596
+ ],
2597
+ providers,
2598
+ exports: [
2599
+ ComposedTenantAuthGuard,
2600
+ TenantAdminGuard,
2601
+ PlanChangePreviewService,
2602
+ SUBSCRIPTION_USAGE_PORT_TOKEN,
2603
+ USAGE_SNAPSHOT_PORT_TOKEN,
2604
+ SUBSCRIPTION_WRITE_PORT_TOKEN,
2605
+ ...hasPendingPlanQueryPort ? [
2606
+ PendingPlanMaterializationService
2607
+ ] : [],
2608
+ ...hasContractFreeze ? [
2609
+ CONTRACT_FREEZE_PORT_TOKEN
2610
+ ] : []
2611
+ ]
2612
+ };
2613
+ }
2614
+ };
2615
+ TenantBillingModule = _ts_decorate18([
2616
+ Module3({})
2617
+ ], TenantBillingModule);
2618
+
2619
+ // src/billing/configurator-catalog-builder.ts
2620
+ import { Injectable as Injectable10 } from "@nestjs/common";
2621
+ function _ts_decorate19(decorators, target, key, desc) {
2622
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2623
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2624
+ 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;
2625
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2626
+ }
2627
+ __name(_ts_decorate19, "_ts_decorate");
2628
+ var ConfiguratorCatalogBuilder = class {
2629
+ static {
2630
+ __name(this, "ConfiguratorCatalogBuilder");
2631
+ }
2632
+ async build(input) {
2633
+ const { sources, marketing } = input;
2634
+ const planRows = await sources.listLivePlans();
2635
+ const planMarketingByPlanId = new Map(marketing.listPlanMarketing().map((m) => [
2636
+ m.planId,
2637
+ m
2638
+ ]));
2639
+ return {
2640
+ cycleDiscount: marketing.getCycleDiscount(),
2641
+ currency: marketing.getCurrency(),
2642
+ vatRate: marketing.getVatRate(),
2643
+ models: planRows.filter((row) => row.marketed).map((row) => buildModel(row, planMarketingByPlanId)).filter((m) => m !== null)
2644
+ };
2645
+ }
2646
+ };
2647
+ ConfiguratorCatalogBuilder = _ts_decorate19([
2648
+ Injectable10()
2649
+ ], ConfiguratorCatalogBuilder);
2650
+ function buildModel(row, marketing) {
2651
+ const m = marketing.get(row.planId);
2652
+ if (!m) return null;
2653
+ return {
2654
+ id: row.planId.toLowerCase(),
2655
+ code: m.code,
2656
+ name: m.name,
2657
+ glyph: m.glyph,
2658
+ tagline: m.tagline,
2659
+ planId: row.planId,
2660
+ monthlyNet: row.monthlyNet,
2661
+ yearlyNet: row.yearlyNet,
2662
+ tags: m.tags,
2663
+ includedFeatureKeys: row.features,
2664
+ quotaBase: normalizeQuotas(row.quotas),
2665
+ ...m.popular ? {
2666
+ popular: true
2667
+ } : {}
2668
+ };
2669
+ }
2670
+ __name(buildModel, "buildModel");
2671
+ function normalizeQuotas(quotas) {
2672
+ const out = {};
2673
+ for (const [k, v] of Object.entries(quotas)) {
2674
+ out[k] = v === -1 ? Number.MAX_SAFE_INTEGER : v;
2675
+ }
2676
+ return out;
2677
+ }
2678
+ __name(normalizeQuotas, "normalizeQuotas");
2679
+
2680
+ // src/billing/subscription-bundle-preview.service.ts
2681
+ import { Inject as Inject12, Injectable as Injectable11, NotFoundException as NotFoundException4, Optional as Optional9 } from "@nestjs/common";
2682
+ import { buildFeatureRequiresIndex as buildFeatureRequiresIndex3, collectUnsatisfiedRequires as collectUnsatisfiedRequires3 } from "@saasicat/types";
2683
+ function _ts_decorate20(decorators, target, key, desc) {
2684
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2685
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2686
+ 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;
2687
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2688
+ }
2689
+ __name(_ts_decorate20, "_ts_decorate");
2690
+ function _ts_metadata14(k, v) {
2691
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
2692
+ }
2693
+ __name(_ts_metadata14, "_ts_metadata");
2694
+ function _ts_param12(paramIndex, decorator) {
2695
+ return function(target, key) {
2696
+ decorator(target, key, paramIndex);
2697
+ };
2698
+ }
2699
+ __name(_ts_param12, "_ts_param");
2700
+ var SubscriptionBundlePreviewService = class {
2701
+ static {
2702
+ __name(this, "SubscriptionBundlePreviewService");
2703
+ }
2704
+ subscriptionBundles;
2705
+ bundles;
2706
+ plans;
2707
+ catalogEntries;
2708
+ blockedBundles;
2709
+ defaultMinTermMonths;
2710
+ constructor(subscriptionBundles, bundles, plans = null, catalogEntries = null, blockedBundles = null, config = {}) {
2711
+ this.subscriptionBundles = subscriptionBundles;
2712
+ this.bundles = bundles;
2713
+ this.plans = plans;
2714
+ this.catalogEntries = catalogEntries;
2715
+ this.blockedBundles = blockedBundles;
2716
+ this.defaultMinTermMonths = config.defaultMinimumTermMonths ?? 12;
2717
+ }
2718
+ async previewAdd(ctx, input, now = /* @__PURE__ */ new Date()) {
2719
+ const bundleVersion = await this.bundles.findVersionById(input.bundleVersionId);
2720
+ if (!bundleVersion) {
2721
+ throw new NotFoundException4(`BundleVersion '${input.bundleVersionId}' nicht gefunden`);
2722
+ }
2723
+ const blockers = [];
2724
+ const warnings = [];
2725
+ this.collectBookabilityBlockers(bundleVersion, ctx.currentPlanKey, blockers);
2726
+ const activeBundleVersions = await this.loadActiveBundleVersions(ctx.subscriptionId);
2727
+ if (activeBundleVersions.some((bv) => bv.id === bundleVersion.id)) {
2728
+ blockers.push({
2729
+ code: "BUNDLE_ALREADY_SUBSCRIBED",
2730
+ message: "Dieses Bundle ist bereits aktiv gebucht."
2731
+ });
2732
+ }
2733
+ const planFeatures = new Set(await this.resolvePlanFeatures(ctx.currentPlanKey, now));
2734
+ const redundantFeatures = this.collectRedundantFeatures(bundleVersion, planFeatures, ctx.currentPlanKey, activeBundleVersions);
2735
+ if (redundantFeatures.length > 0) {
2736
+ warnings.push({
2737
+ code: "REDUNDANT_FEATURES",
2738
+ message: `${redundantFeatures.length} Feature${redundantFeatures.length === 1 ? " ist" : "s sind"} bereits im Plan oder einem anderen gebuchten Bundle enthalten \u2014 das Bundle w\xFCrde daf\xFCr doppelt bezahlt.`
2739
+ });
2740
+ }
2741
+ const missingRequires = await this.collectMissingRequires(bundleVersion, planFeatures, activeBundleVersions);
2742
+ if (missingRequires.length > 0) {
2743
+ blockers.push({
2744
+ code: "BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED",
2745
+ message: `Das Bundle ben\xF6tigt [${missingRequires.join(", ")}] \u2014 weder im Plan noch in den aktiven Bundles enthalten.`
2746
+ });
2747
+ }
2748
+ const priceNet = resolveBundlePriceNet(bundleVersion, ctx.currentPlanKey, ctx.billingCycle);
2749
+ const proration = ctx.status !== "TRIAL" && priceNet !== null ? computeProration({
2750
+ periodStart: ctx.currentPeriodStart ?? ctx.startedAt ?? now,
2751
+ periodEnd: ctx.currentPeriodEnd ?? periodEndAfter(ctx.startedAt, ctx.billingCycle, now),
2752
+ now,
2753
+ currentPriceNet: 0,
2754
+ targetPriceNet: priceNet
2755
+ }) : null;
2756
+ const minimumTermMonths = input.minimumTermMonths ?? this.defaultMinTermMonths;
2757
+ return {
2758
+ action: "add",
2759
+ bundle: toSnapshot(bundleVersion),
2760
+ billingCycle: ctx.billingCycle,
2761
+ proration,
2762
+ nextPeriodPriceNet: priceNet,
2763
+ minimumTermMonths,
2764
+ minimumTermEndsAt: minimumTermMonths > 0 ? addMonths(now, minimumTermMonths) : null,
2765
+ redundantFeatures,
2766
+ missingRequires,
2767
+ blockers,
2768
+ warnings
2769
+ };
2770
+ }
2771
+ async previewCancel(ctx, input, now = /* @__PURE__ */ new Date()) {
2772
+ const existing = await this.subscriptionBundles.findById(input.subscriptionBundleId);
2773
+ if (!existing || existing.subscriptionId !== ctx.subscriptionId) {
2774
+ throw new NotFoundException4(`SubscriptionBundle '${input.subscriptionBundleId}' nicht gefunden`);
2775
+ }
2776
+ const bundleVersion = await this.bundles.findVersionById(existing.bundleVersionId);
2777
+ if (!bundleVersion) {
2778
+ throw new NotFoundException4(`BundleVersion '${existing.bundleVersionId}' nicht gefunden`);
2779
+ }
2780
+ const blockers = [];
2781
+ const warnings = [];
2782
+ if (existing.canceledAt !== null) {
2783
+ blockers.push({
2784
+ code: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELED",
2785
+ message: "Diese Bundle-Buchung ist bereits gek\xFCndigt."
2786
+ });
2787
+ }
2788
+ const effectiveAt = resolveBundleCancelEffectiveAt({
2789
+ canceledAt: now,
2790
+ currentPeriodEnd: ctx.currentPeriodEnd,
2791
+ minimumTermEndsAt: existing.minimumTermEndsAt
2792
+ });
2793
+ const periodEnd = ctx.currentPeriodEnd ?? now;
2794
+ if (existing.minimumTermEndsAt && existing.minimumTermEndsAt.getTime() > periodEnd.getTime()) {
2795
+ warnings.push({
2796
+ code: "MINIMUM_TERM_BINDS",
2797
+ message: "Die Mindestlaufzeit bindet \xFCber das Periodenende hinaus \u2014 die K\xFCndigung wirkt erst zum Mindestlaufzeit-Ende."
2798
+ });
2799
+ }
2800
+ return {
2801
+ action: "cancel",
2802
+ subscriptionBundleId: existing.id,
2803
+ bundle: toSnapshot(bundleVersion),
2804
+ billingCycle: ctx.billingCycle,
2805
+ effectiveAt,
2806
+ nextPeriodSavingsNet: resolveBundlePriceNet(bundleVersion, ctx.currentPlanKey, ctx.billingCycle),
2807
+ blockers,
2808
+ warnings
2809
+ };
2810
+ }
2811
+ /** Bookability checks — same codes as `addBundleToSubscription` (422 path). */
2812
+ collectBookabilityBlockers(bundleVersion, currentPlanKey, blockers) {
2813
+ if (bundleVersion.publishedAt === null) {
2814
+ blockers.push({
2815
+ code: "BUNDLE_VERSION_NOT_PUBLISHED",
2816
+ message: "Diese BundleVersion ist nicht ver\xF6ffentlicht und nicht buchbar."
2817
+ });
2818
+ }
2819
+ if (bundleVersion.supersededAt !== null) {
2820
+ blockers.push({
2821
+ code: "BUNDLE_VERSION_SUPERSEDED",
2822
+ message: "Diese BundleVersion wurde durch eine Nachfolge-Version abgel\xF6st."
2823
+ });
2824
+ }
2825
+ const planIds = bundleVersion.compatibility?.planIds ?? [];
2826
+ if (planIds.length > 0 && !planIds.includes(currentPlanKey)) {
2827
+ blockers.push({
2828
+ code: "BUNDLE_INCOMPATIBLE_WITH_PLAN",
2829
+ message: `Das Bundle ist nicht mit Plan '${currentPlanKey}' kompatibel. Erlaubt: [${planIds.join(", ")}].`
2830
+ });
2831
+ }
2832
+ if (this.blockedBundles?.bundleKeys?.includes(bundleVersion.bundleKey)) {
2833
+ blockers.push({
2834
+ code: "BUNDLE_NOT_SELF_SERVICE",
2835
+ message: `Bundle '${bundleVersion.bundleKey}' wird nur per Sondervertrag aktiviert. Bitte den Vertragsbetreuer kontaktieren.`
2836
+ });
2837
+ }
2838
+ }
2839
+ /** Versions of the active bundle bookings (for redundancy + requires coverage). */
2840
+ async loadActiveBundleVersions(subscriptionId) {
2841
+ const active = await this.subscriptionBundles.listActiveBySubscription(subscriptionId);
2842
+ const versions = await Promise.all(active.map((booking) => this.bundles.findVersionById(booking.bundleVersionId)));
2843
+ return versions.filter((bv) => bv !== null);
2844
+ }
2845
+ /** Features of the currently live PlanVersion state; empty without PlanRepository. */
2846
+ async resolvePlanFeatures(planKey, asOf) {
2847
+ if (!this.plans) return [];
2848
+ const live = await this.plans.findActivePlanVersion?.(planKey, asOf) ?? await this.plans.findLatestLivePlanVersion?.(planKey);
2849
+ return live?.features ?? [];
2850
+ }
2851
+ collectRedundantFeatures(bundleVersion, planFeatures, currentPlanKey, activeBundleVersions) {
2852
+ const hints = [];
2853
+ for (const featureKey of bundleVersion.features ?? []) {
2854
+ if (planFeatures.has(featureKey)) {
2855
+ hints.push({
2856
+ featureKey,
2857
+ coveredBy: "PLAN",
2858
+ coveredByKey: currentPlanKey
2859
+ });
2860
+ continue;
2861
+ }
2862
+ const coveringBundle = activeBundleVersions.find((bv) => (bv.features ?? []).includes(featureKey));
2863
+ if (coveringBundle) {
2864
+ hints.push({
2865
+ featureKey,
2866
+ coveredBy: "BUNDLE",
2867
+ coveredByKey: coveringBundle.bundleKey
2868
+ });
2869
+ }
2870
+ }
2871
+ return hints;
2872
+ }
2873
+ /**
2874
+ * requires of the new bundle that neither the bundle itself nor plan ∪
2875
+ * active bundles cover (#35). Empty without CatalogEntryRepository.
2876
+ */
2877
+ async collectMissingRequires(bundleVersion, planFeatures, activeBundleVersions) {
2878
+ if (!this.catalogEntries) return [];
2879
+ const projectKey = await this.resolveProjectKey(bundleVersion);
2880
+ if (!projectKey) return [];
2881
+ const entries = await this.catalogEntries.listFeatures({
2882
+ projectKey
2883
+ });
2884
+ const requiresIndex = buildFeatureRequiresIndex3(entries);
2885
+ const covered = /* @__PURE__ */ new Set([
2886
+ ...planFeatures,
2887
+ ...activeBundleVersions.flatMap((bv) => bv.features ?? [])
2888
+ ]);
2889
+ return collectUnsatisfiedRequires3(bundleVersion.features ?? [], requiresIndex).filter((key) => !covered.has(key));
2890
+ }
2891
+ /** `projectKey` lives on the bundle stem, not on the version. */
2892
+ async resolveProjectKey(bundleVersion) {
2893
+ const stem = await this.bundles.findById(bundleVersion.bundleId);
2894
+ return stem?.projectKey ?? null;
2895
+ }
2896
+ };
2897
+ SubscriptionBundlePreviewService = _ts_decorate20([
2898
+ Injectable11(),
2899
+ _ts_param12(0, Inject12(SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN)),
2900
+ _ts_param12(1, Inject12(BUNDLE_REPOSITORY_TOKEN)),
2901
+ _ts_param12(2, Optional9()),
2902
+ _ts_param12(2, Inject12(PLAN_REPOSITORY_TOKEN)),
2903
+ _ts_param12(3, Optional9()),
2904
+ _ts_param12(3, Inject12(CATALOG_ENTRY_REPOSITORY_TOKEN)),
2905
+ _ts_param12(4, Optional9()),
2906
+ _ts_param12(4, Inject12(SELF_SERVICE_BLOCKED_BUNDLES_TOKEN)),
2907
+ _ts_param12(5, Optional9()),
2908
+ _ts_param12(5, Inject12(SUBSCRIPTION_BUNDLE_CONFIG_TOKEN)),
2909
+ _ts_metadata14("design:type", Function),
2910
+ _ts_metadata14("design:paramtypes", [
2911
+ typeof SubscriptionBundleRepository === "undefined" ? Object : SubscriptionBundleRepository,
2912
+ typeof BundleRepository === "undefined" ? Object : BundleRepository,
2913
+ Object,
2914
+ Object,
2915
+ Object,
2916
+ typeof SubscriptionBundleConfig === "undefined" ? Object : SubscriptionBundleConfig
2917
+ ])
2918
+ ], SubscriptionBundlePreviewService);
2919
+ function resolveBundlePriceNet(bundleVersion, planKey, billingCycle) {
2920
+ const override = (bundleVersion.pricingOverrides ?? []).find((o) => o.planId === planKey && !o.businessTypeKey);
2921
+ const yearly = billingCycle === "YEARLY";
2922
+ const raw = yearly ? override?.yearlyNet !== void 0 ? override.yearlyNet : bundleVersion.yearlyNet : override?.monthlyNet !== void 0 ? override.monthlyNet : bundleVersion.monthlyNet;
2923
+ if (raw === null || raw === void 0) return null;
2924
+ const parsed = Number.parseFloat(raw);
2925
+ return Number.isFinite(parsed) ? parsed : null;
2926
+ }
2927
+ __name(resolveBundlePriceNet, "resolveBundlePriceNet");
2928
+ function toSnapshot(bundleVersion) {
2929
+ return {
2930
+ bundleKey: bundleVersion.bundleKey,
2931
+ label: bundleVersion.label,
2932
+ bundleVersionId: bundleVersion.id,
2933
+ features: [
2934
+ ...bundleVersion.features ?? []
2935
+ ],
2936
+ quotas: {
2937
+ ...bundleVersion.quotas ?? {}
2938
+ }
2939
+ };
2940
+ }
2941
+ __name(toSnapshot, "toSnapshot");
2942
+
2943
+ // src/billing/subscription-bundles.module.ts
2944
+ import { Module as Module4 } from "@nestjs/common";
2945
+
2946
+ // src/billing/tenant-subscription-bundles.controller.ts
2947
+ import { BadRequestException as BadRequestException2, Body as Body3, Controller as Controller4, Delete, Get as Get3, HttpCode as HttpCode2, HttpStatus as HttpStatus3, Inject as Inject13, Logger as Logger5, NotFoundException as NotFoundException5, Optional as Optional10, Param, ParseUUIDPipe, Post as Post3, Req as Req2, UseGuards as UseGuards3 } from "@nestjs/common";
2948
+
2949
+ // src/billing/dto/subscription-bundles.dto.ts
2950
+ import { IsInt, IsOptional as IsOptional4, IsUUID as IsUUID2, Matches as Matches3, Max, Min } from "class-validator";
2951
+ function _ts_decorate21(decorators, target, key, desc) {
2952
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2953
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2954
+ 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;
2955
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2956
+ }
2957
+ __name(_ts_decorate21, "_ts_decorate");
2958
+ function _ts_metadata15(k, v) {
2959
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
2960
+ }
2961
+ __name(_ts_metadata15, "_ts_metadata");
2962
+ var ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}(T.*)?$/;
2963
+ var AddSubscriptionBundleDto = class {
2964
+ static {
2965
+ __name(this, "AddSubscriptionBundleDto");
2966
+ }
2967
+ bundleVersionId;
2968
+ /**
2969
+ * Optional — override for the minimum term (months). `0` = no
2970
+ * minimum term. Default comes from the `SubscriptionBundleConfig`
2971
+ * (platform = 12 months).
2972
+ */
2973
+ minimumTermMonths;
2974
+ };
2975
+ _ts_decorate21([
2976
+ IsUUID2(),
2977
+ _ts_metadata15("design:type", String)
2978
+ ], AddSubscriptionBundleDto.prototype, "bundleVersionId", void 0);
2979
+ _ts_decorate21([
2980
+ IsOptional4(),
2981
+ IsInt(),
2982
+ Min(0),
2983
+ Max(120),
2984
+ _ts_metadata15("design:type", Number)
2985
+ ], AddSubscriptionBundleDto.prototype, "minimumTermMonths", void 0);
2986
+ var PreviewSubscriptionBundleDto = class {
2987
+ static {
2988
+ __name(this, "PreviewSubscriptionBundleDto");
2989
+ }
2990
+ /** Add preview: BundleVersion to be booked. */
2991
+ bundleVersionId;
2992
+ /** Cancel preview: existing Bundle booking. */
2993
+ subscriptionBundleId;
2994
+ /** Add preview only — override analogous to `AddSubscriptionBundleDto`. */
2995
+ minimumTermMonths;
2996
+ };
2997
+ _ts_decorate21([
2998
+ IsOptional4(),
2999
+ IsUUID2(),
3000
+ _ts_metadata15("design:type", String)
3001
+ ], PreviewSubscriptionBundleDto.prototype, "bundleVersionId", void 0);
3002
+ _ts_decorate21([
3003
+ IsOptional4(),
3004
+ IsUUID2(),
3005
+ _ts_metadata15("design:type", String)
3006
+ ], PreviewSubscriptionBundleDto.prototype, "subscriptionBundleId", void 0);
3007
+ _ts_decorate21([
3008
+ IsOptional4(),
3009
+ IsInt(),
3010
+ Min(0),
3011
+ Max(120),
3012
+ _ts_metadata15("design:type", Number)
3013
+ ], PreviewSubscriptionBundleDto.prototype, "minimumTermMonths", void 0);
3014
+ var CancelSubscriptionBundleDto = class {
3015
+ static {
3016
+ __name(this, "CancelSubscriptionBundleDto");
3017
+ }
3018
+ /**
3019
+ * Optional — default = `new Date()` server-side. Format: ISO-8601
3020
+ * (`YYYY-MM-DD` or full timestamp). Usually not set by the tenant
3021
+ * self-service.
3022
+ */
3023
+ canceledAt;
3024
+ };
3025
+ _ts_decorate21([
3026
+ IsOptional4(),
3027
+ Matches3(ISO_DATE_PATTERN, {
3028
+ message: "canceledAt muss ISO-Datum sein (YYYY-MM-DD)"
3029
+ }),
3030
+ _ts_metadata15("design:type", String)
3031
+ ], CancelSubscriptionBundleDto.prototype, "canceledAt", void 0);
3032
+
3033
+ // src/billing/tenant-subscription-bundles.controller.ts
3034
+ function _ts_decorate22(decorators, target, key, desc) {
3035
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3036
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3037
+ 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;
3038
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3039
+ }
3040
+ __name(_ts_decorate22, "_ts_decorate");
3041
+ function _ts_metadata16(k, v) {
3042
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
3043
+ }
3044
+ __name(_ts_metadata16, "_ts_metadata");
3045
+ function _ts_param13(paramIndex, decorator) {
3046
+ return function(target, key) {
3047
+ decorator(target, key, paramIndex);
3048
+ };
3049
+ }
3050
+ __name(_ts_param13, "_ts_param");
3051
+ function buildTenantSubscriptionBundlesController(extraGuards = []) {
3052
+ let GeneratedTenantSubscriptionBundlesController = class GeneratedTenantSubscriptionBundlesController {
3053
+ static {
3054
+ __name(this, "GeneratedTenantSubscriptionBundlesController");
3055
+ }
3056
+ service;
3057
+ previewService;
3058
+ subscriptionUsage;
3059
+ tenantIdResolver;
3060
+ contractFreeze;
3061
+ logger = new Logger5("TenantSubscriptionBundlesController");
3062
+ constructor(service, previewService, subscriptionUsage, tenantIdResolver, contractFreeze = null) {
3063
+ this.service = service;
3064
+ this.previewService = previewService;
3065
+ this.subscriptionUsage = subscriptionUsage;
3066
+ this.tenantIdResolver = tenantIdResolver;
3067
+ this.contractFreeze = contractFreeze;
3068
+ }
3069
+ async list(req) {
3070
+ const sub = await this.requireSubscription(this.requireTenantId(req));
3071
+ return this.service.listForSubscription(this.requireSubscriptionPk(sub));
3072
+ }
3073
+ async add(req, dto) {
3074
+ const tenantId = this.requireTenantId(req);
3075
+ const sub = await this.requireSubscription(tenantId);
3076
+ const result = await this.service.addBundleToSubscription({
3077
+ subscriptionId: this.requireSubscriptionPk(sub),
3078
+ bundleVersionId: dto.bundleVersionId,
3079
+ // Plan KEY (compatibility.planIds is key-based) — not the planVersion UUID.
3080
+ currentPlanKey: sub.plan,
3081
+ minimumTermMonths: dto.minimumTermMonths
3082
+ });
3083
+ await this.refreezeContract(tenantId, sub);
3084
+ return result;
3085
+ }
3086
+ async preview(req, dto) {
3087
+ const hasAdd = Boolean(dto.bundleVersionId);
3088
+ const hasCancel = Boolean(dto.subscriptionBundleId);
3089
+ if (hasAdd === hasCancel) {
3090
+ throw new BadRequestException2("Genau eines von bundleVersionId (Add-Preview) oder subscriptionBundleId (Cancel-Preview) angeben.");
3091
+ }
3092
+ const sub = await this.requireSubscription(this.requireTenantId(req));
3093
+ const ctx = {
3094
+ subscriptionId: this.requireSubscriptionPk(sub),
3095
+ // Plan KEY (compatibility.planIds is key-based) — not the planVersion UUID.
3096
+ currentPlanKey: sub.plan,
3097
+ billingCycle: sub.billingCycle,
3098
+ status: sub.status,
3099
+ startedAt: sub.startedAt,
3100
+ currentPeriodStart: sub.currentPeriodStart,
3101
+ currentPeriodEnd: sub.currentPeriodEnd
3102
+ };
3103
+ return hasAdd ? this.previewService.previewAdd(ctx, {
3104
+ bundleVersionId: dto.bundleVersionId,
3105
+ minimumTermMonths: dto.minimumTermMonths
3106
+ }) : this.previewService.previewCancel(ctx, {
3107
+ subscriptionBundleId: dto.subscriptionBundleId
3108
+ });
3109
+ }
3110
+ async cancel(req, subscriptionBundleId, dto = {}) {
3111
+ const tenantId = this.requireTenantId(req);
3112
+ const sub = await this.requireSubscription(tenantId);
3113
+ const result = await this.service.cancelBundleFromSubscription({
3114
+ subscriptionBundleId,
3115
+ canceledAt: dto.canceledAt ? new Date(dto.canceledAt) : void 0,
3116
+ currentPeriodEnd: sub.currentPeriodEnd ?? void 0
3117
+ });
3118
+ await this.refreezeContract(tenantId, sub);
3119
+ return result;
3120
+ }
3121
+ async reactivate(req, subscriptionBundleId) {
3122
+ const tenantId = this.requireTenantId(req);
3123
+ const sub = await this.requireSubscription(tenantId);
3124
+ const result = await this.service.reactivateBundle(subscriptionBundleId);
3125
+ await this.refreezeContract(tenantId, sub);
3126
+ return result;
3127
+ }
3128
+ requireSubscriptionPk(sub) {
3129
+ if (!sub.id) {
3130
+ throw new NotFoundException5("SubscriptionUsageRecord ohne `id` \u2014 Adapter muss die Subscription-PK durchreichen (siehe SubscriptionUsageRecord.id).");
3131
+ }
3132
+ return sub.id;
3133
+ }
3134
+ requireTenantId(req) {
3135
+ const tenantId = this.tenantIdResolver(req);
3136
+ if (!tenantId) {
3137
+ throw new NotFoundException5("Tenant-ID nicht im Request gefunden");
3138
+ }
3139
+ return tenantId;
3140
+ }
3141
+ async requireSubscription(tenantId) {
3142
+ const sub = await this.subscriptionUsage.findForTenant(tenantId);
3143
+ if (!sub) {
3144
+ throw new NotFoundException5(`Keine Subscription f\xFCr Tenant ${tenantId}`);
3145
+ }
3146
+ return sub;
3147
+ }
3148
+ /**
3149
+ * Re-freeze with the unchanged plan after bundle add/cancel (#61) —
3150
+ * only if the consumer has wired the ContractFreezePort.
3151
+ */
3152
+ async refreezeContract(tenantId, sub) {
3153
+ if (!this.contractFreeze) return;
3154
+ try {
3155
+ await this.contractFreeze.freezeOnPlanChange(tenantId, sub.planVersion.planId, sub.billingCycle, /* @__PURE__ */ new Date());
3156
+ } catch (err) {
3157
+ this.logger.error(`Contract-Freeze nach Bundle-Mutation fehlgeschlagen (tenant ${tenantId}): ${String(err)}`);
3158
+ }
3159
+ }
3160
+ };
3161
+ _ts_decorate22([
3162
+ Get3(),
3163
+ _ts_param13(0, Req2()),
3164
+ _ts_metadata16("design:type", Function),
3165
+ _ts_metadata16("design:paramtypes", [
3166
+ typeof RequestLike === "undefined" ? Object : RequestLike
3167
+ ]),
3168
+ _ts_metadata16("design:returntype", Promise)
3169
+ ], GeneratedTenantSubscriptionBundlesController.prototype, "list", null);
3170
+ _ts_decorate22([
3171
+ Post3(),
3172
+ _ts_param13(0, Req2()),
3173
+ _ts_param13(1, Body3()),
3174
+ _ts_metadata16("design:type", Function),
3175
+ _ts_metadata16("design:paramtypes", [
3176
+ typeof RequestLike === "undefined" ? Object : RequestLike,
3177
+ typeof AddSubscriptionBundleDto === "undefined" ? Object : AddSubscriptionBundleDto
3178
+ ]),
3179
+ _ts_metadata16("design:returntype", Promise)
3180
+ ], GeneratedTenantSubscriptionBundlesController.prototype, "add", null);
3181
+ _ts_decorate22([
3182
+ Post3("preview"),
3183
+ HttpCode2(HttpStatus3.OK),
3184
+ _ts_param13(0, Req2()),
3185
+ _ts_param13(1, Body3()),
3186
+ _ts_metadata16("design:type", Function),
3187
+ _ts_metadata16("design:paramtypes", [
3188
+ typeof RequestLike === "undefined" ? Object : RequestLike,
3189
+ typeof PreviewSubscriptionBundleDto === "undefined" ? Object : PreviewSubscriptionBundleDto
3190
+ ]),
3191
+ _ts_metadata16("design:returntype", Promise)
3192
+ ], GeneratedTenantSubscriptionBundlesController.prototype, "preview", null);
3193
+ _ts_decorate22([
3194
+ Delete(":id"),
3195
+ HttpCode2(HttpStatus3.OK),
3196
+ _ts_param13(0, Req2()),
3197
+ _ts_param13(1, Param("id", new ParseUUIDPipe())),
3198
+ _ts_param13(2, Body3()),
3199
+ _ts_metadata16("design:type", Function),
3200
+ _ts_metadata16("design:paramtypes", [
3201
+ typeof RequestLike === "undefined" ? Object : RequestLike,
3202
+ String,
3203
+ typeof CancelSubscriptionBundleDto === "undefined" ? Object : CancelSubscriptionBundleDto
3204
+ ]),
3205
+ _ts_metadata16("design:returntype", Promise)
3206
+ ], GeneratedTenantSubscriptionBundlesController.prototype, "cancel", null);
3207
+ _ts_decorate22([
3208
+ Post3(":id/reactivate"),
3209
+ HttpCode2(HttpStatus3.OK),
3210
+ _ts_param13(0, Req2()),
3211
+ _ts_param13(1, Param("id", new ParseUUIDPipe())),
3212
+ _ts_metadata16("design:type", Function),
3213
+ _ts_metadata16("design:paramtypes", [
3214
+ typeof RequestLike === "undefined" ? Object : RequestLike,
3215
+ String
3216
+ ]),
3217
+ _ts_metadata16("design:returntype", Promise)
3218
+ ], GeneratedTenantSubscriptionBundlesController.prototype, "reactivate", null);
3219
+ GeneratedTenantSubscriptionBundlesController = _ts_decorate22([
3220
+ Controller4("billing/subscription-bundles"),
3221
+ UseGuards3(ComposedTenantAuthGuard, ...extraGuards),
3222
+ _ts_param13(0, Inject13(SubscriptionBundlesService)),
3223
+ _ts_param13(1, Inject13(SubscriptionBundlePreviewService)),
3224
+ _ts_param13(2, Inject13(SUBSCRIPTION_USAGE_PORT_TOKEN)),
3225
+ _ts_param13(3, Inject13(TENANT_ID_RESOLVER_TOKEN)),
3226
+ _ts_param13(4, Optional10()),
3227
+ _ts_param13(4, Inject13(CONTRACT_FREEZE_PORT_TOKEN)),
3228
+ _ts_metadata16("design:type", Function),
3229
+ _ts_metadata16("design:paramtypes", [
3230
+ typeof SubscriptionBundlesService === "undefined" ? Object : SubscriptionBundlesService,
3231
+ typeof SubscriptionBundlePreviewService === "undefined" ? Object : SubscriptionBundlePreviewService,
3232
+ typeof SubscriptionUsagePort === "undefined" ? Object : SubscriptionUsagePort,
3233
+ typeof TenantIdResolver === "undefined" ? Object : TenantIdResolver,
3234
+ Object
3235
+ ])
3236
+ ], GeneratedTenantSubscriptionBundlesController);
3237
+ return GeneratedTenantSubscriptionBundlesController;
3238
+ }
3239
+ __name(buildTenantSubscriptionBundlesController, "buildTenantSubscriptionBundlesController");
3240
+
3241
+ // src/billing/subscription-bundles.module.ts
3242
+ function _ts_decorate23(decorators, target, key, desc) {
3243
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3244
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3245
+ 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;
3246
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3247
+ }
3248
+ __name(_ts_decorate23, "_ts_decorate");
3249
+ var SubscriptionBundleModule = class _SubscriptionBundleModule {
3250
+ static {
3251
+ __name(this, "SubscriptionBundleModule");
3252
+ }
3253
+ static forRoot(options) {
3254
+ const providers = [
3255
+ asProvider(SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN, options.subscriptionBundleRepository),
3256
+ ...options.bundleRepository ? [
3257
+ asProvider(BUNDLE_REPOSITORY_TOKEN, options.bundleRepository)
3258
+ ] : [],
3259
+ {
3260
+ provide: SUBSCRIPTION_BUNDLE_CONFIG_TOKEN,
3261
+ useValue: {
3262
+ defaultMinimumTermMonths: options.defaultMinimumTermMonths ?? 12
3263
+ }
3264
+ },
3265
+ ...options.selfServiceBlockedBundles ? [
3266
+ {
3267
+ provide: SELF_SERVICE_BLOCKED_BUNDLES_TOKEN,
3268
+ useValue: options.selfServiceBlockedBundles
3269
+ }
3270
+ ] : [],
3271
+ SubscriptionBundlesService,
3272
+ SubscriptionBundlePreviewService,
3273
+ ...options.extraProviders ?? []
3274
+ ];
3275
+ const controllers = [];
3276
+ if (options.controller) {
3277
+ providers.push(ComposedTenantAuthGuard);
3278
+ if (options.controller.authGuards) {
3279
+ providers.push(asProvider(TENANT_AUTH_GUARDS_TOKEN, options.controller.authGuards));
3280
+ }
3281
+ if (options.controller.subscriptionUsagePort) {
3282
+ providers.push(asProvider(SUBSCRIPTION_USAGE_PORT_TOKEN, options.controller.subscriptionUsagePort));
3283
+ }
3284
+ providers.push({
3285
+ provide: TENANT_ID_RESOLVER_TOKEN,
3286
+ useValue: options.controller.tenantIdResolver ?? ((req) => req.user?.tenantId ?? null)
3287
+ });
3288
+ controllers.push(buildTenantSubscriptionBundlesController(options.controller.extraGuards ?? []));
3289
+ }
3290
+ return {
3291
+ module: _SubscriptionBundleModule,
3292
+ global: options.global ?? false,
3293
+ imports: options.imports ?? [],
3294
+ controllers,
3295
+ providers,
3296
+ exports: [
3297
+ SubscriptionBundlesService,
3298
+ SubscriptionBundlePreviewService,
3299
+ SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN
3300
+ ]
3301
+ };
3302
+ }
3303
+ };
3304
+ SubscriptionBundleModule = _ts_decorate23([
3305
+ Module4({})
3306
+ ], SubscriptionBundleModule);
3307
+
3308
+ export {
3309
+ periodEndAfter,
3310
+ initialPeriodWindow,
3311
+ periodEndWithMinLead,
3312
+ PublishValidationError,
3313
+ assertChangeNote,
3314
+ assertDraftPublishable,
3315
+ assertBaseVersionFresh,
3316
+ assertOptimisticLockHeld,
3317
+ decideRenewal,
3318
+ clearPendingPlanVersionFields,
3319
+ computeNextPeriod,
3320
+ FEATURE_GUARD_CONFIG_TOKEN,
3321
+ UPSELL_OFFER_RESOLVER_TOKEN,
3322
+ UPSELL_OFFER_CURRENCY_TOKEN,
3323
+ FeatureGuard,
3324
+ PUBLIC_CATALOG_PROJECT_KEY_TOKEN,
3325
+ PUBLIC_CATALOG_BUNDLE_REPOSITORY_TOKEN,
3326
+ PUBLIC_CATALOG_BUSINESS_TYPE_REPOSITORY_TOKEN,
3327
+ PUBLIC_CATALOG_MARKETING_REPOSITORY_TOKEN,
3328
+ CatalogBundleUpsellResolver,
3329
+ LimitExceededFilter,
3330
+ PlanCatalogValidationError,
3331
+ loadPlanCatalogFromFile,
3332
+ loadPlanCatalogFromString,
3333
+ PLAN_CATALOG_IMPORT_SINK_TOKEN,
3334
+ PlanCatalogImporterService,
3335
+ PlanCatalogImportDto,
3336
+ buildPlanCatalogImporterController,
3337
+ PlanCatalogImporterModule,
3338
+ FEATURE_UI_REGISTRY_TOKEN,
3339
+ PublicCatalogController,
3340
+ PublicCatalogModule,
3341
+ ComposedTenantAuthGuard,
3342
+ TenantAdminGuard,
3343
+ SELF_SERVICE_BLOCKED_PLANS_TOKEN,
3344
+ SELF_SERVICE_BLOCKED_BUNDLES_TOKEN,
3345
+ computeProration,
3346
+ PlanChangePreviewService,
3347
+ CONTRACT_FREEZE_PORT_TOKEN,
3348
+ CONTRACT_FREEZE_SOURCE_PORT_TOKEN,
3349
+ CONTRACT_FREEZE_PROJECT_KEY_TOKEN,
3350
+ PendingPlanMaterializationService,
3351
+ SubscriptionContractFreezeService,
3352
+ computeCarriedTrialEndsAt,
3353
+ PreviewPlanChangeDto,
3354
+ ChangePlanDto,
3355
+ CancelSubscriptionDto,
3356
+ CompleteOnboardingSubscriptionDto,
3357
+ SubscriptionBundlesService,
3358
+ resolveBundleCancelEffectiveAt,
3359
+ addMonths,
3360
+ TenantBillingController,
3361
+ TenantBillingModule,
3362
+ ConfiguratorCatalogBuilder,
3363
+ SubscriptionBundlePreviewService,
3364
+ resolveBundlePriceNet,
3365
+ buildTenantSubscriptionBundlesController,
3366
+ SubscriptionBundleModule
3367
+ };