@saasicat/core 1.0.0-rc.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/dist/index.cjs ADDED
@@ -0,0 +1,957 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AUTH_ERROR_CODES: () => AUTH_ERROR_CODES,
24
+ BILLING_ERROR_CODES: () => BILLING_ERROR_CODES,
25
+ CATALOG_ERROR_CODES: () => CATALOG_ERROR_CODES,
26
+ CONTRACT_ERROR_CODES: () => CONTRACT_ERROR_CODES,
27
+ ERROR_MESSAGES_DE: () => ERROR_MESSAGES_DE,
28
+ ERROR_MESSAGES_EN: () => ERROR_MESSAGES_EN,
29
+ FEATURE_NOT_LICENSED: () => FEATURE_NOT_LICENSED,
30
+ OTP_RATE_LIMIT_MAX_SENDS: () => OTP_RATE_LIMIT_MAX_SENDS,
31
+ OTP_RATE_LIMIT_WINDOW_MINUTES: () => OTP_RATE_LIMIT_WINDOW_MINUTES,
32
+ OTP_TTL_MINUTES: () => OTP_TTL_MINUTES,
33
+ OTP_VERIFY_MAX_ATTEMPTS: () => OTP_VERIFY_MAX_ATTEMPTS,
34
+ PASSWORD_RESET_TTL_MINUTES: () => PASSWORD_RESET_TTL_MINUTES,
35
+ PENDING_CHECKOUT_TTL_DAYS: () => PENDING_CHECKOUT_TTL_DAYS,
36
+ PENDING_EMAIL_TTL_HOURS: () => PENDING_EMAIL_TTL_HOURS,
37
+ PENDING_ONBOARDING_TTL_DAYS: () => PENDING_ONBOARDING_TTL_DAYS,
38
+ PLATFORM_ERROR_CODES: () => PLATFORM_ERROR_CODES,
39
+ PROMO_ERROR_CODES: () => PROMO_ERROR_CODES,
40
+ PersistenceCapabilityError: () => PersistenceCapabilityError,
41
+ PlatformUserExistsError: () => PlatformUserExistsError,
42
+ REGISTRATION_ERROR_CODES: () => REGISTRATION_ERROR_CODES,
43
+ REGISTRATION_RESUME_TTL_MINUTES: () => REGISTRATION_RESUME_TTL_MINUTES,
44
+ REGISTRATION_STEP_BY_STATUS: () => REGISTRATION_STEP_BY_STATUS,
45
+ SETUP_ERROR_CODES: () => SETUP_ERROR_CODES,
46
+ applyPromo: () => applyPromo,
47
+ assertPersistenceCapabilities: () => assertPersistenceCapabilities,
48
+ buildActivePlanVersionWhere: () => buildActivePlanVersionWhere,
49
+ buildActiveVersionWhere: () => buildActiveVersionWhere,
50
+ buildFeatureRequiresIndex: () => buildFeatureRequiresIndex,
51
+ classifyBundleVersionDiff: () => classifyBundleVersionDiff,
52
+ classifyPlanDiff: () => classifyPlanDiff,
53
+ collectUnsatisfiedRequires: () => collectUnsatisfiedRequires,
54
+ coverageExcludingSelf: () => coverageExcludingSelf,
55
+ formatErrorMessage: () => formatErrorMessage,
56
+ isBundleRedundant: () => isBundleRedundant,
57
+ isPlatformUserExistsError: () => isPlatformUserExistsError,
58
+ isVersionEditable: () => isVersionEditable,
59
+ missingRequiresFor: () => missingRequiresFor,
60
+ pickActivePromo: () => pickActivePromo,
61
+ promoStatus: () => promoStatus,
62
+ resolveBundleAvailability: () => resolveBundleAvailability,
63
+ resolveErrorMessage: () => resolveErrorMessage,
64
+ selectChargeableBundles: () => selectChargeableBundles,
65
+ startOfUtcDay: () => startOfUtcDay
66
+ });
67
+ module.exports = __toCommonJS(index_exports);
68
+
69
+ // src/active-plan-version-query.ts
70
+ function startOfUtcDay(date) {
71
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
72
+ }
73
+ function buildActivePlanVersionWhere(asOf, options = {}) {
74
+ const asOfDayStart = startOfUtcDay(asOf);
75
+ const validityWindow = [
76
+ { OR: [{ validFrom: null }, { validFrom: { lte: asOf } }] },
77
+ { OR: [{ validUntil: null }, { validUntil: { gte: asOfDayStart } }] }
78
+ ];
79
+ if (!options.withEndsAt) {
80
+ return { publishedAt: { not: null }, AND: validityWindow };
81
+ }
82
+ return {
83
+ publishedAt: { not: null },
84
+ AND: [...validityWindow, { OR: [{ endsAt: null }, { endsAt: { gt: asOf } }] }]
85
+ };
86
+ }
87
+ var buildActiveVersionWhere = buildActivePlanVersionWhere;
88
+
89
+ // src/promotion.types.ts
90
+ function promoStatus(promo, today = /* @__PURE__ */ new Date()) {
91
+ const from = new Date(promo.validFrom);
92
+ const to = /* @__PURE__ */ new Date(`${promo.validTo}T23:59:59`);
93
+ if (today < from) return "scheduled";
94
+ if (today > to) return "expired";
95
+ return "active";
96
+ }
97
+ function pickActivePromo(promotions, targetKey, locale, cycle, today = /* @__PURE__ */ new Date(), targetType = "PLAN") {
98
+ const matches = promotions.filter((p) => (p.targetType ?? "PLAN") === targetType).filter((p) => p.appliesTo.includes(targetKey)).filter((p) => p.billingCycle === "both" || p.billingCycle === cycle).filter((p) => !p.onlyLocales || p.onlyLocales.includes(locale)).filter((p) => !p.requiresCoupon).filter((p) => promoStatus(p, today) === "active").sort((a, b) => (b.priority || 0) - (a.priority || 0));
99
+ return matches[0] ?? null;
100
+ }
101
+ function applyPromo(promo, basePrice) {
102
+ if (!promo || basePrice === null || basePrice === void 0) return null;
103
+ if (promo.type === "percent" && typeof promo.value === "number") {
104
+ return {
105
+ kind: "percent",
106
+ discounted: Math.round(basePrice * (100 - promo.value)) / 100,
107
+ original: basePrice,
108
+ pct: promo.value
109
+ };
110
+ }
111
+ if (promo.type === "amount" && typeof promo.value === "number") {
112
+ return {
113
+ kind: "amount",
114
+ discounted: Math.max(0, basePrice - promo.value),
115
+ original: basePrice,
116
+ saved: promo.value
117
+ };
118
+ }
119
+ if (promo.type === "intro" && typeof promo.value === "object") {
120
+ return {
121
+ kind: "intro",
122
+ discounted: promo.value.price,
123
+ original: basePrice,
124
+ months: promo.value.months
125
+ };
126
+ }
127
+ if (promo.type === "freeMonths" && typeof promo.value === "number") {
128
+ return { kind: "free", discounted: 0, original: basePrice, months: promo.value };
129
+ }
130
+ return null;
131
+ }
132
+
133
+ // src/version-diff.ts
134
+ function classifyPlanDiff(oldV, newV) {
135
+ const changes = [];
136
+ appendFeatureChanges(changes, oldV.features, newV.features);
137
+ appendNumberChange(changes, "maxUsers", oldV.maxUsers, newV.maxUsers, "higherIsBetter");
138
+ appendNumberChange(
139
+ changes,
140
+ "maxVehicles",
141
+ oldV.maxVehicles,
142
+ newV.maxVehicles,
143
+ "higherIsBetter"
144
+ );
145
+ appendNumberChange(
146
+ changes,
147
+ "maxStorageGb",
148
+ oldV.maxStorageGb,
149
+ newV.maxStorageGb,
150
+ "higherIsBetter"
151
+ );
152
+ appendDecimalChange(changes, "monthlyNet", oldV.monthlyNet, newV.monthlyNet, "lowerIsBetter");
153
+ appendDecimalChange(changes, "yearlyNet", oldV.yearlyNet, newV.yearlyNet, "lowerIsBetter");
154
+ return buildResult(changes);
155
+ }
156
+ function classifyBundleVersionDiff(oldV, newV) {
157
+ const changes = [];
158
+ appendFeatureChanges(changes, oldV.features, newV.features);
159
+ appendQuotaChanges(changes, oldV.quotas, newV.quotas);
160
+ appendNullableDecimalChange(
161
+ changes,
162
+ "monthlyNet",
163
+ oldV.monthlyNet,
164
+ newV.monthlyNet,
165
+ "lowerIsBetter"
166
+ );
167
+ appendNullableDecimalChange(
168
+ changes,
169
+ "yearlyNet",
170
+ oldV.yearlyNet,
171
+ newV.yearlyNet,
172
+ "lowerIsBetter"
173
+ );
174
+ return buildResult(changes);
175
+ }
176
+ function appendFeatureChanges(out, oldFeatures, newFeatures) {
177
+ const oldSet = new Set(oldFeatures);
178
+ const newSet = new Set(newFeatures);
179
+ const removed = oldFeatures.filter((f) => !newSet.has(f));
180
+ const added = newFeatures.filter((f) => !oldSet.has(f));
181
+ if (removed.length > 0) {
182
+ out.push({
183
+ field: "features.removed",
184
+ oldValue: removed,
185
+ newValue: [],
186
+ direction: "REGRESSION"
187
+ });
188
+ }
189
+ if (added.length > 0) {
190
+ out.push({
191
+ field: "features.added",
192
+ oldValue: [],
193
+ newValue: added,
194
+ direction: "IMPROVEMENT"
195
+ });
196
+ }
197
+ }
198
+ function appendNumberChange(out, field, oldValue, newValue, polarity) {
199
+ if (oldValue === newValue) return;
200
+ const direction = directionFromCmp(newValue - oldValue, polarity);
201
+ out.push({ field, oldValue, newValue, direction });
202
+ }
203
+ function appendDecimalChange(out, field, oldValue, newValue, polarity) {
204
+ const oldNum = toNumber(oldValue);
205
+ const newNum = toNumber(newValue);
206
+ if (oldNum === newNum) return;
207
+ const direction = directionFromCmp(newNum - oldNum, polarity);
208
+ out.push({
209
+ field,
210
+ oldValue: oldNum.toFixed(2),
211
+ newValue: newNum.toFixed(2),
212
+ direction
213
+ });
214
+ }
215
+ function directionFromCmp(delta, polarity) {
216
+ if (delta === 0) return "NEUTRAL";
217
+ const positiveDelta = delta > 0;
218
+ if (polarity === "higherIsBetter") {
219
+ return positiveDelta ? "IMPROVEMENT" : "REGRESSION";
220
+ }
221
+ return positiveDelta ? "REGRESSION" : "IMPROVEMENT";
222
+ }
223
+ function toNumber(v) {
224
+ if (typeof v === "number") return v;
225
+ if (typeof v === "string") return Number(v);
226
+ return v.toNumber();
227
+ }
228
+ function buildResult(changes) {
229
+ const nonRegressive = !changes.some((c) => c.direction === "REGRESSION");
230
+ return { nonRegressive, changes };
231
+ }
232
+ function appendQuotaChanges(out, oldQuotas, newQuotas) {
233
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldQuotas), ...Object.keys(newQuotas)]);
234
+ for (const key of allKeys) {
235
+ const oldValue = oldQuotas[key] ?? 0;
236
+ const newValue = newQuotas[key] ?? 0;
237
+ if (oldValue === newValue) continue;
238
+ const direction = directionFromQuotaCmp(oldValue, newValue);
239
+ out.push({ field: `quotas.${key}`, oldValue, newValue, direction });
240
+ }
241
+ }
242
+ function directionFromQuotaCmp(oldValue, newValue) {
243
+ if (oldValue === -1 && newValue !== -1) return "REGRESSION";
244
+ if (newValue === -1 && oldValue !== -1) return "IMPROVEMENT";
245
+ return newValue > oldValue ? "IMPROVEMENT" : "REGRESSION";
246
+ }
247
+ function appendNullableDecimalChange(out, field, oldValue, newValue, polarity) {
248
+ if (oldValue === null && newValue === null) return;
249
+ if (oldValue === null && newValue !== null) {
250
+ out.push({
251
+ field,
252
+ oldValue: null,
253
+ newValue: toNumber(newValue).toFixed(2),
254
+ direction: polarity === "lowerIsBetter" ? "IMPROVEMENT" : "REGRESSION"
255
+ });
256
+ return;
257
+ }
258
+ if (oldValue !== null && newValue === null) {
259
+ out.push({
260
+ field,
261
+ oldValue: toNumber(oldValue).toFixed(2),
262
+ newValue: null,
263
+ direction: polarity === "lowerIsBetter" ? "REGRESSION" : "IMPROVEMENT"
264
+ });
265
+ return;
266
+ }
267
+ appendDecimalChange(out, field, oldValue, newValue, polarity);
268
+ }
269
+
270
+ // src/ports/persistence-ports.types.ts
271
+ var PersistenceCapabilityError = class extends Error {
272
+ constructor(missing, requiredBy) {
273
+ super(
274
+ `${requiredBy} requires persistence capabilities the configured adapter does not provide: ${missing.join(", ")}. Use an adapter/database that supports them or disable the feature.`
275
+ );
276
+ this.missing = missing;
277
+ this.requiredBy = requiredBy;
278
+ this.name = "PersistenceCapabilityError";
279
+ }
280
+ missing;
281
+ requiredBy;
282
+ code = "PERSISTENCE_CAPABILITY_MISSING";
283
+ };
284
+ function assertPersistenceCapabilities(capabilities, required, requiredBy) {
285
+ const missing = Object.keys(required).filter(
286
+ (key) => required[key] && !capabilities[key]
287
+ );
288
+ if (missing.length > 0) {
289
+ throw new PersistenceCapabilityError(missing, requiredBy);
290
+ }
291
+ }
292
+
293
+ // src/upsell.types.ts
294
+ var FEATURE_NOT_LICENSED = "FEATURE_NOT_LICENSED";
295
+
296
+ // src/error-codes.ts
297
+ var SETUP_ERROR_CODES = {
298
+ /** `SETUP_TOKEN` env not set → setup disabled. */
299
+ SETUP_DISABLED: "SETUP_DISABLED",
300
+ /** Provided token does not match. */
301
+ INVALID_SETUP_TOKEN: "INVALID_SETUP_TOKEN",
302
+ /** A SUPER_ADMIN already exists — self-disable. */
303
+ SETUP_ALREADY_DONE: "SETUP_ALREADY_DONE",
304
+ /** Invalid email in the request. */
305
+ INVALID_EMAIL: "INVALID_EMAIL",
306
+ /** Email already taken (mapped from `PlatformUserExistsError`). */
307
+ EMAIL_EXISTS: "EMAIL_EXISTS"
308
+ };
309
+ var CATALOG_ERROR_CODES = {
310
+ // ── plan master data ──
311
+ PLAN_HAS_DRAFTS: "PLAN_HAS_DRAFTS",
312
+ PLAN_HAS_PUBLISHED_VERSIONS: "PLAN_HAS_PUBLISHED_VERSIONS",
313
+ PLAN_HARD_DELETE_NOT_IMPLEMENTED: "PLAN_HARD_DELETE_NOT_IMPLEMENTED",
314
+ // ── plan versions ──
315
+ PLAN_VERSION_ALREADY_PUBLISHED: "PLAN_VERSION_ALREADY_PUBLISHED",
316
+ PLAN_VERSION_NOT_EDITABLE: "PLAN_VERSION_NOT_EDITABLE",
317
+ PLAN_VERSION_REGRESSION: "PLAN_VERSION_REGRESSION",
318
+ PLAN_VERSION_ZERO_PRICE: "PLAN_VERSION_ZERO_PRICE",
319
+ PLAN_VERSION_DISCARD_NOT_IMPLEMENTED: "PLAN_VERSION_DISCARD_NOT_IMPLEMENTED",
320
+ /** Version was never published, so it cannot be terminated. */
321
+ PLAN_VERSION_NOT_PUBLISHED: "PLAN_VERSION_NOT_PUBLISHED",
322
+ /** Version was replaced by a successor (`supersededAt` set). */
323
+ PLAN_VERSION_SUPERSEDED: "PLAN_VERSION_SUPERSEDED",
324
+ PLAN_VERSION_VALID_FROM_REQUIRED: "PLAN_VERSION_VALID_FROM_REQUIRED",
325
+ PLAN_VERSION_VALID_FROM_INVALID: "PLAN_VERSION_VALID_FROM_INVALID",
326
+ PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS: "PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS",
327
+ PLAN_VERSION_VALID_FROM_NOT_GAPLESS: "PLAN_VERSION_VALID_FROM_NOT_GAPLESS",
328
+ PLAN_VERSION_VALID_UNTIL_INVALID: "PLAN_VERSION_VALID_UNTIL_INVALID",
329
+ PLAN_VERSION_VALID_UNTIL_BEFORE_FROM: "PLAN_VERSION_VALID_UNTIL_BEFORE_FROM",
330
+ PLAN_TERMINATE_INVALID_DATE: "PLAN_TERMINATE_INVALID_DATE",
331
+ PLAN_TERMINATE_DATE_NOT_FUTURE: "PLAN_TERMINATE_DATE_NOT_FUTURE",
332
+ PLAN_TERMINATE_NOT_IMPLEMENTED: "PLAN_TERMINATE_NOT_IMPLEMENTED",
333
+ // ── bundle versions ──
334
+ BUNDLE_VERSION_ALREADY_PUBLISHED: "BUNDLE_VERSION_ALREADY_PUBLISHED",
335
+ BUNDLE_VERSION_NOT_EDITABLE: "BUNDLE_VERSION_NOT_EDITABLE",
336
+ BUNDLE_VERSION_NOT_PUBLISHED: "BUNDLE_VERSION_NOT_PUBLISHED",
337
+ BUNDLE_VERSION_SUPERSEDED: "BUNDLE_VERSION_SUPERSEDED",
338
+ BUNDLE_VERSION_REGRESSION: "BUNDLE_VERSION_REGRESSION",
339
+ BUNDLE_VERSION_ZERO_PRICE: "BUNDLE_VERSION_ZERO_PRICE",
340
+ BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED",
341
+ BUNDLE_VERSION_VALID_FROM_REQUIRED: "BUNDLE_VERSION_VALID_FROM_REQUIRED",
342
+ BUNDLE_VERSION_VALID_FROM_INVALID: "BUNDLE_VERSION_VALID_FROM_INVALID",
343
+ BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS: "BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS",
344
+ BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS: "BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS",
345
+ BUNDLE_VERSION_VALID_FROM_NOT_FUTURE: "BUNDLE_VERSION_VALID_FROM_NOT_FUTURE",
346
+ BUNDLE_VERSION_VALID_UNTIL_INVALID: "BUNDLE_VERSION_VALID_UNTIL_INVALID",
347
+ BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM: "BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM",
348
+ /** Publish blocked by strict mode. Carries `warnings[]` with own codes. */
349
+ STRICT_MODE_VIOLATIONS: "STRICT_MODE_VIOLATIONS",
350
+ // ── resources not found (stage 3) ──
351
+ PLAN_NOT_FOUND: "PLAN_NOT_FOUND",
352
+ PLAN_VERSION_NOT_FOUND: "PLAN_VERSION_NOT_FOUND",
353
+ BUNDLE_NOT_FOUND: "BUNDLE_NOT_FOUND",
354
+ BUNDLE_VERSION_NOT_FOUND: "BUNDLE_VERSION_NOT_FOUND",
355
+ FEATURE_NOT_FOUND: "FEATURE_NOT_FOUND",
356
+ QUOTA_NOT_FOUND: "QUOTA_NOT_FOUND",
357
+ PROMOTION_NOT_FOUND: "PROMOTION_NOT_FOUND",
358
+ MARKETING_PROJECTION_NOT_FOUND: "MARKETING_PROJECTION_NOT_FOUND",
359
+ // ── already exists ──
360
+ PLAN_ALREADY_EXISTS: "PLAN_ALREADY_EXISTS",
361
+ BUNDLE_ALREADY_EXISTS: "BUNDLE_ALREADY_EXISTS",
362
+ MARKETING_PROJECTION_ALREADY_EXISTS: "MARKETING_PROJECTION_ALREADY_EXISTS",
363
+ /** A draft already exists — publish or discard it before creating another. */
364
+ PLAN_DRAFT_ALREADY_EXISTS: "PLAN_DRAFT_ALREADY_EXISTS",
365
+ BUNDLE_DRAFT_ALREADY_EXISTS: "BUNDLE_DRAFT_ALREADY_EXISTS",
366
+ // ── discovery ──
367
+ QUOTA_NOT_IN_DISCOVERY_SNAPSHOT: "QUOTA_NOT_IN_DISCOVERY_SNAPSHOT",
368
+ DISCOVERY_STATUS_TRANSITION_INVALID: "DISCOVERY_STATUS_TRANSITION_INVALID",
369
+ DISCOVERY_NOT_INITIALIZED: "DISCOVERY_NOT_INITIALIZED"
370
+ };
371
+ var BILLING_ERROR_CODES = {
372
+ /**
373
+ * Feature not covered by the plan. Defined in `upsell.types.ts` because the
374
+ * upsell body type is built around it; re-exported here so an exhaustive
375
+ * `PlatformErrorCode` switch covers it.
376
+ */
377
+ FEATURE_NOT_LICENSED,
378
+ BUNDLE_ALREADY_SUBSCRIBED: "BUNDLE_ALREADY_SUBSCRIBED",
379
+ BUNDLE_INCOMPATIBLE_WITH_PLAN: "BUNDLE_INCOMPATIBLE_WITH_PLAN",
380
+ BUNDLE_NOT_SELF_SERVICE: "BUNDLE_NOT_SELF_SERVICE",
381
+ SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED",
382
+ SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SUBSCRIPTION_BUNDLE_NOT_CANCELLED",
383
+ SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE",
384
+ // ── subscriptions and plan changes (stage 3) ──
385
+ SUBSCRIPTION_NOT_FOUND: "SUBSCRIPTION_NOT_FOUND",
386
+ SUBSCRIPTION_TENANT_MISMATCH: "SUBSCRIPTION_TENANT_MISMATCH",
387
+ SUBSCRIPTION_BUNDLE_NOT_FOUND: "SUBSCRIPTION_BUNDLE_NOT_FOUND",
388
+ TENANT_NOT_FOUND: "TENANT_NOT_FOUND",
389
+ /** No plan version is active as of the requested date. */
390
+ NO_ACTIVE_PLAN_VERSION: "NO_ACTIVE_PLAN_VERSION",
391
+ /** Plan is unknown to the loaded plan catalogue. */
392
+ PLAN_NOT_IN_CATALOG: "PLAN_NOT_IN_CATALOG",
393
+ /** Plan exists but cannot be booked via self-service. */
394
+ PLAN_NOT_SELF_SERVICE: "PLAN_NOT_SELF_SERVICE",
395
+ /** Plan change refused. Carries `blockers[]` with their own codes. */
396
+ PLAN_CHANGE_BLOCKED: "PLAN_CHANGE_BLOCKED",
397
+ NO_PENDING_PLAN_VERSION: "NO_PENDING_PLAN_VERSION",
398
+ ONBOARDING_CREATE_FAILED: "ONBOARDING_CREATE_FAILED",
399
+ BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS",
400
+ /**
401
+ * The adapter returned a `SubscriptionUsageRecord` without `id`. A wiring
402
+ * error in the consumer, not a missing subscription — hence its own code.
403
+ */
404
+ SUBSCRIPTION_PK_MISSING: "SUBSCRIPTION_PK_MISSING",
405
+ // ── entitlements ──
406
+ /** Quota exhausted. Carries `dimension`, `used`, `max`. */
407
+ LIMIT_EXCEEDED: "LIMIT_EXCEEDED",
408
+ QUOTA_DIMENSION_UNKNOWN: "QUOTA_DIMENSION_UNKNOWN"
409
+ };
410
+ var CONTRACT_ERROR_CODES = {
411
+ CHECKOUT_OFFER_LINE_ITEMS_REQUIRED: "CHECKOUT_OFFER_LINE_ITEMS_REQUIRED",
412
+ CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED: "CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED",
413
+ CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED",
414
+ CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE: "CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE",
415
+ CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED: "CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED",
416
+ SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED: "SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED",
417
+ SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED",
418
+ SUBSCRIPTION_CONTRACT_INVALID_DATE: "SUBSCRIPTION_CONTRACT_INVALID_DATE",
419
+ SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "SUBSCRIPTION_CONTRACT_INVALID_WINDOW",
420
+ SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START",
421
+ // ── stage 3 ──
422
+ CHECKOUT_OFFER_NOT_FOUND: "CHECKOUT_OFFER_NOT_FOUND",
423
+ CHECKOUT_OFFER_EXPIRED: "CHECKOUT_OFFER_EXPIRED",
424
+ CHECKOUT_OFFER_ALREADY_CONSUMED: "CHECKOUT_OFFER_ALREADY_CONSUMED",
425
+ CHECKOUT_OFFER_NOT_CONSUMED: "CHECKOUT_OFFER_NOT_CONSUMED",
426
+ SUBSCRIPTION_CONTRACT_NOT_FOUND: "SUBSCRIPTION_CONTRACT_NOT_FOUND",
427
+ NO_ACTIVE_SUBSCRIPTION_CONTRACT: "NO_ACTIVE_SUBSCRIPTION_CONTRACT",
428
+ SUBSCRIPTION_CONTRACT_ALREADY_CLOSED: "SUBSCRIPTION_CONTRACT_ALREADY_CLOSED"
429
+ };
430
+ var REGISTRATION_ERROR_CODES = {
431
+ PENDING_REGISTRATION_NOT_FOUND: "PENDING_REGISTRATION_NOT_FOUND",
432
+ PENDING_REGISTRATION_EXPIRED: "PENDING_REGISTRATION_EXPIRED",
433
+ INVALID_REGISTRATION_STATE: "INVALID_REGISTRATION_STATE",
434
+ OTP_INVALID: "OTP_INVALID",
435
+ OTP_EXPIRED: "OTP_EXPIRED",
436
+ OTP_LOCKED: "OTP_LOCKED",
437
+ /** Too many attempts from this origin. Carries `retryAfterSeconds`. */
438
+ RATE_LIMITED: "RATE_LIMITED",
439
+ RESUME_TOKEN_INVALID: "RESUME_TOKEN_INVALID",
440
+ RESUME_NOT_CONFIGURED: "RESUME_NOT_CONFIGURED",
441
+ CONFIGURATOR_NOT_CONFIGURED: "CONFIGURATOR_NOT_CONFIGURED",
442
+ CONFIG_NOT_SAVED: "CONFIG_NOT_SAVED",
443
+ PLAN_NOT_AVAILABLE: "PLAN_NOT_AVAILABLE",
444
+ PLAN_NOT_SELECTED: "PLAN_NOT_SELECTED",
445
+ MODEL_NOT_AVAILABLE: "MODEL_NOT_AVAILABLE"
446
+ };
447
+ var AUTH_ERROR_CODES = {
448
+ /** No authenticated user on the request. */
449
+ NOT_AUTHENTICATED: "NOT_AUTHENTICATED",
450
+ /** Authenticated, but the request carries no tenant. */
451
+ NO_TENANT_ASSIGNED: "NO_TENANT_ASSIGNED",
452
+ /** Neither `tenantId` nor `userId` could be resolved from the request. */
453
+ TENANT_CONTEXT_MISSING: "TENANT_CONTEXT_MISSING",
454
+ TENANT_ADMIN_REQUIRED: "TENANT_ADMIN_REQUIRED",
455
+ SUPER_ADMIN_REQUIRED: "SUPER_ADMIN_REQUIRED",
456
+ /** TOTP MFA has never been set up for this user. */
457
+ MFA_NOT_SET_UP: "MFA_NOT_SET_UP",
458
+ /** Endpoint is MFA-gated and no `X-Mfa-Code` header was sent. */
459
+ MFA_REQUIRED: "MFA_REQUIRED",
460
+ /** The supplied TOTP code did not verify. */
461
+ MFA_FAILED: "MFA_FAILED",
462
+ /** Module misconfiguration, not an end-user condition. */
463
+ AUTH_GUARDS_NOT_CONFIGURED: "AUTH_GUARDS_NOT_CONFIGURED"
464
+ };
465
+ var PROMO_ERROR_CODES = {
466
+ PROMO_CODE_NOT_FOUND: "PROMO_CODE_NOT_FOUND",
467
+ PROMO_CODE_ALREADY_EXISTS: "PROMO_CODE_ALREADY_EXISTS",
468
+ PROMO_CODE_HAS_REDEMPTIONS: "PROMO_CODE_HAS_REDEMPTIONS",
469
+ /** Not redeemable. Carries `reason` (`PromoPreviewInvalidReason`). */
470
+ PROMO_CODE_NOT_REDEEMABLE: "PROMO_CODE_NOT_REDEEMABLE",
471
+ PROMO_CODE_FORMAT_INVALID: "PROMO_CODE_FORMAT_INVALID",
472
+ PROMO_PERCENT_OUT_OF_RANGE: "PROMO_PERCENT_OUT_OF_RANGE",
473
+ PROMO_AMOUNT_NOT_POSITIVE: "PROMO_AMOUNT_NOT_POSITIVE",
474
+ PROMO_ONE_OFF_WITH_DURATION: "PROMO_ONE_OFF_WITH_DURATION",
475
+ PROMO_DURATION_INVALID: "PROMO_DURATION_INVALID",
476
+ PROMO_VALIDITY_WINDOW_INVALID: "PROMO_VALIDITY_WINDOW_INVALID",
477
+ PROMO_PLAN_NOT_DISCOUNTABLE: "PROMO_PLAN_NOT_DISCOUNTABLE",
478
+ PROMO_MIN_AMOUNT_NOT_POSITIVE: "PROMO_MIN_AMOUNT_NOT_POSITIVE",
479
+ PROMO_WOULD_PRODUCE_ZERO_INVOICE: "PROMO_WOULD_PRODUCE_ZERO_INVOICE",
480
+ PROMO_MAX_REDEMPTIONS_LOWERED: "PROMO_MAX_REDEMPTIONS_LOWERED"
481
+ };
482
+ var PLATFORM_ERROR_CODES = {
483
+ ...SETUP_ERROR_CODES,
484
+ ...AUTH_ERROR_CODES,
485
+ ...PROMO_ERROR_CODES,
486
+ ...CATALOG_ERROR_CODES,
487
+ ...BILLING_ERROR_CODES,
488
+ ...CONTRACT_ERROR_CODES,
489
+ ...REGISTRATION_ERROR_CODES
490
+ };
491
+
492
+ // src/errors.ts
493
+ var USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS";
494
+ var PlatformUserExistsError = class extends Error {
495
+ constructor(email, existingRole) {
496
+ super(`User ${email} already exists (role: ${existingRole}).`);
497
+ this.email = email;
498
+ this.existingRole = existingRole;
499
+ this.name = "PlatformUserExistsError";
500
+ }
501
+ email;
502
+ existingRole;
503
+ code = USER_ALREADY_EXISTS;
504
+ };
505
+ function isPlatformUserExistsError(err) {
506
+ return err instanceof Error && err.code === USER_ALREADY_EXISTS;
507
+ }
508
+
509
+ // src/feature-requires.ts
510
+ function buildFeatureRequiresIndex(features) {
511
+ const index = /* @__PURE__ */ new Map();
512
+ for (const feature of features) {
513
+ const requires = (feature.requires ?? []).filter((key) => key !== feature.featureKey);
514
+ if (requires.length > 0) index.set(feature.featureKey, requires);
515
+ }
516
+ return index;
517
+ }
518
+ function collectUnsatisfiedRequires(selected, index) {
519
+ const own = new Set(selected);
520
+ const unsatisfied = /* @__PURE__ */ new Set();
521
+ for (const featureKey of selected) {
522
+ for (const required of index.get(featureKey) ?? []) {
523
+ if (!own.has(required)) unsatisfied.add(required);
524
+ }
525
+ }
526
+ return [...unsatisfied].sort((a, b) => a.localeCompare(b));
527
+ }
528
+ function missingRequiresFor(bundle, coveredFeatures) {
529
+ const missing = (bundle.requiresFeatures ?? []).filter((key) => !coveredFeatures.has(key));
530
+ return [...new Set(missing)].sort((a, b) => a.localeCompare(b));
531
+ }
532
+ function resolveBundleAvailability(bundle, coveredFeatures) {
533
+ if (bundle.features.length > 0 && bundle.features.every((f) => coveredFeatures.has(f))) {
534
+ return "covered";
535
+ }
536
+ if (missingRequiresFor(bundle, coveredFeatures).length > 0) return "missing-requires";
537
+ return "bookable";
538
+ }
539
+ function coverageExcludingSelf(selfVersionId, planFeatures, selectedBundles) {
540
+ const covered = new Set(planFeatures);
541
+ for (const bundle of selectedBundles) {
542
+ if (bundle.bundleVersionId === selfVersionId) continue;
543
+ for (const feature of bundle.features) covered.add(feature);
544
+ }
545
+ return covered;
546
+ }
547
+ function isBundleRedundant(bundle, planFeatures, selectedBundles) {
548
+ const covered = coverageExcludingSelf(bundle.bundleVersionId, planFeatures, selectedBundles);
549
+ return resolveBundleAvailability(bundle, covered) === "covered";
550
+ }
551
+ function compareSelectable(a, b) {
552
+ const orderA = a.sortOrder ?? Number.MAX_SAFE_INTEGER;
553
+ const orderB = b.sortOrder ?? Number.MAX_SAFE_INTEGER;
554
+ if (orderA !== orderB) return orderA - orderB;
555
+ return a.bundleVersionId.localeCompare(b.bundleVersionId);
556
+ }
557
+ function selectChargeableBundles(planFeatures, selectedBundles) {
558
+ const kept = [...selectedBundles].sort(compareSelectable);
559
+ for (; ; ) {
560
+ const redundantIndex = kept.findIndex(
561
+ (bundle) => isBundleRedundant(bundle, planFeatures, kept)
562
+ );
563
+ if (redundantIndex === -1) return kept;
564
+ kept.splice(redundantIndex, 1);
565
+ }
566
+ }
567
+
568
+ // src/registration.types.ts
569
+ var PENDING_EMAIL_TTL_HOURS = 72;
570
+ var PENDING_ONBOARDING_TTL_DAYS = 14;
571
+ var PENDING_CHECKOUT_TTL_DAYS = 30;
572
+ var OTP_TTL_MINUTES = 10;
573
+ var PASSWORD_RESET_TTL_MINUTES = 30;
574
+ var OTP_RATE_LIMIT_MAX_SENDS = 3;
575
+ var OTP_RATE_LIMIT_WINDOW_MINUTES = 15;
576
+ var OTP_VERIFY_MAX_ATTEMPTS = 5;
577
+ var REGISTRATION_STEP_BY_STATUS = {
578
+ PENDING_EMAIL_VERIFICATION: 2,
579
+ EMAIL_VERIFIED: 3,
580
+ PLAN_SELECTED: 4,
581
+ CHECKOUT_STARTED: 4,
582
+ EXPIRED: 1,
583
+ DELETED: 1
584
+ };
585
+ var REGISTRATION_RESUME_TTL_MINUTES = 60;
586
+
587
+ // src/version-editability.ts
588
+ function isVersionEditable(v, now = /* @__PURE__ */ new Date()) {
589
+ if (v.publishedAt === null) {
590
+ return { editable: true, reason: "draft" };
591
+ }
592
+ if (v.supersededAt !== null) {
593
+ return { editable: false, reason: null };
594
+ }
595
+ if (v.isLatestInChain !== true) {
596
+ return { editable: false, reason: null };
597
+ }
598
+ if ((v.subscriptionCount ?? 1) > 0) {
599
+ return { editable: false, reason: null };
600
+ }
601
+ if (v.validFrom === null) {
602
+ return { editable: false, reason: null };
603
+ }
604
+ const from = new Date(v.validFrom);
605
+ if (Number.isNaN(from.getTime())) {
606
+ return { editable: false, reason: null };
607
+ }
608
+ if (from.getTime() <= now.getTime()) {
609
+ return { editable: false, reason: null };
610
+ }
611
+ return { editable: true, reason: "pre-active" };
612
+ }
613
+
614
+ // src/error-messages.ts
615
+ var ERROR_MESSAGES_EN = {
616
+ // ── setup ──
617
+ SETUP_DISABLED: "{envVar} is not set \u2014 setup is disabled.",
618
+ INVALID_SETUP_TOKEN: "The setup token is not valid.",
619
+ SETUP_ALREADY_DONE: "Setup has already been completed.",
620
+ INVALID_EMAIL: "That is not a valid email address.",
621
+ EMAIL_EXISTS: "An account with this email address already exists.",
622
+ // ── auth ──
623
+ NOT_AUTHENTICATED: "Not authenticated",
624
+ NO_TENANT_ASSIGNED: "No tenant assigned",
625
+ TENANT_CONTEXT_MISSING: "No tenant ID found on the request",
626
+ TENANT_ADMIN_REQUIRED: "This action requires the TENANT_ADMIN role.",
627
+ SUPER_ADMIN_REQUIRED: "Only the SUPER_ADMIN role is allowed",
628
+ MFA_NOT_SET_UP: "Run the MFA setup via the CLI first.",
629
+ MFA_REQUIRED: "TOTP code required in the X-Mfa-Code header.",
630
+ MFA_FAILED: "Invalid TOTP code.",
631
+ AUTH_GUARDS_NOT_CONFIGURED: "TenantBillingModule.forRoot.authGuards is not configured.",
632
+ // ── catalog ──
633
+ PLAN_HAS_DRAFTS: "Plan '{planKey}' still has {draftCount} open draft version(s). Discard them first (DELETE /admin/catalog/plan-versions/:id) or publish them.",
634
+ PLAN_HAS_PUBLISHED_VERSIONS: "Plan '{planKey}' cannot be {operation} \u2014 it has {publishedCount} published version(s) ({liveCount} live, {supersededCount} superseded). Existing subscriptions reference those versions (contract protection P1), so the plan record must be preserved.",
635
+ PLAN_HARD_DELETE_NOT_IMPLEMENTED: "Hard delete is not implemented in the current repository. Implement PlanRepository.hardDelete.",
636
+ PLAN_VERSION_ALREADY_PUBLISHED: "PlanVersion '{versionId}' is already published and cannot be discarded.",
637
+ PLAN_VERSION_NOT_EDITABLE: "PlanVersion '{versionId}' is not editable. Only drafts and published versions are editable that are latest-in-chain, bind no subscription yet, and whose validFrom lies in the future.",
638
+ PLAN_VERSION_REGRESSION: "This plan version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
639
+ PLAN_VERSION_ZERO_PRICE: "A plan version cannot be published with a price of 0.00 (guard against seed placeholder). For deliberately free special contracts, set allowZeroPrice.",
640
+ PLAN_VERSION_DISCARD_NOT_IMPLEMENTED: "Discard is not implemented in the current repository. Implement PlanRepository.deletePlanVersionDraft.",
641
+ PLAN_VERSION_NOT_PUBLISHED: "PlanVersion '{versionId}' is not published and cannot be terminated.",
642
+ PLAN_VERSION_SUPERSEDED: "PlanVersion '{versionId}' has already been superseded by a newer version and cannot be terminated.",
643
+ PLAN_VERSION_VALID_FROM_REQUIRED: "validFrom must be set when publishing (on the draft or the publish call)..",
644
+ PLAN_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' is not a valid date",
645
+ PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS: "validFrom ({validFrom}) must be strictly after the validFrom of the previous version ({previousValidFrom}).",
646
+ PLAN_VERSION_VALID_FROM_NOT_GAPLESS: "The predecessor has validUntil={previousValidUntil} \u2014 the successor must start seamlessly on the next day ({requiredValidFrom}). Received: {received}.",
647
+ PLAN_VERSION_VALID_UNTIL_INVALID: "validUntil '{validUntil}' is not a valid date",
648
+ PLAN_VERSION_VALID_UNTIL_BEFORE_FROM: "validUntil ({validUntil}) must be strictly after validFrom ({validFrom}).",
649
+ PLAN_TERMINATE_INVALID_DATE: "endsAt is not a valid date.",
650
+ PLAN_TERMINATE_DATE_NOT_FUTURE: "endsAt ({endsAt}) must lie strictly in the future.",
651
+ PLAN_TERMINATE_NOT_IMPLEMENTED: "Terminate is not implemented in the current repository. Implement PlanRepository.terminate.",
652
+ BUNDLE_VERSION_ALREADY_PUBLISHED: "BundleVersion '{versionId}' is already published and cannot be discarded.",
653
+ BUNDLE_VERSION_NOT_EDITABLE: "BundleVersion '{versionId}' is not editable. Only drafts and published versions are editable that are latest-in-chain, bind no subscription yet, and whose validFrom lies in the future.",
654
+ BUNDLE_VERSION_NOT_PUBLISHED: "BundleVersion '{bundleVersionId}' is not published and cannot be booked.",
655
+ BUNDLE_VERSION_SUPERSEDED: "BundleVersion '{bundleVersionId}' has been superseded by a newer version.",
656
+ BUNDLE_VERSION_REGRESSION: "This bundle version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
657
+ BUNDLE_VERSION_ZERO_PRICE: "A bundle version cannot be published with an explicit price of 0.00 (guard against seed placeholders). For free bundles leave it null, or set allowZeroPrice.",
658
+ BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "Discard is not implemented in the current repository. Implement BundleRepository.deleteDraft.",
659
+ BUNDLE_VERSION_VALID_FROM_REQUIRED: "Published bundle versions must keep a validFrom. Set a new future date, or edit the draft before publishing.",
660
+ BUNDLE_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' is not a valid date",
661
+ BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS: "validFrom ({validFrom}) must be strictly after the validFrom of the previous version ({previousValidFrom}).",
662
+ BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS: "The predecessor has validUntil={previousValidUntil} \u2014 the successor must start seamlessly on the next day ({requiredValidFrom}). Received: {received}.",
663
+ BUNDLE_VERSION_VALID_FROM_NOT_FUTURE: "validFrom ({validFrom}) must, for a published-but-future bundle version, still lie in the future.",
664
+ BUNDLE_VERSION_VALID_UNTIL_INVALID: "validUntil '{validUntil}' is not a valid date",
665
+ BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM: "validUntil ({validUntil}) must be strictly after validFrom ({validFrom}).",
666
+ STRICT_MODE_VIOLATIONS: "The strict-mode check found drift against the discovery snapshot.",
667
+ PLAN_NOT_FOUND: "Plan '{planId}' not found",
668
+ PLAN_VERSION_NOT_FOUND: "PlanVersion '{versionId}' not found",
669
+ BUNDLE_NOT_FOUND: "Bundle '{bundleId}' not found",
670
+ BUNDLE_VERSION_NOT_FOUND: "BundleVersion '{bundleVersionId}' not found",
671
+ FEATURE_NOT_FOUND: "Feature '{featureKey}' not found in project '{projectKey}'",
672
+ QUOTA_NOT_FOUND: "Quota '{quotaKey}' not found in project '{projectKey}'",
673
+ PROMOTION_NOT_FOUND: "Promotion '{promotionId}' not found",
674
+ MARKETING_PROJECTION_NOT_FOUND: "MarketingProjection '{projectionId}' not found",
675
+ PLAN_ALREADY_EXISTS: "Plan '{planKey}' already exists in project '{projectKey}'",
676
+ BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' already exists in project '{projectKey}'",
677
+ MARKETING_PROJECTION_ALREADY_EXISTS: "Marketing projection for {targetType}/{targetVersionId}/{locale} already exists \u2014 use PATCH to edit it",
678
+ PLAN_DRAFT_ALREADY_EXISTS: "Plan '{planKey}' already has a draft version v{draftVersion}; publish or discard it first",
679
+ BUNDLE_DRAFT_ALREADY_EXISTS: "Bundle '{bundleKey}' already has a draft version v{draftVersion}; publish or discard it first",
680
+ QUOTA_NOT_IN_DISCOVERY_SNAPSHOT: "Quota '{quotaKey}' is not in the discovery snapshot \u2014 a quota that is not declared in code cannot be approved",
681
+ DISCOVERY_STATUS_TRANSITION_INVALID: "Transition '{from}' \u2192 '{to}' is not allowed",
682
+ DISCOVERY_NOT_INITIALIZED: "Approval requires a discovery snapshot \u2014 discovery is not initialised (#25)",
683
+ // ── billing ──
684
+ BUNDLE_ALREADY_SUBSCRIBED: "Subscription '{subscriptionId}' has already actively booked this bundle.",
685
+ BUNDLE_INCOMPATIBLE_WITH_PLAN: "BundleVersion '{bundleVersionId}' is not compatible with plan '{planKey}'. Allowed: [{allowedPlanKeys}].",
686
+ BUNDLE_NOT_SELF_SERVICE: "Bundle '{bundleKey}' is only activated via a special contract. Please contact the contract manager.",
687
+ SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' is already cancelled.",
688
+ SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' is not cancelled.",
689
+ SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "Cancellation already in effect \u2014 book the bundle again.",
690
+ SUBSCRIPTION_NOT_FOUND: "No subscription for tenant {tenantId}",
691
+ SUBSCRIPTION_TENANT_MISMATCH: "Subscription does not belong to the tenant",
692
+ SUBSCRIPTION_BUNDLE_NOT_FOUND: "SubscriptionBundle '{subscriptionBundleId}' not found",
693
+ TENANT_NOT_FOUND: "Tenant {slug} not found",
694
+ NO_ACTIVE_PLAN_VERSION: "No plan version for {planId} active as of {asOf} \u2014 neither the validFrom window is satisfied nor is a latest-live version available.",
695
+ PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" is not in the catalog ({projectKey})',
696
+ PLAN_NOT_SELF_SERVICE: "{planKey} is not activated via self-service.",
697
+ PLAN_CHANGE_BLOCKED: "Plan change during onboarding is blocked.",
698
+ NO_PENDING_PLAN_VERSION: "There is no pending plan version awaiting confirmation.",
699
+ ONBOARDING_CREATE_FAILED: "The account could not be created. Please try again.",
700
+ BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "Exactly one of bundleVersionId (add preview) or subscriptionBundleId (cancel preview) must be given.",
701
+ SUBSCRIPTION_PK_MISSING: "The adapter returned a subscription usage record without an id. Pass the subscription primary key through (see SubscriptionUsageRecord.id).",
702
+ LIMIT_EXCEEDED: "The limit for {dimension} has been reached: {used} of {max}.",
703
+ QUOTA_DIMENSION_UNKNOWN: 'Unknown quota dimension "{dimension}".',
704
+ // ── promo ──
705
+ PROMO_CODE_NOT_FOUND: "Code not found",
706
+ PROMO_CODE_ALREADY_EXISTS: "The code already exists.",
707
+ PROMO_CODE_HAS_REDEMPTIONS: "The code already has redemptions \u2014 it cannot be soft-deleted. Pause it instead.",
708
+ PROMO_CODE_NOT_REDEEMABLE: "Code cannot be redeemed: {reason}",
709
+ PROMO_CODE_FORMAT_INVALID: 'The code may only contain upper-case letters, digits, "-" and "_" (4\u201332 characters).',
710
+ PROMO_PERCENT_OUT_OF_RANGE: "The percentage must be between 0 and 100.",
711
+ PROMO_AMOUNT_NOT_POSITIVE: "The amount must be positive.",
712
+ PROMO_ONE_OFF_WITH_DURATION: "A one-off discount must not set a duration.",
713
+ PROMO_DURATION_INVALID: "Invalid duration (at most 24 months or billing periods).",
714
+ PROMO_VALIDITY_WINDOW_INVALID: "Invalid validity window.",
715
+ PROMO_PLAN_NOT_DISCOUNTABLE: "{plan}-Plan cannot be discounted.",
716
+ PROMO_MIN_AMOUNT_NOT_POSITIVE: "The minimum plan gross amount must be positive.",
717
+ PROMO_WOULD_PRODUCE_ZERO_INVOICE: "For absolute amounts the discount must stay below the lowest applicable plan price, or allowZeroInvoice must be enabled.",
718
+ PROMO_MAX_REDEMPTIONS_LOWERED: "maxRedemptions cannot be lowered.",
719
+ // ── contract ──
720
+ CHECKOUT_OFFER_LINE_ITEMS_REQUIRED: "A checkout offer can yield only one contract, and only once its line items are frozen.",
721
+ CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED: "A checkout offer requires a frozen plan line item.",
722
+ CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "Every selected bundle version requires a frozen bundle line item.",
723
+ CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE: "At least one bundle version from the checkout offer is no longer bookable.",
724
+ CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED: "The selected plan does not cover all feature dependencies: [{missingRequires}] are missing from the plan + selected bundles.",
725
+ SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED: "A subscription contract requires at least one line item.",
726
+ SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "A subscription contract requires exactly one plan base item.",
727
+ SUBSCRIPTION_CONTRACT_INVALID_DATE: "{field} must be a valid date.",
728
+ SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "effectiveUntil must be after effectiveFrom.",
729
+ SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "effectiveUntil must be after the effectiveFrom of the contract.",
730
+ CHECKOUT_OFFER_NOT_FOUND: "CheckoutOffer '{offerId}' not found",
731
+ CHECKOUT_OFFER_EXPIRED: "Checkout offer '{offerId}' has expired and cannot be {action}",
732
+ CHECKOUT_OFFER_ALREADY_CONSUMED: "Checkout offer '{offerId}' has already been consumed and cannot be {action}",
733
+ CHECKOUT_OFFER_NOT_CONSUMED: "CheckoutOffer '{offerId}' must be consumed before the contract is created",
734
+ SUBSCRIPTION_CONTRACT_NOT_FOUND: "SubscriptionContract '{contractId}' not found",
735
+ NO_ACTIVE_SUBSCRIPTION_CONTRACT: "No active subscription contract for tenant {tenantId}",
736
+ SUBSCRIPTION_CONTRACT_ALREADY_CLOSED: "SubscriptionContract '{contractId}' is already closed",
737
+ // ── registration ──
738
+ PENDING_REGISTRATION_NOT_FOUND: "This registration could not be found. It may have already been completed or discarded.",
739
+ PENDING_REGISTRATION_EXPIRED: "This registration has expired. Please start again.",
740
+ INVALID_REGISTRATION_STATE: "This step is not available at the current stage of the registration.",
741
+ OTP_INVALID: "That code is not correct. Please check it and try again.",
742
+ OTP_EXPIRED: "That code has expired. Please request a new one.",
743
+ OTP_LOCKED: "Too many incorrect attempts. Please request a new code.",
744
+ RATE_LIMITED: "Too many attempts. Please try again in {retryAfterSeconds} seconds.",
745
+ RESUME_TOKEN_INVALID: "This resume link is no longer valid. Please request a new one.",
746
+ RESUME_NOT_CONFIGURED: "Resuming a registration is not available.",
747
+ CONFIGURATOR_NOT_CONFIGURED: "The configurator is not available.",
748
+ CONFIG_NOT_SAVED: "The configuration could not be saved.",
749
+ PLAN_NOT_AVAILABLE: "This plan is not available.",
750
+ PLAN_NOT_SELECTED: "Please select a plan first.",
751
+ MODEL_NOT_AVAILABLE: "This option is not available.",
752
+ // ── entitlement (code lives in upsell.types.ts) ──
753
+ [FEATURE_NOT_LICENSED]: "Feature {featureKeys} is not included in the current plan."
754
+ };
755
+ function formatErrorMessage(template, params = {}) {
756
+ return template.replace(/\{(\w+)\}/g, (match, name) => {
757
+ const value = params[name];
758
+ return value === void 0 || value === null ? match : String(value);
759
+ });
760
+ }
761
+ function resolveErrorMessage(body, overrides = {}, defaults = ERROR_MESSAGES_EN) {
762
+ const code = typeof body.code === "string" ? body.code : void 0;
763
+ const template = (code && (overrides[code] ?? defaults[code])) ?? void 0;
764
+ if (!template) {
765
+ return typeof body.message === "string" ? body.message : code ?? "";
766
+ }
767
+ const { params, ...rest } = body;
768
+ return formatErrorMessage(template, { ...rest, ...params });
769
+ }
770
+
771
+ // src/error-messages-de.ts
772
+ var ERROR_MESSAGES_DE = {
773
+ // ── setup ──
774
+ SETUP_DISABLED: "{envVar} ist nicht gesetzt \u2014 das Setup ist deaktiviert.",
775
+ INVALID_SETUP_TOKEN: "Das Setup-Token ist ung\xFCltig.",
776
+ SETUP_ALREADY_DONE: "Das Setup wurde bereits abgeschlossen.",
777
+ INVALID_EMAIL: "Das ist keine g\xFCltige E-Mail-Adresse.",
778
+ EMAIL_EXISTS: "Mit dieser E-Mail-Adresse gibt es bereits ein Konto.",
779
+ // ── auth ──
780
+ NOT_AUTHENTICATED: "Nicht authentifiziert",
781
+ NO_TENANT_ASSIGNED: "Kein Mandant zugeordnet",
782
+ TENANT_CONTEXT_MISSING: "Am Request wurde keine Mandanten-ID gefunden",
783
+ TENANT_ADMIN_REQUIRED: "Diese Aktion erfordert die Rolle TENANT_ADMIN.",
784
+ SUPER_ADMIN_REQUIRED: "Nur die Rolle SUPER_ADMIN ist zugelassen",
785
+ MFA_NOT_SET_UP: "Richte MFA zuerst \xFCber die CLI ein.",
786
+ MFA_REQUIRED: "TOTP-Code im Header X-Mfa-Code erforderlich.",
787
+ MFA_FAILED: "Ung\xFCltiger TOTP-Code.",
788
+ AUTH_GUARDS_NOT_CONFIGURED: "TenantBillingModule.forRoot.authGuards ist nicht konfiguriert.",
789
+ // ── catalog ──
790
+ PLAN_HAS_DRAFTS: "Plan '{planKey}' hat noch {draftCount} offene Entwurfsversion(en). Verwirf sie zuerst (DELETE /admin/catalog/plan-versions/:id) oder ver\xF6ffentliche sie.",
791
+ PLAN_HAS_PUBLISHED_VERSIONS: "Plan '{planKey}' kann nicht {operation} werden \u2014 er hat {publishedCount} ver\xF6ffentlichte Version(en) ({liveCount} live, {supersededCount} abgel\xF6st). Bestehende Abonnements verweisen auf diese Versionen (Vertragsschutz P1), deshalb muss der Plan-Datensatz erhalten bleiben.",
792
+ PLAN_HARD_DELETE_NOT_IMPLEMENTED: "Hard Delete ist im aktuellen Repository nicht implementiert. Implementiere PlanRepository.hardDelete.",
793
+ PLAN_VERSION_ALREADY_PUBLISHED: "PlanVersion '{versionId}' ist bereits ver\xF6ffentlicht und kann nicht verworfen werden.",
794
+ PLAN_VERSION_NOT_EDITABLE: "PlanVersion '{versionId}' ist nicht bearbeitbar. Bearbeitbar sind nur Entw\xFCrfe sowie ver\xF6ffentlichte Versionen, die letzte in der Kette sind, noch kein Abonnement binden und deren validFrom in der Zukunft liegt.",
795
+ PLAN_VERSION_REGRESSION: "Diese Planversion ist regressiv (Feature entfernt / Quota gesenkt / Preis erh\xF6ht). Das Ver\xF6ffentlichen erfordert ein ausdr\xFCckliches `forceRegressive: true` (Best\xE4tigungsdialog in der UI mit MFA).",
796
+ PLAN_VERSION_ZERO_PRICE: "Eine Planversion l\xE4sst sich nicht mit einem Preis von 0,00 ver\xF6ffentlichen (Schutz vor Seed-Platzhaltern). F\xFCr bewusst kostenlose Sondervertr\xE4ge setze allowZeroPrice.",
797
+ PLAN_VERSION_DISCARD_NOT_IMPLEMENTED: "Das Verwerfen ist im aktuellen Repository nicht implementiert. Implementiere PlanRepository.deletePlanVersionDraft.",
798
+ PLAN_VERSION_NOT_PUBLISHED: "Die PlanVersion '{versionId}' ist nicht ver\xF6ffentlicht und kann nicht beendet werden.",
799
+ PLAN_VERSION_SUPERSEDED: "Die PlanVersion '{versionId}' wurde bereits von einer Nachfolgeversion abgel\xF6st und kann nicht beendet werden.",
800
+ PLAN_VERSION_VALID_FROM_REQUIRED: "validFrom muss beim Ver\xF6ffentlichen gesetzt sein (am Entwurf oder im Publish-Aufruf)..",
801
+ PLAN_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' ist kein g\xFCltiges Datum",
802
+ PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS: "validFrom ({validFrom}) muss strikt nach dem validFrom der Vorg\xE4ngerversion ({previousValidFrom}) liegen.",
803
+ PLAN_VERSION_VALID_FROM_NOT_GAPLESS: "Der Vorg\xE4nger hat validUntil={previousValidUntil} \u2014 der Nachfolger muss l\xFCckenlos am Folgetag ({requiredValidFrom}) beginnen. Erhalten: {received}.",
804
+ PLAN_VERSION_VALID_UNTIL_INVALID: "validUntil '{validUntil}' ist kein g\xFCltiges Datum",
805
+ PLAN_VERSION_VALID_UNTIL_BEFORE_FROM: "validUntil ({validUntil}) muss strikt nach validFrom ({validFrom}) liegen.",
806
+ PLAN_TERMINATE_INVALID_DATE: "endsAt ist kein g\xFCltiges Datum.",
807
+ PLAN_TERMINATE_DATE_NOT_FUTURE: "endsAt ({endsAt}) muss strikt in der Zukunft liegen.",
808
+ PLAN_TERMINATE_NOT_IMPLEMENTED: "Das Beenden ist im aktuellen Repository nicht implementiert. Implementiere PlanRepository.terminate.",
809
+ BUNDLE_VERSION_ALREADY_PUBLISHED: "BundleVersion '{versionId}' ist bereits ver\xF6ffentlicht und kann nicht verworfen werden.",
810
+ BUNDLE_VERSION_NOT_EDITABLE: "BundleVersion '{versionId}' ist nicht bearbeitbar. Bearbeitbar sind nur Entw\xFCrfe sowie ver\xF6ffentlichte Versionen, die letzte in der Kette sind, noch kein Abonnement binden und deren validFrom in der Zukunft liegt.",
811
+ BUNDLE_VERSION_NOT_PUBLISHED: "BundleVersion '{bundleVersionId}' ist nicht ver\xF6ffentlicht und l\xE4sst sich nicht buchen.",
812
+ BUNDLE_VERSION_SUPERSEDED: "BundleVersion '{bundleVersionId}' wurde durch eine neuere Version abgel\xF6st.",
813
+ BUNDLE_VERSION_REGRESSION: "Diese Bundle-Version ist regressiv (Feature entfernt / Quota gesenkt / Preis erh\xF6ht). Das Ver\xF6ffentlichen erfordert ein ausdr\xFCckliches `forceRegressive: true` (Best\xE4tigungsdialog in der UI mit MFA).",
814
+ BUNDLE_VERSION_ZERO_PRICE: "Eine Bundle-Version l\xE4sst sich nicht mit einem ausdr\xFCcklichen Preis von 0,00 ver\xF6ffentlichen (Schutz vor Seed-Platzhaltern). Kostenlose Bundles l\xE4sst du auf null, oder du setzt allowZeroPrice.",
815
+ BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED: "Das Verwerfen ist im aktuellen Repository nicht implementiert. Implementiere BundleRepository.deleteDraft.",
816
+ BUNDLE_VERSION_VALID_FROM_REQUIRED: "Ver\xF6ffentlichte Bundle-Versionen m\xFCssen ein validFrom behalten. Setze ein neues Datum in der Zukunft, oder bearbeite den Entwurf vor dem Ver\xF6ffentlichen.",
817
+ BUNDLE_VERSION_VALID_FROM_INVALID: "validFrom '{validFrom}' ist kein g\xFCltiges Datum",
818
+ BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS: "validFrom ({validFrom}) muss strikt nach dem validFrom der Vorg\xE4ngerversion ({previousValidFrom}) liegen.",
819
+ BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS: "Der Vorg\xE4nger hat validUntil={previousValidUntil} \u2014 der Nachfolger muss l\xFCckenlos am Folgetag ({requiredValidFrom}) beginnen. Erhalten: {received}.",
820
+ BUNDLE_VERSION_VALID_FROM_NOT_FUTURE: "validFrom ({validFrom}) muss bei einer ver\xF6ffentlichten, aber zuk\xFCnftigen Bundle-Version weiterhin in der Zukunft liegen.",
821
+ BUNDLE_VERSION_VALID_UNTIL_INVALID: "validUntil '{validUntil}' ist kein g\xFCltiges Datum",
822
+ BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM: "validUntil ({validUntil}) muss strikt nach validFrom ({validFrom}) liegen.",
823
+ STRICT_MODE_VIOLATIONS: "Die Strict-Mode-Pr\xFCfung hat eine Abweichung gegen\xFCber dem Discovery-Snapshot gefunden.",
824
+ PLAN_NOT_FOUND: "Plan '{planId}' nicht gefunden",
825
+ PLAN_VERSION_NOT_FOUND: "PlanVersion '{versionId}' nicht gefunden",
826
+ BUNDLE_NOT_FOUND: "Bundle '{bundleId}' nicht gefunden",
827
+ BUNDLE_VERSION_NOT_FOUND: "BundleVersion '{bundleVersionId}' nicht gefunden",
828
+ FEATURE_NOT_FOUND: "Feature '{featureKey}' im Projekt '{projectKey}' nicht gefunden",
829
+ QUOTA_NOT_FOUND: "Quota '{quotaKey}' im Projekt '{projectKey}' nicht gefunden",
830
+ PROMOTION_NOT_FOUND: "Promotion '{promotionId}' nicht gefunden",
831
+ MARKETING_PROJECTION_NOT_FOUND: "MarketingProjection '{projectionId}' nicht gefunden",
832
+ PLAN_ALREADY_EXISTS: "Plan '{planKey}' gibt es im Projekt '{projectKey}' bereits",
833
+ BUNDLE_ALREADY_EXISTS: "Bundle '{bundleKey}' gibt es im Projekt '{projectKey}' bereits",
834
+ MARKETING_PROJECTION_ALREADY_EXISTS: "F\xFCr {targetType}/{targetVersionId}/{locale} gibt es bereits eine Marketing-Projektion \u2014 bearbeite sie per PATCH",
835
+ PLAN_DRAFT_ALREADY_EXISTS: "Plan '{planKey}' hat bereits die Entwurfsversion v{draftVersion}; ver\xF6ffentliche oder verwirf sie zuerst",
836
+ BUNDLE_DRAFT_ALREADY_EXISTS: "Bundle '{bundleKey}' hat bereits die Entwurfsversion v{draftVersion}; ver\xF6ffentliche oder verwirf sie zuerst",
837
+ QUOTA_NOT_IN_DISCOVERY_SNAPSHOT: "Quota '{quotaKey}' steht nicht im Discovery-Snapshot \u2014 eine Quota, die im Code nicht deklariert ist, l\xE4sst sich nicht freigeben",
838
+ DISCOVERY_STATUS_TRANSITION_INVALID: "Der \xDCbergang '{from}' \u2192 '{to}' ist nicht erlaubt",
839
+ DISCOVERY_NOT_INITIALIZED: "Die Freigabe braucht einen Discovery-Snapshot \u2014 Discovery ist nicht initialisiert (#25)",
840
+ // ── billing ──
841
+ BUNDLE_ALREADY_SUBSCRIBED: "Abonnement '{subscriptionId}' hat dieses Bundle bereits aktiv gebucht.",
842
+ BUNDLE_INCOMPATIBLE_WITH_PLAN: "BundleVersion '{bundleVersionId}' passt nicht zum Plan '{planKey}'. Erlaubt: [{allowedPlanKeys}].",
843
+ BUNDLE_NOT_SELF_SERVICE: "Bundle '{bundleKey}' wird nur \xFCber einen Sondervertrag freigeschaltet. Bitte wende dich an die Vertragsverwaltung.",
844
+ SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' ist bereits gek\xFCndigt.",
845
+ SUBSCRIPTION_BUNDLE_NOT_CANCELLED: "SubscriptionBundle '{subscriptionBundleId}' ist nicht gek\xFCndigt.",
846
+ SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE: "Die K\xFCndigung ist bereits wirksam \u2014 buche das Bundle einfach neu.",
847
+ SUBSCRIPTION_NOT_FOUND: "Kein Abonnement f\xFCr Mandant {tenantId}",
848
+ SUBSCRIPTION_TENANT_MISMATCH: "Das Abonnement geh\xF6rt nicht zu diesem Mandanten",
849
+ SUBSCRIPTION_BUNDLE_NOT_FOUND: "SubscriptionBundle '{subscriptionBundleId}' nicht gefunden",
850
+ TENANT_NOT_FOUND: "Mandant {slug} nicht gefunden",
851
+ NO_ACTIVE_PLAN_VERSION: "Zum {asOf} ist keine Planversion f\xFCr {planId} aktiv \u2014 weder ist das validFrom-Fenster erf\xFCllt, noch steht eine j\xFCngste Live-Version bereit.",
852
+ PLAN_NOT_IN_CATALOG: 'Plan "{planKey}" steht nicht im Katalog ({projectKey})',
853
+ PLAN_NOT_SELF_SERVICE: "{planKey} wird nicht per Self-Service freigeschaltet.",
854
+ PLAN_CHANGE_BLOCKED: "W\xE4hrend des Onboardings ist ein Planwechsel gesperrt.",
855
+ NO_PENDING_PLAN_VERSION: "Es wartet keine Planversion auf eine Best\xE4tigung.",
856
+ ONBOARDING_CREATE_FAILED: "Das Konto konnte nicht angelegt werden. Bitte versuche es noch einmal.",
857
+ BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS: "Genau eines von bundleVersionId (Vorschau f\xFCrs Buchen) oder subscriptionBundleId (Vorschau f\xFCrs K\xFCndigen) muss angegeben sein.",
858
+ SUBSCRIPTION_PK_MISSING: "Der Adapter hat einen SubscriptionUsageRecord ohne id geliefert. Bitte den Prim\xE4rschl\xFCssel der Subscription durchreichen (siehe SubscriptionUsageRecord.id).",
859
+ LIMIT_EXCEEDED: "Das Limit f\xFCr {dimension} ist erreicht: {used} von {max}.",
860
+ QUOTA_DIMENSION_UNKNOWN: 'Unbekannte Quota-Dimension "{dimension}".',
861
+ // ── promo ──
862
+ PROMO_CODE_NOT_FOUND: "Code nicht gefunden",
863
+ PROMO_CODE_ALREADY_EXISTS: "Diesen Code gibt es bereits.",
864
+ PROMO_CODE_HAS_REDEMPTIONS: "Der Code wurde bereits eingel\xF6st \u2014 er l\xE4sst sich nicht per Soft-Delete entfernen. Pausiere ihn stattdessen.",
865
+ PROMO_CODE_NOT_REDEEMABLE: "Der Code l\xE4sst sich nicht einl\xF6sen: {reason}",
866
+ PROMO_CODE_FORMAT_INVALID: 'Der Code darf nur Gro\xDFbuchstaben, Ziffern, "-" und "_" enthalten (4\u201332 Zeichen).',
867
+ PROMO_PERCENT_OUT_OF_RANGE: "Der Prozentwert muss zwischen 0 und 100 liegen.",
868
+ PROMO_AMOUNT_NOT_POSITIVE: "Der Betrag muss positiv sein.",
869
+ PROMO_ONE_OFF_WITH_DURATION: "Ein einmaliger Rabatt darf keine Laufzeit haben.",
870
+ PROMO_DURATION_INVALID: "Ung\xFCltige Laufzeit (h\xF6chstens 24 Monate oder Abrechnungsperioden).",
871
+ PROMO_VALIDITY_WINDOW_INVALID: "Ung\xFCltiger G\xFCltigkeitszeitraum.",
872
+ PROMO_PLAN_NOT_DISCOUNTABLE: "Der {plan}-Plan l\xE4sst sich nicht rabattieren.",
873
+ PROMO_MIN_AMOUNT_NOT_POSITIVE: "Der Mindest-Bruttobetrag des Plans muss positiv sein.",
874
+ PROMO_WOULD_PRODUCE_ZERO_INVOICE: "Bei absoluten Betr\xE4gen muss der Rabatt unter dem niedrigsten anwendbaren Planpreis bleiben, oder allowZeroInvoice muss aktiviert sein.",
875
+ PROMO_MAX_REDEMPTIONS_LOWERED: "maxRedemptions l\xE4sst sich nicht senken.",
876
+ // ── contract ──
877
+ CHECKOUT_OFFER_LINE_ITEMS_REQUIRED: "Aus einem Checkout-Angebot entsteht nur ein einziger Vertrag, und das erst, wenn seine Positionen eingefroren sind.",
878
+ CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED: "Ein Checkout-Angebot braucht eine eingefrorene Plan-Position.",
879
+ CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED: "Jede gew\xE4hlte Bundle-Version braucht eine eingefrorene Bundle-Position.",
880
+ CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE: "Mindestens eine Bundle-Version aus dem Checkout-Angebot ist nicht mehr buchbar.",
881
+ CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED: "Der gew\xE4hlte Plan deckt nicht alle Feature-Abh\xE4ngigkeiten ab: [{missingRequires}] fehlen in Plan und gew\xE4hlten Bundles.",
882
+ SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED: "Ein Abo-Vertrag braucht mindestens eine Position.",
883
+ SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED: "Ein Abo-Vertrag braucht genau eine Plan-Grundposition.",
884
+ SUBSCRIPTION_CONTRACT_INVALID_DATE: "{field} muss ein g\xFCltiges Datum sein.",
885
+ SUBSCRIPTION_CONTRACT_INVALID_WINDOW: "effectiveUntil muss nach effectiveFrom liegen.",
886
+ SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START: "effectiveUntil muss nach dem effectiveFrom des Vertrags liegen.",
887
+ CHECKOUT_OFFER_NOT_FOUND: "CheckoutOffer '{offerId}' nicht gefunden",
888
+ CHECKOUT_OFFER_EXPIRED: "Checkout-Angebot '{offerId}' ist abgelaufen \u2014 '{action}' ist nicht mehr m\xF6glich",
889
+ CHECKOUT_OFFER_ALREADY_CONSUMED: "Checkout-Angebot '{offerId}' wurde bereits eingel\xF6st \u2014 '{action}' ist nicht mehr m\xF6glich",
890
+ CHECKOUT_OFFER_NOT_CONSUMED: "CheckoutOffer '{offerId}' muss eingel\xF6st sein, bevor der Vertrag entsteht",
891
+ SUBSCRIPTION_CONTRACT_NOT_FOUND: "SubscriptionContract '{contractId}' nicht gefunden",
892
+ NO_ACTIVE_SUBSCRIPTION_CONTRACT: "Kein aktiver Abo-Vertrag f\xFCr Mandant {tenantId}",
893
+ SUBSCRIPTION_CONTRACT_ALREADY_CLOSED: "SubscriptionContract '{contractId}' ist bereits geschlossen",
894
+ // ── registration ──
895
+ PENDING_REGISTRATION_NOT_FOUND: "Diese Registrierung konnten wir nicht finden. Vielleicht ist sie bereits abgeschlossen oder verworfen.",
896
+ PENDING_REGISTRATION_EXPIRED: "Diese Registrierung ist abgelaufen. Bitte fange noch einmal von vorn an.",
897
+ INVALID_REGISTRATION_STATE: "Dieser Schritt ist an dieser Stelle der Registrierung nicht verf\xFCgbar.",
898
+ OTP_INVALID: "Der Code ist nicht korrekt. Bitte pr\xFCfe ihn und versuche es erneut.",
899
+ OTP_EXPIRED: "Der Code ist abgelaufen. Bitte fordere einen neuen an.",
900
+ OTP_LOCKED: "Zu viele falsche Versuche. Bitte fordere einen neuen Code an.",
901
+ RATE_LIMITED: "Zu viele Versuche. Bitte probiere es in {retryAfterSeconds} Sekunden noch einmal.",
902
+ RESUME_TOKEN_INVALID: "Dieser Link zum Fortsetzen gilt nicht mehr. Bitte fordere einen neuen an.",
903
+ RESUME_NOT_CONFIGURED: "Eine begonnene Registrierung l\xE4sst sich hier nicht fortsetzen.",
904
+ CONFIGURATOR_NOT_CONFIGURED: "Der Konfigurator steht nicht zur Verf\xFCgung.",
905
+ CONFIG_NOT_SAVED: "Die Konfiguration konnte nicht gespeichert werden.",
906
+ PLAN_NOT_AVAILABLE: "Dieser Plan steht nicht zur Verf\xFCgung.",
907
+ PLAN_NOT_SELECTED: "Bitte w\xE4hle zuerst einen Plan aus.",
908
+ MODEL_NOT_AVAILABLE: "Diese Option steht nicht zur Verf\xFCgung.",
909
+ // ── entitlement (code lives in upsell.types.ts) ──
910
+ [FEATURE_NOT_LICENSED]: "Das Feature {featureKeys} ist im aktuellen Plan nicht enthalten."
911
+ };
912
+ // Annotate the CommonJS export names for ESM import in node:
913
+ 0 && (module.exports = {
914
+ AUTH_ERROR_CODES,
915
+ BILLING_ERROR_CODES,
916
+ CATALOG_ERROR_CODES,
917
+ CONTRACT_ERROR_CODES,
918
+ ERROR_MESSAGES_DE,
919
+ ERROR_MESSAGES_EN,
920
+ FEATURE_NOT_LICENSED,
921
+ OTP_RATE_LIMIT_MAX_SENDS,
922
+ OTP_RATE_LIMIT_WINDOW_MINUTES,
923
+ OTP_TTL_MINUTES,
924
+ OTP_VERIFY_MAX_ATTEMPTS,
925
+ PASSWORD_RESET_TTL_MINUTES,
926
+ PENDING_CHECKOUT_TTL_DAYS,
927
+ PENDING_EMAIL_TTL_HOURS,
928
+ PENDING_ONBOARDING_TTL_DAYS,
929
+ PLATFORM_ERROR_CODES,
930
+ PROMO_ERROR_CODES,
931
+ PersistenceCapabilityError,
932
+ PlatformUserExistsError,
933
+ REGISTRATION_ERROR_CODES,
934
+ REGISTRATION_RESUME_TTL_MINUTES,
935
+ REGISTRATION_STEP_BY_STATUS,
936
+ SETUP_ERROR_CODES,
937
+ applyPromo,
938
+ assertPersistenceCapabilities,
939
+ buildActivePlanVersionWhere,
940
+ buildActiveVersionWhere,
941
+ buildFeatureRequiresIndex,
942
+ classifyBundleVersionDiff,
943
+ classifyPlanDiff,
944
+ collectUnsatisfiedRequires,
945
+ coverageExcludingSelf,
946
+ formatErrorMessage,
947
+ isBundleRedundant,
948
+ isPlatformUserExistsError,
949
+ isVersionEditable,
950
+ missingRequiresFor,
951
+ pickActivePromo,
952
+ promoStatus,
953
+ resolveBundleAvailability,
954
+ resolveErrorMessage,
955
+ selectChargeableBundles,
956
+ startOfUtcDay
957
+ });