@saasicat/nest 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/_entries.cjs +1309 -519
  2. package/dist/admin/index.js +1 -1
  3. package/dist/billing/index.d.cts +7 -4
  4. package/dist/billing/index.d.ts +7 -4
  5. package/dist/billing/index.js +6 -6
  6. package/dist/catalog/index.js +4 -4
  7. package/dist/checkout-offer/index.js +2 -2
  8. package/dist/{chunk-Z3FWN3HJ.js → chunk-5ZXBXYXU.js} +3 -3
  9. package/dist/{chunk-RZVSKKXX.js → chunk-6WA4X5U3.js} +46 -45
  10. package/dist/{chunk-V34AA7C7.js → chunk-6WL6VQ7Z.js} +1 -1
  11. package/dist/{chunk-ABB2CRAS.js → chunk-74SCJ4HF.js} +1 -1
  12. package/dist/{chunk-PLJEAUFU.js → chunk-7JIHG53V.js} +76 -39
  13. package/dist/{chunk-7RRWI5LH.js → chunk-AFK2IE2L.js} +10 -10
  14. package/dist/{chunk-32EMXPU2.js → chunk-B7NRY2LV.js} +48 -17
  15. package/dist/{chunk-MDIZUVIK.js → chunk-BSK6YBLI.js} +1 -1
  16. package/dist/{chunk-R2QOSVAA.js → chunk-DQKCK7DX.js} +1 -1
  17. package/dist/{chunk-GLLCB4BP.js → chunk-EUU3B6NB.js} +17 -13
  18. package/dist/{chunk-EXOLOJFZ.js → chunk-F6DCLVBB.js} +29 -8
  19. package/dist/{chunk-5KH2O4SF.js → chunk-I77VS3YH.js} +287 -105
  20. package/dist/{chunk-YITTE646.js → chunk-KUDKD7ZG.js} +34 -14
  21. package/dist/{chunk-P3DP6GJE.js → chunk-LXUBCLRK.js} +486 -160
  22. package/dist/{chunk-GJBXOCRZ.js → chunk-ORWIGSNN.js} +45 -17
  23. package/dist/{chunk-E7AJ42AB.js → chunk-QLJHIIOY.js} +168 -28
  24. package/dist/{chunk-BOADBIGB.js → chunk-YYUXKLXK.js} +2 -2
  25. package/dist/{define-saasicat-Rc0rXrYG.d.ts → define-saasicat-CqkhnHn-.d.ts} +1 -1
  26. package/dist/{define-saasicat-CeDBX9ji.d.cts → define-saasicat-D8c6Bs4T.d.cts} +1 -1
  27. package/dist/discovery/index.d.cts +2 -2
  28. package/dist/discovery/index.d.ts +2 -2
  29. package/dist/discovery/index.js +4 -4
  30. package/dist/{enforce-quota.interceptor-DMw-bPyk.d.cts → enforce-quota.interceptor-Dk0eEtzV.d.cts} +1 -1
  31. package/dist/{enforce-quota.interceptor-CmbWX39L.d.ts → enforce-quota.interceptor-Dp8wTYro.d.ts} +1 -1
  32. package/dist/entitlement/index.js +2 -2
  33. package/dist/index.d.cts +6 -6
  34. package/dist/index.d.ts +6 -6
  35. package/dist/index.js +17 -17
  36. package/dist/{module-DgYJur6f.d.ts → module-BwD-wAor.d.ts} +1 -1
  37. package/dist/{module-CSN8j8Tp.d.cts → module-DsDkbawm.d.cts} +1 -1
  38. package/dist/platform/index.d.cts +8 -8
  39. package/dist/platform/index.d.ts +8 -8
  40. package/dist/platform/index.js +13 -13
  41. package/dist/promo/index.d.cts +3 -3
  42. package/dist/promo/index.d.ts +3 -3
  43. package/dist/promo/index.js +3 -3
  44. package/dist/registration/index.js +1 -1
  45. package/dist/{saas-platform.module-DLhoqKNA.d.ts → saas-platform.module-BQmrDGVU.d.ts} +1 -1
  46. package/dist/{saas-platform.module-D3rUTiho.d.cts → saas-platform.module-DducnQ9s.d.cts} +1 -1
  47. package/dist/{service-DX8KbGXl.d.cts → service-DPZ2W1E-.d.cts} +2 -2
  48. package/dist/{service-DX8KbGXl.d.ts → service-DPZ2W1E-.d.ts} +2 -2
  49. package/dist/subscription-contract/index.js +2 -2
  50. package/dist/{tenant-billing.controller-BwZRnJ3q.d.ts → tenant-billing.controller-b1G1yB09.d.ts} +2 -2
  51. package/dist/{tenant-billing.controller-CG4CaFUP.d.cts → tenant-billing.controller-m42YQ_Ei.d.cts} +2 -2
  52. package/dist/testing/index.d.cts +5 -5
  53. package/dist/testing/index.d.ts +5 -5
  54. package/dist/testing/index.js +33 -33
  55. package/package.json +3 -3
@@ -13,7 +13,7 @@ import {
13
13
  } from "./chunk-YSC46BGM.js";
14
14
  import {
15
15
  DiscoveryScanner
16
- } from "./chunk-Z3FWN3HJ.js";
16
+ } from "./chunk-5ZXBXYXU.js";
17
17
  import {
18
18
  SUBSCRIPTION_REPOSITORY_TOKEN
19
19
  } from "./chunk-WKVBNTYC.js";
@@ -78,7 +78,7 @@ function dependencyWarnings(features, snapshot, code) {
78
78
  if (selected.has(required)) continue;
79
79
  warnings.push({
80
80
  code,
81
- message: `Feature '${feature}' setzt '${required}' voraus, das in dieser Auswahl fehlt. Beim Tenant muss die Abh\xE4ngigkeit anderweitig gedeckt sein (z. B. durch den Plan) \u2014 sonst funktioniert das Feature nicht.`,
81
+ message: `Feature '${feature}' requires '${required}', which is missing from this selection. The tenant must cover that dependency some other way (for example through the plan) \u2014 otherwise the feature will not work.`,
82
82
  field: `features[${index}]`,
83
83
  value: required
84
84
  });
@@ -97,7 +97,7 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
97
97
  if (approved && !approved.features.has(feature)) {
98
98
  warnings.push({
99
99
  code: "BUNDLE_FEATURE_NOT_APPROVED",
100
- message: `Feature '${feature}' ist nicht freigegeben (Status != approved). Nur freigegebene Features sind verkaufbar \u2014 im Discovery-Review freigeben (#20).`,
100
+ message: `Feature '${feature}' is not approved (status != approved). Only approved features are sellable \u2014 approve it in the discovery review (#20).`,
101
101
  field: `features[${index}]`,
102
102
  value: feature
103
103
  });
@@ -106,7 +106,7 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
106
106
  }
107
107
  warnings.push({
108
108
  code: "BUNDLE_FEATURE_UNKNOWN",
109
- message: `Feature '${feature}' wurde im Discovery-Snapshot nicht gefunden. Entweder fehlt im Code eine @ImplementsCapability(..., { feature: '${feature}' })-Annotation, oder das Feature ist veraltet.`,
109
+ message: `Feature '${feature}' was not found in the discovery snapshot. Either the code is missing an @ImplementsCapability(..., { feature: '${feature}' }) annotation, or the feature is obsolete.`,
110
110
  field: `features[${index}]`,
111
111
  value: feature
112
112
  });
@@ -115,14 +115,14 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
115
115
  if (!knownQuotas.has(quotaKey)) {
116
116
  warnings.push({
117
117
  code: "QUOTA_MISSING",
118
- message: `Quota '${quotaKey}' wurde im Discovery-Snapshot nicht gefunden. Im Code muss eine Klasse mit @DefinesQuota({ key: '${quotaKey}', ... }) annotiert sein.`,
118
+ message: `Quota '${quotaKey}' was not found in the discovery snapshot. In code, a class must be annotated with @DefinesQuota({ key: '${quotaKey}', ... }).`,
119
119
  field: `quotas.${quotaKey}`,
120
120
  value: quotaKey
121
121
  });
122
122
  } else if (approved && !approved.quotas.has(quotaKey)) {
123
123
  warnings.push({
124
124
  code: "QUOTA_NOT_APPROVED",
125
- message: `Quota '${quotaKey}' ist nicht freigegeben (Status != approved). Nur freigegebene Quotas sind verkaufbar \u2014 im Discovery-Review freigeben (#20).`,
125
+ message: `Quota '${quotaKey}' is not approved (status != approved). Only approved quotas are sellable \u2014 approve it in the discovery review (#20).`,
126
126
  field: `quotas.${quotaKey}`,
127
127
  value: quotaKey
128
128
  });
@@ -134,7 +134,7 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
134
134
  if (knownPlanKeys.has(planKey)) return;
135
135
  warnings.push({
136
136
  code: "BUNDLE_PLAN_KEY_UNKNOWN",
137
- message: `Plan '${planKey}' aus compatibility.planIds existiert im Project nicht. Entweder den planKey korrigieren oder den Plan-Stamm zuerst anlegen.`,
137
+ message: `Plan '${planKey}' from compatibility.planIds does not exist in the project. Either fix the planKey, or create the plan record first.`,
138
138
  field: `compatibility.planIds[${index}]`,
139
139
  value: planKey
140
140
  });
@@ -154,7 +154,7 @@ function validatePlanDraft(draft, snapshot, marketedOnlyFeatures = /* @__PURE__
154
154
  if (approved && !approved.features.has(feature)) {
155
155
  warnings.push({
156
156
  code: "PLAN_FEATURE_NOT_APPROVED",
157
- message: `Feature '${feature}' ist nicht freigegeben (Status != approved). Nur freigegebene Features sind verkaufbar \u2014 im Discovery-Review freigeben (#20).`,
157
+ message: `Feature '${feature}' is not approved (status != approved). Only approved features are sellable \u2014 approve it in the discovery review (#20).`,
158
158
  field: `features[${index}]`,
159
159
  value: feature
160
160
  });
@@ -163,7 +163,7 @@ function validatePlanDraft(draft, snapshot, marketedOnlyFeatures = /* @__PURE__
163
163
  }
164
164
  warnings.push({
165
165
  code: "PLAN_FEATURE_UNKNOWN",
166
- message: `Feature '${feature}' wurde im Discovery-Snapshot nicht gefunden. Entweder fehlt im Code eine @ImplementsCapability(..., { feature: '${feature}' })-Annotation, oder das Feature ist veraltet.`,
166
+ message: `Feature '${feature}' was not found in the discovery snapshot. Either the code is missing an @ImplementsCapability(..., { feature: '${feature}' }) annotation, or the feature is obsolete.`,
167
167
  field: `features[${index}]`,
168
168
  value: feature
169
169
  });
@@ -172,14 +172,14 @@ function validatePlanDraft(draft, snapshot, marketedOnlyFeatures = /* @__PURE__
172
172
  if (!knownQuotas.has(quotaKey)) {
173
173
  warnings.push({
174
174
  code: "QUOTA_MISSING",
175
- message: `Quota '${quotaKey}' wurde im Discovery-Snapshot nicht gefunden. Im Code muss eine Klasse mit @DefinesQuota({ key: '${quotaKey}', ... }) annotiert sein.`,
175
+ message: `Quota '${quotaKey}' was not found in the discovery snapshot. In code, a class must be annotated with @DefinesQuota({ key: '${quotaKey}', ... }).`,
176
176
  field: `quotas.${quotaKey}`,
177
177
  value: quotaKey
178
178
  });
179
179
  } else if (approved && !approved.quotas.has(quotaKey)) {
180
180
  warnings.push({
181
181
  code: "QUOTA_NOT_APPROVED",
182
- message: `Quota '${quotaKey}' ist nicht freigegeben (Status != approved). Nur freigegebene Quotas sind verkaufbar \u2014 im Discovery-Review freigeben (#20).`,
182
+ message: `Quota '${quotaKey}' is not approved (status != approved). Only approved quotas are sellable \u2014 approve it in the discovery review (#20).`,
183
183
  field: `quotas.${quotaKey}`,
184
184
  value: quotaKey
185
185
  });
@@ -192,7 +192,7 @@ __name(validatePlanDraft, "validatePlanDraft");
192
192
 
193
193
  // src/catalog/bundles.service.ts
194
194
  import { Inject, Injectable, Logger, NotFoundException, Optional, UnprocessableEntityException } from "@nestjs/common";
195
- import { isVersionEditable } from "@saasicat/types";
195
+ import { CATALOG_ERROR_CODES, isVersionEditable } from "@saasicat/types";
196
196
 
197
197
  // src/core/discovery-snapshot-source.ts
198
198
  function resolveDiscoverySnapshot(injected, scanner) {
@@ -247,7 +247,7 @@ var BundlesService = class _BundlesService {
247
247
  this.mode = config.strictModeCheckMode ?? "blocking";
248
248
  this.marketedOnly = new Set(config.marketedOnlyFeatures ?? []);
249
249
  if (this.mode === "blocking" && !hasDiscoverySnapshotSource(this.snapshot, this.scanner)) {
250
- this.logger.error("BundlesService: strictModeCheckMode=blocking ohne DiscoverySnapshot-Quelle \u2014 degradiere auf warn-only. DiscoveryModule wiren, damit die Erzwingung greift (#25).");
250
+ this.logger.error("BundlesService: strictModeCheckMode=blocking without a DiscoverySnapshot source \u2014 degrading to warn-only. Wire DiscoveryModule so that enforcement takes effect (#25).");
251
251
  this.mode = "warn-only";
252
252
  }
253
253
  }
@@ -262,7 +262,15 @@ var BundlesService = class _BundlesService {
262
262
  }
263
263
  async getBundle(bundleId) {
264
264
  const bundle = await this.repo.findById(bundleId);
265
- if (!bundle) throw new NotFoundException(`Bundle '${bundleId}' nicht gefunden`);
265
+ if (!bundle) {
266
+ throw new NotFoundException({
267
+ code: CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
268
+ message: `Bundle '${bundleId}' not found`,
269
+ params: {
270
+ bundleId
271
+ }
272
+ });
273
+ }
266
274
  const versions = await this.annotateEditability(await this.repo.listVersions(bundleId));
267
275
  return {
268
276
  bundle,
@@ -272,18 +280,41 @@ var BundlesService = class _BundlesService {
272
280
  async createBundle(data) {
273
281
  const existing = await this.repo.findByKey(data.projectKey, data.bundleKey);
274
282
  if (existing) {
275
- throw new UnprocessableEntityException(`Bundle '${data.bundleKey}' existiert bereits in Projekt '${data.projectKey}'`);
283
+ throw new UnprocessableEntityException({
284
+ code: CATALOG_ERROR_CODES.BUNDLE_ALREADY_EXISTS,
285
+ message: `Bundle '${data.bundleKey}' already exists in project '${data.projectKey}'`,
286
+ params: {
287
+ bundleKey: data.bundleKey,
288
+ projectKey: data.projectKey
289
+ }
290
+ });
276
291
  }
277
292
  return this.repo.create(data);
278
293
  }
279
294
  async updateBundle(bundleId, data) {
280
295
  const existing = await this.repo.findById(bundleId);
281
- if (!existing) throw new NotFoundException(`Bundle '${bundleId}' nicht gefunden`);
296
+ if (!existing) {
297
+ throw new NotFoundException({
298
+ code: CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
299
+ message: `Bundle '${bundleId}' not found`,
300
+ params: {
301
+ bundleId
302
+ }
303
+ });
304
+ }
282
305
  return this.repo.update(bundleId, data);
283
306
  }
284
307
  async softDeleteBundle(bundleId) {
285
308
  const existing = await this.repo.findById(bundleId);
286
- if (!existing) throw new NotFoundException(`Bundle '${bundleId}' nicht gefunden`);
309
+ if (!existing) {
310
+ throw new NotFoundException({
311
+ code: CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
312
+ message: `Bundle '${bundleId}' not found`,
313
+ params: {
314
+ bundleId
315
+ }
316
+ });
317
+ }
287
318
  if (existing.deletedAt !== null) return;
288
319
  await this.repo.softDelete(bundleId);
289
320
  }
@@ -295,18 +326,27 @@ var BundlesService = class _BundlesService {
295
326
  async discardBundleDraft(versionId) {
296
327
  const existing = await this.repo.findVersionById(versionId);
297
328
  if (!existing) {
298
- throw new NotFoundException(`BundleVersion '${versionId}' nicht gefunden`);
329
+ throw new NotFoundException({
330
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
331
+ message: `BundleVersion '${versionId}' not found`,
332
+ params: {
333
+ versionId
334
+ }
335
+ });
299
336
  }
300
337
  if (existing.publishedAt !== null) {
301
338
  throw new UnprocessableEntityException({
302
- code: "BUNDLE_VERSION_ALREADY_PUBLISHED",
303
- message: `BundleVersion '${versionId}' ist bereits published und kann nicht verworfen werden.`
339
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_ALREADY_PUBLISHED,
340
+ message: `BundleVersion '${versionId}' is already published and cannot be discarded.`,
341
+ params: {
342
+ versionId
343
+ }
304
344
  });
305
345
  }
306
346
  if (typeof this.repo.deleteDraft !== "function") {
307
347
  throw new UnprocessableEntityException({
308
- code: "BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED",
309
- message: "Discard ist im aktuellen Repository nicht implementiert. Implementiere BundleRepository.deleteDraft."
348
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED,
349
+ message: "Discard is not implemented in the current repository. Implement BundleRepository.deleteDraft."
310
350
  });
311
351
  }
312
352
  await this.repo.deleteDraft(versionId);
@@ -320,7 +360,13 @@ var BundlesService = class _BundlesService {
320
360
  async getBundleVersion(versionId) {
321
361
  const version = await this.repo.findVersionById(versionId);
322
362
  if (!version) {
323
- throw new NotFoundException(`BundleVersion '${versionId}' nicht gefunden`);
363
+ throw new NotFoundException({
364
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
365
+ message: `BundleVersion '${versionId}' not found`,
366
+ params: {
367
+ versionId
368
+ }
369
+ });
324
370
  }
325
371
  const [annotated] = await this.annotateEditability([
326
372
  version
@@ -362,11 +408,24 @@ var BundlesService = class _BundlesService {
362
408
  async createBundleDraft(data) {
363
409
  const bundle = await this.repo.findById(data.bundleId);
364
410
  if (!bundle) {
365
- throw new NotFoundException(`Bundle '${data.bundleId}' nicht gefunden`);
411
+ throw new NotFoundException({
412
+ code: CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
413
+ message: `Bundle '${data.bundleId}' not found`,
414
+ params: {
415
+ bundleId: data.bundleId
416
+ }
417
+ });
366
418
  }
367
419
  const existingDraft = await this.repo.findCurrentDraft(data.bundleId);
368
420
  if (existingDraft) {
369
- throw new UnprocessableEntityException(`Bundle '${bundle.bundleKey}' hat bereits eine Draft-Version v${existingDraft.version}; bitte erst publishen oder verwerfen`);
421
+ throw new UnprocessableEntityException({
422
+ code: CATALOG_ERROR_CODES.BUNDLE_DRAFT_ALREADY_EXISTS,
423
+ message: `Bundle '${bundle.bundleKey}' already has a draft version v${existingDraft.version}; publish or discard it first`,
424
+ params: {
425
+ bundleKey: bundle.bundleKey,
426
+ draftVersion: existingDraft.version
427
+ }
428
+ });
370
429
  }
371
430
  if (data.baseVersionId === void 0) {
372
431
  const latestLive = await this.repo.findLatestLive(data.bundleId);
@@ -398,7 +457,13 @@ var BundlesService = class _BundlesService {
398
457
  async updateBundleDraft(versionId, data) {
399
458
  const existing = await this.repo.findVersionById(versionId);
400
459
  if (!existing) {
401
- throw new NotFoundException(`BundleVersion '${versionId}' nicht gefunden`);
460
+ throw new NotFoundException({
461
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
462
+ message: `BundleVersion '${versionId}' not found`,
463
+ params: {
464
+ versionId
465
+ }
466
+ });
402
467
  }
403
468
  const [annotated] = await this.annotateEditability([
404
469
  existing
@@ -406,8 +471,11 @@ var BundlesService = class _BundlesService {
406
471
  const { editable } = isVersionEditable(annotated);
407
472
  if (!editable) {
408
473
  throw new UnprocessableEntityException({
409
- code: "BUNDLE_VERSION_NOT_EDITABLE",
410
- message: `BundleVersion '${versionId}' ist nicht editierbar. Editierbar sind nur Drafts und published Versionen, die latest-in-chain sind, noch keine Subscription binden und deren validFrom in der Zukunft liegt.`
474
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_EDITABLE,
475
+ message: `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.`,
476
+ params: {
477
+ versionId
478
+ }
411
479
  });
412
480
  }
413
481
  const merged = {
@@ -438,10 +506,22 @@ var BundlesService = class _BundlesService {
438
506
  async publishBundleVersion(versionId, publishMeta) {
439
507
  const draft = await this.repo.findVersionById(versionId);
440
508
  if (!draft) {
441
- throw new NotFoundException(`BundleVersion '${versionId}' nicht gefunden`);
509
+ throw new NotFoundException({
510
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
511
+ message: `BundleVersion '${versionId}' not found`,
512
+ params: {
513
+ versionId
514
+ }
515
+ });
442
516
  }
443
517
  if (draft.publishedAt !== null) {
444
- throw new UnprocessableEntityException(`BundleVersion '${versionId}' ist bereits published`);
518
+ throw new UnprocessableEntityException({
519
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_ALREADY_PUBLISHED,
520
+ message: `BundleVersion '${versionId}' is already published`,
521
+ params: {
522
+ versionId
523
+ }
524
+ });
445
525
  }
446
526
  const bundle = await this.repo.findById(draft.bundleId);
447
527
  const warnings = await this.runStrictCheck({
@@ -454,8 +534,8 @@ var BundlesService = class _BundlesService {
454
534
  const explicitZero = /* @__PURE__ */ __name((v) => v !== null && v !== void 0 && Number.parseFloat(String(v)) <= 0, "explicitZero");
455
535
  if (explicitZero(draft.monthlyNet) || explicitZero(draft.yearlyNet)) {
456
536
  throw new UnprocessableEntityException({
457
- code: "BUNDLE_VERSION_ZERO_PRICE",
458
- message: "BundleVersion kann nicht mit explizitem Preis 0,00 published werden (Schutz gegen Seed-Platzhalter). Preisfreie Bundles: null lassen oder allowZeroPrice setzen.",
537
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_ZERO_PRICE,
538
+ message: "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.",
459
539
  monthlyNet: draft.monthlyNet,
460
540
  yearlyNet: draft.yearlyNet
461
541
  });
@@ -465,23 +545,30 @@ var BundlesService = class _BundlesService {
465
545
  const validFromInput = publishMeta.validFrom ?? draft.validFrom;
466
546
  if (!validFromInput) {
467
547
  throw new UnprocessableEntityException({
468
- code: "BUNDLE_VERSION_VALID_FROM_REQUIRED",
469
- message: "Beim Publish muss validFrom gesetzt sein (auf Draft oder Publish-Aufruf). SPEC_V2 \xA74.2."
548
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
549
+ message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
470
550
  });
471
551
  }
472
552
  const validFrom = new Date(validFromInput);
473
553
  if (Number.isNaN(validFrom.getTime())) {
474
554
  throw new UnprocessableEntityException({
475
- code: "BUNDLE_VERSION_VALID_FROM_INVALID",
476
- message: `validFrom '${validFromInput}' ist kein g\xFCltiges Datum`
555
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_INVALID,
556
+ message: `validFrom '${validFromInput}' is not a valid date`,
557
+ params: {
558
+ validFrom: validFromInput
559
+ }
477
560
  });
478
561
  }
479
562
  if (previous?.validFrom) {
480
563
  const prevFrom = new Date(previous.validFrom);
481
564
  if (validFrom <= prevFrom) {
482
565
  throw new UnprocessableEntityException({
483
- code: "BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS",
484
- message: `validFrom (${validFrom.toISOString()}) muss strikt nach validFrom der Vorg\xE4nger-Version (${previous.validFrom}) liegen.`
566
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
567
+ message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
568
+ params: {
569
+ validFrom: validFrom.toISOString(),
570
+ previousValidFrom: previous.validFrom
571
+ }
485
572
  });
486
573
  }
487
574
  if (previous.validUntil) {
@@ -490,8 +577,11 @@ var BundlesService = class _BundlesService {
490
577
  const requiredStart = new Date(prevUntil.getTime() + dayMs);
491
578
  if (validFrom.getTime() !== requiredStart.getTime()) {
492
579
  throw new UnprocessableEntityException({
493
- code: "BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS",
494
- message: `Vorg\xE4nger hat validUntil=${previous.validUntil.slice(0, 10)} \u2014 der Nachfolger muss nahtlos am Folgetag (${requiredStart.toISOString().slice(0, 10)}) starten. Geliefert: ${validFrom.toISOString().slice(0, 10)}.`,
580
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
581
+ message: `The predecessor has validUntil=${previous.validUntil.slice(0, 10)} \u2014 the successor must start seamlessly on the next day (${requiredStart.toISOString().slice(0, 10)}). Received: ${validFrom.toISOString().slice(0, 10)}.`,
582
+ params: {
583
+ received: validFrom.toISOString().slice(0, 10)
584
+ },
495
585
  requiredValidFrom: requiredStart.toISOString().slice(0, 10),
496
586
  previousValidUntil: previous.validUntil
497
587
  });
@@ -502,14 +592,21 @@ var BundlesService = class _BundlesService {
502
592
  const validUntil = validUntilInput ? new Date(validUntilInput) : null;
503
593
  if (validUntil && Number.isNaN(validUntil.getTime())) {
504
594
  throw new UnprocessableEntityException({
505
- code: "BUNDLE_VERSION_VALID_UNTIL_INVALID",
506
- message: `validUntil '${validUntilInput}' ist kein g\xFCltiges Datum`
595
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_INVALID,
596
+ message: `validUntil '${validUntilInput}' is not a valid date`,
597
+ params: {
598
+ validUntil: validUntilInput
599
+ }
507
600
  });
508
601
  }
509
602
  if (validUntil && validUntil <= validFrom) {
510
603
  throw new UnprocessableEntityException({
511
- code: "BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM",
512
- message: `validUntil (${validUntil.toISOString()}) muss strikt nach validFrom (${validFrom.toISOString()}) liegen.`
604
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM,
605
+ message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
606
+ params: {
607
+ validUntil: validUntil.toISOString(),
608
+ validFrom: validFrom.toISOString()
609
+ }
513
610
  });
514
611
  }
515
612
  const diff = previous ? classifyBundleVersionDiff({
@@ -528,8 +625,8 @@ var BundlesService = class _BundlesService {
528
625
  };
529
626
  if (!diff.nonRegressive && !publishMeta.forceRegressive) {
530
627
  throw new UnprocessableEntityException({
531
- code: "BUNDLE_VERSION_REGRESSION",
532
- message: "Diese BundleVersion ist regressiv (Feature entfernt / Quota gesenkt / Preis erh\xF6ht). Publishen erfordert explizites `forceRegressive: true` (UI-Best\xE4tigungsmodal mit MFA).",
628
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_REGRESSION,
629
+ message: "This bundle version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
533
630
  changes: diff.changes
534
631
  });
535
632
  }
@@ -568,8 +665,8 @@ var BundlesService = class _BundlesService {
568
665
  const hasHardBlockingWarning = blocking.some((w) => w.code === "BUNDLE_PLAN_KEY_UNKNOWN");
569
666
  if (this.mode === "blocking" || hasHardBlockingWarning) {
570
667
  throw new UnprocessableEntityException({
571
- code: "STRICT_MODE_VIOLATIONS",
572
- message: "Strict-Mode-Check hat Drift gegen den Discovery-Snapshot gefunden.",
668
+ code: CATALOG_ERROR_CODES.STRICT_MODE_VIOLATIONS,
669
+ message: "The strict-mode check found drift against the discovery snapshot.",
573
670
  warnings
574
671
  });
575
672
  }
@@ -582,8 +679,8 @@ var BundlesService = class _BundlesService {
582
679
  if (!validFromInput) {
583
680
  if (existing.publishedAt !== null) {
584
681
  throw new UnprocessableEntityException({
585
- code: "BUNDLE_VERSION_VALID_FROM_REQUIRED",
586
- message: "Published BundleVersionen m\xFCssen ein validFrom behalten. Setze ein neues zuk\xFCnftiges Datum oder bearbeite die Draft vor dem Publish."
682
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
683
+ message: "Published bundle versions must keep a validFrom. Set a new future date, or edit the draft before publishing."
587
684
  });
588
685
  }
589
686
  return;
@@ -591,14 +688,21 @@ var BundlesService = class _BundlesService {
591
688
  const validFrom = this.parseRequiredBundleDate(validFromInput, "validFrom", "BUNDLE_VERSION_VALID_FROM_INVALID");
592
689
  if (existing.publishedAt !== null && validFrom.getTime() <= Date.now()) {
593
690
  throw new UnprocessableEntityException({
594
- code: "BUNDLE_VERSION_VALID_FROM_NOT_FUTURE",
595
- message: `validFrom (${validFrom.toISOString()}) muss f\xFCr eine published-but-future BundleVersion weiterhin in der Zukunft liegen.`
691
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_FUTURE,
692
+ message: `validFrom (${validFrom.toISOString()}) must, for a published-but-future bundle version, still lie in the future.`,
693
+ params: {
694
+ validFrom: validFrom.toISOString()
695
+ }
596
696
  });
597
697
  }
598
698
  if (validUntil && validUntil <= validFrom) {
599
699
  throw new UnprocessableEntityException({
600
- code: "BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM",
601
- message: `validUntil (${validUntil.toISOString()}) muss strikt nach validFrom (${validFrom.toISOString()}) liegen.`
700
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM,
701
+ message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
702
+ params: {
703
+ validUntil: validUntil.toISOString(),
704
+ validFrom: validFrom.toISOString()
705
+ }
602
706
  });
603
707
  }
604
708
  const previous = await this.findPreviousBundleVersion(existing);
@@ -607,8 +711,12 @@ var BundlesService = class _BundlesService {
607
711
  if (Number.isNaN(previousValidFrom.getTime())) return;
608
712
  if (validFrom <= previousValidFrom) {
609
713
  throw new UnprocessableEntityException({
610
- code: "BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS",
611
- message: `validFrom (${validFrom.toISOString()}) muss strikt nach validFrom der Vorg\xE4nger-Version (${previous.validFrom}) liegen.`
714
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
715
+ message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
716
+ params: {
717
+ validFrom: validFrom.toISOString(),
718
+ previousValidFrom: previous.validFrom
719
+ }
612
720
  });
613
721
  }
614
722
  if (!previous.validUntil) return;
@@ -618,8 +726,11 @@ var BundlesService = class _BundlesService {
618
726
  const requiredStart = new Date(previousValidUntil.getTime() + dayMs);
619
727
  if (validFrom.getTime() !== requiredStart.getTime()) {
620
728
  throw new UnprocessableEntityException({
621
- code: "BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS",
622
- message: `Vorg\xE4nger hat validUntil=${previous.validUntil.slice(0, 10)} \u2014 der Nachfolger muss nahtlos am Folgetag (${requiredStart.toISOString().slice(0, 10)}) starten. Geliefert: ${validFrom.toISOString().slice(0, 10)}.`,
729
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
730
+ message: `The predecessor has validUntil=${previous.validUntil.slice(0, 10)} \u2014 the successor must start seamlessly on the next day (${requiredStart.toISOString().slice(0, 10)}). Received: ${validFrom.toISOString().slice(0, 10)}.`,
731
+ params: {
732
+ received: validFrom.toISOString().slice(0, 10)
733
+ },
623
734
  requiredValidFrom: requiredStart.toISOString().slice(0, 10),
624
735
  previousValidUntil: previous.validUntil
625
736
  });
@@ -638,7 +749,11 @@ var BundlesService = class _BundlesService {
638
749
  if (Number.isNaN(date.getTime())) {
639
750
  throw new UnprocessableEntityException({
640
751
  code,
641
- message: `${field} '${input}' ist kein g\xFCltiges Datum`
752
+ message: `${field} '${input}' is not a valid date`,
753
+ params: {
754
+ field,
755
+ value: input
756
+ }
642
757
  });
643
758
  }
644
759
  return date;
@@ -673,6 +788,7 @@ BundlesService = _ts_decorate([
673
788
 
674
789
  // src/catalog/marketing-projections.service.ts
675
790
  import { ConflictException, Inject as Inject2, Injectable as Injectable2, NotFoundException as NotFoundException2 } from "@nestjs/common";
791
+ import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES2 } from "@saasicat/types";
676
792
  function _ts_decorate2(decorators, target, key, desc) {
677
793
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
678
794
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -704,7 +820,13 @@ var MarketingProjectionsService = class {
704
820
  async getById(id) {
705
821
  const row = await this.repo.findById(id);
706
822
  if (!row) {
707
- throw new NotFoundException2(`MarketingProjection '${id}' nicht gefunden`);
823
+ throw new NotFoundException2({
824
+ code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_NOT_FOUND,
825
+ message: `MarketingProjection '${id}' not found`,
826
+ params: {
827
+ projectionId: id
828
+ }
829
+ });
708
830
  }
709
831
  return row;
710
832
  }
@@ -712,7 +834,15 @@ var MarketingProjectionsService = class {
712
834
  const locale = data.locale ?? "de";
713
835
  const existing = await this.repo.findByTarget(data.targetType, data.targetVersionId, locale);
714
836
  if (existing) {
715
- throw new ConflictException(`MarketingProjection f\xFCr ${data.targetType}/${data.targetVersionId}/${locale} existiert bereits \u2014 nutze PATCH zum Editieren`);
837
+ throw new ConflictException({
838
+ code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_ALREADY_EXISTS,
839
+ message: `Marketing projection for ${data.targetType}/${data.targetVersionId}/${locale} already exists \u2014 use PATCH to edit it`,
840
+ params: {
841
+ targetType: data.targetType,
842
+ targetVersionId: data.targetVersionId,
843
+ locale
844
+ }
845
+ });
716
846
  }
717
847
  return this.repo.create({
718
848
  ...data,
@@ -722,14 +852,26 @@ var MarketingProjectionsService = class {
722
852
  async update(id, data) {
723
853
  const existing = await this.repo.findById(id);
724
854
  if (!existing) {
725
- throw new NotFoundException2(`MarketingProjection '${id}' nicht gefunden`);
855
+ throw new NotFoundException2({
856
+ code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_NOT_FOUND,
857
+ message: `MarketingProjection '${id}' not found`,
858
+ params: {
859
+ projectionId: id
860
+ }
861
+ });
726
862
  }
727
863
  return this.repo.update(id, data);
728
864
  }
729
865
  async delete(id) {
730
866
  const existing = await this.repo.findById(id);
731
867
  if (!existing) {
732
- throw new NotFoundException2(`MarketingProjection '${id}' nicht gefunden`);
868
+ throw new NotFoundException2({
869
+ code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_NOT_FOUND,
870
+ message: `MarketingProjection '${id}' not found`,
871
+ params: {
872
+ projectionId: id
873
+ }
874
+ });
733
875
  }
734
876
  await this.repo.delete(id);
735
877
  }
@@ -767,6 +909,7 @@ __name(keyListSegment, "keyListSegment");
767
909
 
768
910
  // src/catalog/catalog-entries.service.ts
769
911
  import { Inject as Inject3, Injectable as Injectable3, Logger as Logger2, NotFoundException as NotFoundException3, Optional as Optional2, UnprocessableEntityException as UnprocessableEntityException2 } from "@nestjs/common";
912
+ import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES3 } from "@saasicat/types";
770
913
  function _ts_decorate3(decorators, target, key, desc) {
771
914
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
772
915
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -855,14 +998,14 @@ var CatalogEntriesService = class _CatalogEntriesService {
855
998
  if (!this.autoSyncAtBoot) return;
856
999
  const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
857
1000
  if (!snapshot) {
858
- this.logger.warn("Discovery-Auto-Sync \xFCbersprungen: kein Snapshot (weder Token noch DiscoveryScanner injiziert). DiscoveryModule wiren (#25).");
1001
+ this.logger.warn("Discovery auto-sync skipped: no snapshot (neither token nor DiscoveryScanner injected). Wire up DiscoveryModule (#25).");
859
1002
  return;
860
1003
  }
861
1004
  try {
862
1005
  const result = await this.syncFromSnapshot(snapshot);
863
1006
  this.logger.log(`Discovery-Auto-Sync (${snapshot.app.key}): Features ${result.features.discovered}\u2191/${result.features.retired}\u2193/${result.features.outdated}\u26A0, Quotas ${result.quotas.discovered}\u2191/${result.quotas.retired}\u2193/${result.quotas.outdated}\u26A0, Capabilities ${result.capabilities.discovered}\u2191/${result.capabilities.retired}\u2193`);
864
1007
  } catch (err) {
865
- this.logger.error(`Discovery-Auto-Sync beim Boot fehlgeschlagen (${snapshot.app.key}) \u2014 Katalog ggf. veraltet; manuell via POST /admin/catalog/discovery/sync nachziehen`, err instanceof Error ? err.stack : String(err));
1008
+ this.logger.error(`Discovery auto-sync on boot failed (${snapshot.app.key}) \u2014 the catalog may be stale; re-sync manually via POST /admin/catalog/discovery/sync`, err instanceof Error ? err.stack : String(err));
866
1009
  }
867
1010
  }
868
1011
  listCapabilities(projectKey, codeStatus) {
@@ -892,7 +1035,14 @@ var CatalogEntriesService = class _CatalogEntriesService {
892
1035
  async reviewFeature(projectKey, featureKey, data, reviewedBy) {
893
1036
  const existing = await this.repo.findFeature(projectKey, featureKey);
894
1037
  if (!existing) {
895
- throw new NotFoundException3(`Feature '${featureKey}' in Projekt '${projectKey}' nicht gefunden`);
1038
+ throw new NotFoundException3({
1039
+ code: CATALOG_ERROR_CODES3.FEATURE_NOT_FOUND,
1040
+ message: `Feature '${featureKey}' not found in project '${projectKey}'`,
1041
+ params: {
1042
+ featureKey,
1043
+ projectKey
1044
+ }
1045
+ });
896
1046
  }
897
1047
  return this.repo.setFeatureReview(projectKey, featureKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => featureApprovalSignature(featureKey, snapshot)));
898
1048
  }
@@ -900,24 +1050,47 @@ var CatalogEntriesService = class _CatalogEntriesService {
900
1050
  async reviewQuota(projectKey, quotaKey, data, reviewedBy) {
901
1051
  const existing = await this.repo.findQuota(projectKey, quotaKey);
902
1052
  if (!existing) {
903
- throw new NotFoundException3(`Quota '${quotaKey}' in Projekt '${projectKey}' nicht gefunden`);
1053
+ throw new NotFoundException3({
1054
+ code: CATALOG_ERROR_CODES3.QUOTA_NOT_FOUND,
1055
+ message: `Quota '${quotaKey}' not found in project '${projectKey}'`,
1056
+ params: {
1057
+ quotaKey,
1058
+ projectKey
1059
+ }
1060
+ });
904
1061
  }
905
1062
  return this.repo.setQuotaReview(projectKey, quotaKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => {
906
1063
  const discovered = snapshot.quotas.find((q) => q.quotaKey === quotaKey);
907
1064
  if (!discovered) {
908
- throw new UnprocessableEntityException2(`Quota '${quotaKey}' ist nicht im Discovery-Snapshot \u2014 eine nicht im Code deklarierte Quota kann nicht freigegeben werden`);
1065
+ throw new UnprocessableEntityException2({
1066
+ code: CATALOG_ERROR_CODES3.QUOTA_NOT_IN_DISCOVERY_SNAPSHOT,
1067
+ message: `Quota '${quotaKey}' is not in the discovery snapshot \u2014 a quota that is not declared in code cannot be approved`,
1068
+ params: {
1069
+ quotaKey
1070
+ }
1071
+ });
909
1072
  }
910
1073
  return quotaApprovalSignature(discovered);
911
1074
  }));
912
1075
  }
913
1076
  resolveReviewUpdate(existing, target, reviewedBy, signatureOf) {
914
1077
  if (!REVIEW_TRANSITIONS[existing.discoveryStatus].includes(target)) {
915
- throw new UnprocessableEntityException2(`\xDCbergang '${existing.discoveryStatus}' \u2192 '${target}' ist nicht erlaubt`);
1078
+ throw new UnprocessableEntityException2({
1079
+ code: CATALOG_ERROR_CODES3.DISCOVERY_STATUS_TRANSITION_INVALID,
1080
+ message: `Transition '${existing.discoveryStatus}' \u2192 '${target}' is not allowed`,
1081
+ params: {
1082
+ from: existing.discoveryStatus,
1083
+ to: target
1084
+ }
1085
+ });
916
1086
  }
917
1087
  if (target === "approved") {
918
1088
  const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
919
1089
  if (!snapshot) {
920
- throw new UnprocessableEntityException2("Freigabe braucht einen Discovery-Snapshot \u2014 Discovery ist nicht initialisiert (#25)");
1090
+ throw new UnprocessableEntityException2({
1091
+ code: CATALOG_ERROR_CODES3.DISCOVERY_NOT_INITIALIZED,
1092
+ message: "Approval requires a discovery snapshot \u2014 discovery is not initialised (#25)"
1093
+ });
921
1094
  }
922
1095
  return {
923
1096
  discoveryStatus: "approved",
@@ -1110,7 +1283,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
1110
1283
  const claimants = claimantsByOldKey.get(entry.key);
1111
1284
  if (claimants && claimants.length > 0) {
1112
1285
  if (claimants.length > 1) {
1113
- this.logger.warn(`${type} '${entry.key}' wird von mehreren replaces-Deklarationen beansprucht (${claimants.join(", ")}); '${claimants[0]}' gewinnt \u2014 replaces im Code bereinigen (#39)`);
1286
+ this.logger.warn(`${type} '${entry.key}' is claimed by several replaces declarations (${claimants.join(", ")}); '${claimants[0]}' wins \u2014 clean up the replaces in the code (#39)`);
1114
1287
  }
1115
1288
  target = claimants[0];
1116
1289
  }
@@ -1123,7 +1296,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
1123
1296
  if (changes.length === 0) return 0;
1124
1297
  const setSuccessor = type === "feature" ? this.repo.setFeatureSuccessor?.bind(this.repo) : this.repo.setQuotaSuccessor?.bind(this.repo);
1125
1298
  if (!setSuccessor) {
1126
- this.logger.warn(`CatalogEntryRepository implementiert set${type === "feature" ? "Feature" : "Quota"}Successor nicht \u2014 ${changes.length} Nachfolger-Pointer (#39) werden nicht persistiert`);
1299
+ this.logger.warn(`CatalogEntryRepository does not implement set${type === "feature" ? "Feature" : "Quota"}Successor \u2014 ${changes.length} successor pointers (#39) are not persisted`);
1127
1300
  return 0;
1128
1301
  }
1129
1302
  let replaced = 0;
@@ -1194,7 +1367,7 @@ var ListCatalogEntriesQueryDto = class {
1194
1367
  _ts_decorate4([
1195
1368
  IsString(),
1196
1369
  Matches(PROJECT_KEY_PATTERN, {
1197
- message: "projectKey muss kebab-case sein"
1370
+ message: "projectKey must be kebab-case"
1198
1371
  }),
1199
1372
  _ts_metadata4("design:type", String)
1200
1373
  ], ListCatalogEntriesQueryDto.prototype, "projectKey", void 0);
@@ -1219,7 +1392,7 @@ var ReviewCatalogEntryDto = class {
1219
1392
  _ts_decorate4([
1220
1393
  IsString(),
1221
1394
  IsIn(REVIEW_STATUSES, {
1222
- message: "discoveryStatus muss pending|approved|outdated|obsolete sein"
1395
+ message: "discoveryStatus must be pending|approved|outdated|obsolete"
1223
1396
  }),
1224
1397
  _ts_metadata4("design:type", typeof DiscoveryStatus === "undefined" ? Object : DiscoveryStatus)
1225
1398
  ], ReviewCatalogEntryDto.prototype, "discoveryStatus", void 0);
@@ -1617,6 +1790,7 @@ __name(buildCatalogEntriesController, "buildCatalogEntriesController");
1617
1790
 
1618
1791
  // src/catalog/promotions.service.ts
1619
1792
  import { Inject as Inject6, Injectable as Injectable5, NotFoundException as NotFoundException4 } from "@nestjs/common";
1793
+ import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES4 } from "@saasicat/types";
1620
1794
  function _ts_decorate7(decorators, target, key, desc) {
1621
1795
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1622
1796
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1650,7 +1824,13 @@ var PromotionsService = class {
1650
1824
  async getById(id) {
1651
1825
  const row = await this.repo.findById(id);
1652
1826
  if (!row) {
1653
- throw new NotFoundException4(`Promotion '${id}' nicht gefunden`);
1827
+ throw new NotFoundException4({
1828
+ code: CATALOG_ERROR_CODES4.PROMOTION_NOT_FOUND,
1829
+ message: `Promotion '${id}' not found`,
1830
+ params: {
1831
+ promotionId: id
1832
+ }
1833
+ });
1654
1834
  }
1655
1835
  return row;
1656
1836
  }
@@ -1660,14 +1840,26 @@ var PromotionsService = class {
1660
1840
  async update(id, data) {
1661
1841
  const existing = await this.repo.findById(id);
1662
1842
  if (!existing) {
1663
- throw new NotFoundException4(`Promotion '${id}' nicht gefunden`);
1843
+ throw new NotFoundException4({
1844
+ code: CATALOG_ERROR_CODES4.PROMOTION_NOT_FOUND,
1845
+ message: `Promotion '${id}' not found`,
1846
+ params: {
1847
+ promotionId: id
1848
+ }
1849
+ });
1664
1850
  }
1665
1851
  return this.repo.update(id, data);
1666
1852
  }
1667
1853
  async delete(id) {
1668
1854
  const existing = await this.repo.findById(id);
1669
1855
  if (!existing) {
1670
- throw new NotFoundException4(`Promotion '${id}' nicht gefunden`);
1856
+ throw new NotFoundException4({
1857
+ code: CATALOG_ERROR_CODES4.PROMOTION_NOT_FOUND,
1858
+ message: `Promotion '${id}' not found`,
1859
+ params: {
1860
+ promotionId: id
1861
+ }
1862
+ });
1671
1863
  }
1672
1864
  await this.repo.delete(id);
1673
1865
  }
@@ -1721,7 +1913,7 @@ var ListPromotionsQueryDto = class {
1721
1913
  _ts_decorate8([
1722
1914
  IsString2(),
1723
1915
  Matches2(PROJECT_KEY_PATTERN2, {
1724
- message: "projectKey muss kebab-case sein"
1916
+ message: "projectKey must be kebab-case"
1725
1917
  }),
1726
1918
  _ts_metadata8("design:type", String)
1727
1919
  ], ListPromotionsQueryDto.prototype, "projectKey", void 0);
@@ -1789,14 +1981,14 @@ _ts_decorate8([
1789
1981
  _ts_decorate8([
1790
1982
  IsString2(),
1791
1983
  Matches2(ISO_DATE_PATTERN, {
1792
- message: "validFrom muss ISO-Datum sein"
1984
+ message: "validFrom must be an ISO date"
1793
1985
  }),
1794
1986
  _ts_metadata8("design:type", String)
1795
1987
  ], CreatePromotionDto.prototype, "validFrom", void 0);
1796
1988
  _ts_decorate8([
1797
1989
  IsString2(),
1798
1990
  Matches2(ISO_DATE_PATTERN, {
1799
- message: "validTo muss ISO-Datum sein"
1991
+ message: "validTo must be an ISO date"
1800
1992
  }),
1801
1993
  _ts_metadata8("design:type", String)
1802
1994
  ], CreatePromotionDto.prototype, "validTo", void 0);
@@ -2115,7 +2307,7 @@ var ListMarketingSettingsQueryDto = class {
2115
2307
  _ts_decorate11([
2116
2308
  IsString3(),
2117
2309
  Matches3(PROJECT_KEY_PATTERN3, {
2118
- message: "projectKey muss kebab-case sein"
2310
+ message: "projectKey must be kebab-case"
2119
2311
  }),
2120
2312
  _ts_metadata11("design:type", String)
2121
2313
  ], ListMarketingSettingsQueryDto.prototype, "projectKey", void 0);
@@ -2129,7 +2321,7 @@ var UpdateMarketingSettingsDto = class {
2129
2321
  _ts_decorate11([
2130
2322
  IsString3(),
2131
2323
  Matches3(PROJECT_KEY_PATTERN3, {
2132
- message: "projectKey muss kebab-case sein"
2324
+ message: "projectKey must be kebab-case"
2133
2325
  }),
2134
2326
  _ts_metadata11("design:type", String)
2135
2327
  ], UpdateMarketingSettingsDto.prototype, "projectKey", void 0);
@@ -2141,7 +2333,7 @@ _ts_decorate11([
2141
2333
  }),
2142
2334
  Matches3(LOCALE_PATTERN, {
2143
2335
  each: true,
2144
- message: "Locale muss ISO-639-1 sein"
2336
+ message: "Locale must be ISO-639-1"
2145
2337
  }),
2146
2338
  _ts_metadata11("design:type", Array)
2147
2339
  ], UpdateMarketingSettingsDto.prototype, "activeLocales", void 0);
@@ -2630,6 +2822,7 @@ __name(parseAsOf, "parseAsOf");
2630
2822
 
2631
2823
  // src/catalog/plans.service.ts
2632
2824
  import { Inject as Inject12, Injectable as Injectable8, NotFoundException as NotFoundException5, Optional as Optional6, UnprocessableEntityException as UnprocessableEntityException3 } from "@nestjs/common";
2825
+ import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES5 } from "@saasicat/types";
2633
2826
  function _ts_decorate15(decorators, target, key, desc) {
2634
2827
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2635
2828
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2676,24 +2869,55 @@ var PlansService = class {
2676
2869
  }
2677
2870
  async getPlan(planId) {
2678
2871
  const plan = await this.repo.findById(planId);
2679
- if (!plan) throw new NotFoundException5(`Plan '${planId}' nicht gefunden`);
2872
+ if (!plan) {
2873
+ throw new NotFoundException5({
2874
+ code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
2875
+ message: `Plan '${planId}' not found`,
2876
+ params: {
2877
+ planId
2878
+ }
2879
+ });
2880
+ }
2680
2881
  return plan;
2681
2882
  }
2682
2883
  async createPlan(data) {
2683
2884
  const existing = await this.repo.findByKey(data.projectKey, data.planKey);
2684
2885
  if (existing) {
2685
- throw new UnprocessableEntityException3(`Plan '${data.planKey}' existiert bereits in Projekt '${data.projectKey}'`);
2886
+ throw new UnprocessableEntityException3({
2887
+ code: CATALOG_ERROR_CODES5.PLAN_ALREADY_EXISTS,
2888
+ message: `Plan '${data.planKey}' already exists in project '${data.projectKey}'`,
2889
+ params: {
2890
+ planKey: data.planKey,
2891
+ projectKey: data.projectKey
2892
+ }
2893
+ });
2686
2894
  }
2687
2895
  return this.repo.create(data);
2688
2896
  }
2689
2897
  async updatePlan(planId, data) {
2690
2898
  const existing = await this.repo.findById(planId);
2691
- if (!existing) throw new NotFoundException5(`Plan '${planId}' nicht gefunden`);
2899
+ if (!existing) {
2900
+ throw new NotFoundException5({
2901
+ code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
2902
+ message: `Plan '${planId}' not found`,
2903
+ params: {
2904
+ planId
2905
+ }
2906
+ });
2907
+ }
2692
2908
  return this.repo.update(planId, data);
2693
2909
  }
2694
2910
  async softDeletePlan(planId) {
2695
2911
  const existing = await this.repo.findById(planId);
2696
- if (!existing) throw new NotFoundException5(`Plan '${planId}' nicht gefunden`);
2912
+ if (!existing) {
2913
+ throw new NotFoundException5({
2914
+ code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
2915
+ message: `Plan '${planId}' not found`,
2916
+ params: {
2917
+ planId
2918
+ }
2919
+ });
2920
+ }
2697
2921
  if (existing.deletedAt !== null) return;
2698
2922
  await this.assertNoPublishedVersions(existing.planKey, "soft-delete");
2699
2923
  await this.repo.softDelete(planId);
@@ -2710,13 +2934,21 @@ var PlansService = class {
2710
2934
  */
2711
2935
  async hardDeletePlan(planId) {
2712
2936
  const existing = await this.repo.findById(planId);
2713
- if (!existing) throw new NotFoundException5(`Plan '${planId}' nicht gefunden`);
2937
+ if (!existing) {
2938
+ throw new NotFoundException5({
2939
+ code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
2940
+ message: `Plan '${planId}' not found`,
2941
+ params: {
2942
+ planId
2943
+ }
2944
+ });
2945
+ }
2714
2946
  await this.assertNoPublishedVersions(existing.planKey, "hard-delete");
2715
2947
  await this.assertNoDrafts(existing.planKey);
2716
2948
  if (typeof this.repo.hardDelete !== "function") {
2717
2949
  throw new UnprocessableEntityException3({
2718
- code: "PLAN_HARD_DELETE_NOT_IMPLEMENTED",
2719
- message: "Hartes L\xF6schen ist im aktuellen Repository nicht implementiert. Implementiere PlanRepository.hardDelete."
2950
+ code: CATALOG_ERROR_CODES5.PLAN_HARD_DELETE_NOT_IMPLEMENTED,
2951
+ message: "Hard delete is not implemented in the current repository. Implement PlanRepository.hardDelete."
2720
2952
  });
2721
2953
  }
2722
2954
  await this.repo.hardDelete(planId);
@@ -2736,8 +2968,12 @@ var PlansService = class {
2736
2968
  const live = published.filter((v) => v.supersededAt === null).length;
2737
2969
  const superseded = published.length - live;
2738
2970
  throw new UnprocessableEntityException3({
2739
- code: "PLAN_HAS_PUBLISHED_VERSIONS",
2740
- message: `Plan '${planKey}' kann nicht ${op === "hard-delete" ? "gel\xF6scht" : "archiviert"} werden \u2014 der Plan hat ${published.length} published Version(en) (${live} live, ${superseded} superseded). Bestand-Subscriptions referenzieren diese Versionen (Vertragsschutz P1), der Plan-Stamm muss erhalten bleiben.`,
2971
+ code: CATALOG_ERROR_CODES5.PLAN_HAS_PUBLISHED_VERSIONS,
2972
+ message: `Plan '${planKey}' cannot be ${op === "hard-delete" ? "deleted" : "archived"} \u2014 it has ${published.length} published version(s) (${live} live, ${superseded} superseded). Existing subscriptions reference those versions (contract protection P1), so the plan record must be preserved.`,
2973
+ params: {
2974
+ planKey,
2975
+ operation: op
2976
+ },
2741
2977
  publishedCount: published.length,
2742
2978
  liveCount: live,
2743
2979
  supersededCount: superseded
@@ -2749,8 +2985,11 @@ var PlansService = class {
2749
2985
  const drafts = versions.filter((v) => v.publishedAt === null);
2750
2986
  if (drafts.length === 0) return;
2751
2987
  throw new UnprocessableEntityException3({
2752
- code: "PLAN_HAS_DRAFTS",
2753
- message: `Plan '${planKey}' hat noch ${drafts.length} offene Draft-Version(en). Verwerfe sie zuerst (DELETE /admin/catalog/plan-versions/:id) oder publishe sie.`,
2988
+ code: CATALOG_ERROR_CODES5.PLAN_HAS_DRAFTS,
2989
+ message: `Plan '${planKey}' still has ${drafts.length} open draft version(s). Discard them first (DELETE /admin/catalog/plan-versions/:id) or publish them.`,
2990
+ params: {
2991
+ planKey
2992
+ },
2754
2993
  draftCount: drafts.length
2755
2994
  });
2756
2995
  }
@@ -2769,7 +3008,7 @@ PlansService = _ts_decorate15([
2769
3008
 
2770
3009
  // src/catalog/plan-versions.service.ts
2771
3010
  import { Inject as Inject13, Injectable as Injectable9, Logger as Logger3, NotFoundException as NotFoundException6, Optional as Optional7, UnprocessableEntityException as UnprocessableEntityException4 } from "@nestjs/common";
2772
- import { isVersionEditable as isVersionEditable2 } from "@saasicat/types";
3011
+ import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES6, isVersionEditable as isVersionEditable2 } from "@saasicat/types";
2773
3012
  function _ts_decorate16(decorators, target, key, desc) {
2774
3013
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2775
3014
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2808,7 +3047,7 @@ var PlanVersionsService = class _PlanVersionsService {
2808
3047
  this.mode = config.strictModeCheckMode ?? "blocking";
2809
3048
  this.marketedOnly = new Set(config.marketedOnlyFeatures ?? []);
2810
3049
  if (this.mode === "blocking" && !hasDiscoverySnapshotSource(this.snapshot, this.scanner)) {
2811
- this.logger.error("PlanVersionsService: strictModeCheckMode=blocking ohne DiscoverySnapshot-Quelle \u2014 degradiere auf warn-only. DiscoveryModule wiren (#25).");
3050
+ this.logger.error("PlanVersionsService: strictModeCheckMode=blocking without a DiscoverySnapshot source \u2014 degrading to warn-only. Wire DiscoveryModule (#25).");
2812
3051
  this.mode = "warn-only";
2813
3052
  }
2814
3053
  const required = [
@@ -2822,7 +3061,7 @@ var PlanVersionsService = class _PlanVersionsService {
2822
3061
  ];
2823
3062
  for (const key of required) {
2824
3063
  if (typeof this.repo[key] !== "function") {
2825
- throw new Error(`PlanVersionsService: PlanRepository.${String(key)} fehlt \u2014 das Repository implementiert die Lifecycle-Methoden nicht.`);
3064
+ throw new Error(`PlanVersionsService: PlanRepository.${String(key)} is missing \u2014 the repository does not implement the lifecycle methods.`);
2826
3065
  }
2827
3066
  }
2828
3067
  }
@@ -2838,18 +3077,27 @@ var PlanVersionsService = class _PlanVersionsService {
2838
3077
  async discardPlanDraft(versionId) {
2839
3078
  const existing = await this.repo.findVersionById(versionId);
2840
3079
  if (!existing) {
2841
- throw new NotFoundException6(`PlanVersion '${versionId}' nicht gefunden`);
3080
+ throw new NotFoundException6({
3081
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
3082
+ message: `PlanVersion '${versionId}' not found`,
3083
+ params: {
3084
+ versionId
3085
+ }
3086
+ });
2842
3087
  }
2843
3088
  if (existing.publishedAt !== null) {
2844
3089
  throw new UnprocessableEntityException4({
2845
- code: "PLAN_VERSION_ALREADY_PUBLISHED",
2846
- message: `PlanVersion '${versionId}' ist bereits published und kann nicht verworfen werden.`
3090
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_ALREADY_PUBLISHED,
3091
+ message: `PlanVersion '${versionId}' is already published and cannot be discarded.`,
3092
+ params: {
3093
+ versionId
3094
+ }
2847
3095
  });
2848
3096
  }
2849
3097
  if (typeof this.repo.deletePlanVersionDraft !== "function") {
2850
3098
  throw new UnprocessableEntityException4({
2851
- code: "PLAN_VERSION_DISCARD_NOT_IMPLEMENTED",
2852
- message: "Discard ist im aktuellen Repository nicht implementiert. Implementiere PlanRepository.deletePlanVersionDraft."
3099
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_DISCARD_NOT_IMPLEMENTED,
3100
+ message: "Discard is not implemented in the current repository. Implement PlanRepository.deletePlanVersionDraft."
2853
3101
  });
2854
3102
  }
2855
3103
  await this.repo.deletePlanVersionDraft(versionId);
@@ -2862,7 +3110,13 @@ var PlanVersionsService = class _PlanVersionsService {
2862
3110
  async listPlanVersions(planUuid) {
2863
3111
  const plan = await this.repo.findById(planUuid);
2864
3112
  if (!plan) {
2865
- throw new NotFoundException6(`Plan '${planUuid}' nicht gefunden`);
3113
+ throw new NotFoundException6({
3114
+ code: CATALOG_ERROR_CODES6.PLAN_NOT_FOUND,
3115
+ message: `Plan '${planUuid}' not found`,
3116
+ params: {
3117
+ planId: planUuid
3118
+ }
3119
+ });
2866
3120
  }
2867
3121
  const versions = await this.repo.listVersions(plan.planKey);
2868
3122
  return this.annotateEditability(versions);
@@ -2870,7 +3124,13 @@ var PlanVersionsService = class _PlanVersionsService {
2870
3124
  async getPlanVersion(versionId) {
2871
3125
  const version = await this.repo.findVersionById(versionId);
2872
3126
  if (!version) {
2873
- throw new NotFoundException6(`PlanVersion '${versionId}' nicht gefunden`);
3127
+ throw new NotFoundException6({
3128
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
3129
+ message: `PlanVersion '${versionId}' not found`,
3130
+ params: {
3131
+ versionId
3132
+ }
3133
+ });
2874
3134
  }
2875
3135
  const [annotated] = await this.annotateEditability([
2876
3136
  version
@@ -2908,12 +3168,25 @@ var PlanVersionsService = class _PlanVersionsService {
2908
3168
  async createPlanDraft(data) {
2909
3169
  const plan = await this.repo.findById(data.planId);
2910
3170
  if (!plan) {
2911
- throw new NotFoundException6(`Plan '${data.planId}' nicht gefunden`);
3171
+ throw new NotFoundException6({
3172
+ code: CATALOG_ERROR_CODES6.PLAN_NOT_FOUND,
3173
+ message: `Plan '${data.planId}' not found`,
3174
+ params: {
3175
+ planId: data.planId
3176
+ }
3177
+ });
2912
3178
  }
2913
3179
  const planKey = plan.planKey;
2914
3180
  const existingDraft = await this.repo.findCurrentDraft(planKey);
2915
3181
  if (existingDraft) {
2916
- throw new UnprocessableEntityException4(`Plan '${planKey}' hat bereits eine Draft-Version v${existingDraft.version}; bitte erst publishen oder verwerfen`);
3182
+ throw new UnprocessableEntityException4({
3183
+ code: CATALOG_ERROR_CODES6.PLAN_DRAFT_ALREADY_EXISTS,
3184
+ message: `Plan '${planKey}' already has a draft version v${existingDraft.version}; publish or discard it first`,
3185
+ params: {
3186
+ planKey,
3187
+ draftVersion: existingDraft.version
3188
+ }
3189
+ });
2917
3190
  }
2918
3191
  let baseVersionId = data.baseVersionId;
2919
3192
  if (baseVersionId === void 0) {
@@ -2938,7 +3211,13 @@ var PlanVersionsService = class _PlanVersionsService {
2938
3211
  async updatePlanDraft(versionId, data) {
2939
3212
  const existing = await this.repo.findVersionById(versionId);
2940
3213
  if (!existing) {
2941
- throw new NotFoundException6(`PlanVersion '${versionId}' nicht gefunden`);
3214
+ throw new NotFoundException6({
3215
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
3216
+ message: `PlanVersion '${versionId}' not found`,
3217
+ params: {
3218
+ versionId
3219
+ }
3220
+ });
2942
3221
  }
2943
3222
  const [annotated] = await this.annotateEditability([
2944
3223
  existing
@@ -2946,8 +3225,11 @@ var PlanVersionsService = class _PlanVersionsService {
2946
3225
  const { editable } = isVersionEditable2(annotated);
2947
3226
  if (!editable) {
2948
3227
  throw new UnprocessableEntityException4({
2949
- code: "PLAN_VERSION_NOT_EDITABLE",
2950
- message: `PlanVersion '${versionId}' ist nicht editierbar. Editierbar sind nur Drafts und published Versionen, die latest-in-chain sind, noch keine Subscription binden und deren validFrom in der Zukunft liegt.`
3228
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_EDITABLE,
3229
+ message: `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.`,
3230
+ params: {
3231
+ versionId
3232
+ }
2951
3233
  });
2952
3234
  }
2953
3235
  const merged = {
@@ -2965,10 +3247,22 @@ var PlanVersionsService = class _PlanVersionsService {
2965
3247
  async publishPlanVersion(versionId, publishMeta) {
2966
3248
  const draft = await this.repo.findVersionById(versionId);
2967
3249
  if (!draft) {
2968
- throw new NotFoundException6(`PlanVersion '${versionId}' nicht gefunden`);
3250
+ throw new NotFoundException6({
3251
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
3252
+ message: `PlanVersion '${versionId}' not found`,
3253
+ params: {
3254
+ versionId
3255
+ }
3256
+ });
2969
3257
  }
2970
3258
  if (draft.publishedAt !== null) {
2971
- throw new UnprocessableEntityException4(`PlanVersion '${versionId}' ist bereits published`);
3259
+ throw new UnprocessableEntityException4({
3260
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_ALREADY_PUBLISHED,
3261
+ message: `PlanVersion '${versionId}' is already published`,
3262
+ params: {
3263
+ versionId
3264
+ }
3265
+ });
2972
3266
  }
2973
3267
  const warnings = await this.runStrictCheck({
2974
3268
  features: draft.features,
@@ -2980,8 +3274,8 @@ var PlanVersionsService = class _PlanVersionsService {
2980
3274
  const yearly = Number.parseFloat(String(draft.yearlyNet ?? "0"));
2981
3275
  if (monthly <= 0 || yearly <= 0) {
2982
3276
  throw new UnprocessableEntityException4({
2983
- code: "PLAN_VERSION_ZERO_PRICE",
2984
- message: "PlanVersion kann nicht mit Preis 0,00 published werden (Schutz gegen Seed-Platzhalter). F\xFCr bewusst kostenlose Sondervertr\xE4ge: allowZeroPrice setzen.",
3277
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_ZERO_PRICE,
3278
+ message: "A plan version cannot be published with a price of 0.00 (guard against seed placeholder). For deliberately free special contracts, set allowZeroPrice.",
2985
3279
  monthlyNet: String(draft.monthlyNet),
2986
3280
  yearlyNet: String(draft.yearlyNet)
2987
3281
  });
@@ -2991,23 +3285,30 @@ var PlanVersionsService = class _PlanVersionsService {
2991
3285
  const validFromInput = publishMeta.validFrom ?? draft.validFrom;
2992
3286
  if (!validFromInput) {
2993
3287
  throw new UnprocessableEntityException4({
2994
- code: "PLAN_VERSION_VALID_FROM_REQUIRED",
2995
- message: "Beim Publish muss validFrom gesetzt sein (auf Draft oder Publish-Aufruf). SPEC_V2 \xA74.2."
3288
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_REQUIRED,
3289
+ message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
2996
3290
  });
2997
3291
  }
2998
3292
  const validFrom = new Date(validFromInput);
2999
3293
  if (Number.isNaN(validFrom.getTime())) {
3000
3294
  throw new UnprocessableEntityException4({
3001
- code: "PLAN_VERSION_VALID_FROM_INVALID",
3002
- message: `validFrom '${validFromInput}' ist kein g\xFCltiges Datum`
3295
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_INVALID,
3296
+ message: `validFrom '${validFromInput}' is not a valid date`,
3297
+ params: {
3298
+ validFrom: validFromInput
3299
+ }
3003
3300
  });
3004
3301
  }
3005
3302
  if (previous?.validFrom) {
3006
3303
  const prevFrom = new Date(previous.validFrom);
3007
3304
  if (validFrom <= prevFrom) {
3008
3305
  throw new UnprocessableEntityException4({
3009
- code: "PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS",
3010
- message: `validFrom (${validFrom.toISOString()}) muss strikt nach validFrom der Vorg\xE4nger-Version (${previous.validFrom}) liegen.`
3306
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
3307
+ message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
3308
+ params: {
3309
+ validFrom: validFrom.toISOString(),
3310
+ previousValidFrom: previous.validFrom
3311
+ }
3011
3312
  });
3012
3313
  }
3013
3314
  if (previous.validUntil) {
@@ -3016,8 +3317,11 @@ var PlanVersionsService = class _PlanVersionsService {
3016
3317
  const requiredStart = new Date(prevUntil.getTime() + dayMs);
3017
3318
  if (validFrom.getTime() !== requiredStart.getTime()) {
3018
3319
  throw new UnprocessableEntityException4({
3019
- code: "PLAN_VERSION_VALID_FROM_NOT_GAPLESS",
3020
- message: `Vorg\xE4nger hat validUntil=${previous.validUntil.slice(0, 10)} \u2014 der Nachfolger muss nahtlos am Folgetag (${requiredStart.toISOString().slice(0, 10)}) starten. Geliefert: ${validFrom.toISOString().slice(0, 10)}.`,
3320
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_NOT_GAPLESS,
3321
+ message: `The predecessor has validUntil=${previous.validUntil.slice(0, 10)} \u2014 the successor must start seamlessly on the next day (${requiredStart.toISOString().slice(0, 10)}). Received: ${validFrom.toISOString().slice(0, 10)}.`,
3322
+ params: {
3323
+ received: validFrom.toISOString().slice(0, 10)
3324
+ },
3021
3325
  requiredValidFrom: requiredStart.toISOString().slice(0, 10),
3022
3326
  previousValidUntil: previous.validUntil
3023
3327
  });
@@ -3028,14 +3332,21 @@ var PlanVersionsService = class _PlanVersionsService {
3028
3332
  const validUntil = validUntilInput ? new Date(validUntilInput) : null;
3029
3333
  if (validUntil && Number.isNaN(validUntil.getTime())) {
3030
3334
  throw new UnprocessableEntityException4({
3031
- code: "PLAN_VERSION_VALID_UNTIL_INVALID",
3032
- message: `validUntil '${validUntilInput}' ist kein g\xFCltiges Datum`
3335
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_UNTIL_INVALID,
3336
+ message: `validUntil '${validUntilInput}' is not a valid date`,
3337
+ params: {
3338
+ validUntil: validUntilInput
3339
+ }
3033
3340
  });
3034
3341
  }
3035
3342
  if (validUntil && validUntil <= validFrom) {
3036
3343
  throw new UnprocessableEntityException4({
3037
- code: "PLAN_VERSION_VALID_UNTIL_BEFORE_FROM",
3038
- message: `validUntil (${validUntil.toISOString()}) muss strikt nach validFrom (${validFrom.toISOString()}) liegen.`
3344
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_UNTIL_BEFORE_FROM,
3345
+ message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
3346
+ params: {
3347
+ validUntil: validUntil.toISOString(),
3348
+ validFrom: validFrom.toISOString()
3349
+ }
3039
3350
  });
3040
3351
  }
3041
3352
  const fieldsFor = /* @__PURE__ */ __name((v) => ({
@@ -3052,8 +3363,8 @@ var PlanVersionsService = class _PlanVersionsService {
3052
3363
  };
3053
3364
  if (!diff.nonRegressive && !publishMeta.forceRegressive) {
3054
3365
  throw new UnprocessableEntityException4({
3055
- code: "PLAN_VERSION_REGRESSION",
3056
- message: "Diese PlanVersion ist regressiv (Feature entfernt / Quota gesenkt / Preis erh\xF6ht). Publishen erfordert explizites `forceRegressive: true` (UI-Best\xE4tigungsmodal mit MFA).",
3366
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_REGRESSION,
3367
+ message: "This plan version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
3057
3368
  changes: diff.changes
3058
3369
  });
3059
3370
  }
@@ -3088,36 +3399,51 @@ var PlanVersionsService = class _PlanVersionsService {
3088
3399
  async terminatePlanVersion(versionId, endsAt) {
3089
3400
  const existing = await this.repo.findVersionById(versionId);
3090
3401
  if (!existing) {
3091
- throw new NotFoundException6(`PlanVersion '${versionId}' nicht gefunden`);
3402
+ throw new NotFoundException6({
3403
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
3404
+ message: `PlanVersion '${versionId}' not found`,
3405
+ params: {
3406
+ versionId
3407
+ }
3408
+ });
3092
3409
  }
3093
3410
  if (existing.publishedAt === null) {
3094
3411
  throw new UnprocessableEntityException4({
3095
- code: "PLAN_VERSION_NOT_LIVE",
3096
- message: `PlanVersion '${versionId}' ist nicht published und kann nicht terminiert werden.`
3412
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_LIVE,
3413
+ message: `PlanVersion '${versionId}' is not published and cannot be terminated.`,
3414
+ params: {
3415
+ versionId
3416
+ }
3097
3417
  });
3098
3418
  }
3099
3419
  if (existing.supersededAt !== null) {
3100
3420
  throw new UnprocessableEntityException4({
3101
- code: "PLAN_VERSION_NOT_LIVE",
3102
- message: `PlanVersion '${versionId}' wurde bereits von einer Nachfolge-Version abgel\xF6st (supersededAt) und kann nicht terminiert werden.`
3421
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_LIVE,
3422
+ message: `PlanVersion '${versionId}' has already been superseded by a newer version (supersededAt) and cannot be terminated.`,
3423
+ params: {
3424
+ versionId
3425
+ }
3103
3426
  });
3104
3427
  }
3105
3428
  if (Number.isNaN(endsAt.getTime())) {
3106
3429
  throw new UnprocessableEntityException4({
3107
- code: "PLAN_TERMINATE_INVALID_DATE",
3108
- message: "endsAt ist kein g\xFCltiges Datum."
3430
+ code: CATALOG_ERROR_CODES6.PLAN_TERMINATE_INVALID_DATE,
3431
+ message: "endsAt is not a valid date."
3109
3432
  });
3110
3433
  }
3111
3434
  if (endsAt.getTime() <= Date.now()) {
3112
3435
  throw new UnprocessableEntityException4({
3113
- code: "PLAN_TERMINATE_DATE_NOT_FUTURE",
3114
- message: `endsAt (${endsAt.toISOString()}) muss strikt in der Zukunft liegen.`
3436
+ code: CATALOG_ERROR_CODES6.PLAN_TERMINATE_DATE_NOT_FUTURE,
3437
+ message: `endsAt (${endsAt.toISOString()}) must lie strictly in the future.`,
3438
+ params: {
3439
+ endsAt: endsAt.toISOString()
3440
+ }
3115
3441
  });
3116
3442
  }
3117
3443
  if (typeof this.repo.terminate !== "function") {
3118
3444
  throw new UnprocessableEntityException4({
3119
- code: "PLAN_TERMINATE_NOT_IMPLEMENTED",
3120
- message: "Terminate ist im aktuellen Repository nicht implementiert. Implementiere PlanRepository.terminate."
3445
+ code: CATALOG_ERROR_CODES6.PLAN_TERMINATE_NOT_IMPLEMENTED,
3446
+ message: "Terminate is not implemented in the current repository. Implement PlanRepository.terminate."
3121
3447
  });
3122
3448
  }
3123
3449
  return this.repo.terminate(versionId, endsAt);
@@ -3135,8 +3461,8 @@ var PlanVersionsService = class _PlanVersionsService {
3135
3461
  if (blockingStrictModeWarnings(warnings).length === 0) return;
3136
3462
  if (this.mode === "blocking") {
3137
3463
  throw new UnprocessableEntityException4({
3138
- code: "STRICT_MODE_VIOLATIONS",
3139
- message: "Strict-Mode-Check hat Drift gegen den Discovery-Snapshot gefunden.",
3464
+ code: CATALOG_ERROR_CODES6.STRICT_MODE_VIOLATIONS,
3465
+ message: "The strict-mode check found drift against the discovery snapshot.",
3140
3466
  warnings
3141
3467
  });
3142
3468
  }
@@ -3200,7 +3526,7 @@ var CreateBundleDto = class {
3200
3526
  _ts_decorate17([
3201
3527
  IsString4(),
3202
3528
  Matches4(PROJECT_KEY_PATTERN4, {
3203
- message: 'projectKey muss kebab-case sein (z. B. "my-app")'
3529
+ message: 'projectKey must be kebab-case (e.g. "my-app")'
3204
3530
  }),
3205
3531
  MaxLength3(64),
3206
3532
  _ts_metadata17("design:type", String)
@@ -3208,7 +3534,7 @@ _ts_decorate17([
3208
3534
  _ts_decorate17([
3209
3535
  IsString4(),
3210
3536
  Matches4(KEY_PATTERN, {
3211
- message: 'bundleKey muss SCREAMING_SNAKE_CASE sein (z. B. "BANKING")'
3537
+ message: 'bundleKey must be SCREAMING_SNAKE_CASE (e.g. "BANKING")'
3212
3538
  }),
3213
3539
  MaxLength3(64),
3214
3540
  _ts_metadata17("design:type", String)
@@ -3308,7 +3634,7 @@ _ts_decorate17([
3308
3634
  }),
3309
3635
  Matches4(FEATURE_KEY_PATTERN, {
3310
3636
  each: true,
3311
- message: "features-Eintr\xE4ge m\xFCssen SCREAMING_SNAKE_CASE sein"
3637
+ message: "features entries must be SCREAMING_SNAKE_CASE"
3312
3638
  }),
3313
3639
  _ts_metadata17("design:type", Array)
3314
3640
  ], CreateBundleVersionDraftDto.prototype, "features", void 0);
@@ -3334,7 +3660,7 @@ _ts_decorate17([
3334
3660
  no_symbols: false
3335
3661
  }),
3336
3662
  Matches4(DECIMAL_PATTERN, {
3337
- message: 'monthlyNet muss Decimal mit max. 2 Nachkommastellen sein (z. B. "9.90")'
3663
+ message: 'monthlyNet must be a decimal with at most 2 fraction digits (e.g. "9.90")'
3338
3664
  }),
3339
3665
  _ts_metadata17("design:type", Object)
3340
3666
  ], CreateBundleVersionDraftDto.prototype, "monthlyNet", void 0);
@@ -3345,7 +3671,7 @@ _ts_decorate17([
3345
3671
  no_symbols: false
3346
3672
  }),
3347
3673
  Matches4(DECIMAL_PATTERN, {
3348
- message: "yearlyNet muss Decimal mit max. 2 Nachkommastellen sein"
3674
+ message: "yearlyNet must be a decimal with at most 2 fraction digits"
3349
3675
  }),
3350
3676
  _ts_metadata17("design:type", Object)
3351
3677
  ], CreateBundleVersionDraftDto.prototype, "yearlyNet", void 0);
@@ -3369,7 +3695,7 @@ _ts_decorate17([
3369
3695
  IsOptional3(),
3370
3696
  ValidateIf((_o, value) => value !== null),
3371
3697
  Matches4(ISO_DATE_PATTERN2, {
3372
- message: "validFrom muss ISO-Datum sein (YYYY-MM-DD)"
3698
+ message: "validFrom must be an ISO date (YYYY-MM-DD)"
3373
3699
  }),
3374
3700
  _ts_metadata17("design:type", Object)
3375
3701
  ], CreateBundleVersionDraftDto.prototype, "validFrom", void 0);
@@ -3377,7 +3703,7 @@ _ts_decorate17([
3377
3703
  IsOptional3(),
3378
3704
  ValidateIf((_o, value) => value !== null),
3379
3705
  Matches4(ISO_DATE_PATTERN2, {
3380
- message: "validUntil muss ISO-Datum sein (YYYY-MM-DD)"
3706
+ message: "validUntil must be an ISO date (YYYY-MM-DD)"
3381
3707
  }),
3382
3708
  _ts_metadata17("design:type", Object)
3383
3709
  ], CreateBundleVersionDraftDto.prototype, "validUntil", void 0);
@@ -3449,7 +3775,7 @@ _ts_decorate17([
3449
3775
  IsOptional3(),
3450
3776
  ValidateIf((_o, value) => value !== null),
3451
3777
  Matches4(ISO_DATE_PATTERN2, {
3452
- message: "validFrom muss ISO-Datum sein (YYYY-MM-DD)"
3778
+ message: "validFrom must be an ISO date (YYYY-MM-DD)"
3453
3779
  }),
3454
3780
  _ts_metadata17("design:type", Object)
3455
3781
  ], UpdateBundleVersionDraftDto.prototype, "validFrom", void 0);
@@ -3457,7 +3783,7 @@ _ts_decorate17([
3457
3783
  IsOptional3(),
3458
3784
  ValidateIf((_o, value) => value !== null),
3459
3785
  Matches4(ISO_DATE_PATTERN2, {
3460
- message: "validUntil muss ISO-Datum sein (YYYY-MM-DD)"
3786
+ message: "validUntil must be an ISO date (YYYY-MM-DD)"
3461
3787
  }),
3462
3788
  _ts_metadata17("design:type", Object)
3463
3789
  ], UpdateBundleVersionDraftDto.prototype, "validUntil", void 0);
@@ -3494,7 +3820,7 @@ _ts_decorate17([
3494
3820
  IsOptional3(),
3495
3821
  ValidateIf((_o, value) => value !== null),
3496
3822
  Matches4(ISO_DATE_PATTERN2, {
3497
- message: "validFrom muss ISO-Datum sein (YYYY-MM-DD)"
3823
+ message: "validFrom must be an ISO date (YYYY-MM-DD)"
3498
3824
  }),
3499
3825
  _ts_metadata17("design:type", Object)
3500
3826
  ], PublishBundleVersionDto.prototype, "validFrom", void 0);
@@ -3502,7 +3828,7 @@ _ts_decorate17([
3502
3828
  IsOptional3(),
3503
3829
  ValidateIf((_o, value) => value !== null),
3504
3830
  Matches4(ISO_DATE_PATTERN2, {
3505
- message: "validUntil muss ISO-Datum sein (YYYY-MM-DD)"
3831
+ message: "validUntil must be an ISO date (YYYY-MM-DD)"
3506
3832
  }),
3507
3833
  _ts_metadata17("design:type", Object)
3508
3834
  ], PublishBundleVersionDto.prototype, "validUntil", void 0);
@@ -3804,7 +4130,7 @@ var CreateMarketingProjectionDto = class {
3804
4130
  _ts_decorate19([
3805
4131
  IsString5(),
3806
4132
  Matches5(PROJECT_KEY_PATTERN5, {
3807
- message: "projectKey muss kebab-case sein"
4133
+ message: "projectKey must be kebab-case"
3808
4134
  }),
3809
4135
  MaxLength4(64),
3810
4136
  _ts_metadata19("design:type", String)
@@ -3822,7 +4148,7 @@ _ts_decorate19([
3822
4148
  IsOptional4(),
3823
4149
  IsString5(),
3824
4150
  Matches5(LOCALE_PATTERN2, {
3825
- message: 'locale muss ISO-639-1 sein (z. B. "de", "en", "de-AT")'
4151
+ message: 'locale must be ISO-639-1 (e.g. "de", "en", "de-AT")'
3826
4152
  }),
3827
4153
  _ts_metadata19("design:type", String)
3828
4154
  ], CreateMarketingProjectionDto.prototype, "locale", void 0);
@@ -4151,7 +4477,7 @@ var CreatePlanDto = class {
4151
4477
  _ts_decorate21([
4152
4478
  IsString6(),
4153
4479
  Matches6(PROJECT_KEY_PATTERN6, {
4154
- message: 'projectKey muss kebab-case sein (z. B. "my-app")'
4480
+ message: 'projectKey must be kebab-case (e.g. "my-app")'
4155
4481
  }),
4156
4482
  MaxLength5(64),
4157
4483
  _ts_metadata21("design:type", String)
@@ -4159,7 +4485,7 @@ _ts_decorate21([
4159
4485
  _ts_decorate21([
4160
4486
  IsString6(),
4161
4487
  Matches6(KEY_PATTERN2, {
4162
- message: 'planKey muss SCREAMING_SNAKE_CASE sein (z. B. "STARTER")'
4488
+ message: 'planKey must be SCREAMING_SNAKE_CASE (e.g. "STARTER")'
4163
4489
  }),
4164
4490
  MaxLength5(64),
4165
4491
  _ts_metadata21("design:type", String)
@@ -4268,7 +4594,7 @@ _ts_decorate22([
4268
4594
  }),
4269
4595
  Matches7(FEATURE_KEY_PATTERN2, {
4270
4596
  each: true,
4271
- message: "features-Eintr\xE4ge m\xFCssen SCREAMING_SNAKE_CASE sein"
4597
+ message: "features entries must be SCREAMING_SNAKE_CASE"
4272
4598
  }),
4273
4599
  _ts_metadata22("design:type", Array)
4274
4600
  ], CreatePlanVersionDraftDto.prototype, "features", void 0);
@@ -4280,7 +4606,7 @@ _ts_decorate22([
4280
4606
  }),
4281
4607
  Matches7(FEATURE_KEY_PATTERN2, {
4282
4608
  each: true,
4283
- message: "bundles-Eintr\xE4ge m\xFCssen SCREAMING_SNAKE_CASE sein"
4609
+ message: "bundles entries must be SCREAMING_SNAKE_CASE"
4284
4610
  }),
4285
4611
  _ts_metadata22("design:type", Array)
4286
4612
  ], CreatePlanVersionDraftDto.prototype, "bundles", void 0);
@@ -4290,13 +4616,13 @@ _ts_decorate22([
4290
4616
  ], CreatePlanVersionDraftDto.prototype, "quotas", void 0);
4291
4617
  _ts_decorate22([
4292
4618
  Matches7(DECIMAL_PATTERN2, {
4293
- message: 'monthlyNet muss Decimal mit max. 2 Nachkommastellen sein (z. B. "9.90")'
4619
+ message: 'monthlyNet must be a decimal with at most 2 fraction digits (e.g. "9.90")'
4294
4620
  }),
4295
4621
  _ts_metadata22("design:type", String)
4296
4622
  ], CreatePlanVersionDraftDto.prototype, "monthlyNet", void 0);
4297
4623
  _ts_decorate22([
4298
4624
  Matches7(DECIMAL_PATTERN2, {
4299
- message: "yearlyNet muss Decimal mit max. 2 Nachkommastellen sein"
4625
+ message: "yearlyNet must be a decimal with at most 2 fraction digits"
4300
4626
  }),
4301
4627
  _ts_metadata22("design:type", String)
4302
4628
  ], CreatePlanVersionDraftDto.prototype, "yearlyNet", void 0);
@@ -4541,7 +4867,7 @@ function buildPlansController(guards) {
4541
4867
  }
4542
4868
  assertVersionsService() {
4543
4869
  if (!this.versionsService) {
4544
- throw new Error("PlanVersionsService nicht registriert \u2014 App hat den Plan-Lifecycle-Editor nicht aktiviert.");
4870
+ throw new Error("PlanVersionsService is not registered \u2014 the app has not enabled the plan lifecycle editor.");
4545
4871
  }
4546
4872
  }
4547
4873
  };