@saasicat/nest 0.18.1 → 0.21.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 (56) hide show
  1. package/dist/_entries.cjs +1341 -590
  2. package/dist/admin/index.js +2 -1
  3. package/dist/billing/index.d.cts +10 -6
  4. package/dist/billing/index.d.ts +10 -6
  5. package/dist/billing/index.js +7 -6
  6. package/dist/catalog/index.js +4 -4
  7. package/dist/checkout-offer/index.js +2 -2
  8. package/dist/{chunk-RZVSKKXX.js → chunk-3DRZ5GCP.js} +56 -105
  9. package/dist/{chunk-GLLCB4BP.js → chunk-4O6R6ARQ.js} +23 -13
  10. package/dist/{chunk-7RRWI5LH.js → chunk-55QGAZRQ.js} +10 -10
  11. package/dist/{chunk-Z3FWN3HJ.js → chunk-5ZXBXYXU.js} +3 -3
  12. package/dist/{chunk-V34AA7C7.js → chunk-6WL6VQ7Z.js} +1 -1
  13. package/dist/{chunk-ABB2CRAS.js → chunk-74SCJ4HF.js} +1 -1
  14. package/dist/{chunk-EXOLOJFZ.js → chunk-AGOG5B77.js} +36 -9
  15. package/dist/{chunk-32EMXPU2.js → chunk-B7NRY2LV.js} +48 -17
  16. package/dist/{chunk-MDIZUVIK.js → chunk-BSK6YBLI.js} +1 -1
  17. package/dist/{chunk-R2QOSVAA.js → chunk-DQKCK7DX.js} +1 -1
  18. package/dist/chunk-EBJJNTW6.js +22 -0
  19. package/dist/{chunk-PLJEAUFU.js → chunk-FYWLPYBK.js} +79 -39
  20. package/dist/{chunk-P3DP6GJE.js → chunk-GC5AKN44.js} +487 -160
  21. package/dist/{chunk-5KH2O4SF.js → chunk-NZLAYCEX.js} +286 -110
  22. package/dist/{chunk-E7AJ42AB.js → chunk-VQQWF7MW.js} +172 -28
  23. package/dist/{chunk-GJBXOCRZ.js → chunk-XTVAFJ3P.js} +46 -17
  24. package/dist/{chunk-BOADBIGB.js → chunk-YYUXKLXK.js} +2 -2
  25. package/dist/{chunk-YITTE646.js → chunk-ZH5PYEMI.js} +41 -23
  26. package/dist/{define-saasicat-Rc0rXrYG.d.ts → define-saasicat-B20Ms8UC.d.ts} +1 -1
  27. package/dist/{define-saasicat-CeDBX9ji.d.cts → define-saasicat-Cf9DpEsY.d.cts} +1 -1
  28. package/dist/discovery/index.d.cts +2 -2
  29. package/dist/discovery/index.d.ts +2 -2
  30. package/dist/discovery/index.js +4 -4
  31. package/dist/{enforce-quota.interceptor-CmbWX39L.d.ts → enforce-quota.interceptor-CUyfR4cQ.d.ts} +1 -1
  32. package/dist/{enforce-quota.interceptor-DMw-bPyk.d.cts → enforce-quota.interceptor-Dtg5WdFj.d.cts} +1 -1
  33. package/dist/entitlement/index.js +3 -2
  34. package/dist/index.d.cts +27 -8
  35. package/dist/index.d.ts +27 -8
  36. package/dist/index.js +21 -17
  37. package/dist/{module-DgYJur6f.d.ts → module-BE17XFLg.d.ts} +1 -1
  38. package/dist/{module-CSN8j8Tp.d.cts → module-CHMIyWlf.d.cts} +1 -1
  39. package/dist/platform/index.d.cts +8 -8
  40. package/dist/platform/index.d.ts +8 -8
  41. package/dist/platform/index.js +14 -13
  42. package/dist/promo/index.d.cts +3 -3
  43. package/dist/promo/index.d.ts +3 -3
  44. package/dist/promo/index.js +4 -3
  45. package/dist/registration/index.js +2 -1
  46. package/dist/{saas-platform.module-D3rUTiho.d.cts → saas-platform.module-BfaYfL-G.d.cts} +1 -1
  47. package/dist/{saas-platform.module-DLhoqKNA.d.ts → saas-platform.module-DChhDrR-.d.ts} +1 -1
  48. package/dist/{service-DX8KbGXl.d.cts → service-D48clObR.d.cts} +3 -3
  49. package/dist/{service-DX8KbGXl.d.ts → service-D48clObR.d.ts} +3 -3
  50. package/dist/subscription-contract/index.js +2 -2
  51. package/dist/{tenant-billing.controller-CG4CaFUP.d.cts → tenant-billing.controller-BIF-mstX.d.cts} +2 -2
  52. package/dist/{tenant-billing.controller-BwZRnJ3q.d.ts → tenant-billing.controller-DT5UrnDX.d.ts} +2 -2
  53. package/dist/testing/index.d.cts +5 -5
  54. package/dist/testing/index.d.ts +5 -5
  55. package/dist/testing/index.js +34 -33
  56. 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
+ bundleVersionId: 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
+ bundleVersionId: 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
+ bundleVersionId: 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
+ bundleVersionId: 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,12 @@ 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
+ // Keyed by the field name so these params match the publish
754
+ // path, where the same codes carry `validFrom` / `validUntil`.
755
+ params: {
756
+ [field]: input
757
+ }
642
758
  });
643
759
  }
644
760
  return date;
@@ -673,6 +789,7 @@ BundlesService = _ts_decorate([
673
789
 
674
790
  // src/catalog/marketing-projections.service.ts
675
791
  import { ConflictException, Inject as Inject2, Injectable as Injectable2, NotFoundException as NotFoundException2 } from "@nestjs/common";
792
+ import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES2 } from "@saasicat/types";
676
793
  function _ts_decorate2(decorators, target, key, desc) {
677
794
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
678
795
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -704,7 +821,13 @@ var MarketingProjectionsService = class {
704
821
  async getById(id) {
705
822
  const row = await this.repo.findById(id);
706
823
  if (!row) {
707
- throw new NotFoundException2(`MarketingProjection '${id}' nicht gefunden`);
824
+ throw new NotFoundException2({
825
+ code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_NOT_FOUND,
826
+ message: `MarketingProjection '${id}' not found`,
827
+ params: {
828
+ projectionId: id
829
+ }
830
+ });
708
831
  }
709
832
  return row;
710
833
  }
@@ -712,7 +835,15 @@ var MarketingProjectionsService = class {
712
835
  const locale = data.locale ?? "de";
713
836
  const existing = await this.repo.findByTarget(data.targetType, data.targetVersionId, locale);
714
837
  if (existing) {
715
- throw new ConflictException(`MarketingProjection f\xFCr ${data.targetType}/${data.targetVersionId}/${locale} existiert bereits \u2014 nutze PATCH zum Editieren`);
838
+ throw new ConflictException({
839
+ code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_ALREADY_EXISTS,
840
+ message: `Marketing projection for ${data.targetType}/${data.targetVersionId}/${locale} already exists \u2014 use PATCH to edit it`,
841
+ params: {
842
+ targetType: data.targetType,
843
+ targetVersionId: data.targetVersionId,
844
+ locale
845
+ }
846
+ });
716
847
  }
717
848
  return this.repo.create({
718
849
  ...data,
@@ -722,14 +853,26 @@ var MarketingProjectionsService = class {
722
853
  async update(id, data) {
723
854
  const existing = await this.repo.findById(id);
724
855
  if (!existing) {
725
- throw new NotFoundException2(`MarketingProjection '${id}' nicht gefunden`);
856
+ throw new NotFoundException2({
857
+ code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_NOT_FOUND,
858
+ message: `MarketingProjection '${id}' not found`,
859
+ params: {
860
+ projectionId: id
861
+ }
862
+ });
726
863
  }
727
864
  return this.repo.update(id, data);
728
865
  }
729
866
  async delete(id) {
730
867
  const existing = await this.repo.findById(id);
731
868
  if (!existing) {
732
- throw new NotFoundException2(`MarketingProjection '${id}' nicht gefunden`);
869
+ throw new NotFoundException2({
870
+ code: CATALOG_ERROR_CODES2.MARKETING_PROJECTION_NOT_FOUND,
871
+ message: `MarketingProjection '${id}' not found`,
872
+ params: {
873
+ projectionId: id
874
+ }
875
+ });
733
876
  }
734
877
  await this.repo.delete(id);
735
878
  }
@@ -767,6 +910,7 @@ __name(keyListSegment, "keyListSegment");
767
910
 
768
911
  // src/catalog/catalog-entries.service.ts
769
912
  import { Inject as Inject3, Injectable as Injectable3, Logger as Logger2, NotFoundException as NotFoundException3, Optional as Optional2, UnprocessableEntityException as UnprocessableEntityException2 } from "@nestjs/common";
913
+ import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES3 } from "@saasicat/types";
770
914
  function _ts_decorate3(decorators, target, key, desc) {
771
915
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
772
916
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -855,14 +999,14 @@ var CatalogEntriesService = class _CatalogEntriesService {
855
999
  if (!this.autoSyncAtBoot) return;
856
1000
  const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
857
1001
  if (!snapshot) {
858
- this.logger.warn("Discovery-Auto-Sync \xFCbersprungen: kein Snapshot (weder Token noch DiscoveryScanner injiziert). DiscoveryModule wiren (#25).");
1002
+ this.logger.warn("Discovery auto-sync skipped: no snapshot (neither token nor DiscoveryScanner injected). Wire up DiscoveryModule (#25).");
859
1003
  return;
860
1004
  }
861
1005
  try {
862
1006
  const result = await this.syncFromSnapshot(snapshot);
863
1007
  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
1008
  } 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));
1009
+ 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
1010
  }
867
1011
  }
868
1012
  listCapabilities(projectKey, codeStatus) {
@@ -892,7 +1036,14 @@ var CatalogEntriesService = class _CatalogEntriesService {
892
1036
  async reviewFeature(projectKey, featureKey, data, reviewedBy) {
893
1037
  const existing = await this.repo.findFeature(projectKey, featureKey);
894
1038
  if (!existing) {
895
- throw new NotFoundException3(`Feature '${featureKey}' in Projekt '${projectKey}' nicht gefunden`);
1039
+ throw new NotFoundException3({
1040
+ code: CATALOG_ERROR_CODES3.FEATURE_NOT_FOUND,
1041
+ message: `Feature '${featureKey}' not found in project '${projectKey}'`,
1042
+ params: {
1043
+ featureKey,
1044
+ projectKey
1045
+ }
1046
+ });
896
1047
  }
897
1048
  return this.repo.setFeatureReview(projectKey, featureKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => featureApprovalSignature(featureKey, snapshot)));
898
1049
  }
@@ -900,24 +1051,47 @@ var CatalogEntriesService = class _CatalogEntriesService {
900
1051
  async reviewQuota(projectKey, quotaKey, data, reviewedBy) {
901
1052
  const existing = await this.repo.findQuota(projectKey, quotaKey);
902
1053
  if (!existing) {
903
- throw new NotFoundException3(`Quota '${quotaKey}' in Projekt '${projectKey}' nicht gefunden`);
1054
+ throw new NotFoundException3({
1055
+ code: CATALOG_ERROR_CODES3.QUOTA_NOT_FOUND,
1056
+ message: `Quota '${quotaKey}' not found in project '${projectKey}'`,
1057
+ params: {
1058
+ quotaKey,
1059
+ projectKey
1060
+ }
1061
+ });
904
1062
  }
905
1063
  return this.repo.setQuotaReview(projectKey, quotaKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => {
906
1064
  const discovered = snapshot.quotas.find((q) => q.quotaKey === quotaKey);
907
1065
  if (!discovered) {
908
- throw new UnprocessableEntityException2(`Quota '${quotaKey}' ist nicht im Discovery-Snapshot \u2014 eine nicht im Code deklarierte Quota kann nicht freigegeben werden`);
1066
+ throw new UnprocessableEntityException2({
1067
+ code: CATALOG_ERROR_CODES3.QUOTA_NOT_IN_DISCOVERY_SNAPSHOT,
1068
+ message: `Quota '${quotaKey}' is not in the discovery snapshot \u2014 a quota that is not declared in code cannot be approved`,
1069
+ params: {
1070
+ quotaKey
1071
+ }
1072
+ });
909
1073
  }
910
1074
  return quotaApprovalSignature(discovered);
911
1075
  }));
912
1076
  }
913
1077
  resolveReviewUpdate(existing, target, reviewedBy, signatureOf) {
914
1078
  if (!REVIEW_TRANSITIONS[existing.discoveryStatus].includes(target)) {
915
- throw new UnprocessableEntityException2(`\xDCbergang '${existing.discoveryStatus}' \u2192 '${target}' ist nicht erlaubt`);
1079
+ throw new UnprocessableEntityException2({
1080
+ code: CATALOG_ERROR_CODES3.DISCOVERY_STATUS_TRANSITION_INVALID,
1081
+ message: `Transition '${existing.discoveryStatus}' \u2192 '${target}' is not allowed`,
1082
+ params: {
1083
+ from: existing.discoveryStatus,
1084
+ to: target
1085
+ }
1086
+ });
916
1087
  }
917
1088
  if (target === "approved") {
918
1089
  const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
919
1090
  if (!snapshot) {
920
- throw new UnprocessableEntityException2("Freigabe braucht einen Discovery-Snapshot \u2014 Discovery ist nicht initialisiert (#25)");
1091
+ throw new UnprocessableEntityException2({
1092
+ code: CATALOG_ERROR_CODES3.DISCOVERY_NOT_INITIALIZED,
1093
+ message: "Approval requires a discovery snapshot \u2014 discovery is not initialised (#25)"
1094
+ });
921
1095
  }
922
1096
  return {
923
1097
  discoveryStatus: "approved",
@@ -1110,7 +1284,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
1110
1284
  const claimants = claimantsByOldKey.get(entry.key);
1111
1285
  if (claimants && claimants.length > 0) {
1112
1286
  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)`);
1287
+ 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
1288
  }
1115
1289
  target = claimants[0];
1116
1290
  }
@@ -1123,7 +1297,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
1123
1297
  if (changes.length === 0) return 0;
1124
1298
  const setSuccessor = type === "feature" ? this.repo.setFeatureSuccessor?.bind(this.repo) : this.repo.setQuotaSuccessor?.bind(this.repo);
1125
1299
  if (!setSuccessor) {
1126
- this.logger.warn(`CatalogEntryRepository implementiert set${type === "feature" ? "Feature" : "Quota"}Successor nicht \u2014 ${changes.length} Nachfolger-Pointer (#39) werden nicht persistiert`);
1300
+ this.logger.warn(`CatalogEntryRepository does not implement set${type === "feature" ? "Feature" : "Quota"}Successor \u2014 ${changes.length} successor pointers (#39) are not persisted`);
1127
1301
  return 0;
1128
1302
  }
1129
1303
  let replaced = 0;
@@ -1194,7 +1368,7 @@ var ListCatalogEntriesQueryDto = class {
1194
1368
  _ts_decorate4([
1195
1369
  IsString(),
1196
1370
  Matches(PROJECT_KEY_PATTERN, {
1197
- message: "projectKey muss kebab-case sein"
1371
+ message: "projectKey must be kebab-case"
1198
1372
  }),
1199
1373
  _ts_metadata4("design:type", String)
1200
1374
  ], ListCatalogEntriesQueryDto.prototype, "projectKey", void 0);
@@ -1219,7 +1393,7 @@ var ReviewCatalogEntryDto = class {
1219
1393
  _ts_decorate4([
1220
1394
  IsString(),
1221
1395
  IsIn(REVIEW_STATUSES, {
1222
- message: "discoveryStatus muss pending|approved|outdated|obsolete sein"
1396
+ message: "discoveryStatus must be pending|approved|outdated|obsolete"
1223
1397
  }),
1224
1398
  _ts_metadata4("design:type", typeof DiscoveryStatus === "undefined" ? Object : DiscoveryStatus)
1225
1399
  ], ReviewCatalogEntryDto.prototype, "discoveryStatus", void 0);
@@ -1617,6 +1791,7 @@ __name(buildCatalogEntriesController, "buildCatalogEntriesController");
1617
1791
 
1618
1792
  // src/catalog/promotions.service.ts
1619
1793
  import { Inject as Inject6, Injectable as Injectable5, NotFoundException as NotFoundException4 } from "@nestjs/common";
1794
+ import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES4 } from "@saasicat/types";
1620
1795
  function _ts_decorate7(decorators, target, key, desc) {
1621
1796
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1622
1797
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1650,7 +1825,13 @@ var PromotionsService = class {
1650
1825
  async getById(id) {
1651
1826
  const row = await this.repo.findById(id);
1652
1827
  if (!row) {
1653
- throw new NotFoundException4(`Promotion '${id}' nicht gefunden`);
1828
+ throw new NotFoundException4({
1829
+ code: CATALOG_ERROR_CODES4.PROMOTION_NOT_FOUND,
1830
+ message: `Promotion '${id}' not found`,
1831
+ params: {
1832
+ promotionId: id
1833
+ }
1834
+ });
1654
1835
  }
1655
1836
  return row;
1656
1837
  }
@@ -1660,14 +1841,26 @@ var PromotionsService = class {
1660
1841
  async update(id, data) {
1661
1842
  const existing = await this.repo.findById(id);
1662
1843
  if (!existing) {
1663
- throw new NotFoundException4(`Promotion '${id}' nicht gefunden`);
1844
+ throw new NotFoundException4({
1845
+ code: CATALOG_ERROR_CODES4.PROMOTION_NOT_FOUND,
1846
+ message: `Promotion '${id}' not found`,
1847
+ params: {
1848
+ promotionId: id
1849
+ }
1850
+ });
1664
1851
  }
1665
1852
  return this.repo.update(id, data);
1666
1853
  }
1667
1854
  async delete(id) {
1668
1855
  const existing = await this.repo.findById(id);
1669
1856
  if (!existing) {
1670
- throw new NotFoundException4(`Promotion '${id}' nicht gefunden`);
1857
+ throw new NotFoundException4({
1858
+ code: CATALOG_ERROR_CODES4.PROMOTION_NOT_FOUND,
1859
+ message: `Promotion '${id}' not found`,
1860
+ params: {
1861
+ promotionId: id
1862
+ }
1863
+ });
1671
1864
  }
1672
1865
  await this.repo.delete(id);
1673
1866
  }
@@ -1721,7 +1914,7 @@ var ListPromotionsQueryDto = class {
1721
1914
  _ts_decorate8([
1722
1915
  IsString2(),
1723
1916
  Matches2(PROJECT_KEY_PATTERN2, {
1724
- message: "projectKey muss kebab-case sein"
1917
+ message: "projectKey must be kebab-case"
1725
1918
  }),
1726
1919
  _ts_metadata8("design:type", String)
1727
1920
  ], ListPromotionsQueryDto.prototype, "projectKey", void 0);
@@ -1789,14 +1982,14 @@ _ts_decorate8([
1789
1982
  _ts_decorate8([
1790
1983
  IsString2(),
1791
1984
  Matches2(ISO_DATE_PATTERN, {
1792
- message: "validFrom muss ISO-Datum sein"
1985
+ message: "validFrom must be an ISO date"
1793
1986
  }),
1794
1987
  _ts_metadata8("design:type", String)
1795
1988
  ], CreatePromotionDto.prototype, "validFrom", void 0);
1796
1989
  _ts_decorate8([
1797
1990
  IsString2(),
1798
1991
  Matches2(ISO_DATE_PATTERN, {
1799
- message: "validTo muss ISO-Datum sein"
1992
+ message: "validTo must be an ISO date"
1800
1993
  }),
1801
1994
  _ts_metadata8("design:type", String)
1802
1995
  ], CreatePromotionDto.prototype, "validTo", void 0);
@@ -2115,7 +2308,7 @@ var ListMarketingSettingsQueryDto = class {
2115
2308
  _ts_decorate11([
2116
2309
  IsString3(),
2117
2310
  Matches3(PROJECT_KEY_PATTERN3, {
2118
- message: "projectKey muss kebab-case sein"
2311
+ message: "projectKey must be kebab-case"
2119
2312
  }),
2120
2313
  _ts_metadata11("design:type", String)
2121
2314
  ], ListMarketingSettingsQueryDto.prototype, "projectKey", void 0);
@@ -2129,7 +2322,7 @@ var UpdateMarketingSettingsDto = class {
2129
2322
  _ts_decorate11([
2130
2323
  IsString3(),
2131
2324
  Matches3(PROJECT_KEY_PATTERN3, {
2132
- message: "projectKey muss kebab-case sein"
2325
+ message: "projectKey must be kebab-case"
2133
2326
  }),
2134
2327
  _ts_metadata11("design:type", String)
2135
2328
  ], UpdateMarketingSettingsDto.prototype, "projectKey", void 0);
@@ -2141,7 +2334,7 @@ _ts_decorate11([
2141
2334
  }),
2142
2335
  Matches3(LOCALE_PATTERN, {
2143
2336
  each: true,
2144
- message: "Locale muss ISO-639-1 sein"
2337
+ message: "Locale must be ISO-639-1"
2145
2338
  }),
2146
2339
  _ts_metadata11("design:type", Array)
2147
2340
  ], UpdateMarketingSettingsDto.prototype, "activeLocales", void 0);
@@ -2630,6 +2823,7 @@ __name(parseAsOf, "parseAsOf");
2630
2823
 
2631
2824
  // src/catalog/plans.service.ts
2632
2825
  import { Inject as Inject12, Injectable as Injectable8, NotFoundException as NotFoundException5, Optional as Optional6, UnprocessableEntityException as UnprocessableEntityException3 } from "@nestjs/common";
2826
+ import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES5 } from "@saasicat/types";
2633
2827
  function _ts_decorate15(decorators, target, key, desc) {
2634
2828
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2635
2829
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2676,24 +2870,55 @@ var PlansService = class {
2676
2870
  }
2677
2871
  async getPlan(planId) {
2678
2872
  const plan = await this.repo.findById(planId);
2679
- if (!plan) throw new NotFoundException5(`Plan '${planId}' nicht gefunden`);
2873
+ if (!plan) {
2874
+ throw new NotFoundException5({
2875
+ code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
2876
+ message: `Plan '${planId}' not found`,
2877
+ params: {
2878
+ planId
2879
+ }
2880
+ });
2881
+ }
2680
2882
  return plan;
2681
2883
  }
2682
2884
  async createPlan(data) {
2683
2885
  const existing = await this.repo.findByKey(data.projectKey, data.planKey);
2684
2886
  if (existing) {
2685
- throw new UnprocessableEntityException3(`Plan '${data.planKey}' existiert bereits in Projekt '${data.projectKey}'`);
2887
+ throw new UnprocessableEntityException3({
2888
+ code: CATALOG_ERROR_CODES5.PLAN_ALREADY_EXISTS,
2889
+ message: `Plan '${data.planKey}' already exists in project '${data.projectKey}'`,
2890
+ params: {
2891
+ planKey: data.planKey,
2892
+ projectKey: data.projectKey
2893
+ }
2894
+ });
2686
2895
  }
2687
2896
  return this.repo.create(data);
2688
2897
  }
2689
2898
  async updatePlan(planId, data) {
2690
2899
  const existing = await this.repo.findById(planId);
2691
- if (!existing) throw new NotFoundException5(`Plan '${planId}' nicht gefunden`);
2900
+ if (!existing) {
2901
+ throw new NotFoundException5({
2902
+ code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
2903
+ message: `Plan '${planId}' not found`,
2904
+ params: {
2905
+ planId
2906
+ }
2907
+ });
2908
+ }
2692
2909
  return this.repo.update(planId, data);
2693
2910
  }
2694
2911
  async softDeletePlan(planId) {
2695
2912
  const existing = await this.repo.findById(planId);
2696
- if (!existing) throw new NotFoundException5(`Plan '${planId}' nicht gefunden`);
2913
+ if (!existing) {
2914
+ throw new NotFoundException5({
2915
+ code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
2916
+ message: `Plan '${planId}' not found`,
2917
+ params: {
2918
+ planId
2919
+ }
2920
+ });
2921
+ }
2697
2922
  if (existing.deletedAt !== null) return;
2698
2923
  await this.assertNoPublishedVersions(existing.planKey, "soft-delete");
2699
2924
  await this.repo.softDelete(planId);
@@ -2710,13 +2935,21 @@ var PlansService = class {
2710
2935
  */
2711
2936
  async hardDeletePlan(planId) {
2712
2937
  const existing = await this.repo.findById(planId);
2713
- if (!existing) throw new NotFoundException5(`Plan '${planId}' nicht gefunden`);
2938
+ if (!existing) {
2939
+ throw new NotFoundException5({
2940
+ code: CATALOG_ERROR_CODES5.PLAN_NOT_FOUND,
2941
+ message: `Plan '${planId}' not found`,
2942
+ params: {
2943
+ planId
2944
+ }
2945
+ });
2946
+ }
2714
2947
  await this.assertNoPublishedVersions(existing.planKey, "hard-delete");
2715
2948
  await this.assertNoDrafts(existing.planKey);
2716
2949
  if (typeof this.repo.hardDelete !== "function") {
2717
2950
  throw new UnprocessableEntityException3({
2718
- code: "PLAN_HARD_DELETE_NOT_IMPLEMENTED",
2719
- message: "Hartes L\xF6schen ist im aktuellen Repository nicht implementiert. Implementiere PlanRepository.hardDelete."
2951
+ code: CATALOG_ERROR_CODES5.PLAN_HARD_DELETE_NOT_IMPLEMENTED,
2952
+ message: "Hard delete is not implemented in the current repository. Implement PlanRepository.hardDelete."
2720
2953
  });
2721
2954
  }
2722
2955
  await this.repo.hardDelete(planId);
@@ -2736,8 +2969,12 @@ var PlansService = class {
2736
2969
  const live = published.filter((v) => v.supersededAt === null).length;
2737
2970
  const superseded = published.length - live;
2738
2971
  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.`,
2972
+ code: CATALOG_ERROR_CODES5.PLAN_HAS_PUBLISHED_VERSIONS,
2973
+ 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.`,
2974
+ params: {
2975
+ planKey,
2976
+ operation: op
2977
+ },
2741
2978
  publishedCount: published.length,
2742
2979
  liveCount: live,
2743
2980
  supersededCount: superseded
@@ -2749,8 +2986,11 @@ var PlansService = class {
2749
2986
  const drafts = versions.filter((v) => v.publishedAt === null);
2750
2987
  if (drafts.length === 0) return;
2751
2988
  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.`,
2989
+ code: CATALOG_ERROR_CODES5.PLAN_HAS_DRAFTS,
2990
+ message: `Plan '${planKey}' still has ${drafts.length} open draft version(s). Discard them first (DELETE /admin/catalog/plan-versions/:id) or publish them.`,
2991
+ params: {
2992
+ planKey
2993
+ },
2754
2994
  draftCount: drafts.length
2755
2995
  });
2756
2996
  }
@@ -2769,7 +3009,7 @@ PlansService = _ts_decorate15([
2769
3009
 
2770
3010
  // src/catalog/plan-versions.service.ts
2771
3011
  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";
3012
+ import { CATALOG_ERROR_CODES as CATALOG_ERROR_CODES6, isVersionEditable as isVersionEditable2 } from "@saasicat/types";
2773
3013
  function _ts_decorate16(decorators, target, key, desc) {
2774
3014
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2775
3015
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2808,7 +3048,7 @@ var PlanVersionsService = class _PlanVersionsService {
2808
3048
  this.mode = config.strictModeCheckMode ?? "blocking";
2809
3049
  this.marketedOnly = new Set(config.marketedOnlyFeatures ?? []);
2810
3050
  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).");
3051
+ this.logger.error("PlanVersionsService: strictModeCheckMode=blocking without a DiscoverySnapshot source \u2014 degrading to warn-only. Wire DiscoveryModule (#25).");
2812
3052
  this.mode = "warn-only";
2813
3053
  }
2814
3054
  const required = [
@@ -2822,7 +3062,7 @@ var PlanVersionsService = class _PlanVersionsService {
2822
3062
  ];
2823
3063
  for (const key of required) {
2824
3064
  if (typeof this.repo[key] !== "function") {
2825
- throw new Error(`PlanVersionsService: PlanRepository.${String(key)} fehlt \u2014 das Repository implementiert die Lifecycle-Methoden nicht.`);
3065
+ throw new Error(`PlanVersionsService: PlanRepository.${String(key)} is missing \u2014 the repository does not implement the lifecycle methods.`);
2826
3066
  }
2827
3067
  }
2828
3068
  }
@@ -2838,18 +3078,27 @@ var PlanVersionsService = class _PlanVersionsService {
2838
3078
  async discardPlanDraft(versionId) {
2839
3079
  const existing = await this.repo.findVersionById(versionId);
2840
3080
  if (!existing) {
2841
- throw new NotFoundException6(`PlanVersion '${versionId}' nicht gefunden`);
3081
+ throw new NotFoundException6({
3082
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
3083
+ message: `PlanVersion '${versionId}' not found`,
3084
+ params: {
3085
+ versionId
3086
+ }
3087
+ });
2842
3088
  }
2843
3089
  if (existing.publishedAt !== null) {
2844
3090
  throw new UnprocessableEntityException4({
2845
- code: "PLAN_VERSION_ALREADY_PUBLISHED",
2846
- message: `PlanVersion '${versionId}' ist bereits published und kann nicht verworfen werden.`
3091
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_ALREADY_PUBLISHED,
3092
+ message: `PlanVersion '${versionId}' is already published and cannot be discarded.`,
3093
+ params: {
3094
+ versionId
3095
+ }
2847
3096
  });
2848
3097
  }
2849
3098
  if (typeof this.repo.deletePlanVersionDraft !== "function") {
2850
3099
  throw new UnprocessableEntityException4({
2851
- code: "PLAN_VERSION_DISCARD_NOT_IMPLEMENTED",
2852
- message: "Discard ist im aktuellen Repository nicht implementiert. Implementiere PlanRepository.deletePlanVersionDraft."
3100
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_DISCARD_NOT_IMPLEMENTED,
3101
+ message: "Discard is not implemented in the current repository. Implement PlanRepository.deletePlanVersionDraft."
2853
3102
  });
2854
3103
  }
2855
3104
  await this.repo.deletePlanVersionDraft(versionId);
@@ -2862,7 +3111,13 @@ var PlanVersionsService = class _PlanVersionsService {
2862
3111
  async listPlanVersions(planUuid) {
2863
3112
  const plan = await this.repo.findById(planUuid);
2864
3113
  if (!plan) {
2865
- throw new NotFoundException6(`Plan '${planUuid}' nicht gefunden`);
3114
+ throw new NotFoundException6({
3115
+ code: CATALOG_ERROR_CODES6.PLAN_NOT_FOUND,
3116
+ message: `Plan '${planUuid}' not found`,
3117
+ params: {
3118
+ planId: planUuid
3119
+ }
3120
+ });
2866
3121
  }
2867
3122
  const versions = await this.repo.listVersions(plan.planKey);
2868
3123
  return this.annotateEditability(versions);
@@ -2870,7 +3125,13 @@ var PlanVersionsService = class _PlanVersionsService {
2870
3125
  async getPlanVersion(versionId) {
2871
3126
  const version = await this.repo.findVersionById(versionId);
2872
3127
  if (!version) {
2873
- throw new NotFoundException6(`PlanVersion '${versionId}' nicht gefunden`);
3128
+ throw new NotFoundException6({
3129
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
3130
+ message: `PlanVersion '${versionId}' not found`,
3131
+ params: {
3132
+ versionId
3133
+ }
3134
+ });
2874
3135
  }
2875
3136
  const [annotated] = await this.annotateEditability([
2876
3137
  version
@@ -2908,12 +3169,25 @@ var PlanVersionsService = class _PlanVersionsService {
2908
3169
  async createPlanDraft(data) {
2909
3170
  const plan = await this.repo.findById(data.planId);
2910
3171
  if (!plan) {
2911
- throw new NotFoundException6(`Plan '${data.planId}' nicht gefunden`);
3172
+ throw new NotFoundException6({
3173
+ code: CATALOG_ERROR_CODES6.PLAN_NOT_FOUND,
3174
+ message: `Plan '${data.planId}' not found`,
3175
+ params: {
3176
+ planId: data.planId
3177
+ }
3178
+ });
2912
3179
  }
2913
3180
  const planKey = plan.planKey;
2914
3181
  const existingDraft = await this.repo.findCurrentDraft(planKey);
2915
3182
  if (existingDraft) {
2916
- throw new UnprocessableEntityException4(`Plan '${planKey}' hat bereits eine Draft-Version v${existingDraft.version}; bitte erst publishen oder verwerfen`);
3183
+ throw new UnprocessableEntityException4({
3184
+ code: CATALOG_ERROR_CODES6.PLAN_DRAFT_ALREADY_EXISTS,
3185
+ message: `Plan '${planKey}' already has a draft version v${existingDraft.version}; publish or discard it first`,
3186
+ params: {
3187
+ planKey,
3188
+ draftVersion: existingDraft.version
3189
+ }
3190
+ });
2917
3191
  }
2918
3192
  let baseVersionId = data.baseVersionId;
2919
3193
  if (baseVersionId === void 0) {
@@ -2938,7 +3212,13 @@ var PlanVersionsService = class _PlanVersionsService {
2938
3212
  async updatePlanDraft(versionId, data) {
2939
3213
  const existing = await this.repo.findVersionById(versionId);
2940
3214
  if (!existing) {
2941
- throw new NotFoundException6(`PlanVersion '${versionId}' nicht gefunden`);
3215
+ throw new NotFoundException6({
3216
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
3217
+ message: `PlanVersion '${versionId}' not found`,
3218
+ params: {
3219
+ versionId
3220
+ }
3221
+ });
2942
3222
  }
2943
3223
  const [annotated] = await this.annotateEditability([
2944
3224
  existing
@@ -2946,8 +3226,11 @@ var PlanVersionsService = class _PlanVersionsService {
2946
3226
  const { editable } = isVersionEditable2(annotated);
2947
3227
  if (!editable) {
2948
3228
  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.`
3229
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_EDITABLE,
3230
+ 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.`,
3231
+ params: {
3232
+ versionId
3233
+ }
2951
3234
  });
2952
3235
  }
2953
3236
  const merged = {
@@ -2965,10 +3248,22 @@ var PlanVersionsService = class _PlanVersionsService {
2965
3248
  async publishPlanVersion(versionId, publishMeta) {
2966
3249
  const draft = await this.repo.findVersionById(versionId);
2967
3250
  if (!draft) {
2968
- throw new NotFoundException6(`PlanVersion '${versionId}' nicht gefunden`);
3251
+ throw new NotFoundException6({
3252
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
3253
+ message: `PlanVersion '${versionId}' not found`,
3254
+ params: {
3255
+ versionId
3256
+ }
3257
+ });
2969
3258
  }
2970
3259
  if (draft.publishedAt !== null) {
2971
- throw new UnprocessableEntityException4(`PlanVersion '${versionId}' ist bereits published`);
3260
+ throw new UnprocessableEntityException4({
3261
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_ALREADY_PUBLISHED,
3262
+ message: `PlanVersion '${versionId}' is already published`,
3263
+ params: {
3264
+ versionId
3265
+ }
3266
+ });
2972
3267
  }
2973
3268
  const warnings = await this.runStrictCheck({
2974
3269
  features: draft.features,
@@ -2980,8 +3275,8 @@ var PlanVersionsService = class _PlanVersionsService {
2980
3275
  const yearly = Number.parseFloat(String(draft.yearlyNet ?? "0"));
2981
3276
  if (monthly <= 0 || yearly <= 0) {
2982
3277
  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.",
3278
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_ZERO_PRICE,
3279
+ 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
3280
  monthlyNet: String(draft.monthlyNet),
2986
3281
  yearlyNet: String(draft.yearlyNet)
2987
3282
  });
@@ -2991,23 +3286,30 @@ var PlanVersionsService = class _PlanVersionsService {
2991
3286
  const validFromInput = publishMeta.validFrom ?? draft.validFrom;
2992
3287
  if (!validFromInput) {
2993
3288
  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."
3289
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_REQUIRED,
3290
+ message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
2996
3291
  });
2997
3292
  }
2998
3293
  const validFrom = new Date(validFromInput);
2999
3294
  if (Number.isNaN(validFrom.getTime())) {
3000
3295
  throw new UnprocessableEntityException4({
3001
- code: "PLAN_VERSION_VALID_FROM_INVALID",
3002
- message: `validFrom '${validFromInput}' ist kein g\xFCltiges Datum`
3296
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_INVALID,
3297
+ message: `validFrom '${validFromInput}' is not a valid date`,
3298
+ params: {
3299
+ validFrom: validFromInput
3300
+ }
3003
3301
  });
3004
3302
  }
3005
3303
  if (previous?.validFrom) {
3006
3304
  const prevFrom = new Date(previous.validFrom);
3007
3305
  if (validFrom <= prevFrom) {
3008
3306
  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.`
3307
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
3308
+ message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
3309
+ params: {
3310
+ validFrom: validFrom.toISOString(),
3311
+ previousValidFrom: previous.validFrom
3312
+ }
3011
3313
  });
3012
3314
  }
3013
3315
  if (previous.validUntil) {
@@ -3016,8 +3318,11 @@ var PlanVersionsService = class _PlanVersionsService {
3016
3318
  const requiredStart = new Date(prevUntil.getTime() + dayMs);
3017
3319
  if (validFrom.getTime() !== requiredStart.getTime()) {
3018
3320
  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)}.`,
3321
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_NOT_GAPLESS,
3322
+ 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)}.`,
3323
+ params: {
3324
+ received: validFrom.toISOString().slice(0, 10)
3325
+ },
3021
3326
  requiredValidFrom: requiredStart.toISOString().slice(0, 10),
3022
3327
  previousValidUntil: previous.validUntil
3023
3328
  });
@@ -3028,14 +3333,21 @@ var PlanVersionsService = class _PlanVersionsService {
3028
3333
  const validUntil = validUntilInput ? new Date(validUntilInput) : null;
3029
3334
  if (validUntil && Number.isNaN(validUntil.getTime())) {
3030
3335
  throw new UnprocessableEntityException4({
3031
- code: "PLAN_VERSION_VALID_UNTIL_INVALID",
3032
- message: `validUntil '${validUntilInput}' ist kein g\xFCltiges Datum`
3336
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_UNTIL_INVALID,
3337
+ message: `validUntil '${validUntilInput}' is not a valid date`,
3338
+ params: {
3339
+ validUntil: validUntilInput
3340
+ }
3033
3341
  });
3034
3342
  }
3035
3343
  if (validUntil && validUntil <= validFrom) {
3036
3344
  throw new UnprocessableEntityException4({
3037
- code: "PLAN_VERSION_VALID_UNTIL_BEFORE_FROM",
3038
- message: `validUntil (${validUntil.toISOString()}) muss strikt nach validFrom (${validFrom.toISOString()}) liegen.`
3345
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_UNTIL_BEFORE_FROM,
3346
+ message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
3347
+ params: {
3348
+ validUntil: validUntil.toISOString(),
3349
+ validFrom: validFrom.toISOString()
3350
+ }
3039
3351
  });
3040
3352
  }
3041
3353
  const fieldsFor = /* @__PURE__ */ __name((v) => ({
@@ -3052,8 +3364,8 @@ var PlanVersionsService = class _PlanVersionsService {
3052
3364
  };
3053
3365
  if (!diff.nonRegressive && !publishMeta.forceRegressive) {
3054
3366
  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).",
3367
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_REGRESSION,
3368
+ message: "This plan version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
3057
3369
  changes: diff.changes
3058
3370
  });
3059
3371
  }
@@ -3088,36 +3400,51 @@ var PlanVersionsService = class _PlanVersionsService {
3088
3400
  async terminatePlanVersion(versionId, endsAt) {
3089
3401
  const existing = await this.repo.findVersionById(versionId);
3090
3402
  if (!existing) {
3091
- throw new NotFoundException6(`PlanVersion '${versionId}' nicht gefunden`);
3403
+ throw new NotFoundException6({
3404
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_FOUND,
3405
+ message: `PlanVersion '${versionId}' not found`,
3406
+ params: {
3407
+ versionId
3408
+ }
3409
+ });
3092
3410
  }
3093
3411
  if (existing.publishedAt === null) {
3094
3412
  throw new UnprocessableEntityException4({
3095
- code: "PLAN_VERSION_NOT_LIVE",
3096
- message: `PlanVersion '${versionId}' ist nicht published und kann nicht terminiert werden.`
3413
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_PUBLISHED,
3414
+ message: `PlanVersion '${versionId}' is not published and cannot be terminated.`,
3415
+ params: {
3416
+ versionId
3417
+ }
3097
3418
  });
3098
3419
  }
3099
3420
  if (existing.supersededAt !== null) {
3100
3421
  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.`
3422
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_SUPERSEDED,
3423
+ message: `PlanVersion '${versionId}' has already been superseded by a newer version (supersededAt) and cannot be terminated.`,
3424
+ params: {
3425
+ versionId
3426
+ }
3103
3427
  });
3104
3428
  }
3105
3429
  if (Number.isNaN(endsAt.getTime())) {
3106
3430
  throw new UnprocessableEntityException4({
3107
- code: "PLAN_TERMINATE_INVALID_DATE",
3108
- message: "endsAt ist kein g\xFCltiges Datum."
3431
+ code: CATALOG_ERROR_CODES6.PLAN_TERMINATE_INVALID_DATE,
3432
+ message: "endsAt is not a valid date."
3109
3433
  });
3110
3434
  }
3111
3435
  if (endsAt.getTime() <= Date.now()) {
3112
3436
  throw new UnprocessableEntityException4({
3113
- code: "PLAN_TERMINATE_DATE_NOT_FUTURE",
3114
- message: `endsAt (${endsAt.toISOString()}) muss strikt in der Zukunft liegen.`
3437
+ code: CATALOG_ERROR_CODES6.PLAN_TERMINATE_DATE_NOT_FUTURE,
3438
+ message: `endsAt (${endsAt.toISOString()}) must lie strictly in the future.`,
3439
+ params: {
3440
+ endsAt: endsAt.toISOString()
3441
+ }
3115
3442
  });
3116
3443
  }
3117
3444
  if (typeof this.repo.terminate !== "function") {
3118
3445
  throw new UnprocessableEntityException4({
3119
- code: "PLAN_TERMINATE_NOT_IMPLEMENTED",
3120
- message: "Terminate ist im aktuellen Repository nicht implementiert. Implementiere PlanRepository.terminate."
3446
+ code: CATALOG_ERROR_CODES6.PLAN_TERMINATE_NOT_IMPLEMENTED,
3447
+ message: "Terminate is not implemented in the current repository. Implement PlanRepository.terminate."
3121
3448
  });
3122
3449
  }
3123
3450
  return this.repo.terminate(versionId, endsAt);
@@ -3135,8 +3462,8 @@ var PlanVersionsService = class _PlanVersionsService {
3135
3462
  if (blockingStrictModeWarnings(warnings).length === 0) return;
3136
3463
  if (this.mode === "blocking") {
3137
3464
  throw new UnprocessableEntityException4({
3138
- code: "STRICT_MODE_VIOLATIONS",
3139
- message: "Strict-Mode-Check hat Drift gegen den Discovery-Snapshot gefunden.",
3465
+ code: CATALOG_ERROR_CODES6.STRICT_MODE_VIOLATIONS,
3466
+ message: "The strict-mode check found drift against the discovery snapshot.",
3140
3467
  warnings
3141
3468
  });
3142
3469
  }
@@ -3200,7 +3527,7 @@ var CreateBundleDto = class {
3200
3527
  _ts_decorate17([
3201
3528
  IsString4(),
3202
3529
  Matches4(PROJECT_KEY_PATTERN4, {
3203
- message: 'projectKey muss kebab-case sein (z. B. "my-app")'
3530
+ message: 'projectKey must be kebab-case (e.g. "my-app")'
3204
3531
  }),
3205
3532
  MaxLength3(64),
3206
3533
  _ts_metadata17("design:type", String)
@@ -3208,7 +3535,7 @@ _ts_decorate17([
3208
3535
  _ts_decorate17([
3209
3536
  IsString4(),
3210
3537
  Matches4(KEY_PATTERN, {
3211
- message: 'bundleKey muss SCREAMING_SNAKE_CASE sein (z. B. "BANKING")'
3538
+ message: 'bundleKey must be SCREAMING_SNAKE_CASE (e.g. "BANKING")'
3212
3539
  }),
3213
3540
  MaxLength3(64),
3214
3541
  _ts_metadata17("design:type", String)
@@ -3308,7 +3635,7 @@ _ts_decorate17([
3308
3635
  }),
3309
3636
  Matches4(FEATURE_KEY_PATTERN, {
3310
3637
  each: true,
3311
- message: "features-Eintr\xE4ge m\xFCssen SCREAMING_SNAKE_CASE sein"
3638
+ message: "features entries must be SCREAMING_SNAKE_CASE"
3312
3639
  }),
3313
3640
  _ts_metadata17("design:type", Array)
3314
3641
  ], CreateBundleVersionDraftDto.prototype, "features", void 0);
@@ -3334,7 +3661,7 @@ _ts_decorate17([
3334
3661
  no_symbols: false
3335
3662
  }),
3336
3663
  Matches4(DECIMAL_PATTERN, {
3337
- message: 'monthlyNet muss Decimal mit max. 2 Nachkommastellen sein (z. B. "9.90")'
3664
+ message: 'monthlyNet must be a decimal with at most 2 fraction digits (e.g. "9.90")'
3338
3665
  }),
3339
3666
  _ts_metadata17("design:type", Object)
3340
3667
  ], CreateBundleVersionDraftDto.prototype, "monthlyNet", void 0);
@@ -3345,7 +3672,7 @@ _ts_decorate17([
3345
3672
  no_symbols: false
3346
3673
  }),
3347
3674
  Matches4(DECIMAL_PATTERN, {
3348
- message: "yearlyNet muss Decimal mit max. 2 Nachkommastellen sein"
3675
+ message: "yearlyNet must be a decimal with at most 2 fraction digits"
3349
3676
  }),
3350
3677
  _ts_metadata17("design:type", Object)
3351
3678
  ], CreateBundleVersionDraftDto.prototype, "yearlyNet", void 0);
@@ -3369,7 +3696,7 @@ _ts_decorate17([
3369
3696
  IsOptional3(),
3370
3697
  ValidateIf((_o, value) => value !== null),
3371
3698
  Matches4(ISO_DATE_PATTERN2, {
3372
- message: "validFrom muss ISO-Datum sein (YYYY-MM-DD)"
3699
+ message: "validFrom must be an ISO date (YYYY-MM-DD)"
3373
3700
  }),
3374
3701
  _ts_metadata17("design:type", Object)
3375
3702
  ], CreateBundleVersionDraftDto.prototype, "validFrom", void 0);
@@ -3377,7 +3704,7 @@ _ts_decorate17([
3377
3704
  IsOptional3(),
3378
3705
  ValidateIf((_o, value) => value !== null),
3379
3706
  Matches4(ISO_DATE_PATTERN2, {
3380
- message: "validUntil muss ISO-Datum sein (YYYY-MM-DD)"
3707
+ message: "validUntil must be an ISO date (YYYY-MM-DD)"
3381
3708
  }),
3382
3709
  _ts_metadata17("design:type", Object)
3383
3710
  ], CreateBundleVersionDraftDto.prototype, "validUntil", void 0);
@@ -3449,7 +3776,7 @@ _ts_decorate17([
3449
3776
  IsOptional3(),
3450
3777
  ValidateIf((_o, value) => value !== null),
3451
3778
  Matches4(ISO_DATE_PATTERN2, {
3452
- message: "validFrom muss ISO-Datum sein (YYYY-MM-DD)"
3779
+ message: "validFrom must be an ISO date (YYYY-MM-DD)"
3453
3780
  }),
3454
3781
  _ts_metadata17("design:type", Object)
3455
3782
  ], UpdateBundleVersionDraftDto.prototype, "validFrom", void 0);
@@ -3457,7 +3784,7 @@ _ts_decorate17([
3457
3784
  IsOptional3(),
3458
3785
  ValidateIf((_o, value) => value !== null),
3459
3786
  Matches4(ISO_DATE_PATTERN2, {
3460
- message: "validUntil muss ISO-Datum sein (YYYY-MM-DD)"
3787
+ message: "validUntil must be an ISO date (YYYY-MM-DD)"
3461
3788
  }),
3462
3789
  _ts_metadata17("design:type", Object)
3463
3790
  ], UpdateBundleVersionDraftDto.prototype, "validUntil", void 0);
@@ -3494,7 +3821,7 @@ _ts_decorate17([
3494
3821
  IsOptional3(),
3495
3822
  ValidateIf((_o, value) => value !== null),
3496
3823
  Matches4(ISO_DATE_PATTERN2, {
3497
- message: "validFrom muss ISO-Datum sein (YYYY-MM-DD)"
3824
+ message: "validFrom must be an ISO date (YYYY-MM-DD)"
3498
3825
  }),
3499
3826
  _ts_metadata17("design:type", Object)
3500
3827
  ], PublishBundleVersionDto.prototype, "validFrom", void 0);
@@ -3502,7 +3829,7 @@ _ts_decorate17([
3502
3829
  IsOptional3(),
3503
3830
  ValidateIf((_o, value) => value !== null),
3504
3831
  Matches4(ISO_DATE_PATTERN2, {
3505
- message: "validUntil muss ISO-Datum sein (YYYY-MM-DD)"
3832
+ message: "validUntil must be an ISO date (YYYY-MM-DD)"
3506
3833
  }),
3507
3834
  _ts_metadata17("design:type", Object)
3508
3835
  ], PublishBundleVersionDto.prototype, "validUntil", void 0);
@@ -3804,7 +4131,7 @@ var CreateMarketingProjectionDto = class {
3804
4131
  _ts_decorate19([
3805
4132
  IsString5(),
3806
4133
  Matches5(PROJECT_KEY_PATTERN5, {
3807
- message: "projectKey muss kebab-case sein"
4134
+ message: "projectKey must be kebab-case"
3808
4135
  }),
3809
4136
  MaxLength4(64),
3810
4137
  _ts_metadata19("design:type", String)
@@ -3822,7 +4149,7 @@ _ts_decorate19([
3822
4149
  IsOptional4(),
3823
4150
  IsString5(),
3824
4151
  Matches5(LOCALE_PATTERN2, {
3825
- message: 'locale muss ISO-639-1 sein (z. B. "de", "en", "de-AT")'
4152
+ message: 'locale must be ISO-639-1 (e.g. "de", "en", "de-AT")'
3826
4153
  }),
3827
4154
  _ts_metadata19("design:type", String)
3828
4155
  ], CreateMarketingProjectionDto.prototype, "locale", void 0);
@@ -4151,7 +4478,7 @@ var CreatePlanDto = class {
4151
4478
  _ts_decorate21([
4152
4479
  IsString6(),
4153
4480
  Matches6(PROJECT_KEY_PATTERN6, {
4154
- message: 'projectKey muss kebab-case sein (z. B. "my-app")'
4481
+ message: 'projectKey must be kebab-case (e.g. "my-app")'
4155
4482
  }),
4156
4483
  MaxLength5(64),
4157
4484
  _ts_metadata21("design:type", String)
@@ -4159,7 +4486,7 @@ _ts_decorate21([
4159
4486
  _ts_decorate21([
4160
4487
  IsString6(),
4161
4488
  Matches6(KEY_PATTERN2, {
4162
- message: 'planKey muss SCREAMING_SNAKE_CASE sein (z. B. "STARTER")'
4489
+ message: 'planKey must be SCREAMING_SNAKE_CASE (e.g. "STARTER")'
4163
4490
  }),
4164
4491
  MaxLength5(64),
4165
4492
  _ts_metadata21("design:type", String)
@@ -4268,7 +4595,7 @@ _ts_decorate22([
4268
4595
  }),
4269
4596
  Matches7(FEATURE_KEY_PATTERN2, {
4270
4597
  each: true,
4271
- message: "features-Eintr\xE4ge m\xFCssen SCREAMING_SNAKE_CASE sein"
4598
+ message: "features entries must be SCREAMING_SNAKE_CASE"
4272
4599
  }),
4273
4600
  _ts_metadata22("design:type", Array)
4274
4601
  ], CreatePlanVersionDraftDto.prototype, "features", void 0);
@@ -4280,7 +4607,7 @@ _ts_decorate22([
4280
4607
  }),
4281
4608
  Matches7(FEATURE_KEY_PATTERN2, {
4282
4609
  each: true,
4283
- message: "bundles-Eintr\xE4ge m\xFCssen SCREAMING_SNAKE_CASE sein"
4610
+ message: "bundles entries must be SCREAMING_SNAKE_CASE"
4284
4611
  }),
4285
4612
  _ts_metadata22("design:type", Array)
4286
4613
  ], CreatePlanVersionDraftDto.prototype, "bundles", void 0);
@@ -4290,13 +4617,13 @@ _ts_decorate22([
4290
4617
  ], CreatePlanVersionDraftDto.prototype, "quotas", void 0);
4291
4618
  _ts_decorate22([
4292
4619
  Matches7(DECIMAL_PATTERN2, {
4293
- message: 'monthlyNet muss Decimal mit max. 2 Nachkommastellen sein (z. B. "9.90")'
4620
+ message: 'monthlyNet must be a decimal with at most 2 fraction digits (e.g. "9.90")'
4294
4621
  }),
4295
4622
  _ts_metadata22("design:type", String)
4296
4623
  ], CreatePlanVersionDraftDto.prototype, "monthlyNet", void 0);
4297
4624
  _ts_decorate22([
4298
4625
  Matches7(DECIMAL_PATTERN2, {
4299
- message: "yearlyNet muss Decimal mit max. 2 Nachkommastellen sein"
4626
+ message: "yearlyNet must be a decimal with at most 2 fraction digits"
4300
4627
  }),
4301
4628
  _ts_metadata22("design:type", String)
4302
4629
  ], CreatePlanVersionDraftDto.prototype, "yearlyNet", void 0);
@@ -4541,7 +4868,7 @@ function buildPlansController(guards) {
4541
4868
  }
4542
4869
  assertVersionsService() {
4543
4870
  if (!this.versionsService) {
4544
- throw new Error("PlanVersionsService nicht registriert \u2014 App hat den Plan-Lifecycle-Editor nicht aktiviert.");
4871
+ throw new Error("PlanVersionsService is not registered \u2014 the app has not enabled the plan lifecycle editor.");
4545
4872
  }
4546
4873
  }
4547
4874
  };