@saasicat/nest 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +53 -0
- package/dist/admin/index.cjs +1283 -0
- package/dist/admin/index.d.cts +225 -0
- package/dist/admin/index.d.ts +225 -0
- package/dist/admin/index.js +58 -0
- package/dist/admin-audit.service-9IqXMlZm.d.cts +30 -0
- package/dist/admin-audit.service-9IqXMlZm.d.ts +30 -0
- package/dist/admin-manifest.config-DyrQNT7M.d.cts +10 -0
- package/dist/admin-manifest.config-DyrQNT7M.d.ts +10 -0
- package/dist/aggregation-CJ3qQf92.d.cts +288 -0
- package/dist/aggregation-Dvz9e8X2.d.ts +288 -0
- package/dist/billing/index.cjs +5205 -0
- package/dist/billing/index.d.cts +1326 -0
- package/dist/billing/index.d.ts +1326 -0
- package/dist/billing/index.js +197 -0
- package/dist/catalog/index.cjs +6449 -0
- package/dist/catalog/index.d.cts +1073 -0
- package/dist/catalog/index.d.ts +1073 -0
- package/dist/catalog/index.js +158 -0
- package/dist/checkout-offer/index.cjs +807 -0
- package/dist/checkout-offer/index.d.cts +120 -0
- package/dist/checkout-offer/index.d.ts +120 -0
- package/dist/checkout-offer/index.js +20 -0
- package/dist/chunk-2FR6ZL7R.js +0 -0
- package/dist/chunk-2PNX2QL2.js +347 -0
- package/dist/chunk-2VNZYRDV.js +326 -0
- package/dist/chunk-7MLGHWKO.js +12 -0
- package/dist/chunk-ABB2CRAS.js +31 -0
- package/dist/chunk-AJ3EPELS.js +157 -0
- package/dist/chunk-E56W4U2P.js +90 -0
- package/dist/chunk-HRU3CP57.js +71 -0
- package/dist/chunk-L4SUWH5B.js +14 -0
- package/dist/chunk-M6CZQYPY.js +314 -0
- package/dist/chunk-MDIZUVIK.js +87 -0
- package/dist/chunk-MV6LW5UB.js +217 -0
- package/dist/chunk-NYLON2VC.js +6 -0
- package/dist/chunk-P6MYZMXQ.js +951 -0
- package/dist/chunk-Q53N43LQ.js +13 -0
- package/dist/chunk-QJVPRD3R.js +573 -0
- package/dist/chunk-RIGLQ4BV.js +3367 -0
- package/dist/chunk-SABTXESR.js +24 -0
- package/dist/chunk-SHUYVCID.js +6 -0
- package/dist/chunk-SYEVGPFC.js +686 -0
- package/dist/chunk-V7HUZ6UQ.js +5923 -0
- package/dist/chunk-WPUVJRIN.js +29 -0
- package/dist/chunk-XCZ3TTFK.js +22 -0
- package/dist/chunk-XSGU2Y2O.js +1611 -0
- package/dist/chunk-YNEW3L63.js +190 -0
- package/dist/chunk-YXGHPILG.js +614 -0
- package/dist/chunk-ZL4LLE2Z.js +465 -0
- package/dist/di-CcNeq9v-.d.cts +16 -0
- package/dist/di-CcNeq9v-.d.ts +16 -0
- package/dist/discovery/index.cjs +588 -0
- package/dist/discovery/index.d.cts +299 -0
- package/dist/discovery/index.d.ts +299 -0
- package/dist/discovery/index.js +49 -0
- package/dist/discovery.scanner-CUYLKlYT.d.cts +59 -0
- package/dist/discovery.scanner-CUYLKlYT.d.ts +59 -0
- package/dist/entitlement/index.cjs +805 -0
- package/dist/entitlement/index.d.cts +60 -0
- package/dist/entitlement/index.d.ts +60 -0
- package/dist/entitlement/index.js +57 -0
- package/dist/index.cjs +15918 -0
- package/dist/index.d.cts +85 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +956 -0
- package/dist/plan-resolution-CFCoUkrE.d.cts +41 -0
- package/dist/plan-resolution-CFCoUkrE.d.ts +41 -0
- package/dist/platform/index.cjs +2772 -0
- package/dist/platform/index.d.cts +118 -0
- package/dist/platform/index.d.ts +118 -0
- package/dist/platform/index.js +37 -0
- package/dist/promo/index.cjs +995 -0
- package/dist/promo/index.d.cts +121 -0
- package/dist/promo/index.d.ts +121 -0
- package/dist/promo/index.js +58 -0
- package/dist/registration/index.cjs +1687 -0
- package/dist/registration/index.d.cts +444 -0
- package/dist/registration/index.d.ts +444 -0
- package/dist/registration/index.js +82 -0
- package/dist/saas-platform.module-CgZ2omgg.d.cts +180 -0
- package/dist/saas-platform.module-DT9TnCzk.d.ts +180 -0
- package/dist/service-DX8KbGXl.d.cts +109 -0
- package/dist/service-DX8KbGXl.d.ts +109 -0
- package/dist/subscription-contract/index.cjs +471 -0
- package/dist/subscription-contract/index.d.cts +17 -0
- package/dist/subscription-contract/index.d.ts +17 -0
- package/dist/subscription-contract/index.js +21 -0
- package/dist/subscription-contract.service--cm47ZJJ.d.cts +39 -0
- package/dist/subscription-contract.service--cm47ZJJ.d.ts +39 -0
- package/dist/testing/index.cjs +3942 -0
- package/dist/testing/index.d.cts +279 -0
- package/dist/testing/index.d.ts +279 -0
- package/dist/testing/index.js +1195 -0
- package/package.json +185 -0
|
@@ -0,0 +1,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
|
+
};
|