@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
package/dist/_entries.cjs CHANGED
@@ -87,6 +87,7 @@ var RLS_BYPASS_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/RlsBypassP
87
87
 
88
88
  // src/admin/super-admin.guard.ts
89
89
  var import_common = require("@nestjs/common");
90
+ var import_types = require("@saasicat/types");
90
91
  function _ts_decorate(decorators, target, key, desc) {
91
92
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
92
93
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -100,10 +101,18 @@ var SuperAdminGuard = class {
100
101
  }
101
102
  canActivate(context) {
102
103
  const request = context.switchToHttp().getRequest();
103
- if (!request.user) throw new import_common.ForbiddenException("Nicht authentifiziert");
104
+ if (!request.user) {
105
+ throw new import_common.ForbiddenException({
106
+ code: import_types.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
107
+ message: "Not authenticated"
108
+ });
109
+ }
104
110
  const role = request.user.platformRole ?? request.user.role;
105
111
  if (role !== "SUPER_ADMIN") {
106
- throw new import_common.ForbiddenException("Nur SUPER_ADMIN-Rolle erlaubt");
112
+ throw new import_common.ForbiddenException({
113
+ code: import_types.AUTH_ERROR_CODES.SUPER_ADMIN_REQUIRED,
114
+ message: "Only the SUPER_ADMIN role is allowed"
115
+ });
107
116
  }
108
117
  return true;
109
118
  }
@@ -115,6 +124,22 @@ SuperAdminGuard = _ts_decorate([
115
124
  // src/admin/mfa.guard.ts
116
125
  var import_common3 = require("@nestjs/common");
117
126
  var import_core = require("@nestjs/core");
127
+ var import_types3 = require("@saasicat/types");
128
+
129
+ // src/errors/coded-error.ts
130
+ var import_types2 = require("@saasicat/types");
131
+ function codedError(code, params) {
132
+ const message = (0, import_types2.formatErrorMessage)(import_types2.ERROR_MESSAGES_EN[code], params ?? {});
133
+ return params ? {
134
+ code,
135
+ message,
136
+ params
137
+ } : {
138
+ code,
139
+ message
140
+ };
141
+ }
142
+ __name(codedError, "codedError");
118
143
 
119
144
  // src/admin/mfa.ts
120
145
  var import_common2 = require("@nestjs/common");
@@ -194,7 +219,7 @@ var MfaService = class _MfaService {
194
219
  return false;
195
220
  }
196
221
  const message = error instanceof Error ? error.message : String(error);
197
- this.logger.warn(`TOTP-Verify f\xFCr User ${input.userId} unerwartet fehlgeschlagen: ${message}`);
222
+ this.logger.warn(`TOTP verification for user ${input.userId} failed unexpectedly: ${message}`);
198
223
  return false;
199
224
  }
200
225
  }
@@ -260,22 +285,30 @@ var MfaGuard = class _MfaGuard {
260
285
  }
261
286
  const request = context.switchToHttp().getRequest();
262
287
  const user = request.user;
263
- if (!user) throw new import_common3.UnauthorizedException({
264
- reason: "NOT_AUTHENTICATED"
265
- });
288
+ if (!user) {
289
+ throw new import_common3.UnauthorizedException({
290
+ ...codedError(import_types3.AUTH_ERROR_CODES.NOT_AUTHENTICATED),
291
+ // `reason` is superseded by `code` and will be removed.
292
+ reason: "NOT_AUTHENTICATED"
293
+ });
294
+ }
266
295
  const enabled = await this.mfaService.isEnabled(user.id);
267
296
  if (!enabled) {
268
297
  throw new import_common3.UnauthorizedException({
298
+ code: import_types3.AUTH_ERROR_CODES.MFA_NOT_SET_UP,
299
+ // `reason` is superseded by `code` and will be removed.
269
300
  reason: "MFA_NOT_SET_UP",
270
- message: "Bitte zuerst MFA-Setup \xFCber das CLI ausf\xFChren."
301
+ message: "Run the MFA setup via the CLI first."
271
302
  });
272
303
  }
273
304
  const headerVal = request.headers?.["x-mfa-code"];
274
305
  const code = Array.isArray(headerVal) ? headerVal[0] : headerVal;
275
306
  if (!code) {
276
307
  throw new import_common3.UnauthorizedException({
308
+ code: import_types3.AUTH_ERROR_CODES.MFA_REQUIRED,
309
+ // `reason` is superseded by `code` and will be removed.
277
310
  reason: "MFA_REQUIRED",
278
- message: "TOTP-Code im Header X-Mfa-Code erforderlich."
311
+ message: "TOTP code required in the X-Mfa-Code header."
279
312
  });
280
313
  }
281
314
  const valid = await this.mfaService.verify({
@@ -284,8 +317,10 @@ var MfaGuard = class _MfaGuard {
284
317
  });
285
318
  if (!valid) {
286
319
  throw new import_common3.UnauthorizedException({
320
+ code: import_types3.AUTH_ERROR_CODES.MFA_FAILED,
321
+ // `reason` is superseded by `code` and will be removed.
287
322
  reason: "MFA_FAILED",
288
- message: "TOTP-Code ung\xFCltig."
323
+ message: "Invalid TOTP code."
289
324
  });
290
325
  }
291
326
  return true;
@@ -799,7 +834,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
799
834
  actions: [
800
835
  {
801
836
  id: "platform.tenants.suspend",
802
- label: "Mandant suspendieren",
837
+ label: "Suspend tenant",
803
838
  actionKey: "tenants.suspend",
804
839
  requiredCapability: "tenants.suspend",
805
840
  requiresMfa: true,
@@ -807,7 +842,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
807
842
  },
808
843
  {
809
844
  id: "platform.tenants.reactivate",
810
- label: "Mandant reaktivieren",
845
+ label: "Reactivate tenant",
811
846
  actionKey: "tenants.reactivate",
812
847
  requiredCapability: "tenants.reactivate",
813
848
  requiresMfa: true,
@@ -823,7 +858,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
823
858
  },
824
859
  {
825
860
  id: "platform.tenants.export",
826
- label: "DSGVO-Export",
861
+ label: "GDPR export",
827
862
  actionKey: "tenants.export",
828
863
  requiredCapability: "tenants.export",
829
864
  requiresMfa: true,
@@ -831,7 +866,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
831
866
  },
832
867
  {
833
868
  id: "platform.subscriptions.cancel",
834
- label: "Subscription k\xFCndigen",
869
+ label: "Cancel subscription",
835
870
  actionKey: "subscriptions.cancel",
836
871
  requiredCapability: "subscriptions.cancel",
837
872
  requiresMfa: true,
@@ -839,7 +874,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
839
874
  },
840
875
  {
841
876
  id: "platform.pilots.grant",
842
- label: "Pilot-Status setzen",
877
+ label: "Set pilot status",
843
878
  actionKey: "pilots.grant",
844
879
  requiredCapability: "pilots.grant",
845
880
  requiresMfa: true,
@@ -847,7 +882,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
847
882
  },
848
883
  {
849
884
  id: "platform.pilots.revoke",
850
- label: "Pilot-Status entziehen",
885
+ label: "Revoke pilot status",
851
886
  actionKey: "pilots.revoke",
852
887
  requiredCapability: "pilots.revoke",
853
888
  requiresMfa: true,
@@ -855,7 +890,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
855
890
  },
856
891
  {
857
892
  id: "platform.pilots.extend",
858
- label: "Pilot verl\xE4ngern",
893
+ label: "Extend pilot",
859
894
  actionKey: "pilots.extend",
860
895
  requiredCapability: "pilots.extend",
861
896
  requiresMfa: true,
@@ -867,102 +902,102 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
867
902
  actions: [
868
903
  {
869
904
  key: "TENANT_SUSPEND",
870
- label: "Mandant suspendiert",
905
+ label: "Tenant suspended",
871
906
  severity: "high"
872
907
  },
873
908
  {
874
909
  key: "TENANT_REACTIVATE",
875
- label: "Mandant reaktiviert",
910
+ label: "Tenant reactivated",
876
911
  severity: "medium"
877
912
  },
878
913
  {
879
914
  key: "TENANT_IMPERSONATE",
880
- label: "Impersonation gestartet",
915
+ label: "Impersonation started",
881
916
  severity: "high"
882
917
  },
883
918
  {
884
919
  key: "TENANT_EXPORT",
885
- label: "DSGVO-Export ausgel\xF6st",
920
+ label: "GDPR export triggered",
886
921
  severity: "medium"
887
922
  },
888
923
  {
889
924
  key: "PILOT_CREATE",
890
- label: "Pilot-Tenant angelegt",
925
+ label: "Pilot tenant created",
891
926
  severity: "medium"
892
927
  },
893
928
  {
894
929
  key: "PILOT_GRANT",
895
- label: "Pilot-Status gesetzt",
930
+ label: "Pilot status set",
896
931
  severity: "medium"
897
932
  },
898
933
  {
899
934
  key: "PILOT_REVOKE",
900
- label: "Pilot-Status entzogen",
935
+ label: "Pilot status revoked",
901
936
  severity: "medium"
902
937
  },
903
938
  {
904
939
  key: "PILOT_EXTEND",
905
- label: "Pilot verl\xE4ngert",
940
+ label: "Pilot extended",
906
941
  severity: "low"
907
942
  },
908
943
  {
909
944
  key: "PLAN_VERSION_PUBLISH",
910
- label: "Plan-Version freigegeben",
945
+ label: "Plan version approved",
911
946
  severity: "high"
912
947
  },
913
948
  {
914
949
  key: "PLAN_VERSION_TERMINATE",
915
- label: "Plan-Version terminiert",
950
+ label: "Plan version terminated",
916
951
  severity: "high"
917
952
  },
918
953
  {
919
954
  key: "SUBSCRIPTION_CANCEL",
920
- label: "Subscription gek\xFCndigt",
955
+ label: "Subscription cancelled",
921
956
  severity: "high"
922
957
  },
923
958
  {
924
959
  key: "USER_RESET_PASSWORD",
925
- label: "Passwort-Reset ausgel\xF6st",
960
+ label: "Password reset triggered",
926
961
  severity: "medium"
927
962
  },
928
963
  {
929
964
  key: "USER_DEACTIVATE",
930
- label: "User deaktiviert",
965
+ label: "User deactivated",
931
966
  severity: "high"
932
967
  },
933
968
  {
934
969
  key: "USER_REASSIGN_ADMIN",
935
- label: "Admin-Rolle \xFCbertragen",
970
+ label: "Admin role transferred",
936
971
  severity: "high"
937
972
  },
938
973
  {
939
974
  key: "PLATFORM_EMAIL_PROVIDER_CREATE",
940
- label: "Plattform-Absender angelegt",
975
+ label: "Platform sender created",
941
976
  severity: "medium"
942
977
  },
943
978
  {
944
979
  key: "PLATFORM_EMAIL_PROVIDER_UPDATE",
945
- label: "Plattform-Absender ge\xE4ndert",
980
+ label: "Platform sender changed",
946
981
  severity: "medium"
947
982
  },
948
983
  {
949
984
  key: "PLATFORM_EMAIL_PROVIDER_DELETE",
950
- label: "Plattform-Absender gel\xF6scht",
985
+ label: "Platform sender deleted",
951
986
  severity: "medium"
952
987
  },
953
988
  {
954
989
  key: "PLATFORM_EMAIL_PROVIDER_TEST",
955
- label: "Plattform-Absender Test-Versand",
990
+ label: "Platform sender test send",
956
991
  severity: "low"
957
992
  },
958
993
  {
959
994
  key: "EMAIL_HISTORY_DELETE",
960
- label: "Plattform-E-Mail aus Verlauf entfernt",
995
+ label: "Platform email removed from history",
961
996
  severity: "medium"
962
997
  },
963
998
  {
964
999
  key: "EMAIL_HISTORY_RESEND",
965
- label: "Plattform-E-Mail erneut versendet",
1000
+ label: "Platform email resent",
966
1001
  severity: "low"
967
1002
  }
968
1003
  ]
@@ -1081,7 +1116,7 @@ var AdminManifestModule = class _AdminManifestModule {
1081
1116
  ];
1082
1117
  if (includeManifestController) {
1083
1118
  if (options.guards === void 0) {
1084
- throw new Error("AdminManifestModule.forRoot: `guards` ist Pflicht, wenn `includeManifestController` aktiv ist (Default `true`). Setze `guards: [JwtAuthGuard, SuperAdminGuard]` (oder analog) \u2014 oder `guards: []`, um den Endpoint absichtlich auth-frei zu betreiben. Alternative: `includeManifestController: false` und einen eigenen Wrapping-Controller registrieren.");
1119
+ throw new Error("AdminManifestModule.forRoot: `guards` is required when `includeManifestController` is enabled (default `true`). Set `guards: [JwtAuthGuard, SuperAdminGuard]` (or equivalent) \u2014 or `guards: []` to deliberately leave the endpoint unauthenticated. Alternative: `includeManifestController: false` plus registering your own wrapping controller.");
1085
1120
  }
1086
1121
  controllers.push(buildManifestController(options.guards, options.reloadGuards ?? []));
1087
1122
  }
@@ -1318,6 +1353,7 @@ AdminStatsModule = _ts_decorate12([
1318
1353
  // src/admin/admin-resources.module.ts
1319
1354
  var import_common14 = require("@nestjs/common");
1320
1355
  var import_class_validator = require("class-validator");
1356
+ var import_types4 = require("@saasicat/types");
1321
1357
  function _ts_decorate13(decorators, target, key, desc) {
1322
1358
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1323
1359
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1363,7 +1399,15 @@ var AdminResourcesService = class {
1363
1399
  }
1364
1400
  async getTenantDetail(slug) {
1365
1401
  const tenant = await this.resources.getTenantDetail(slug);
1366
- if (!tenant) throw new import_common14.NotFoundException(`Tenant ${slug} not found`);
1402
+ if (!tenant) {
1403
+ throw new import_common14.NotFoundException({
1404
+ code: import_types4.BILLING_ERROR_CODES.TENANT_NOT_FOUND,
1405
+ message: `Tenant ${slug} not found`,
1406
+ params: {
1407
+ slug
1408
+ }
1409
+ });
1410
+ }
1367
1411
  return tenant;
1368
1412
  }
1369
1413
  listUsers(filter) {
@@ -1389,7 +1433,15 @@ var AdminResourcesService = class {
1389
1433
  }
1390
1434
  async setTenantActive(slug, active, subscriptionStatus, actor, audit) {
1391
1435
  const result = await this.resources.setTenantActive(slug, active, subscriptionStatus);
1392
- if (!result) throw new import_common14.NotFoundException(`Tenant ${slug} not found`);
1436
+ if (!result) {
1437
+ throw new import_common14.NotFoundException({
1438
+ code: import_types4.BILLING_ERROR_CODES.TENANT_NOT_FOUND,
1439
+ message: `Tenant ${slug} not found`,
1440
+ params: {
1441
+ slug
1442
+ }
1443
+ });
1444
+ }
1393
1445
  await this.audit.log({
1394
1446
  actor,
1395
1447
  entity: "Tenant",
@@ -1631,8 +1683,8 @@ __export(billing_exports, {
1631
1683
  buildPlanCatalogFromSnapshot: () => buildPlanCatalogFromSnapshot,
1632
1684
  buildPlanCatalogImporterController: () => buildPlanCatalogImporterController,
1633
1685
  buildTenantSubscriptionBundlesController: () => buildTenantSubscriptionBundlesController,
1634
- classifyBundleVersionDiff: () => import_types.classifyBundleVersionDiff,
1635
- classifyPlanDiff: () => import_types.classifyPlanDiff,
1686
+ classifyBundleVersionDiff: () => import_types5.classifyBundleVersionDiff,
1687
+ classifyPlanDiff: () => import_types5.classifyPlanDiff,
1636
1688
  clearPendingPlanVersionFields: () => clearPendingPlanVersionFields,
1637
1689
  computeCarriedTrialEndsAt: () => computeCarriedTrialEndsAt,
1638
1690
  computeNextPeriod: () => computeNextPeriod,
@@ -1693,7 +1745,7 @@ function periodEndWithMinLead(startedAt, cycle, now, minLeadDays = 42) {
1693
1745
  __name(periodEndWithMinLead, "periodEndWithMinLead");
1694
1746
 
1695
1747
  // src/billing/version-diff.ts
1696
- var import_types = require("@saasicat/types");
1748
+ var import_types5 = require("@saasicat/types");
1697
1749
 
1698
1750
  // src/billing/version-publish.ts
1699
1751
  var PublishValidationError = class extends Error {
@@ -1709,35 +1761,35 @@ var PublishValidationError = class extends Error {
1709
1761
  function assertChangeNote(changeNote) {
1710
1762
  const trimmed = (changeNote ?? "").trim();
1711
1763
  if (trimmed.length === 0) {
1712
- throw new PublishValidationError("CHANGE_NOTE_REQUIRED", "changeNote ist Pflicht beim Ver\xF6ffentlichen einer Version.");
1764
+ throw new PublishValidationError("CHANGE_NOTE_REQUIRED", "changeNote is required when publishing a version.");
1713
1765
  }
1714
1766
  return trimmed;
1715
1767
  }
1716
1768
  __name(assertChangeNote, "assertChangeNote");
1717
1769
  function assertDraftPublishable(draft, draftId) {
1718
1770
  if (!draft) {
1719
- throw new PublishValidationError("NOT_FOUND", `PlanVersion ${draftId} nicht gefunden`);
1771
+ throw new PublishValidationError("NOT_FOUND", `PlanVersion ${draftId} not found`);
1720
1772
  }
1721
1773
  if (draft.publishedAt !== null) {
1722
- throw new PublishValidationError("ALREADY_PUBLISHED", `PlanVersion ${draftId} ist bereits ver\xF6ffentlicht.`);
1774
+ throw new PublishValidationError("ALREADY_PUBLISHED", `PlanVersion ${draftId} has already been published.`);
1723
1775
  }
1724
1776
  if (!draft.baseVersionId) {
1725
- throw new PublishValidationError("NO_BASE_VERSION", `Draft ${draftId} hat keine baseVersion \u2014 wahrscheinlich manuell angelegtes v1, das nur per Migration ver\xF6ffentlicht werden darf.`);
1777
+ throw new PublishValidationError("NO_BASE_VERSION", `Draft ${draftId} has no baseVersion \u2014 probably a manually created v1 that may only be published via a migration.`);
1726
1778
  }
1727
1779
  }
1728
1780
  __name(assertDraftPublishable, "assertDraftPublishable");
1729
1781
  function assertBaseVersionFresh(base, baseId, planContext) {
1730
1782
  if (!base) {
1731
- throw new PublishValidationError("BASE_NOT_FOUND", `baseVersion ${baseId} nicht mehr vorhanden.`);
1783
+ throw new PublishValidationError("BASE_NOT_FOUND", `baseVersion ${baseId} no longer exists.`);
1732
1784
  }
1733
1785
  if (base.supersededAt !== null) {
1734
- throw new PublishValidationError("BASE_SUPERSEDED", `Draft basiert auf einer abgel\xF6sten Version (${planContext}). Bitte den Draft gegen die aktuelle Live-Version rebasen oder neu anlegen.`);
1786
+ throw new PublishValidationError("BASE_SUPERSEDED", `The draft is based on a superseded version (${planContext}). Rebase the draft onto the current live version, or create a new one.`);
1735
1787
  }
1736
1788
  }
1737
1789
  __name(assertBaseVersionFresh, "assertBaseVersionFresh");
1738
1790
  function assertOptimisticLockHeld(updateCount, draftId) {
1739
1791
  if (updateCount !== 1) {
1740
- throw new PublishValidationError("OPTIMISTIC_LOCK_CONFLICT", `Draft ${draftId} wurde zwischenzeitlich von einem anderen Admin ver\xF6ffentlicht.`);
1792
+ throw new PublishValidationError("OPTIMISTIC_LOCK_CONFLICT", `Draft ${draftId} has meanwhile been published by another admin.`);
1741
1793
  }
1742
1794
  }
1743
1795
  __name(assertOptimisticLockHeld, "assertOptimisticLockHeld");
@@ -1788,10 +1840,11 @@ var FEATURE_GUARD_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/Featu
1788
1840
  // src/billing/feature.guard.ts
1789
1841
  var import_common18 = require("@nestjs/common");
1790
1842
  var import_core2 = require("@nestjs/core");
1791
- var import_types2 = require("@saasicat/types");
1843
+ var import_types7 = require("@saasicat/types");
1792
1844
 
1793
1845
  // src/entitlement/service.ts
1794
1846
  var import_common17 = require("@nestjs/common");
1847
+ var import_types6 = require("@saasicat/types");
1795
1848
 
1796
1849
  // src/catalog/tokens.ts
1797
1850
  var PLAN_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/PlanRepository");
@@ -1966,7 +2019,7 @@ __name(findPlan, "findPlan");
1966
2019
  function getPlanOrThrow(catalog, planId) {
1967
2020
  const plan = findPlan(catalog, planId);
1968
2021
  if (!plan) {
1969
- throw new Error(`Plan "${planId}" nicht im Catalog (${catalog.projectKey}) gefunden`);
2022
+ throw new Error(`Plan "${planId}" not found in the catalog (${catalog.projectKey})`);
1970
2023
  }
1971
2024
  return plan;
1972
2025
  }
@@ -2236,7 +2289,7 @@ function assertNoReplacesCycle(index) {
2236
2289
  ...path.slice(cycleStart),
2237
2290
  key
2238
2291
  ].join(" \u2192 ");
2239
- throw new Error(`replaces-Zyklus in den Discovery-Metadaten: ${cycle}. Feature-Ersetzungen m\xFCssen azyklisch sein \u2014 replaces-Deklarationen im Code korrigieren (#39).`);
2292
+ throw new Error(`replaces cycle in the discovery metadata: ${cycle}. Feature replacements must be acyclic \u2014 fix the replaces declarations in code (#39).`);
2240
2293
  }
2241
2294
  state.set(key, IN_PROGRESS);
2242
2295
  for (const successor of index.get(key) ?? []) {
@@ -2262,7 +2315,7 @@ var LimitExceededError = class extends Error {
2262
2315
  used;
2263
2316
  code = LIMIT_EXCEEDED;
2264
2317
  constructor(dimension, max, used) {
2265
- super(`Limit f\xFCr ${dimension} erreicht: ${used}/${max}.`), this.dimension = dimension, this.max = max, this.used = used;
2318
+ super(`Limit for ${dimension} reached: ${used}/${max}.`), this.dimension = dimension, this.max = max, this.used = used;
2266
2319
  this.name = "LimitExceededError";
2267
2320
  }
2268
2321
  };
@@ -2357,7 +2410,13 @@ var EntitlementService = class {
2357
2410
  if (cached) return cached;
2358
2411
  const sub = await this.subscriptions.findByTenantId(tenantId);
2359
2412
  if (!sub) {
2360
- throw new import_common17.NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
2413
+ throw new import_common17.NotFoundException({
2414
+ code: import_types6.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
2415
+ message: `No subscription for tenant ${tenantId}`,
2416
+ params: {
2417
+ tenantId
2418
+ }
2419
+ });
2361
2420
  }
2362
2421
  const limits = await this.deriveLimits(sub, now);
2363
2422
  this.writeCache(tenantId, limits, now.getTime());
@@ -2430,7 +2489,7 @@ var EntitlementService = class {
2430
2489
  return Promise.all(active.map(async (booking) => {
2431
2490
  const bv = await this.bundles.findVersionById(booking.bundleVersionId, tx);
2432
2491
  if (!bv) {
2433
- throw new Error(`BundleVersion '${booking.bundleVersionId}' aus aktiver SubscriptionBundle nicht gefunden`);
2492
+ throw new Error(`BundleVersion '${booking.bundleVersionId}' from an active subscription bundle not found`);
2434
2493
  }
2435
2494
  return {
2436
2495
  bundleKey: bv.bundleKey,
@@ -2452,12 +2511,21 @@ var EntitlementService = class {
2452
2511
  return this.tx.run(async (tx) => {
2453
2512
  const sub = await this.subscriptions.findByTenantIdLocked(input.tenantId, tx);
2454
2513
  if (!sub) {
2455
- throw new import_common17.NotFoundException(`Keine Subscription f\xFCr Tenant ${input.tenantId}`);
2514
+ throw new import_common17.NotFoundException({
2515
+ code: import_types6.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
2516
+ message: `No subscription for tenant ${input.tenantId}`,
2517
+ params: {
2518
+ tenantId: input.tenantId
2519
+ }
2520
+ });
2456
2521
  }
2457
2522
  const limits = await this.deriveLimits(sub, now, tx);
2458
2523
  const max = limits.quotas[input.dimension];
2459
2524
  if (max === void 0) {
2460
- throw new Error(`Quota-Dimension "${input.dimension}" nicht im PlanCatalog (${this.catalog.projectKey}).`);
2525
+ throw new import_common17.InternalServerErrorException(codedError(import_types6.BILLING_ERROR_CODES.QUOTA_DIMENSION_UNKNOWN, {
2526
+ dimension: input.dimension,
2527
+ projectKey: this.catalog.projectKey
2528
+ }));
2461
2529
  }
2462
2530
  const used = await input.currentUsage(tx, input.tenantId);
2463
2531
  if (max !== -1 && used + delta > max) {
@@ -2478,7 +2546,15 @@ var EntitlementService = class {
2478
2546
  async findActivePlanVersionOrFallback(planId, asOf, tx) {
2479
2547
  const v = this.planVersions.findActive ? await this.planVersions.findActive(planId, asOf, tx) : await this.planVersions.findLatestLive(planId, tx);
2480
2548
  if (!v) {
2481
- throw new import_common17.NotFoundException(`Keine zu ${asOf.toISOString().slice(0, 10)} aktive PlanVersion f\xFCr ${planId} \u2014 weder validFrom-window erf\xFCllt noch latest-live verf\xFCgbar.`);
2549
+ const asOfDate = asOf.toISOString().slice(0, 10);
2550
+ throw new import_common17.NotFoundException({
2551
+ code: import_types6.BILLING_ERROR_CODES.NO_ACTIVE_PLAN_VERSION,
2552
+ message: `No plan version for ${planId} active as of ${asOfDate} \u2014 neither the validFrom window is satisfied nor is a latest-live version available.`,
2553
+ params: {
2554
+ planId,
2555
+ asOf: asOfDate
2556
+ }
2557
+ });
2482
2558
  }
2483
2559
  return v;
2484
2560
  }
@@ -2581,11 +2657,21 @@ var FeatureGuard = class _FeatureGuard {
2581
2657
  if (!required || required.length === 0) return true;
2582
2658
  const request = context.switchToHttp().getRequest();
2583
2659
  const user = request.user;
2584
- if (!user) throw new import_common18.ForbiddenException("Nicht authentifiziert");
2660
+ if (!user) {
2661
+ throw new import_common18.ForbiddenException({
2662
+ code: import_types7.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
2663
+ message: "Not authenticated"
2664
+ });
2665
+ }
2585
2666
  const role = this.config?.userRoleResolver ? this.config.userRoleResolver(user) : user.role ?? user.platformRole;
2586
2667
  if (role === "SUPER_ADMIN") return true;
2587
2668
  const tenantId = this.config?.tenantIdResolver ? this.config.tenantIdResolver(request) : request.tenantId ?? user.tenantId;
2588
- if (!tenantId) throw new import_common18.ForbiddenException("Kein Mandant zugeordnet");
2669
+ if (!tenantId) {
2670
+ throw new import_common18.ForbiddenException({
2671
+ code: import_types7.AUTH_ERROR_CODES.NO_TENANT_ASSIGNED,
2672
+ message: "No tenant assigned"
2673
+ });
2674
+ }
2589
2675
  const compute = /* @__PURE__ */ __name(() => this.entitlements.computeLimits(tenantId), "compute");
2590
2676
  const limits = this.config?.tenantContextRunner ? await this.config.tenantContextRunner(tenantId, compute) : await compute();
2591
2677
  const allowed = required.some((f) => limits.features.has(f));
@@ -2595,25 +2681,27 @@ var FeatureGuard = class _FeatureGuard {
2595
2681
  return true;
2596
2682
  }
2597
2683
  /**
2598
- * Upsell response (#36): with a registered `UpsellOfferResolver` the 403
2599
- * becomes machine-readable (`FeatureNotLicensedBody`), so consumer UIs can
2600
- * render a purchase offer. Without a resolver the previous plain 403
2601
- * remainsno breaking change.
2684
+ * Upsell response (#36): the 403 body is always the full
2685
+ * `FeatureNotLicensedBody`, so a consumer that matches on `code` can read
2686
+ * `featureKey`/`offers` unconditionally. Without a registered
2687
+ * `UpsellOfferResolver` there is simply nothing to offer `offers` is
2688
+ * empty, the same degradation a failing resolver produces.
2602
2689
  *
2603
2690
  * Deliberately 403 + `code` field instead of 402 — rationale in
2604
2691
  * `@saasicat/types` upsell.types.ts (402 is reserved / inconsistently
2605
2692
  * supported; SPA interceptors must not treat the 403 as an auth error, the
2606
2693
  * distinction runs through `code`).
2694
+ *
2695
+ * The message must keep starting with the word `Feature` — consumers
2696
+ * classify the response by that prefix.
2607
2697
  */
2608
2698
  async buildNotLicensedException(required, tenantId) {
2609
- const message = `Feature ${required.join(" / ")} nicht im aktuellen Paket enthalten.`;
2610
- if (!this.upsellResolver) return new import_common18.ForbiddenException(message);
2611
2699
  const body = {
2612
- code: import_types2.FEATURE_NOT_LICENSED,
2700
+ code: import_types7.FEATURE_NOT_LICENSED,
2613
2701
  featureKey: required[0],
2614
2702
  featureKeys: required,
2615
- offers: await this.resolveOffersSafe(required, tenantId),
2616
- message
2703
+ offers: this.upsellResolver ? await this.resolveOffersSafe(required, tenantId) : [],
2704
+ message: `Feature ${required.join(" / ")} is not included in the current plan.`
2617
2705
  };
2618
2706
  return new import_common18.ForbiddenException(body);
2619
2707
  }
@@ -2625,7 +2713,7 @@ var FeatureGuard = class _FeatureGuard {
2625
2713
  try {
2626
2714
  return await this.upsellResolver.resolveOffers(required, tenantId);
2627
2715
  } catch (error) {
2628
- this.logger.warn(`UpsellOfferResolver fehlgeschlagen f\xFCr [${required.join(", ")}]: ${String(error)}`);
2716
+ this.logger.warn(`UpsellOfferResolver failed for [${required.join(", ")}]: ${String(error)}`);
2629
2717
  return [];
2630
2718
  }
2631
2719
  }
@@ -2649,7 +2737,7 @@ FeatureGuard = _ts_decorate16([
2649
2737
 
2650
2738
  // src/billing/catalog-bundle-upsell-resolver.ts
2651
2739
  var import_common19 = require("@nestjs/common");
2652
- var import_types3 = require("@saasicat/types");
2740
+ var import_types8 = require("@saasicat/types");
2653
2741
 
2654
2742
  // src/billing/public-catalog.tokens.ts
2655
2743
  var PUBLIC_CATALOG_PROJECT_KEY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/PublicCatalogProjectKey");
@@ -2719,7 +2807,7 @@ var CatalogBundleUpsellResolver = class {
2719
2807
  const rows = await this.catalogEntryRepo.listFeatures({
2720
2808
  projectKey: this.projectKey
2721
2809
  });
2722
- return (0, import_types3.collectUnsatisfiedRequires)(featureKeys, (0, import_types3.buildFeatureRequiresIndex)(rows));
2810
+ return (0, import_types8.collectUnsatisfiedRequires)(featureKeys, (0, import_types8.buildFeatureRequiresIndex)(rows));
2723
2811
  }
2724
2812
  /** Only published-and-live AND marketed — non-marketed bundles are not an offer. */
2725
2813
  async listLiveMarketedBundles() {
@@ -2778,6 +2866,7 @@ __name(compareRankedOffers, "compareRankedOffers");
2778
2866
  // src/billing/limit-exceeded.filter.ts
2779
2867
  var import_common20 = require("@nestjs/common");
2780
2868
  var import_core3 = require("@nestjs/core");
2869
+ var import_types9 = require("@saasicat/types");
2781
2870
  function _ts_decorate18(decorators, target, key, desc) {
2782
2871
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2783
2872
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2798,11 +2887,14 @@ var LimitExceededFilter = class _LimitExceededFilter extends import_core3.BaseEx
2798
2887
  const ctx = host.switchToHttp();
2799
2888
  const response = ctx.getResponse();
2800
2889
  const request = ctx.getRequest();
2801
- this.logger.warn(`Limit ${exception.dimension} \xFCberschritten: ${exception.used}/${exception.max} bei ${request.method ?? "?"} ${request.url ?? ""}`);
2890
+ this.logger.warn(`Limit ${exception.dimension} exceeded: ${exception.used}/${exception.max} on ${request.method ?? "?"} ${request.url ?? ""}`);
2802
2891
  response.status(import_common20.HttpStatus.PAYMENT_REQUIRED).send({
2803
2892
  statusCode: import_common20.HttpStatus.PAYMENT_REQUIRED,
2804
2893
  error: "PaymentRequired",
2805
- reason: "LIMIT_EXCEEDED",
2894
+ code: import_types9.BILLING_ERROR_CODES.LIMIT_EXCEEDED,
2895
+ // Deprecated: superseded by `code`, kept until consumers have
2896
+ // migrated. Same value, only under the older field name.
2897
+ reason: import_types9.BILLING_ERROR_CODES.LIMIT_EXCEEDED,
2806
2898
  dimension: exception.dimension,
2807
2899
  used: exception.used,
2808
2900
  max: exception.max,
@@ -2828,8 +2920,8 @@ var PlanCatalogValidationError = class extends Error {
2828
2920
  source;
2829
2921
  errors;
2830
2922
  constructor(source, errors) {
2831
- const messages = errors.map((e) => `${e.instancePath || "/"}: ${e.message ?? "unbekannt"}`).join("\n ");
2832
- super(`Plan-Catalog-Validierung fehlgeschlagen f\xFCr ${source}:
2923
+ const messages = errors.map((e) => `${e.instancePath || "/"}: ${e.message ?? "unknown"}`).join("\n ");
2924
+ super(`Plan catalog validation failed for ${source}:
2833
2925
  ${messages}`), this.source = source, this.errors = errors;
2834
2926
  this.name = "PlanCatalogValidationError";
2835
2927
  }
@@ -2846,7 +2938,7 @@ __name(loadPlanCatalogFromFile, "loadPlanCatalogFromFile");
2846
2938
  function loadPlanCatalogFromString(yamlContent, opts) {
2847
2939
  const parsed = import_js_yaml.default.load(yamlContent);
2848
2940
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2849
- throw new Error(`YAML-Inhalt von ${opts.source} ist kein Objekt`);
2941
+ throw new Error(`The YAML content of ${opts.source} is not an object`);
2850
2942
  }
2851
2943
  const ajv = new import__.default({
2852
2944
  strict: false,
@@ -2870,19 +2962,19 @@ function validateConsistency(catalog, source) {
2870
2962
  const planIds = /* @__PURE__ */ new Set();
2871
2963
  for (const plan of catalog.plans ?? []) {
2872
2964
  if (planIds.has(plan.id)) {
2873
- errors.push(`plans[].id: Doppelte Plan-ID "${plan.id}"`);
2965
+ errors.push(`plans[].id: duplicate plan id "${plan.id}"`);
2874
2966
  }
2875
2967
  planIds.add(plan.id);
2876
2968
  if (catalog.features) {
2877
2969
  for (const fk of plan.features) {
2878
2970
  if (!declaredFeatureKeys.has(fk)) {
2879
- errors.push(`plans[id=${plan.id}].features: Unbekannter featureKey "${fk}" \u2014 nicht in catalog.features deklariert`);
2971
+ errors.push(`plans[id=${plan.id}].features: Unknown featureKey "${fk}" \u2014 not declared in catalog.features`);
2880
2972
  }
2881
2973
  }
2882
2974
  }
2883
2975
  }
2884
2976
  if (errors.length > 0) {
2885
- const err = new Error(`Plan-Catalog-Konsistenz-Fehler in ${source}:
2977
+ const err = new Error(`Plan catalog consistency error in ${source}:
2886
2978
  ${errors.join("\n ")}`);
2887
2979
  err.name = "PlanCatalogConsistencyError";
2888
2980
  throw err;
@@ -2965,7 +3057,7 @@ var PlanCatalogImporterService = class {
2965
3057
  if (planResult.created) report.plansCreated++;
2966
3058
  else report.plansSkipped++;
2967
3059
  if (plan.monthlyNet === null || plan.monthlyNet === void 0) {
2968
- report.warnings.push(`Plan '${plan.id}' hat keinen monthlyNet \u2014 \xFCbersprungen (auf-Anfrage-Plans brauchen manuelle PlanVersion).`);
3060
+ report.warnings.push(`Plan '${plan.id}' has no monthlyNet \u2014 skipped (on-request plans need a manual plan version).`);
2969
3061
  continue;
2970
3062
  }
2971
3063
  const versionResult = await this.sink.upsertPlanVersion({
@@ -3114,7 +3206,7 @@ var FEATURE_UI_REGISTRY_TOKEN2 = /* @__PURE__ */ Symbol.for("saas-platform/Featu
3114
3206
 
3115
3207
  // src/billing/public-catalog.controller.ts
3116
3208
  var import_common24 = require("@nestjs/common");
3117
- var import_types4 = require("@saasicat/types");
3209
+ var import_types10 = require("@saasicat/types");
3118
3210
  function _ts_decorate22(decorators, target, key, desc) {
3119
3211
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3120
3212
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -3234,7 +3326,7 @@ var PublicCatalogController = class {
3234
3326
  const rows = await this.catalogEntryRepo.listFeatures({
3235
3327
  projectKey: this.projectKey
3236
3328
  });
3237
- return (0, import_types4.buildFeatureRequiresIndex)(rows);
3329
+ return (0, import_types10.buildFeatureRequiresIndex)(rows);
3238
3330
  } catch {
3239
3331
  return /* @__PURE__ */ new Map();
3240
3332
  }
@@ -3274,7 +3366,7 @@ var PublicCatalogController = class {
3274
3366
  quotas: live.quotas,
3275
3367
  monthlyNet: live.monthlyNet,
3276
3368
  yearlyNet: live.yearlyNet,
3277
- requiresFeatures: (0, import_types4.collectUnsatisfiedRequires)(features, requiresIndex),
3369
+ requiresFeatures: (0, import_types10.collectUnsatisfiedRequires)(features, requiresIndex),
3278
3370
  marketing
3279
3371
  };
3280
3372
  }
@@ -3398,6 +3490,7 @@ var PENDING_PLAN_QUERY_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/Pe
3398
3490
 
3399
3491
  // src/billing/composed-tenant-auth.guard.ts
3400
3492
  var import_common26 = require("@nestjs/common");
3493
+ var import_types11 = require("@saasicat/types");
3401
3494
  function _ts_decorate24(decorators, target, key, desc) {
3402
3495
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3403
3496
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -3425,7 +3518,10 @@ var ComposedTenantAuthGuard = class {
3425
3518
  }
3426
3519
  async canActivate(context) {
3427
3520
  if (!this.guards || this.guards.length === 0) {
3428
- throw new import_common26.ForbiddenException("TenantBillingModule.forRoot.authGuards ist nicht konfiguriert.");
3521
+ throw new import_common26.ForbiddenException({
3522
+ code: import_types11.AUTH_ERROR_CODES.AUTH_GUARDS_NOT_CONFIGURED,
3523
+ message: "TenantBillingModule.forRoot.authGuards is not configured."
3524
+ });
3429
3525
  }
3430
3526
  for (const guard of this.guards) {
3431
3527
  const result = await Promise.resolve(guard.canActivate(context));
@@ -3446,6 +3542,7 @@ ComposedTenantAuthGuard = _ts_decorate24([
3446
3542
 
3447
3543
  // src/billing/tenant-admin.guard.ts
3448
3544
  var import_common27 = require("@nestjs/common");
3545
+ var import_types12 = require("@saasicat/types");
3449
3546
  function _ts_decorate25(decorators, target, key, desc) {
3450
3547
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3451
3548
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -3464,11 +3561,17 @@ var TenantAdminGuard = class {
3464
3561
  canActivate(context) {
3465
3562
  const request = context.switchToHttp().getRequest();
3466
3563
  if (!request.user) {
3467
- throw new import_common27.ForbiddenException("Nicht authentifiziert");
3564
+ throw new import_common27.ForbiddenException({
3565
+ code: import_types12.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
3566
+ message: "Not authenticated"
3567
+ });
3468
3568
  }
3469
3569
  const role = request.user.platformRole ?? request.user.role;
3470
3570
  if (!role || !ADMIN_ROLES.has(role)) {
3471
- throw new import_common27.ForbiddenException("Diese Aktion erfordert die TENANT_ADMIN-Rolle.");
3571
+ throw new import_common27.ForbiddenException({
3572
+ code: import_types12.AUTH_ERROR_CODES.TENANT_ADMIN_REQUIRED,
3573
+ message: "This action requires the TENANT_ADMIN role."
3574
+ });
3472
3575
  }
3473
3576
  return true;
3474
3577
  }
@@ -3506,6 +3609,7 @@ __name(round22, "round2");
3506
3609
 
3507
3610
  // src/billing/plan-change-preview.service.ts
3508
3611
  var import_common29 = require("@nestjs/common");
3612
+ var import_types13 = require("@saasicat/types");
3509
3613
 
3510
3614
  // src/entitlement/index.ts
3511
3615
  var entitlement_exports = {};
@@ -3634,11 +3738,24 @@ var PlanChangePreviewService = class {
3634
3738
  async preview(tenantId, targetPlan, targetCycle, now = /* @__PURE__ */ new Date()) {
3635
3739
  const sub = await this.subscriptions.findForTenant(tenantId);
3636
3740
  if (!sub) {
3637
- throw new import_common29.NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
3741
+ throw new import_common29.NotFoundException({
3742
+ code: import_types13.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
3743
+ message: `No subscription for tenant ${tenantId}`,
3744
+ params: {
3745
+ tenantId
3746
+ }
3747
+ });
3638
3748
  }
3639
3749
  const targetPlanDef = findPlan(this.catalog, targetPlan);
3640
3750
  if (!targetPlanDef) {
3641
- throw new import_common29.NotFoundException(`Plan "${targetPlan}" nicht im Catalog (${this.catalog.projectKey})`);
3751
+ throw new import_common29.NotFoundException({
3752
+ code: import_types13.BILLING_ERROR_CODES.PLAN_NOT_IN_CATALOG,
3753
+ message: `Plan "${targetPlan}" is not in the catalog (${this.catalog.projectKey})`,
3754
+ params: {
3755
+ planKey: targetPlan,
3756
+ projectKey: this.catalog.projectKey
3757
+ }
3758
+ });
3642
3759
  }
3643
3760
  const ctx = {
3644
3761
  currentPeriodStart: sub.currentPeriodStart,
@@ -3703,13 +3820,13 @@ var PlanChangePreviewService = class {
3703
3820
  if (blockedTargets.includes(targetPlan)) {
3704
3821
  blockers.push({
3705
3822
  code: `${targetPlan}_NOT_SELF_SERVICE`,
3706
- message: `${targetSnap.name} wird nur per Sondervertrag aktiviert. Bitte den Vertragsbetreuer kontaktieren.`
3823
+ message: `${targetSnap.name} is only activated via a special contract. Please contact the contract manager.`
3707
3824
  });
3708
3825
  }
3709
3826
  if (blockedSources.includes(sub.plan)) {
3710
3827
  blockers.push({
3711
3828
  code: `${sub.plan}_LOCKED`,
3712
- message: `Aktiver ${currentSnap.name}-Sondervertrag \u2014 bitte den Vertragsbetreuer f\xFCr Plan-Wechsel kontaktieren.`
3829
+ message: `Active ${currentSnap.name} special contract \u2014 please contact the contract manager to change plans.`
3713
3830
  });
3714
3831
  }
3715
3832
  for (const [key, row] of Object.entries(limitsCheck)) {
@@ -3717,19 +3834,19 @@ var PlanChangePreviewService = class {
3717
3834
  const usedDisplay = isFloatQuota(key) ? row.used.toFixed(1) : row.used.toString();
3718
3835
  blockers.push({
3719
3836
  code: `${key.toUpperCase()}_OVER_TARGET`,
3720
- message: `Aktueller Verbrauch ${usedDisplay} \xFCberschreitet das Ziel-Limit ${row.targetMax} (${key}) im ${targetSnap.name}-Paket. Bitte Verbrauch reduzieren.`
3837
+ message: `Current usage ${usedDisplay} exceeds the target limit ${row.targetMax} (${key}) in the ${targetSnap.name} plan. Please reduce usage first.`
3721
3838
  });
3722
3839
  }
3723
3840
  if (featuresLost.length > 0) {
3724
3841
  warnings.push({
3725
3842
  code: "FEATURES_LOST",
3726
- message: `Mit dem Wechsel verlierst du Zugriff auf ${featuresLost.length} Feature${featuresLost.length === 1 ? "" : "s"}. Bestehende Daten bleiben erhalten und werden nicht gel\xF6scht \u2014 du kannst sie wieder freischalten, wenn du erneut upgradest.`
3843
+ message: `Switching means losing access to ${featuresLost.length} feature${featuresLost.length === 1 ? "" : "s"}. Existing data is retained and never deleted \u2014 upgrading again unlocks it.`
3727
3844
  });
3728
3845
  }
3729
3846
  if (changeType === "NOOP") {
3730
3847
  warnings.push({
3731
3848
  code: "NO_CHANGE",
3732
- message: "Ziel-Paket und Abrechnungszyklus stimmen mit dem aktuellen Stand \xFCberein."
3849
+ message: "Target plan and billing cycle already match the current state."
3733
3850
  });
3734
3851
  }
3735
3852
  const projectedTrialEndsAt = this.trialProjection && sub.status === "TRIAL" ? await this.trialProjection.projectTrialEndsAt({
@@ -3887,13 +4004,13 @@ var PendingPlanMaterializationService = class _PendingPlanMaterializationService
3887
4004
  this.entitlements.invalidateTenant(change.tenantId);
3888
4005
  applied += 1;
3889
4006
  } catch (err) {
3890
- this.logger.error(`Pending-Plan-Materialisierung fehlgeschlagen (tenant ${change.tenantId}): ${String(err)}`);
4007
+ this.logger.error(`Pending plan materialisation failed (tenant ${change.tenantId}): ${String(err)}`);
3891
4008
  continue;
3892
4009
  }
3893
4010
  await this.tryFreeze(change.tenantId, change.pendingPlan, cycle, now);
3894
4011
  }
3895
4012
  if (applied > 0) {
3896
- this.logger.log(`Pending-Plan-Materialisierung: ${applied} geplante(r) Plan-Wechsel angewandt.`);
4013
+ this.logger.log(`Pending plan materialisation: applied ${applied} scheduled plan change(s).`);
3897
4014
  }
3898
4015
  return {
3899
4016
  applied
@@ -3904,7 +4021,7 @@ var PendingPlanMaterializationService = class _PendingPlanMaterializationService
3904
4021
  try {
3905
4022
  await this.contractFreeze.freezeOnPlanChange(tenantId, plan, cycle, now);
3906
4023
  } catch (err) {
3907
- this.logger.error(`Contract-Freeze nach Materialisierung fehlgeschlagen (tenant ${tenantId}): ${String(err)}`);
4024
+ this.logger.error(`Contract freeze after materialisation failed (tenant ${tenantId}): ${String(err)}`);
3908
4025
  }
3909
4026
  }
3910
4027
  };
@@ -3978,7 +4095,7 @@ function createDiscountLineItem(input, discountNet) {
3978
4095
  kind: "discount",
3979
4096
  sourceKey: promoCode?.code ?? firstPromotion?.id ?? "price-discount",
3980
4097
  sourceVersionId: null,
3981
- titleSnapshot: promoCode?.label ?? firstPromotion?.label ?? "Rabatt",
4098
+ titleSnapshot: promoCode?.label ?? firstPromotion?.label ?? "Discount",
3982
4099
  descriptionSnapshot: promoCode ? `Promo-Code ${promoCode.code}` : null,
3983
4100
  quantity: 1,
3984
4101
  unit: null,
@@ -4021,6 +4138,7 @@ function cloneJsonValue(value) {
4021
4138
  __name(cloneJsonValue, "cloneJsonValue");
4022
4139
 
4023
4140
  // src/subscription-contract/subscription-contract.service.ts
4141
+ var import_types14 = require("@saasicat/types");
4024
4142
  function _ts_decorate29(decorators, target, key, desc) {
4025
4143
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4026
4144
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -4052,7 +4170,13 @@ var SubscriptionContractService = class {
4052
4170
  async getById(contractId) {
4053
4171
  const row = await this.repo.findById(contractId);
4054
4172
  if (!row) {
4055
- throw new import_common31.NotFoundException(`SubscriptionContract '${contractId}' nicht gefunden`);
4173
+ throw new import_common31.NotFoundException({
4174
+ code: import_types14.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_NOT_FOUND,
4175
+ message: `SubscriptionContract '${contractId}' not found`,
4176
+ params: {
4177
+ contractId
4178
+ }
4179
+ });
4056
4180
  }
4057
4181
  return row;
4058
4182
  }
@@ -4062,7 +4186,14 @@ var SubscriptionContractService = class {
4062
4186
  async getActiveInvoiceSnapshotForTenant(tenantId, asOf = /* @__PURE__ */ new Date()) {
4063
4187
  const contract = await this.repo.findActiveByTenantId(tenantId, asOf);
4064
4188
  if (!contract) {
4065
- throw new import_common31.NotFoundException(`Kein aktiver SubscriptionContract f\xFCr Tenant ${tenantId}`);
4189
+ throw new import_common31.NotFoundException({
4190
+ code: import_types14.CONTRACT_ERROR_CODES.NO_ACTIVE_SUBSCRIPTION_CONTRACT,
4191
+ message: `No active subscription contract for tenant ${tenantId}`,
4192
+ params: {
4193
+ tenantId,
4194
+ asOf: asOf.toISOString()
4195
+ }
4196
+ });
4066
4197
  }
4067
4198
  return subscriptionContractToInvoiceSnapshot(contract);
4068
4199
  }
@@ -4095,7 +4226,14 @@ var SubscriptionContractService = class {
4095
4226
  }
4096
4227
  createDataFromOffer(offer, options) {
4097
4228
  if (offer.status !== "consumed") {
4098
- throw new import_common31.ConflictException(`CheckoutOffer '${offer.id}' muss vor Contract-Erstellung consumed sein`);
4229
+ throw new import_common31.ConflictException({
4230
+ code: import_types14.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_NOT_CONSUMED,
4231
+ message: `CheckoutOffer '${offer.id}' must be consumed before the contract is created`,
4232
+ params: {
4233
+ offerId: offer.id,
4234
+ status: offer.status
4235
+ }
4236
+ });
4099
4237
  }
4100
4238
  const lineItems = this.lineItemsFromOffer(offer);
4101
4239
  return {
@@ -4128,8 +4266,8 @@ var SubscriptionContractService = class {
4128
4266
  const source = offer.lineItems ?? [];
4129
4267
  if (source.length === 0) {
4130
4268
  throw new import_common31.UnprocessableEntityException({
4131
- code: "CHECKOUT_OFFER_LINE_ITEMS_REQUIRED",
4132
- message: "Aus einem CheckoutOffer kann nur ein Contract entstehen, wenn LineItems eingefroren sind."
4269
+ code: import_types14.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_LINE_ITEMS_REQUIRED,
4270
+ message: "A checkout offer can yield only one contract, and only once its line items are frozen."
4133
4271
  });
4134
4272
  }
4135
4273
  return appendImplicitDiscountLineItem({
@@ -4167,8 +4305,11 @@ var SubscriptionContractService = class {
4167
4305
  const date = value instanceof Date ? new Date(value) : new Date(value);
4168
4306
  if (Number.isNaN(date.getTime())) {
4169
4307
  throw new import_common31.UnprocessableEntityException({
4170
- code: "SUBSCRIPTION_CONTRACT_INVALID_DATE",
4171
- message: `${field} muss ein g\xFCltiges Datum sein.`
4308
+ code: import_types14.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_DATE,
4309
+ message: `${field} must be a valid date.`,
4310
+ params: {
4311
+ field
4312
+ }
4172
4313
  });
4173
4314
  }
4174
4315
  return date;
@@ -4188,32 +4329,39 @@ var SubscriptionContractService = class {
4188
4329
  assertCreateData(data) {
4189
4330
  if (data.lineItems.length === 0) {
4190
4331
  throw new import_common31.UnprocessableEntityException({
4191
- code: "SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED",
4192
- message: "SubscriptionContract ben\xF6tigt mindestens eine LineItem."
4332
+ code: import_types14.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED,
4333
+ message: "A subscription contract requires at least one line item."
4193
4334
  });
4194
4335
  }
4195
4336
  if (data.effectiveUntil && data.effectiveUntil <= data.effectiveFrom) {
4196
4337
  throw new import_common31.UnprocessableEntityException({
4197
- code: "SUBSCRIPTION_CONTRACT_INVALID_WINDOW",
4198
- message: "effectiveUntil muss nach effectiveFrom liegen."
4338
+ code: import_types14.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_WINDOW,
4339
+ message: "effectiveUntil must be after effectiveFrom."
4199
4340
  });
4200
4341
  }
4201
4342
  const planLineItems = data.lineItems.filter((item) => item.kind === "plan");
4202
4343
  if (planLineItems.length !== 1) {
4203
4344
  throw new import_common31.UnprocessableEntityException({
4204
- code: "SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED",
4205
- message: "SubscriptionContract ben\xF6tigt genau eine Plan-Grundposition."
4345
+ code: import_types14.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED,
4346
+ message: "A subscription contract requires exactly one plan base item."
4206
4347
  });
4207
4348
  }
4208
4349
  }
4209
4350
  assertTerminable(existing, data) {
4210
4351
  if (existing.status === "terminated" || existing.status === "superseded") {
4211
- throw new import_common31.ConflictException(`SubscriptionContract '${existing.id}' ist bereits geschlossen`);
4352
+ throw new import_common31.ConflictException({
4353
+ code: import_types14.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_ALREADY_CLOSED,
4354
+ message: `SubscriptionContract '${existing.id}' is already closed`,
4355
+ params: {
4356
+ contractId: existing.id,
4357
+ status: existing.status
4358
+ }
4359
+ });
4212
4360
  }
4213
4361
  if (data.effectiveUntil <= existing.effectiveFrom) {
4214
4362
  throw new import_common31.UnprocessableEntityException({
4215
- code: "SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START",
4216
- message: "effectiveUntil muss nach effectiveFrom des Contracts liegen."
4363
+ code: import_types14.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START,
4364
+ message: "effectiveUntil must be after the effectiveFrom of the contract."
4217
4365
  });
4218
4366
  }
4219
4367
  }
@@ -4505,14 +4653,14 @@ var PreviewPlanChangeDto = class {
4505
4653
  _ts_decorate32([
4506
4654
  (0, import_class_validator3.IsString)(),
4507
4655
  (0, import_class_validator3.Matches)(PLAN_OR_CYCLE_PATTERN, {
4508
- message: "plan muss SCREAMING_SNAKE_CASE sein"
4656
+ message: "plan must be SCREAMING_SNAKE_CASE"
4509
4657
  }),
4510
4658
  _ts_metadata22("design:type", String)
4511
4659
  ], PreviewPlanChangeDto.prototype, "plan", void 0);
4512
4660
  _ts_decorate32([
4513
4661
  (0, import_class_validator3.IsString)(),
4514
4662
  (0, import_class_validator3.Matches)(PLAN_OR_CYCLE_PATTERN, {
4515
- message: "billingCycle muss SCREAMING_SNAKE_CASE sein (z. B. MONTHLY, YEARLY)"
4663
+ message: "billingCycle must be SCREAMING_SNAKE_CASE (e.g. MONTHLY, YEARLY)"
4516
4664
  }),
4517
4665
  _ts_metadata22("design:type", String)
4518
4666
  ], PreviewPlanChangeDto.prototype, "billingCycle", void 0);
@@ -4576,14 +4724,14 @@ var CompleteOnboardingSubscriptionDto = class {
4576
4724
  _ts_decorate33([
4577
4725
  (0, import_class_validator4.IsString)(),
4578
4726
  (0, import_class_validator4.Matches)(PLAN_OR_CYCLE_PATTERN2, {
4579
- message: "plan muss SCREAMING_SNAKE_CASE sein"
4727
+ message: "plan must be SCREAMING_SNAKE_CASE"
4580
4728
  }),
4581
4729
  _ts_metadata23("design:type", String)
4582
4730
  ], CompleteOnboardingSubscriptionDto.prototype, "plan", void 0);
4583
4731
  _ts_decorate33([
4584
4732
  (0, import_class_validator4.IsString)(),
4585
4733
  (0, import_class_validator4.Matches)(PLAN_OR_CYCLE_PATTERN2, {
4586
- message: "billingCycle muss SCREAMING_SNAKE_CASE sein (z. B. MONTHLY, YEARLY)"
4734
+ message: "billingCycle must be SCREAMING_SNAKE_CASE (e.g. MONTHLY, YEARLY)"
4587
4735
  }),
4588
4736
  _ts_metadata23("design:type", String)
4589
4737
  ], CompleteOnboardingSubscriptionDto.prototype, "billingCycle", void 0);
@@ -4591,7 +4739,7 @@ _ts_decorate33([
4591
4739
  (0, import_class_validator4.IsOptional)(),
4592
4740
  (0, import_class_validator4.IsString)(),
4593
4741
  (0, import_class_validator4.Matches)(PROMO_CODE_PATTERN, {
4594
- message: 'promoCode muss aus A\u2013Z, 0\u20139, "-", "_" bestehen (4\u201332 Zeichen)'
4742
+ message: 'promoCode must consist of A\u2013Z, 0\u20139, "-", "_" (4\u201332 characters)'
4595
4743
  }),
4596
4744
  _ts_metadata23("design:type", String)
4597
4745
  ], CompleteOnboardingSubscriptionDto.prototype, "promoCode", void 0);
@@ -4607,9 +4755,11 @@ _ts_decorate33([
4607
4755
 
4608
4756
  // src/billing/tenant-billing.controller.ts
4609
4757
  var import_common36 = require("@nestjs/common");
4758
+ var import_types17 = require("@saasicat/types");
4610
4759
 
4611
4760
  // src/promo/service.ts
4612
4761
  var import_common34 = require("@nestjs/common");
4762
+ var import_types15 = require("@saasicat/types");
4613
4763
 
4614
4764
  // src/promo/tokens.ts
4615
4765
  var PROMO_CODE_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/PromoCodeRepository");
@@ -4663,15 +4813,15 @@ function buildLabel(code, cycle) {
4663
4813
  maximumFractionDigits: 2
4664
4814
  })} \u20AC`;
4665
4815
  if (code.durationType === "ONCE") {
4666
- return `${valueStr} einmalig`;
4816
+ return `${valueStr} once`;
4667
4817
  }
4668
4818
  if (code.durationType === "MONTHS") {
4669
4819
  const m = code.durationValue ?? 0;
4670
- return m === 1 ? `${valueStr} im ersten Monat` : `${valueStr} f\xFCr ${m} Monate`;
4820
+ return m === 1 ? `${valueStr} for the first month` : `${valueStr} for ${m} months`;
4671
4821
  }
4672
4822
  const n = code.durationValue ?? 0;
4673
- if (n === 1) return cycle === "YEARLY" ? `${valueStr} im ersten Jahr` : `${valueStr} im ersten Monat`;
4674
- return cycle === "YEARLY" ? `${valueStr} in den ersten ${n} Jahren` : `${valueStr} in den ersten ${n} Monaten`;
4823
+ if (n === 1) return cycle === "YEARLY" ? `${valueStr} for the first year` : `${valueStr} for the first month`;
4824
+ return cycle === "YEARLY" ? `${valueStr} for the first ${n} years` : `${valueStr} for the first ${n} months`;
4675
4825
  }
4676
4826
  __name(buildLabel, "buildLabel");
4677
4827
 
@@ -4693,7 +4843,23 @@ function _ts_param22(paramIndex, decorator) {
4693
4843
  };
4694
4844
  }
4695
4845
  __name(_ts_param22, "_ts_param");
4696
- var CODE_PATTERN = /^[A-Z0-9_-]{4,32}$/;
4846
+ var CODE_MIN_LENGTH = 4;
4847
+ var CODE_MAX_LENGTH = 32;
4848
+ var CODE_PATTERN = new RegExp(`^[A-Z0-9_-]{${CODE_MIN_LENGTH},${CODE_MAX_LENGTH}}$`);
4849
+ var PERCENT_MIN_EXCLUSIVE = 0;
4850
+ var PERCENT_MAX = 100;
4851
+ var DURATION_MIN = 1;
4852
+ var DURATION_MAX = 24;
4853
+ function notRedeemable(reason) {
4854
+ return new import_common34.BadRequestException({
4855
+ code: import_types15.PROMO_ERROR_CODES.PROMO_CODE_NOT_REDEEMABLE,
4856
+ message: `Code cannot be redeemed: ${reason}`,
4857
+ params: {
4858
+ reason
4859
+ }
4860
+ });
4861
+ }
4862
+ __name(notRedeemable, "notRedeemable");
4697
4863
  var PromoCodesService = class {
4698
4864
  static {
4699
4865
  __name(this, "PromoCodesService");
@@ -4722,46 +4888,118 @@ var PromoCodesService = class {
4722
4888
  async create(input) {
4723
4889
  const code = input.code.trim().toUpperCase();
4724
4890
  if (!CODE_PATTERN.test(code)) {
4725
- throw new import_common34.BadRequestException('Code darf nur Gro\xDFbuchstaben, Ziffern, \u201E-" und \u201E_" enthalten (4\u201332 Zeichen).');
4891
+ throw new import_common34.BadRequestException({
4892
+ code: import_types15.PROMO_ERROR_CODES.PROMO_CODE_FORMAT_INVALID,
4893
+ message: 'The code may only contain upper-case letters, digits, "-" and "_" (4\u201332 characters).',
4894
+ params: {
4895
+ promoCode: code,
4896
+ minLength: CODE_MIN_LENGTH,
4897
+ maxLength: CODE_MAX_LENGTH
4898
+ }
4899
+ });
4726
4900
  }
4727
4901
  if (input.valueType === "PERCENT") {
4728
- if (!(input.value > 0 && input.value <= 100)) {
4729
- throw new import_common34.BadRequestException("Prozentsatz muss zwischen 0 und 100 liegen.");
4902
+ if (!(input.value > PERCENT_MIN_EXCLUSIVE && input.value <= PERCENT_MAX)) {
4903
+ throw new import_common34.BadRequestException({
4904
+ code: import_types15.PROMO_ERROR_CODES.PROMO_PERCENT_OUT_OF_RANGE,
4905
+ message: "The percentage must be between 0 and 100.",
4906
+ params: {
4907
+ value: input.value,
4908
+ min: PERCENT_MIN_EXCLUSIVE,
4909
+ max: PERCENT_MAX
4910
+ }
4911
+ });
4730
4912
  }
4731
4913
  } else if (!(input.value > 0)) {
4732
- throw new import_common34.BadRequestException("Betrag muss positiv sein.");
4914
+ throw new import_common34.BadRequestException({
4915
+ code: import_types15.PROMO_ERROR_CODES.PROMO_AMOUNT_NOT_POSITIVE,
4916
+ message: "The amount must be positive.",
4917
+ params: {
4918
+ value: input.value,
4919
+ min: 0
4920
+ }
4921
+ });
4733
4922
  }
4734
4923
  if (input.durationType === "ONCE") {
4735
4924
  if (input.durationValue != null) {
4736
- throw new import_common34.BadRequestException("Bei einmaligem Rabatt darf keine Laufzeit gesetzt sein.");
4925
+ throw new import_common34.BadRequestException({
4926
+ code: import_types15.PROMO_ERROR_CODES.PROMO_ONE_OFF_WITH_DURATION,
4927
+ message: "A one-off discount must not set a duration.",
4928
+ params: {
4929
+ durationValue: input.durationValue
4930
+ }
4931
+ });
4737
4932
  }
4738
4933
  } else {
4739
4934
  const v = input.durationValue;
4740
- if (v == null || v < 1 || v > 24) {
4741
- throw new import_common34.BadRequestException("Laufzeit ung\xFCltig (max. 24 Monate bzw. Abrechnungsperioden).");
4935
+ if (v == null || v < DURATION_MIN || v > DURATION_MAX) {
4936
+ throw new import_common34.BadRequestException({
4937
+ code: import_types15.PROMO_ERROR_CODES.PROMO_DURATION_INVALID,
4938
+ message: "Invalid duration (at most 24 months or billing periods).",
4939
+ params: {
4940
+ durationValue: v ?? null,
4941
+ min: DURATION_MIN,
4942
+ max: DURATION_MAX
4943
+ }
4944
+ });
4742
4945
  }
4743
4946
  }
4744
4947
  if (input.validFrom && input.validUntil && input.validFrom >= input.validUntil) {
4745
- throw new import_common34.BadRequestException("G\xFCltigkeits-Fenster ung\xFCltig.");
4948
+ throw new import_common34.BadRequestException({
4949
+ code: import_types15.PROMO_ERROR_CODES.PROMO_VALIDITY_WINDOW_INVALID,
4950
+ message: "Invalid validity window.",
4951
+ params: {
4952
+ validFrom: input.validFrom.toISOString(),
4953
+ validUntil: input.validUntil.toISOString()
4954
+ }
4955
+ });
4746
4956
  }
4747
4957
  const plans = input.appliesToPlans ?? [];
4748
4958
  const blocked = this.config.nonRedeemablePlans ?? [];
4749
4959
  for (const p of plans) {
4750
4960
  if (blocked.includes(p)) {
4751
- throw new import_common34.BadRequestException(`${p}-Plan kann nicht rabattiert werden.`);
4961
+ throw new import_common34.BadRequestException({
4962
+ code: import_types15.PROMO_ERROR_CODES.PROMO_PLAN_NOT_DISCOUNTABLE,
4963
+ message: `${p}-Plan cannot be discounted.`,
4964
+ params: {
4965
+ plan: p
4966
+ }
4967
+ });
4752
4968
  }
4753
4969
  }
4754
4970
  if (input.minimumPlanAmountGross != null && input.minimumPlanAmountGross <= 0) {
4755
- throw new import_common34.BadRequestException("Mindest-Plan-Brutto muss positiv sein.");
4971
+ throw new import_common34.BadRequestException({
4972
+ code: import_types15.PROMO_ERROR_CODES.PROMO_MIN_AMOUNT_NOT_POSITIVE,
4973
+ message: "The minimum plan gross amount must be positive.",
4974
+ params: {
4975
+ value: input.minimumPlanAmountGross,
4976
+ min: 0
4977
+ }
4978
+ });
4756
4979
  }
4757
4980
  if (input.valueType === "ABSOLUTE" && !input.allowZeroInvoice) {
4758
4981
  const lowestApplicableGross = this.lowestApplicablePlanGross(plans);
4759
4982
  if (lowestApplicableGross != null && input.value >= lowestApplicableGross) {
4760
- throw new import_common34.BadRequestException("Bei Absolutbetr\xE4gen muss der Rabatt unter dem niedrigsten anwendbaren Plan-Preis liegen oder allowZeroInvoice aktiviert sein.");
4983
+ throw new import_common34.BadRequestException({
4984
+ code: import_types15.PROMO_ERROR_CODES.PROMO_WOULD_PRODUCE_ZERO_INVOICE,
4985
+ message: "For absolute amounts the discount must stay below the lowest applicable plan price, or allowZeroInvoice must be enabled.",
4986
+ params: {
4987
+ value: input.value,
4988
+ lowestApplicablePlanGross: lowestApplicableGross
4989
+ }
4990
+ });
4761
4991
  }
4762
4992
  }
4763
4993
  const exists = await this.promoRepo.findByCode(code);
4764
- if (exists) throw new import_common34.BadRequestException("Code existiert bereits.");
4994
+ if (exists) {
4995
+ throw new import_common34.BadRequestException({
4996
+ code: import_types15.PROMO_ERROR_CODES.PROMO_CODE_ALREADY_EXISTS,
4997
+ message: "The code already exists.",
4998
+ params: {
4999
+ promoCode: code
5000
+ }
5001
+ });
5002
+ }
4765
5003
  return this.promoRepo.create({
4766
5004
  ...input,
4767
5005
  code
@@ -4769,10 +5007,26 @@ var PromoCodesService = class {
4769
5007
  }
4770
5008
  async update(id, input) {
4771
5009
  const existing = await this.promoRepo.findById(id);
4772
- if (!existing) throw new import_common34.NotFoundException("Code nicht gefunden");
5010
+ if (!existing) {
5011
+ throw new import_common34.NotFoundException({
5012
+ code: import_types15.PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
5013
+ message: "Code not found",
5014
+ params: {
5015
+ promoCodeId: id,
5016
+ promoCode: null
5017
+ }
5018
+ });
5019
+ }
4773
5020
  if (input.maxRedemptions != null && existing.maxRedemptions != null) {
4774
5021
  if (input.maxRedemptions < existing.maxRedemptions) {
4775
- throw new import_common34.BadRequestException("maxRedemptions kann nicht gesenkt werden.");
5022
+ throw new import_common34.BadRequestException({
5023
+ code: import_types15.PROMO_ERROR_CODES.PROMO_MAX_REDEMPTIONS_LOWERED,
5024
+ message: "maxRedemptions cannot be lowered.",
5025
+ params: {
5026
+ current: existing.maxRedemptions,
5027
+ requested: input.maxRedemptions
5028
+ }
5029
+ });
4776
5030
  }
4777
5031
  }
4778
5032
  return this.promoRepo.update(id, input);
@@ -4780,7 +5034,14 @@ var PromoCodesService = class {
4780
5034
  async softDelete(id) {
4781
5035
  const redemptions = await this.redemptionRepo.countByPromoCode(id);
4782
5036
  if (redemptions > 0) {
4783
- throw new import_common34.BadRequestException("Code hat bereits Einl\xF6sungen \u2014 Soft-Delete nicht m\xF6glich. Stattdessen pausieren.");
5037
+ throw new import_common34.BadRequestException({
5038
+ code: import_types15.PROMO_ERROR_CODES.PROMO_CODE_HAS_REDEMPTIONS,
5039
+ message: "The code already has redemptions \u2014 it cannot be soft-deleted. Pause it instead.",
5040
+ params: {
5041
+ promoCodeId: id,
5042
+ redemptions
5043
+ }
5044
+ });
4784
5045
  }
4785
5046
  await this.promoRepo.softDelete(id);
4786
5047
  }
@@ -4791,7 +5052,16 @@ var PromoCodesService = class {
4791
5052
  async findOne(id) {
4792
5053
  await this.lazyExpire();
4793
5054
  const code = await this.promoRepo.findById(id);
4794
- if (!code || code.deletedAt) throw new import_common34.NotFoundException("Code nicht gefunden");
5055
+ if (!code || code.deletedAt) {
5056
+ throw new import_common34.NotFoundException({
5057
+ code: import_types15.PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
5058
+ message: "Code not found",
5059
+ params: {
5060
+ promoCodeId: id,
5061
+ promoCode: null
5062
+ }
5063
+ });
5064
+ }
4795
5065
  return code;
4796
5066
  }
4797
5067
  async stats(id) {
@@ -4919,28 +5189,51 @@ var PromoCodesService = class {
4919
5189
  const code = input.code.trim().toUpperCase();
4920
5190
  const promo = await this.promoRepo.findByCode(code, tx);
4921
5191
  if (!promo || promo.deletedAt) {
4922
- throw new import_common34.BadRequestException("Code nicht gefunden");
5192
+ throw new import_common34.BadRequestException({
5193
+ code: import_types15.PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
5194
+ // The lookup runs on the code string, so there is no id to report.
5195
+ message: "Code not found",
5196
+ params: {
5197
+ promoCodeId: null,
5198
+ promoCode: code
5199
+ }
5200
+ });
4923
5201
  }
4924
5202
  const sub = await this.subscriptionLookup.findById(input.subscriptionId, tx);
4925
- if (!sub) throw new import_common34.NotFoundException("Subscription nicht gefunden");
5203
+ if (!sub) {
5204
+ throw new import_common34.NotFoundException({
5205
+ code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5206
+ message: "Subscription not found",
5207
+ params: {
5208
+ tenantId: input.tenantId
5209
+ }
5210
+ });
5211
+ }
4926
5212
  if (sub.tenantId !== input.tenantId) {
4927
- throw new import_common34.BadRequestException("Subscription geh\xF6rt nicht zum Tenant");
5213
+ throw new import_common34.BadRequestException({
5214
+ code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_TENANT_MISMATCH,
5215
+ message: "Subscription does not belong to the tenant",
5216
+ params: {
5217
+ subscriptionId: sub.id,
5218
+ tenantId: input.tenantId
5219
+ }
5220
+ });
4928
5221
  }
4929
5222
  const reason = this.checkEligibility(promo, {
4930
5223
  code,
4931
5224
  planId: sub.plan,
4932
5225
  billingCycle: sub.billingCycle
4933
5226
  });
4934
- if (reason) throw new import_common34.BadRequestException(`Code nicht einl\xF6sbar: ${reason}`);
5227
+ if (reason) throw notRedeemable(reason);
4935
5228
  const firstTimeReason = await this.checkFirstTimeCustomer(promo, input.email, {
4936
5229
  requireEmail: true
4937
5230
  });
4938
5231
  if (firstTimeReason) {
4939
- throw new import_common34.BadRequestException(`Code nicht einl\xF6sbar: ${firstTimeReason}`);
5232
+ throw notRedeemable(firstTimeReason);
4940
5233
  }
4941
5234
  const claimed = await this.promoRepo.claimSlot(promo.id, tx);
4942
5235
  if (!claimed) {
4943
- throw new import_common34.BadRequestException("Code nicht einl\xF6sbar: EXHAUSTED");
5236
+ throw notRedeemable("EXHAUSTED");
4944
5237
  }
4945
5238
  await this.promoRepo.markExhaustedIfFull(promo.id, tx);
4946
5239
  const startsAt = sub.startedAt ?? /* @__PURE__ */ new Date();
@@ -5051,6 +5344,7 @@ PromoCodesService = _ts_decorate34([
5051
5344
 
5052
5345
  // src/billing/subscription-bundles.service.ts
5053
5346
  var import_common35 = require("@nestjs/common");
5347
+ var import_types16 = require("@saasicat/types");
5054
5348
  function _ts_decorate35(decorators, target, key, desc) {
5055
5349
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5056
5350
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -5098,38 +5392,61 @@ var SubscriptionBundlesService = class {
5098
5392
  async addBundleToSubscription(input) {
5099
5393
  const bundleVersion = await this.bundles.findVersionById(input.bundleVersionId);
5100
5394
  if (!bundleVersion) {
5101
- throw new import_common35.NotFoundException(`BundleVersion '${input.bundleVersionId}' nicht gefunden`);
5395
+ throw new import_common35.NotFoundException({
5396
+ code: import_types16.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
5397
+ message: `BundleVersion '${input.bundleVersionId}' not found`,
5398
+ params: {
5399
+ bundleVersionId: input.bundleVersionId
5400
+ }
5401
+ });
5102
5402
  }
5103
5403
  if (bundleVersion.publishedAt === null) {
5104
5404
  throw new import_common35.UnprocessableEntityException({
5105
- code: "BUNDLE_VERSION_NOT_PUBLISHED",
5106
- message: `BundleVersion '${input.bundleVersionId}' ist nicht published und kann nicht gebucht werden.`
5405
+ code: import_types16.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_PUBLISHED,
5406
+ message: `BundleVersion '${input.bundleVersionId}' is not published and cannot be booked.`,
5407
+ params: {
5408
+ bundleVersionId: input.bundleVersionId
5409
+ }
5107
5410
  });
5108
5411
  }
5109
5412
  if (bundleVersion.supersededAt !== null) {
5110
5413
  throw new import_common35.UnprocessableEntityException({
5111
- code: "BUNDLE_VERSION_SUPERSEDED",
5112
- message: `BundleVersion '${input.bundleVersionId}' wurde durch eine Nachfolge-Version abgel\xF6st.`
5414
+ code: import_types16.CATALOG_ERROR_CODES.BUNDLE_VERSION_SUPERSEDED,
5415
+ message: `BundleVersion '${input.bundleVersionId}' has been superseded by a newer version.`,
5416
+ params: {
5417
+ bundleVersionId: input.bundleVersionId
5418
+ }
5113
5419
  });
5114
5420
  }
5115
5421
  if (this.blockedBundles?.bundleKeys?.includes(bundleVersion.bundleKey)) {
5116
5422
  throw new import_common35.UnprocessableEntityException({
5117
- code: "BUNDLE_NOT_SELF_SERVICE",
5118
- message: `Bundle '${bundleVersion.bundleKey}' wird nur per Sondervertrag aktiviert. Bitte den Vertragsbetreuer kontaktieren.`
5423
+ code: import_types16.BILLING_ERROR_CODES.BUNDLE_NOT_SELF_SERVICE,
5424
+ message: `Bundle '${bundleVersion.bundleKey}' is only activated via a special contract. Please contact the contract manager.`,
5425
+ params: {
5426
+ bundleKey: bundleVersion.bundleKey
5427
+ }
5119
5428
  });
5120
5429
  }
5121
5430
  const planIds = bundleVersion.compatibility?.planIds ?? [];
5122
5431
  if (planIds.length > 0 && !planIds.includes(input.currentPlanKey)) {
5123
5432
  throw new import_common35.UnprocessableEntityException({
5124
- code: "BUNDLE_INCOMPATIBLE_WITH_PLAN",
5125
- message: `BundleVersion '${input.bundleVersionId}' ist nicht mit Plan '${input.currentPlanKey}' kompatibel. Erlaubt: [${planIds.join(", ")}].`
5433
+ code: import_types16.BILLING_ERROR_CODES.BUNDLE_INCOMPATIBLE_WITH_PLAN,
5434
+ message: `BundleVersion '${input.bundleVersionId}' is not compatible with plan '${input.currentPlanKey}'. Allowed: [${planIds.join(", ")}].`,
5435
+ params: {
5436
+ bundleVersionId: input.bundleVersionId,
5437
+ planKey: input.currentPlanKey,
5438
+ allowedPlanKeys: planIds
5439
+ }
5126
5440
  });
5127
5441
  }
5128
5442
  const active = await this.repo.listActiveBySubscription(input.subscriptionId);
5129
5443
  if (active.some((b) => b.bundleVersionId === input.bundleVersionId)) {
5130
5444
  throw new import_common35.UnprocessableEntityException({
5131
- code: "BUNDLE_ALREADY_SUBSCRIBED",
5132
- message: `Subscription '${input.subscriptionId}' hat dieses Bundle bereits aktiv gebucht.`
5445
+ code: import_types16.BILLING_ERROR_CODES.BUNDLE_ALREADY_SUBSCRIBED,
5446
+ message: `Subscription '${input.subscriptionId}' has already actively booked this bundle.`,
5447
+ params: {
5448
+ subscriptionId: input.subscriptionId
5449
+ }
5133
5450
  });
5134
5451
  }
5135
5452
  const startedAt = input.startedAt ?? /* @__PURE__ */ new Date();
@@ -5145,12 +5462,21 @@ var SubscriptionBundlesService = class {
5145
5462
  async cancelBundleFromSubscription(input) {
5146
5463
  const existing = await this.repo.findById(input.subscriptionBundleId);
5147
5464
  if (!existing) {
5148
- throw new import_common35.NotFoundException(`SubscriptionBundle '${input.subscriptionBundleId}' nicht gefunden`);
5465
+ throw new import_common35.NotFoundException({
5466
+ code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_FOUND,
5467
+ message: `SubscriptionBundle '${input.subscriptionBundleId}' not found`,
5468
+ params: {
5469
+ subscriptionBundleId: input.subscriptionBundleId
5470
+ }
5471
+ });
5149
5472
  }
5150
5473
  if (existing.canceledAt !== null) {
5151
5474
  throw new import_common35.UnprocessableEntityException({
5152
- code: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELED",
5153
- message: `SubscriptionBundle '${input.subscriptionBundleId}' ist bereits gek\xFCndigt.`
5475
+ code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED,
5476
+ message: `SubscriptionBundle '${input.subscriptionBundleId}' is already cancelled.`,
5477
+ params: {
5478
+ subscriptionBundleId: input.subscriptionBundleId
5479
+ }
5154
5480
  });
5155
5481
  }
5156
5482
  const canceledAt = input.canceledAt ?? /* @__PURE__ */ new Date();
@@ -5171,18 +5497,27 @@ var SubscriptionBundlesService = class {
5171
5497
  async reactivateBundle(subscriptionBundleId) {
5172
5498
  const existing = await this.repo.findById(subscriptionBundleId);
5173
5499
  if (!existing) {
5174
- throw new import_common35.NotFoundException(`SubscriptionBundle '${subscriptionBundleId}' nicht gefunden`);
5500
+ throw new import_common35.NotFoundException({
5501
+ code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_FOUND,
5502
+ message: `SubscriptionBundle '${subscriptionBundleId}' not found`,
5503
+ params: {
5504
+ subscriptionBundleId
5505
+ }
5506
+ });
5175
5507
  }
5176
5508
  if (existing.canceledAt === null) {
5177
5509
  throw new import_common35.UnprocessableEntityException({
5178
- code: "SUBSCRIPTION_BUNDLE_NOT_CANCELED",
5179
- message: `SubscriptionBundle '${subscriptionBundleId}' ist nicht gek\xFCndigt.`
5510
+ code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_CANCELLED,
5511
+ message: `SubscriptionBundle '${subscriptionBundleId}' is not cancelled.`,
5512
+ params: {
5513
+ subscriptionBundleId
5514
+ }
5180
5515
  });
5181
5516
  }
5182
5517
  if (existing.canceledEffectiveAt !== null && existing.canceledEffectiveAt <= /* @__PURE__ */ new Date()) {
5183
5518
  throw new import_common35.UnprocessableEntityException({
5184
- code: "SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE",
5185
- message: "K\xFCndigung bereits wirksam \u2014 bitte das Bundle neu buchen."
5519
+ code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE,
5520
+ message: "Cancellation already in effect \u2014 book the bundle again."
5186
5521
  });
5187
5522
  }
5188
5523
  return this.repo.reactivate(subscriptionBundleId);
@@ -5288,7 +5623,13 @@ var TenantBillingController = class _TenantBillingController {
5288
5623
  this.usageSnapshot.snapshot(tenantId)
5289
5624
  ]);
5290
5625
  if (!sub) {
5291
- throw new import_common36.NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
5626
+ throw new import_common36.NotFoundException({
5627
+ code: import_types17.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5628
+ message: `No subscription for tenant ${tenantId}`,
5629
+ params: {
5630
+ tenantId
5631
+ }
5632
+ });
5292
5633
  }
5293
5634
  const usage = {};
5294
5635
  for (const key of /* @__PURE__ */ new Set([
@@ -5337,14 +5678,29 @@ var TenantBillingController = class _TenantBillingController {
5337
5678
  const userId = this.requireUserId(req);
5338
5679
  const blockedTargets = this.blockedPlans?.asTarget ?? [];
5339
5680
  if (blockedTargets.includes(dto.plan)) {
5340
- throw new import_common36.ForbiddenException(`${dto.plan} wird nicht per Self-Service aktiviert.`);
5681
+ throw new import_common36.ForbiddenException({
5682
+ code: import_types17.BILLING_ERROR_CODES.PLAN_NOT_SELF_SERVICE,
5683
+ message: `${dto.plan} is not activated via self-service.`,
5684
+ params: {
5685
+ planKey: dto.plan
5686
+ }
5687
+ });
5341
5688
  }
5342
5689
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
5343
- if (!sub) throw new import_common36.NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
5690
+ if (!sub) {
5691
+ throw new import_common36.NotFoundException({
5692
+ code: import_types17.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5693
+ message: `No subscription for tenant ${tenantId}`,
5694
+ params: {
5695
+ tenantId
5696
+ }
5697
+ });
5698
+ }
5344
5699
  const blockers = await this.planPreview.assertChangeAllowed(tenantId, dto.plan, dto.billingCycle);
5345
5700
  if (blockers.length > 0) {
5346
5701
  throw new import_common36.BadRequestException({
5347
- message: "Plan-Wechsel blockiert.",
5702
+ code: import_types17.BILLING_ERROR_CODES.PLAN_CHANGE_BLOCKED,
5703
+ message: "Plan change is blocked.",
5348
5704
  blockers
5349
5705
  });
5350
5706
  }
@@ -5414,14 +5770,29 @@ var TenantBillingController = class _TenantBillingController {
5414
5770
  const warnings = [];
5415
5771
  const blockedTargets = this.blockedPlans?.asTarget ?? [];
5416
5772
  if (blockedTargets.includes(dto.plan)) {
5417
- throw new import_common36.ForbiddenException(`${dto.plan} wird nicht per Self-Service aktiviert.`);
5773
+ throw new import_common36.ForbiddenException({
5774
+ code: import_types17.BILLING_ERROR_CODES.PLAN_NOT_SELF_SERVICE,
5775
+ message: `${dto.plan} is not activated via self-service.`,
5776
+ params: {
5777
+ planKey: dto.plan
5778
+ }
5779
+ });
5418
5780
  }
5419
5781
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
5420
- if (!sub) throw new import_common36.NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
5782
+ if (!sub) {
5783
+ throw new import_common36.NotFoundException({
5784
+ code: import_types17.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5785
+ message: `No subscription for tenant ${tenantId}`,
5786
+ params: {
5787
+ tenantId
5788
+ }
5789
+ });
5790
+ }
5421
5791
  const blockers = await this.planPreview.assertChangeAllowed(tenantId, dto.plan, dto.billingCycle);
5422
5792
  if (blockers.length > 0) {
5423
5793
  throw new import_common36.BadRequestException({
5424
- message: "Plan-Wechsel im Onboarding blockiert.",
5794
+ code: import_types17.BILLING_ERROR_CODES.PLAN_CHANGE_BLOCKED,
5795
+ message: "Plan change during onboarding is blocked.",
5425
5796
  blockers
5426
5797
  });
5427
5798
  }
@@ -5455,8 +5826,13 @@ var TenantBillingController = class _TenantBillingController {
5455
5826
  warnings.push(...this.collectPromoSkipReasons(dto.promoCode, sub));
5456
5827
  }
5457
5828
  } catch (err) {
5829
+ const reason = err instanceof Error ? err.message : null;
5458
5830
  throw new import_common36.BadRequestException({
5459
- message: err instanceof Error ? `Onboarding-Anlage fehlgeschlagen: ${err.message}` : "Onboarding-Anlage fehlgeschlagen"
5831
+ code: import_types17.BILLING_ERROR_CODES.ONBOARDING_CREATE_FAILED,
5832
+ message: reason ? `Onboarding creation failed: ${reason}` : "Onboarding creation failed",
5833
+ params: {
5834
+ reason
5835
+ }
5460
5836
  });
5461
5837
  }
5462
5838
  } else {
@@ -5469,9 +5845,9 @@ var TenantBillingController = class _TenantBillingController {
5469
5845
  });
5470
5846
  if (dto.promoCode) {
5471
5847
  if (!this.promoCodes) {
5472
- warnings.push("Promo-Code wurde gesendet, aber PromoCodesModule ist nicht geladen \u2014 der Code wurde NICHT eingel\xF6st.");
5848
+ warnings.push("A promo code was sent, but PromoCodesModule is not loaded \u2014 the code was NOT redeemed.");
5473
5849
  } else if (!sub.id) {
5474
- warnings.push("Promo-Code wurde gesendet, aber der Adapter liefert keine SubscriptionUsageRecord.id \u2014 der Code wurde NICHT eingel\xF6st.");
5850
+ warnings.push("A promo code was sent, but the adapter returns no SubscriptionUsageRecord.id \u2014 the code was NOT redeemed.");
5475
5851
  } else {
5476
5852
  try {
5477
5853
  const redemption = await this.promoCodes.redeem({
@@ -5482,8 +5858,8 @@ var TenantBillingController = class _TenantBillingController {
5482
5858
  });
5483
5859
  promoRedemption = this.toResponseRedemption(redemption, dto.promoCode);
5484
5860
  } catch (err) {
5485
- const message = err instanceof Error ? err.message : "Unbekannter Fehler";
5486
- warnings.push(`Promo-Code konnte nicht eingel\xF6st werden: ${message}`);
5861
+ const message = err instanceof Error ? err.message : "Unknown error";
5862
+ warnings.push(`The promo code could not be redeemed: ${message}`);
5487
5863
  }
5488
5864
  }
5489
5865
  }
@@ -5509,11 +5885,11 @@ var TenantBillingController = class _TenantBillingController {
5509
5885
  });
5510
5886
  bundlesAdded += 1;
5511
5887
  } catch (err) {
5512
- warnings.push(`Bundle '${bundleVersionId}' konnte nicht gebucht werden: ` + (err instanceof Error ? err.message : String(err)));
5888
+ warnings.push(`Bundle '${bundleVersionId}' could not be booked: ` + (err instanceof Error ? err.message : String(err)));
5513
5889
  }
5514
5890
  }
5515
5891
  } else if (bundleVersionIds.length > 0 && !this.subscriptionBundles) {
5516
- warnings.push("Bundle-Buchungen im Onboarding angefordert, aber SubscriptionBundleModule ist nicht im Konsumenten registriert. Bundles wurden nicht angelegt.");
5892
+ warnings.push("Bundle bookings were requested during onboarding, but SubscriptionBundleModule is not registered in the consumer. No bundles were created.");
5517
5893
  }
5518
5894
  return {
5519
5895
  plan: planResult.plan,
@@ -5533,9 +5909,20 @@ var TenantBillingController = class _TenantBillingController {
5533
5909
  const tenantId = this.requireTenantId(req);
5534
5910
  const userId = this.requireUserId(req);
5535
5911
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
5536
- if (!sub) throw new import_common36.NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
5912
+ if (!sub) {
5913
+ throw new import_common36.NotFoundException({
5914
+ code: import_types17.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5915
+ message: `No subscription for tenant ${tenantId}`,
5916
+ params: {
5917
+ tenantId
5918
+ }
5919
+ });
5920
+ }
5537
5921
  if (!sub.pendingPlanVersion) {
5538
- throw new import_common36.BadRequestException("Es liegt aktuell keine Pending-Plan-Version zur Best\xE4tigung vor.");
5922
+ throw new import_common36.BadRequestException({
5923
+ code: import_types17.BILLING_ERROR_CODES.NO_PENDING_PLAN_VERSION,
5924
+ message: "There is no pending plan version awaiting confirmation."
5925
+ });
5539
5926
  }
5540
5927
  const result = await this.subscriptionWrite.acceptPendingPlanVersion(tenantId, userId, /* @__PURE__ */ new Date());
5541
5928
  if (!result.alreadyAccepted) {
@@ -5580,7 +5967,10 @@ var TenantBillingController = class _TenantBillingController {
5580
5967
  const resolver = this.tenantIdResolver ?? ((r) => r.user?.tenantId ?? null);
5581
5968
  const tenantId = resolver(req);
5582
5969
  if (!tenantId) {
5583
- throw new import_common36.NotFoundException("Tenant-ID nicht im Request gefunden");
5970
+ throw new import_common36.NotFoundException({
5971
+ code: import_types17.AUTH_ERROR_CODES.TENANT_CONTEXT_MISSING,
5972
+ message: "No tenant ID found on the request"
5973
+ });
5584
5974
  }
5585
5975
  return tenantId;
5586
5976
  }
@@ -5588,7 +5978,10 @@ var TenantBillingController = class _TenantBillingController {
5588
5978
  const resolver = this.userIdResolver ?? ((r) => r.user?.sub ?? r.user?.id ?? null);
5589
5979
  const userId = resolver(req);
5590
5980
  if (!userId) {
5591
- throw new import_common36.NotFoundException("User-ID nicht im Request gefunden");
5981
+ throw new import_common36.NotFoundException({
5982
+ code: import_types17.AUTH_ERROR_CODES.TENANT_CONTEXT_MISSING,
5983
+ message: "No user ID found on the request"
5984
+ });
5592
5985
  }
5593
5986
  return userId;
5594
5987
  }
@@ -5628,10 +6021,10 @@ var TenantBillingController = class _TenantBillingController {
5628
6021
  collectPromoSkipReasons(promoCode, sub) {
5629
6022
  const reasons = [];
5630
6023
  if (!this.promoCodes) {
5631
- reasons.push(`Promo-Code "${promoCode}" wurde gesendet, aber PromoCodesModule ist nicht geladen \u2014 der Code wurde NICHT eingel\xF6st.`);
6024
+ reasons.push(`Promo-Code "${promoCode}" was sent, but PromoCodesModule is not loaded \u2014 the code was NOT redeemed.`);
5632
6025
  }
5633
6026
  if (!sub.id) {
5634
- reasons.push(`Promo-Code "${promoCode}" wurde gesendet, aber der Adapter liefert keine SubscriptionUsageRecord.id \u2014 der Code wurde NICHT eingel\xF6st.`);
6027
+ reasons.push(`Promo-Code "${promoCode}" was sent, but the adapter returns no SubscriptionUsageRecord.id \u2014 the code was NOT redeemed.`);
5635
6028
  }
5636
6029
  return reasons;
5637
6030
  }
@@ -5646,7 +6039,7 @@ var TenantBillingController = class _TenantBillingController {
5646
6039
  try {
5647
6040
  await this.contractFreeze.freezeOnPlanChange(tenantId, plan, cycle, /* @__PURE__ */ new Date());
5648
6041
  } catch (err) {
5649
- this.logger.error(`Contract-Freeze nach Plan-Wechsel fehlgeschlagen (tenant ${tenantId}): ${String(err)}`);
6042
+ this.logger.error(`Contract freeze after plan change failed (tenant ${tenantId}): ${String(err)}`);
5650
6043
  }
5651
6044
  }
5652
6045
  /**
@@ -5957,7 +6350,7 @@ __name(normalizeQuotas, "normalizeQuotas");
5957
6350
 
5958
6351
  // src/billing/subscription-bundle-preview.service.ts
5959
6352
  var import_common39 = require("@nestjs/common");
5960
- var import_types5 = require("@saasicat/types");
6353
+ var import_types18 = require("@saasicat/types");
5961
6354
  function _ts_decorate39(decorators, target, key, desc) {
5962
6355
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5963
6356
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -5996,7 +6389,13 @@ var SubscriptionBundlePreviewService = class {
5996
6389
  async previewAdd(ctx, input, now = /* @__PURE__ */ new Date()) {
5997
6390
  const bundleVersion = await this.bundles.findVersionById(input.bundleVersionId);
5998
6391
  if (!bundleVersion) {
5999
- throw new import_common39.NotFoundException(`BundleVersion '${input.bundleVersionId}' nicht gefunden`);
6392
+ throw new import_common39.NotFoundException({
6393
+ code: import_types18.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
6394
+ message: `BundleVersion '${input.bundleVersionId}' not found`,
6395
+ params: {
6396
+ bundleVersionId: input.bundleVersionId
6397
+ }
6398
+ });
6000
6399
  }
6001
6400
  const blockers = [];
6002
6401
  const warnings = [];
@@ -6005,7 +6404,7 @@ var SubscriptionBundlePreviewService = class {
6005
6404
  if (activeBundleVersions.some((bv) => bv.id === bundleVersion.id)) {
6006
6405
  blockers.push({
6007
6406
  code: "BUNDLE_ALREADY_SUBSCRIBED",
6008
- message: "Dieses Bundle ist bereits aktiv gebucht."
6407
+ message: "This bundle is already actively booked."
6009
6408
  });
6010
6409
  }
6011
6410
  const planFeatures = new Set(await this.resolvePlanFeatures(ctx.currentPlanKey, now));
@@ -6013,14 +6412,14 @@ var SubscriptionBundlePreviewService = class {
6013
6412
  if (redundantFeatures.length > 0) {
6014
6413
  warnings.push({
6015
6414
  code: "REDUNDANT_FEATURES",
6016
- message: `${redundantFeatures.length} Feature${redundantFeatures.length === 1 ? " ist" : "s sind"} bereits im Plan oder einem anderen gebuchten Bundle enthalten \u2014 das Bundle w\xFCrde daf\xFCr doppelt bezahlt.`
6415
+ message: `${redundantFeatures.length} feature${redundantFeatures.length === 1 ? " is" : "s are"} already included in the plan or another booked bundle \u2014 the bundle would be paid for twice.`
6017
6416
  });
6018
6417
  }
6019
6418
  const missingRequires = await this.collectMissingRequires(bundleVersion, planFeatures, activeBundleVersions);
6020
6419
  if (missingRequires.length > 0) {
6021
6420
  blockers.push({
6022
6421
  code: "BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED",
6023
- message: `Das Bundle ben\xF6tigt [${missingRequires.join(", ")}] \u2014 weder im Plan noch in den aktiven Bundles enthalten.`
6422
+ message: `The bundle requires [${missingRequires.join(", ")}] \u2014 present neither in the plan nor in the active bundles.`
6024
6423
  });
6025
6424
  }
6026
6425
  const priceNet = resolveBundlePriceNet(bundleVersion, ctx.currentPlanKey, ctx.billingCycle);
@@ -6049,18 +6448,30 @@ var SubscriptionBundlePreviewService = class {
6049
6448
  async previewCancel(ctx, input, now = /* @__PURE__ */ new Date()) {
6050
6449
  const existing = await this.subscriptionBundles.findById(input.subscriptionBundleId);
6051
6450
  if (!existing || existing.subscriptionId !== ctx.subscriptionId) {
6052
- throw new import_common39.NotFoundException(`SubscriptionBundle '${input.subscriptionBundleId}' nicht gefunden`);
6451
+ throw new import_common39.NotFoundException({
6452
+ code: import_types18.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_FOUND,
6453
+ message: `SubscriptionBundle '${input.subscriptionBundleId}' not found`,
6454
+ params: {
6455
+ subscriptionBundleId: input.subscriptionBundleId
6456
+ }
6457
+ });
6053
6458
  }
6054
6459
  const bundleVersion = await this.bundles.findVersionById(existing.bundleVersionId);
6055
6460
  if (!bundleVersion) {
6056
- throw new import_common39.NotFoundException(`BundleVersion '${existing.bundleVersionId}' nicht gefunden`);
6461
+ throw new import_common39.NotFoundException({
6462
+ code: import_types18.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
6463
+ message: `BundleVersion '${existing.bundleVersionId}' not found`,
6464
+ params: {
6465
+ bundleVersionId: existing.bundleVersionId
6466
+ }
6467
+ });
6057
6468
  }
6058
6469
  const blockers = [];
6059
6470
  const warnings = [];
6060
6471
  if (existing.canceledAt !== null) {
6061
6472
  blockers.push({
6062
- code: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELED",
6063
- message: "Diese Bundle-Buchung ist bereits gek\xFCndigt."
6473
+ code: import_types18.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED,
6474
+ message: "This bundle booking is already cancelled."
6064
6475
  });
6065
6476
  }
6066
6477
  const effectiveAt = resolveBundleCancelEffectiveAt({
@@ -6072,7 +6483,7 @@ var SubscriptionBundlePreviewService = class {
6072
6483
  if (existing.minimumTermEndsAt && existing.minimumTermEndsAt.getTime() > periodEnd.getTime()) {
6073
6484
  warnings.push({
6074
6485
  code: "MINIMUM_TERM_BINDS",
6075
- message: "Die Mindestlaufzeit bindet \xFCber das Periodenende hinaus \u2014 die K\xFCndigung wirkt erst zum Mindestlaufzeit-Ende."
6486
+ message: "The minimum term extends beyond the end of the period \u2014 the cancellation only takes effect when the minimum term ends."
6076
6487
  });
6077
6488
  }
6078
6489
  return {
@@ -6091,26 +6502,26 @@ var SubscriptionBundlePreviewService = class {
6091
6502
  if (bundleVersion.publishedAt === null) {
6092
6503
  blockers.push({
6093
6504
  code: "BUNDLE_VERSION_NOT_PUBLISHED",
6094
- message: "Diese BundleVersion ist nicht ver\xF6ffentlicht und nicht buchbar."
6505
+ message: "This bundle version is not published and cannot be booked."
6095
6506
  });
6096
6507
  }
6097
6508
  if (bundleVersion.supersededAt !== null) {
6098
6509
  blockers.push({
6099
6510
  code: "BUNDLE_VERSION_SUPERSEDED",
6100
- message: "Diese BundleVersion wurde durch eine Nachfolge-Version abgel\xF6st."
6511
+ message: "This bundle version has been superseded by a newer one."
6101
6512
  });
6102
6513
  }
6103
6514
  const planIds = bundleVersion.compatibility?.planIds ?? [];
6104
6515
  if (planIds.length > 0 && !planIds.includes(currentPlanKey)) {
6105
6516
  blockers.push({
6106
6517
  code: "BUNDLE_INCOMPATIBLE_WITH_PLAN",
6107
- message: `Das Bundle ist nicht mit Plan '${currentPlanKey}' kompatibel. Erlaubt: [${planIds.join(", ")}].`
6518
+ message: `The bundle is not compatible with plan '${currentPlanKey}'. Allowed: [${planIds.join(", ")}].`
6108
6519
  });
6109
6520
  }
6110
6521
  if (this.blockedBundles?.bundleKeys?.includes(bundleVersion.bundleKey)) {
6111
6522
  blockers.push({
6112
6523
  code: "BUNDLE_NOT_SELF_SERVICE",
6113
- message: `Bundle '${bundleVersion.bundleKey}' wird nur per Sondervertrag aktiviert. Bitte den Vertragsbetreuer kontaktieren.`
6524
+ message: `Bundle '${bundleVersion.bundleKey}' is only activated via a special contract. Please contact the contract manager.`
6114
6525
  });
6115
6526
  }
6116
6527
  }
@@ -6159,12 +6570,12 @@ var SubscriptionBundlePreviewService = class {
6159
6570
  const entries = await this.catalogEntries.listFeatures({
6160
6571
  projectKey
6161
6572
  });
6162
- const requiresIndex = (0, import_types5.buildFeatureRequiresIndex)(entries);
6573
+ const requiresIndex = (0, import_types18.buildFeatureRequiresIndex)(entries);
6163
6574
  const covered = /* @__PURE__ */ new Set([
6164
6575
  ...planFeatures,
6165
6576
  ...activeBundleVersions.flatMap((bv) => bv.features ?? [])
6166
6577
  ]);
6167
- return (0, import_types5.collectUnsatisfiedRequires)(bundleVersion.features ?? [], requiresIndex).filter((key) => !covered.has(key));
6578
+ return (0, import_types18.collectUnsatisfiedRequires)(bundleVersion.features ?? [], requiresIndex).filter((key) => !covered.has(key));
6168
6579
  }
6169
6580
  /** `projectKey` lives on the bundle stem, not on the version. */
6170
6581
  async resolveProjectKey(bundleVersion) {
@@ -6223,6 +6634,7 @@ var import_common41 = require("@nestjs/common");
6223
6634
 
6224
6635
  // src/billing/tenant-subscription-bundles.controller.ts
6225
6636
  var import_common40 = require("@nestjs/common");
6637
+ var import_types19 = require("@saasicat/types");
6226
6638
 
6227
6639
  // src/billing/dto/subscription-bundles.dto.ts
6228
6640
  var import_class_validator5 = require("class-validator");
@@ -6303,7 +6715,7 @@ var CancelSubscriptionBundleDto = class {
6303
6715
  _ts_decorate40([
6304
6716
  (0, import_class_validator5.IsOptional)(),
6305
6717
  (0, import_class_validator5.Matches)(ISO_DATE_PATTERN, {
6306
- message: "canceledAt muss ISO-Datum sein (YYYY-MM-DD)"
6718
+ message: "canceledAt must be an ISO date (YYYY-MM-DD)"
6307
6719
  }),
6308
6720
  _ts_metadata28("design:type", String)
6309
6721
  ], CancelSubscriptionBundleDto.prototype, "canceledAt", void 0);
@@ -6367,7 +6779,10 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
6367
6779
  const hasAdd = Boolean(dto.bundleVersionId);
6368
6780
  const hasCancel = Boolean(dto.subscriptionBundleId);
6369
6781
  if (hasAdd === hasCancel) {
6370
- throw new import_common40.BadRequestException("Genau eines von bundleVersionId (Add-Preview) oder subscriptionBundleId (Cancel-Preview) angeben.");
6782
+ throw new import_common40.BadRequestException({
6783
+ code: import_types19.BILLING_ERROR_CODES.BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS,
6784
+ message: "Exactly one of bundleVersionId (add preview) or subscriptionBundleId (cancel preview) must be given."
6785
+ });
6371
6786
  }
6372
6787
  const sub = await this.requireSubscription(this.requireTenantId(req));
6373
6788
  const ctx = {
@@ -6407,21 +6822,30 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
6407
6822
  }
6408
6823
  requireSubscriptionPk(sub) {
6409
6824
  if (!sub.id) {
6410
- throw new import_common40.NotFoundException("SubscriptionUsageRecord ohne `id` \u2014 Adapter muss die Subscription-PK durchreichen (siehe SubscriptionUsageRecord.id).");
6825
+ throw new import_common40.NotFoundException(codedError(import_types19.BILLING_ERROR_CODES.SUBSCRIPTION_PK_MISSING));
6411
6826
  }
6412
6827
  return sub.id;
6413
6828
  }
6414
6829
  requireTenantId(req) {
6415
6830
  const tenantId = this.tenantIdResolver(req);
6416
6831
  if (!tenantId) {
6417
- throw new import_common40.NotFoundException("Tenant-ID nicht im Request gefunden");
6832
+ throw new import_common40.NotFoundException({
6833
+ code: import_types19.AUTH_ERROR_CODES.TENANT_CONTEXT_MISSING,
6834
+ message: "No tenant ID found on the request"
6835
+ });
6418
6836
  }
6419
6837
  return tenantId;
6420
6838
  }
6421
6839
  async requireSubscription(tenantId) {
6422
6840
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
6423
6841
  if (!sub) {
6424
- throw new import_common40.NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
6842
+ throw new import_common40.NotFoundException({
6843
+ code: import_types19.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
6844
+ message: `No subscription for tenant ${tenantId}`,
6845
+ params: {
6846
+ tenantId
6847
+ }
6848
+ });
6425
6849
  }
6426
6850
  return sub;
6427
6851
  }
@@ -6440,7 +6864,7 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
6440
6864
  try {
6441
6865
  await this.contractFreeze.freezeOnPlanChange(tenantId, sub.planVersion.planId, sub.billingCycle, /* @__PURE__ */ new Date());
6442
6866
  } catch (err) {
6443
- this.logger.error(`Contract-Freeze nach Bundle-Mutation fehlgeschlagen (tenant ${tenantId}): ${String(err)}`);
6867
+ this.logger.error(`Contract freeze after bundle mutation failed (tenant ${tenantId}): ${String(err)}`);
6444
6868
  }
6445
6869
  }
6446
6870
  };
@@ -6659,7 +7083,7 @@ __export(catalog_exports, {
6659
7083
 
6660
7084
  // src/catalog/bundles.service.ts
6661
7085
  var import_common43 = require("@nestjs/common");
6662
- var import_types6 = require("@saasicat/types");
7086
+ var import_types20 = require("@saasicat/types");
6663
7087
 
6664
7088
  // src/discovery/discovery.scanner.ts
6665
7089
  var import_node_crypto2 = require("crypto");
@@ -6728,7 +7152,7 @@ var DiscoveryScanner = class _DiscoveryScanner {
6728
7152
  (0, import_node_fs2.writeFileSync)(path, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
6729
7153
  this.logger.log(`Discovery snapshot persisted to ${path}`);
6730
7154
  } catch (err) {
6731
- this.logger.warn(`Discovery snapshot konnte nicht nach ${path} geschrieben werden: ${err.message}`);
7155
+ this.logger.warn(`The discovery snapshot could not be written to ${path}: ${err.message}`);
6732
7156
  }
6733
7157
  }
6734
7158
  /**
@@ -6813,7 +7237,7 @@ var DiscoveryScanner = class _DiscoveryScanner {
6813
7237
  registerCapability(target, meta, declaredAt) {
6814
7238
  const existing = target.get(meta.capabilityKey);
6815
7239
  if (existing) {
6816
- this.logger.warn(`Capability '${meta.capabilityKey}' wird an mehreren Stellen deklariert (${existing.declaredAt} und ${declaredAt}); erste Deklaration gewinnt`);
7240
+ this.logger.warn(`Capability '${meta.capabilityKey}' is declared in several places (${existing.declaredAt} and ${declaredAt}); the first declaration wins`);
6817
7241
  return;
6818
7242
  }
6819
7243
  target.set(meta.capabilityKey, {
@@ -6834,7 +7258,7 @@ var DiscoveryScanner = class _DiscoveryScanner {
6834
7258
  registerQuota(target, opts, declaredAt) {
6835
7259
  const existing = target.get(opts.key);
6836
7260
  if (existing) {
6837
- this.logger.warn(`Quota '${opts.key}' wird an mehreren Stellen definiert (${existing.declaredAt} und ${declaredAt}); erste Definition gewinnt`);
7261
+ this.logger.warn(`Quota '${opts.key}' is defined in several places (${existing.declaredAt} and ${declaredAt}); the first definition wins`);
6838
7262
  return;
6839
7263
  }
6840
7264
  target.set(opts.key, {
@@ -7013,7 +7437,7 @@ function dependencyWarnings(features, snapshot, code) {
7013
7437
  if (selected.has(required)) continue;
7014
7438
  warnings.push({
7015
7439
  code,
7016
- 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.`,
7440
+ 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.`,
7017
7441
  field: `features[${index}]`,
7018
7442
  value: required
7019
7443
  });
@@ -7032,7 +7456,7 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
7032
7456
  if (approved && !approved.features.has(feature)) {
7033
7457
  warnings.push({
7034
7458
  code: "BUNDLE_FEATURE_NOT_APPROVED",
7035
- message: `Feature '${feature}' ist nicht freigegeben (Status != approved). Nur freigegebene Features sind verkaufbar \u2014 im Discovery-Review freigeben (#20).`,
7459
+ message: `Feature '${feature}' is not approved (status != approved). Only approved features are sellable \u2014 approve it in the discovery review (#20).`,
7036
7460
  field: `features[${index}]`,
7037
7461
  value: feature
7038
7462
  });
@@ -7041,7 +7465,7 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
7041
7465
  }
7042
7466
  warnings.push({
7043
7467
  code: "BUNDLE_FEATURE_UNKNOWN",
7044
- message: `Feature '${feature}' wurde im Discovery-Snapshot nicht gefunden. Entweder fehlt im Code eine @ImplementsCapability(..., { feature: '${feature}' })-Annotation, oder das Feature ist veraltet.`,
7468
+ 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.`,
7045
7469
  field: `features[${index}]`,
7046
7470
  value: feature
7047
7471
  });
@@ -7050,14 +7474,14 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
7050
7474
  if (!knownQuotas.has(quotaKey)) {
7051
7475
  warnings.push({
7052
7476
  code: "QUOTA_MISSING",
7053
- message: `Quota '${quotaKey}' wurde im Discovery-Snapshot nicht gefunden. Im Code muss eine Klasse mit @DefinesQuota({ key: '${quotaKey}', ... }) annotiert sein.`,
7477
+ message: `Quota '${quotaKey}' was not found in the discovery snapshot. In code, a class must be annotated with @DefinesQuota({ key: '${quotaKey}', ... }).`,
7054
7478
  field: `quotas.${quotaKey}`,
7055
7479
  value: quotaKey
7056
7480
  });
7057
7481
  } else if (approved && !approved.quotas.has(quotaKey)) {
7058
7482
  warnings.push({
7059
7483
  code: "QUOTA_NOT_APPROVED",
7060
- message: `Quota '${quotaKey}' ist nicht freigegeben (Status != approved). Nur freigegebene Quotas sind verkaufbar \u2014 im Discovery-Review freigeben (#20).`,
7484
+ message: `Quota '${quotaKey}' is not approved (status != approved). Only approved quotas are sellable \u2014 approve it in the discovery review (#20).`,
7061
7485
  field: `quotas.${quotaKey}`,
7062
7486
  value: quotaKey
7063
7487
  });
@@ -7069,7 +7493,7 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
7069
7493
  if (knownPlanKeys.has(planKey)) return;
7070
7494
  warnings.push({
7071
7495
  code: "BUNDLE_PLAN_KEY_UNKNOWN",
7072
- message: `Plan '${planKey}' aus compatibility.planIds existiert im Project nicht. Entweder den planKey korrigieren oder den Plan-Stamm zuerst anlegen.`,
7496
+ message: `Plan '${planKey}' from compatibility.planIds does not exist in the project. Either fix the planKey, or create the plan record first.`,
7073
7497
  field: `compatibility.planIds[${index}]`,
7074
7498
  value: planKey
7075
7499
  });
@@ -7089,7 +7513,7 @@ function validatePlanDraft(draft, snapshot, marketedOnlyFeatures = /* @__PURE__
7089
7513
  if (approved && !approved.features.has(feature)) {
7090
7514
  warnings.push({
7091
7515
  code: "PLAN_FEATURE_NOT_APPROVED",
7092
- message: `Feature '${feature}' ist nicht freigegeben (Status != approved). Nur freigegebene Features sind verkaufbar \u2014 im Discovery-Review freigeben (#20).`,
7516
+ message: `Feature '${feature}' is not approved (status != approved). Only approved features are sellable \u2014 approve it in the discovery review (#20).`,
7093
7517
  field: `features[${index}]`,
7094
7518
  value: feature
7095
7519
  });
@@ -7098,7 +7522,7 @@ function validatePlanDraft(draft, snapshot, marketedOnlyFeatures = /* @__PURE__
7098
7522
  }
7099
7523
  warnings.push({
7100
7524
  code: "PLAN_FEATURE_UNKNOWN",
7101
- message: `Feature '${feature}' wurde im Discovery-Snapshot nicht gefunden. Entweder fehlt im Code eine @ImplementsCapability(..., { feature: '${feature}' })-Annotation, oder das Feature ist veraltet.`,
7525
+ 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.`,
7102
7526
  field: `features[${index}]`,
7103
7527
  value: feature
7104
7528
  });
@@ -7107,14 +7531,14 @@ function validatePlanDraft(draft, snapshot, marketedOnlyFeatures = /* @__PURE__
7107
7531
  if (!knownQuotas.has(quotaKey)) {
7108
7532
  warnings.push({
7109
7533
  code: "QUOTA_MISSING",
7110
- message: `Quota '${quotaKey}' wurde im Discovery-Snapshot nicht gefunden. Im Code muss eine Klasse mit @DefinesQuota({ key: '${quotaKey}', ... }) annotiert sein.`,
7534
+ message: `Quota '${quotaKey}' was not found in the discovery snapshot. In code, a class must be annotated with @DefinesQuota({ key: '${quotaKey}', ... }).`,
7111
7535
  field: `quotas.${quotaKey}`,
7112
7536
  value: quotaKey
7113
7537
  });
7114
7538
  } else if (approved && !approved.quotas.has(quotaKey)) {
7115
7539
  warnings.push({
7116
7540
  code: "QUOTA_NOT_APPROVED",
7117
- message: `Quota '${quotaKey}' ist nicht freigegeben (Status != approved). Nur freigegebene Quotas sind verkaufbar \u2014 im Discovery-Review freigeben (#20).`,
7541
+ message: `Quota '${quotaKey}' is not approved (status != approved). Only approved quotas are sellable \u2014 approve it in the discovery review (#20).`,
7118
7542
  field: `quotas.${quotaKey}`,
7119
7543
  value: quotaKey
7120
7544
  });
@@ -7166,7 +7590,7 @@ var BundlesService = class _BundlesService {
7166
7590
  this.mode = config.strictModeCheckMode ?? "blocking";
7167
7591
  this.marketedOnly = new Set(config.marketedOnlyFeatures ?? []);
7168
7592
  if (this.mode === "blocking" && !hasDiscoverySnapshotSource(this.snapshot, this.scanner)) {
7169
- this.logger.error("BundlesService: strictModeCheckMode=blocking ohne DiscoverySnapshot-Quelle \u2014 degradiere auf warn-only. DiscoveryModule wiren, damit die Erzwingung greift (#25).");
7593
+ this.logger.error("BundlesService: strictModeCheckMode=blocking without a DiscoverySnapshot source \u2014 degrading to warn-only. Wire DiscoveryModule so that enforcement takes effect (#25).");
7170
7594
  this.mode = "warn-only";
7171
7595
  }
7172
7596
  }
@@ -7181,7 +7605,15 @@ var BundlesService = class _BundlesService {
7181
7605
  }
7182
7606
  async getBundle(bundleId) {
7183
7607
  const bundle = await this.repo.findById(bundleId);
7184
- if (!bundle) throw new import_common43.NotFoundException(`Bundle '${bundleId}' nicht gefunden`);
7608
+ if (!bundle) {
7609
+ throw new import_common43.NotFoundException({
7610
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
7611
+ message: `Bundle '${bundleId}' not found`,
7612
+ params: {
7613
+ bundleId
7614
+ }
7615
+ });
7616
+ }
7185
7617
  const versions = await this.annotateEditability(await this.repo.listVersions(bundleId));
7186
7618
  return {
7187
7619
  bundle,
@@ -7191,18 +7623,41 @@ var BundlesService = class _BundlesService {
7191
7623
  async createBundle(data) {
7192
7624
  const existing = await this.repo.findByKey(data.projectKey, data.bundleKey);
7193
7625
  if (existing) {
7194
- throw new import_common43.UnprocessableEntityException(`Bundle '${data.bundleKey}' existiert bereits in Projekt '${data.projectKey}'`);
7626
+ throw new import_common43.UnprocessableEntityException({
7627
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_ALREADY_EXISTS,
7628
+ message: `Bundle '${data.bundleKey}' already exists in project '${data.projectKey}'`,
7629
+ params: {
7630
+ bundleKey: data.bundleKey,
7631
+ projectKey: data.projectKey
7632
+ }
7633
+ });
7195
7634
  }
7196
7635
  return this.repo.create(data);
7197
7636
  }
7198
7637
  async updateBundle(bundleId, data) {
7199
7638
  const existing = await this.repo.findById(bundleId);
7200
- if (!existing) throw new import_common43.NotFoundException(`Bundle '${bundleId}' nicht gefunden`);
7639
+ if (!existing) {
7640
+ throw new import_common43.NotFoundException({
7641
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
7642
+ message: `Bundle '${bundleId}' not found`,
7643
+ params: {
7644
+ bundleId
7645
+ }
7646
+ });
7647
+ }
7201
7648
  return this.repo.update(bundleId, data);
7202
7649
  }
7203
7650
  async softDeleteBundle(bundleId) {
7204
7651
  const existing = await this.repo.findById(bundleId);
7205
- if (!existing) throw new import_common43.NotFoundException(`Bundle '${bundleId}' nicht gefunden`);
7652
+ if (!existing) {
7653
+ throw new import_common43.NotFoundException({
7654
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
7655
+ message: `Bundle '${bundleId}' not found`,
7656
+ params: {
7657
+ bundleId
7658
+ }
7659
+ });
7660
+ }
7206
7661
  if (existing.deletedAt !== null) return;
7207
7662
  await this.repo.softDelete(bundleId);
7208
7663
  }
@@ -7214,18 +7669,27 @@ var BundlesService = class _BundlesService {
7214
7669
  async discardBundleDraft(versionId) {
7215
7670
  const existing = await this.repo.findVersionById(versionId);
7216
7671
  if (!existing) {
7217
- throw new import_common43.NotFoundException(`BundleVersion '${versionId}' nicht gefunden`);
7672
+ throw new import_common43.NotFoundException({
7673
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
7674
+ message: `BundleVersion '${versionId}' not found`,
7675
+ params: {
7676
+ bundleVersionId: versionId
7677
+ }
7678
+ });
7218
7679
  }
7219
7680
  if (existing.publishedAt !== null) {
7220
7681
  throw new import_common43.UnprocessableEntityException({
7221
- code: "BUNDLE_VERSION_ALREADY_PUBLISHED",
7222
- message: `BundleVersion '${versionId}' ist bereits published und kann nicht verworfen werden.`
7682
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_ALREADY_PUBLISHED,
7683
+ message: `BundleVersion '${versionId}' is already published and cannot be discarded.`,
7684
+ params: {
7685
+ versionId
7686
+ }
7223
7687
  });
7224
7688
  }
7225
7689
  if (typeof this.repo.deleteDraft !== "function") {
7226
7690
  throw new import_common43.UnprocessableEntityException({
7227
- code: "BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED",
7228
- message: "Discard ist im aktuellen Repository nicht implementiert. Implementiere BundleRepository.deleteDraft."
7691
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED,
7692
+ message: "Discard is not implemented in the current repository. Implement BundleRepository.deleteDraft."
7229
7693
  });
7230
7694
  }
7231
7695
  await this.repo.deleteDraft(versionId);
@@ -7239,7 +7703,13 @@ var BundlesService = class _BundlesService {
7239
7703
  async getBundleVersion(versionId) {
7240
7704
  const version = await this.repo.findVersionById(versionId);
7241
7705
  if (!version) {
7242
- throw new import_common43.NotFoundException(`BundleVersion '${versionId}' nicht gefunden`);
7706
+ throw new import_common43.NotFoundException({
7707
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
7708
+ message: `BundleVersion '${versionId}' not found`,
7709
+ params: {
7710
+ bundleVersionId: versionId
7711
+ }
7712
+ });
7243
7713
  }
7244
7714
  const [annotated] = await this.annotateEditability([
7245
7715
  version
@@ -7281,11 +7751,24 @@ var BundlesService = class _BundlesService {
7281
7751
  async createBundleDraft(data) {
7282
7752
  const bundle = await this.repo.findById(data.bundleId);
7283
7753
  if (!bundle) {
7284
- throw new import_common43.NotFoundException(`Bundle '${data.bundleId}' nicht gefunden`);
7754
+ throw new import_common43.NotFoundException({
7755
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
7756
+ message: `Bundle '${data.bundleId}' not found`,
7757
+ params: {
7758
+ bundleId: data.bundleId
7759
+ }
7760
+ });
7285
7761
  }
7286
7762
  const existingDraft = await this.repo.findCurrentDraft(data.bundleId);
7287
7763
  if (existingDraft) {
7288
- throw new import_common43.UnprocessableEntityException(`Bundle '${bundle.bundleKey}' hat bereits eine Draft-Version v${existingDraft.version}; bitte erst publishen oder verwerfen`);
7764
+ throw new import_common43.UnprocessableEntityException({
7765
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_DRAFT_ALREADY_EXISTS,
7766
+ message: `Bundle '${bundle.bundleKey}' already has a draft version v${existingDraft.version}; publish or discard it first`,
7767
+ params: {
7768
+ bundleKey: bundle.bundleKey,
7769
+ draftVersion: existingDraft.version
7770
+ }
7771
+ });
7289
7772
  }
7290
7773
  if (data.baseVersionId === void 0) {
7291
7774
  const latestLive = await this.repo.findLatestLive(data.bundleId);
@@ -7317,16 +7800,25 @@ var BundlesService = class _BundlesService {
7317
7800
  async updateBundleDraft(versionId, data) {
7318
7801
  const existing = await this.repo.findVersionById(versionId);
7319
7802
  if (!existing) {
7320
- throw new import_common43.NotFoundException(`BundleVersion '${versionId}' nicht gefunden`);
7803
+ throw new import_common43.NotFoundException({
7804
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
7805
+ message: `BundleVersion '${versionId}' not found`,
7806
+ params: {
7807
+ bundleVersionId: versionId
7808
+ }
7809
+ });
7321
7810
  }
7322
7811
  const [annotated] = await this.annotateEditability([
7323
7812
  existing
7324
7813
  ]);
7325
- const { editable } = (0, import_types6.isVersionEditable)(annotated);
7814
+ const { editable } = (0, import_types20.isVersionEditable)(annotated);
7326
7815
  if (!editable) {
7327
7816
  throw new import_common43.UnprocessableEntityException({
7328
- code: "BUNDLE_VERSION_NOT_EDITABLE",
7329
- 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.`
7817
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_EDITABLE,
7818
+ 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.`,
7819
+ params: {
7820
+ versionId
7821
+ }
7330
7822
  });
7331
7823
  }
7332
7824
  const merged = {
@@ -7357,10 +7849,22 @@ var BundlesService = class _BundlesService {
7357
7849
  async publishBundleVersion(versionId, publishMeta) {
7358
7850
  const draft = await this.repo.findVersionById(versionId);
7359
7851
  if (!draft) {
7360
- throw new import_common43.NotFoundException(`BundleVersion '${versionId}' nicht gefunden`);
7852
+ throw new import_common43.NotFoundException({
7853
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
7854
+ message: `BundleVersion '${versionId}' not found`,
7855
+ params: {
7856
+ bundleVersionId: versionId
7857
+ }
7858
+ });
7361
7859
  }
7362
7860
  if (draft.publishedAt !== null) {
7363
- throw new import_common43.UnprocessableEntityException(`BundleVersion '${versionId}' ist bereits published`);
7861
+ throw new import_common43.UnprocessableEntityException({
7862
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_ALREADY_PUBLISHED,
7863
+ message: `BundleVersion '${versionId}' is already published`,
7864
+ params: {
7865
+ versionId
7866
+ }
7867
+ });
7364
7868
  }
7365
7869
  const bundle = await this.repo.findById(draft.bundleId);
7366
7870
  const warnings = await this.runStrictCheck({
@@ -7373,8 +7877,8 @@ var BundlesService = class _BundlesService {
7373
7877
  const explicitZero = /* @__PURE__ */ __name((v) => v !== null && v !== void 0 && Number.parseFloat(String(v)) <= 0, "explicitZero");
7374
7878
  if (explicitZero(draft.monthlyNet) || explicitZero(draft.yearlyNet)) {
7375
7879
  throw new import_common43.UnprocessableEntityException({
7376
- code: "BUNDLE_VERSION_ZERO_PRICE",
7377
- message: "BundleVersion kann nicht mit explizitem Preis 0,00 published werden (Schutz gegen Seed-Platzhalter). Preisfreie Bundles: null lassen oder allowZeroPrice setzen.",
7880
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_ZERO_PRICE,
7881
+ 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.",
7378
7882
  monthlyNet: draft.monthlyNet,
7379
7883
  yearlyNet: draft.yearlyNet
7380
7884
  });
@@ -7384,23 +7888,30 @@ var BundlesService = class _BundlesService {
7384
7888
  const validFromInput = publishMeta.validFrom ?? draft.validFrom;
7385
7889
  if (!validFromInput) {
7386
7890
  throw new import_common43.UnprocessableEntityException({
7387
- code: "BUNDLE_VERSION_VALID_FROM_REQUIRED",
7388
- message: "Beim Publish muss validFrom gesetzt sein (auf Draft oder Publish-Aufruf). SPEC_V2 \xA74.2."
7891
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
7892
+ message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
7389
7893
  });
7390
7894
  }
7391
7895
  const validFrom = new Date(validFromInput);
7392
7896
  if (Number.isNaN(validFrom.getTime())) {
7393
7897
  throw new import_common43.UnprocessableEntityException({
7394
- code: "BUNDLE_VERSION_VALID_FROM_INVALID",
7395
- message: `validFrom '${validFromInput}' ist kein g\xFCltiges Datum`
7898
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_INVALID,
7899
+ message: `validFrom '${validFromInput}' is not a valid date`,
7900
+ params: {
7901
+ validFrom: validFromInput
7902
+ }
7396
7903
  });
7397
7904
  }
7398
7905
  if (previous?.validFrom) {
7399
7906
  const prevFrom = new Date(previous.validFrom);
7400
7907
  if (validFrom <= prevFrom) {
7401
7908
  throw new import_common43.UnprocessableEntityException({
7402
- code: "BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS",
7403
- message: `validFrom (${validFrom.toISOString()}) muss strikt nach validFrom der Vorg\xE4nger-Version (${previous.validFrom}) liegen.`
7909
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
7910
+ message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
7911
+ params: {
7912
+ validFrom: validFrom.toISOString(),
7913
+ previousValidFrom: previous.validFrom
7914
+ }
7404
7915
  });
7405
7916
  }
7406
7917
  if (previous.validUntil) {
@@ -7409,8 +7920,11 @@ var BundlesService = class _BundlesService {
7409
7920
  const requiredStart = new Date(prevUntil.getTime() + dayMs);
7410
7921
  if (validFrom.getTime() !== requiredStart.getTime()) {
7411
7922
  throw new import_common43.UnprocessableEntityException({
7412
- code: "BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS",
7413
- 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)}.`,
7923
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
7924
+ 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)}.`,
7925
+ params: {
7926
+ received: validFrom.toISOString().slice(0, 10)
7927
+ },
7414
7928
  requiredValidFrom: requiredStart.toISOString().slice(0, 10),
7415
7929
  previousValidUntil: previous.validUntil
7416
7930
  });
@@ -7421,17 +7935,24 @@ var BundlesService = class _BundlesService {
7421
7935
  const validUntil = validUntilInput ? new Date(validUntilInput) : null;
7422
7936
  if (validUntil && Number.isNaN(validUntil.getTime())) {
7423
7937
  throw new import_common43.UnprocessableEntityException({
7424
- code: "BUNDLE_VERSION_VALID_UNTIL_INVALID",
7425
- message: `validUntil '${validUntilInput}' ist kein g\xFCltiges Datum`
7938
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_INVALID,
7939
+ message: `validUntil '${validUntilInput}' is not a valid date`,
7940
+ params: {
7941
+ validUntil: validUntilInput
7942
+ }
7426
7943
  });
7427
7944
  }
7428
7945
  if (validUntil && validUntil <= validFrom) {
7429
7946
  throw new import_common43.UnprocessableEntityException({
7430
- code: "BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM",
7431
- message: `validUntil (${validUntil.toISOString()}) muss strikt nach validFrom (${validFrom.toISOString()}) liegen.`
7947
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM,
7948
+ message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
7949
+ params: {
7950
+ validUntil: validUntil.toISOString(),
7951
+ validFrom: validFrom.toISOString()
7952
+ }
7432
7953
  });
7433
7954
  }
7434
- const diff = previous ? (0, import_types.classifyBundleVersionDiff)({
7955
+ const diff = previous ? (0, import_types5.classifyBundleVersionDiff)({
7435
7956
  features: previous.features,
7436
7957
  quotas: previous.quotas,
7437
7958
  monthlyNet: previous.monthlyNet,
@@ -7447,8 +7968,8 @@ var BundlesService = class _BundlesService {
7447
7968
  };
7448
7969
  if (!diff.nonRegressive && !publishMeta.forceRegressive) {
7449
7970
  throw new import_common43.UnprocessableEntityException({
7450
- code: "BUNDLE_VERSION_REGRESSION",
7451
- message: "Diese BundleVersion ist regressiv (Feature entfernt / Quota gesenkt / Preis erh\xF6ht). Publishen erfordert explizites `forceRegressive: true` (UI-Best\xE4tigungsmodal mit MFA).",
7971
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_REGRESSION,
7972
+ message: "This bundle version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
7452
7973
  changes: diff.changes
7453
7974
  });
7454
7975
  }
@@ -7487,8 +8008,8 @@ var BundlesService = class _BundlesService {
7487
8008
  const hasHardBlockingWarning = blocking.some((w) => w.code === "BUNDLE_PLAN_KEY_UNKNOWN");
7488
8009
  if (this.mode === "blocking" || hasHardBlockingWarning) {
7489
8010
  throw new import_common43.UnprocessableEntityException({
7490
- code: "STRICT_MODE_VIOLATIONS",
7491
- message: "Strict-Mode-Check hat Drift gegen den Discovery-Snapshot gefunden.",
8011
+ code: import_types20.CATALOG_ERROR_CODES.STRICT_MODE_VIOLATIONS,
8012
+ message: "The strict-mode check found drift against the discovery snapshot.",
7492
8013
  warnings
7493
8014
  });
7494
8015
  }
@@ -7501,8 +8022,8 @@ var BundlesService = class _BundlesService {
7501
8022
  if (!validFromInput) {
7502
8023
  if (existing.publishedAt !== null) {
7503
8024
  throw new import_common43.UnprocessableEntityException({
7504
- code: "BUNDLE_VERSION_VALID_FROM_REQUIRED",
7505
- message: "Published BundleVersionen m\xFCssen ein validFrom behalten. Setze ein neues zuk\xFCnftiges Datum oder bearbeite die Draft vor dem Publish."
8025
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
8026
+ message: "Published bundle versions must keep a validFrom. Set a new future date, or edit the draft before publishing."
7506
8027
  });
7507
8028
  }
7508
8029
  return;
@@ -7510,14 +8031,21 @@ var BundlesService = class _BundlesService {
7510
8031
  const validFrom = this.parseRequiredBundleDate(validFromInput, "validFrom", "BUNDLE_VERSION_VALID_FROM_INVALID");
7511
8032
  if (existing.publishedAt !== null && validFrom.getTime() <= Date.now()) {
7512
8033
  throw new import_common43.UnprocessableEntityException({
7513
- code: "BUNDLE_VERSION_VALID_FROM_NOT_FUTURE",
7514
- message: `validFrom (${validFrom.toISOString()}) muss f\xFCr eine published-but-future BundleVersion weiterhin in der Zukunft liegen.`
8034
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_FUTURE,
8035
+ message: `validFrom (${validFrom.toISOString()}) must, for a published-but-future bundle version, still lie in the future.`,
8036
+ params: {
8037
+ validFrom: validFrom.toISOString()
8038
+ }
7515
8039
  });
7516
8040
  }
7517
8041
  if (validUntil && validUntil <= validFrom) {
7518
8042
  throw new import_common43.UnprocessableEntityException({
7519
- code: "BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM",
7520
- message: `validUntil (${validUntil.toISOString()}) muss strikt nach validFrom (${validFrom.toISOString()}) liegen.`
8043
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM,
8044
+ message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
8045
+ params: {
8046
+ validUntil: validUntil.toISOString(),
8047
+ validFrom: validFrom.toISOString()
8048
+ }
7521
8049
  });
7522
8050
  }
7523
8051
  const previous = await this.findPreviousBundleVersion(existing);
@@ -7526,8 +8054,12 @@ var BundlesService = class _BundlesService {
7526
8054
  if (Number.isNaN(previousValidFrom.getTime())) return;
7527
8055
  if (validFrom <= previousValidFrom) {
7528
8056
  throw new import_common43.UnprocessableEntityException({
7529
- code: "BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS",
7530
- message: `validFrom (${validFrom.toISOString()}) muss strikt nach validFrom der Vorg\xE4nger-Version (${previous.validFrom}) liegen.`
8057
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
8058
+ message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
8059
+ params: {
8060
+ validFrom: validFrom.toISOString(),
8061
+ previousValidFrom: previous.validFrom
8062
+ }
7531
8063
  });
7532
8064
  }
7533
8065
  if (!previous.validUntil) return;
@@ -7537,8 +8069,11 @@ var BundlesService = class _BundlesService {
7537
8069
  const requiredStart = new Date(previousValidUntil.getTime() + dayMs);
7538
8070
  if (validFrom.getTime() !== requiredStart.getTime()) {
7539
8071
  throw new import_common43.UnprocessableEntityException({
7540
- code: "BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS",
7541
- 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)}.`,
8072
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
8073
+ 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)}.`,
8074
+ params: {
8075
+ received: validFrom.toISOString().slice(0, 10)
8076
+ },
7542
8077
  requiredValidFrom: requiredStart.toISOString().slice(0, 10),
7543
8078
  previousValidUntil: previous.validUntil
7544
8079
  });
@@ -7557,7 +8092,12 @@ var BundlesService = class _BundlesService {
7557
8092
  if (Number.isNaN(date.getTime())) {
7558
8093
  throw new import_common43.UnprocessableEntityException({
7559
8094
  code,
7560
- message: `${field} '${input}' ist kein g\xFCltiges Datum`
8095
+ message: `${field} '${input}' is not a valid date`,
8096
+ // Keyed by the field name so these params match the publish
8097
+ // path, where the same codes carry `validFrom` / `validUntil`.
8098
+ params: {
8099
+ [field]: input
8100
+ }
7561
8101
  });
7562
8102
  }
7563
8103
  return date;
@@ -7592,6 +8132,7 @@ BundlesService = _ts_decorate44([
7592
8132
 
7593
8133
  // src/catalog/marketing-projections.service.ts
7594
8134
  var import_common44 = require("@nestjs/common");
8135
+ var import_types21 = require("@saasicat/types");
7595
8136
  function _ts_decorate45(decorators, target, key, desc) {
7596
8137
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
7597
8138
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -7623,7 +8164,13 @@ var MarketingProjectionsService = class {
7623
8164
  async getById(id) {
7624
8165
  const row = await this.repo.findById(id);
7625
8166
  if (!row) {
7626
- throw new import_common44.NotFoundException(`MarketingProjection '${id}' nicht gefunden`);
8167
+ throw new import_common44.NotFoundException({
8168
+ code: import_types21.CATALOG_ERROR_CODES.MARKETING_PROJECTION_NOT_FOUND,
8169
+ message: `MarketingProjection '${id}' not found`,
8170
+ params: {
8171
+ projectionId: id
8172
+ }
8173
+ });
7627
8174
  }
7628
8175
  return row;
7629
8176
  }
@@ -7631,7 +8178,15 @@ var MarketingProjectionsService = class {
7631
8178
  const locale = data.locale ?? "de";
7632
8179
  const existing = await this.repo.findByTarget(data.targetType, data.targetVersionId, locale);
7633
8180
  if (existing) {
7634
- throw new import_common44.ConflictException(`MarketingProjection f\xFCr ${data.targetType}/${data.targetVersionId}/${locale} existiert bereits \u2014 nutze PATCH zum Editieren`);
8181
+ throw new import_common44.ConflictException({
8182
+ code: import_types21.CATALOG_ERROR_CODES.MARKETING_PROJECTION_ALREADY_EXISTS,
8183
+ message: `Marketing projection for ${data.targetType}/${data.targetVersionId}/${locale} already exists \u2014 use PATCH to edit it`,
8184
+ params: {
8185
+ targetType: data.targetType,
8186
+ targetVersionId: data.targetVersionId,
8187
+ locale
8188
+ }
8189
+ });
7635
8190
  }
7636
8191
  return this.repo.create({
7637
8192
  ...data,
@@ -7641,14 +8196,26 @@ var MarketingProjectionsService = class {
7641
8196
  async update(id, data) {
7642
8197
  const existing = await this.repo.findById(id);
7643
8198
  if (!existing) {
7644
- throw new import_common44.NotFoundException(`MarketingProjection '${id}' nicht gefunden`);
8199
+ throw new import_common44.NotFoundException({
8200
+ code: import_types21.CATALOG_ERROR_CODES.MARKETING_PROJECTION_NOT_FOUND,
8201
+ message: `MarketingProjection '${id}' not found`,
8202
+ params: {
8203
+ projectionId: id
8204
+ }
8205
+ });
7645
8206
  }
7646
8207
  return this.repo.update(id, data);
7647
8208
  }
7648
8209
  async delete(id) {
7649
8210
  const existing = await this.repo.findById(id);
7650
8211
  if (!existing) {
7651
- throw new import_common44.NotFoundException(`MarketingProjection '${id}' nicht gefunden`);
8212
+ throw new import_common44.NotFoundException({
8213
+ code: import_types21.CATALOG_ERROR_CODES.MARKETING_PROJECTION_NOT_FOUND,
8214
+ message: `MarketingProjection '${id}' not found`,
8215
+ params: {
8216
+ projectionId: id
8217
+ }
8218
+ });
7652
8219
  }
7653
8220
  await this.repo.delete(id);
7654
8221
  }
@@ -7664,6 +8231,7 @@ MarketingProjectionsService = _ts_decorate45([
7664
8231
 
7665
8232
  // src/catalog/catalog-entries.service.ts
7666
8233
  var import_common45 = require("@nestjs/common");
8234
+ var import_types22 = require("@saasicat/types");
7667
8235
 
7668
8236
  // src/catalog/approval-signature.ts
7669
8237
  function featureApprovalSignature(featureKey, snapshot) {
@@ -7776,14 +8344,14 @@ var CatalogEntriesService = class _CatalogEntriesService {
7776
8344
  if (!this.autoSyncAtBoot) return;
7777
8345
  const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
7778
8346
  if (!snapshot) {
7779
- this.logger.warn("Discovery-Auto-Sync \xFCbersprungen: kein Snapshot (weder Token noch DiscoveryScanner injiziert). DiscoveryModule wiren (#25).");
8347
+ this.logger.warn("Discovery auto-sync skipped: no snapshot (neither token nor DiscoveryScanner injected). Wire up DiscoveryModule (#25).");
7780
8348
  return;
7781
8349
  }
7782
8350
  try {
7783
8351
  const result = await this.syncFromSnapshot(snapshot);
7784
8352
  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`);
7785
8353
  } catch (err) {
7786
- 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));
8354
+ 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));
7787
8355
  }
7788
8356
  }
7789
8357
  listCapabilities(projectKey, codeStatus) {
@@ -7813,7 +8381,14 @@ var CatalogEntriesService = class _CatalogEntriesService {
7813
8381
  async reviewFeature(projectKey, featureKey, data, reviewedBy) {
7814
8382
  const existing = await this.repo.findFeature(projectKey, featureKey);
7815
8383
  if (!existing) {
7816
- throw new import_common45.NotFoundException(`Feature '${featureKey}' in Projekt '${projectKey}' nicht gefunden`);
8384
+ throw new import_common45.NotFoundException({
8385
+ code: import_types22.CATALOG_ERROR_CODES.FEATURE_NOT_FOUND,
8386
+ message: `Feature '${featureKey}' not found in project '${projectKey}'`,
8387
+ params: {
8388
+ featureKey,
8389
+ projectKey
8390
+ }
8391
+ });
7817
8392
  }
7818
8393
  return this.repo.setFeatureReview(projectKey, featureKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => featureApprovalSignature(featureKey, snapshot)));
7819
8394
  }
@@ -7821,24 +8396,47 @@ var CatalogEntriesService = class _CatalogEntriesService {
7821
8396
  async reviewQuota(projectKey, quotaKey, data, reviewedBy) {
7822
8397
  const existing = await this.repo.findQuota(projectKey, quotaKey);
7823
8398
  if (!existing) {
7824
- throw new import_common45.NotFoundException(`Quota '${quotaKey}' in Projekt '${projectKey}' nicht gefunden`);
8399
+ throw new import_common45.NotFoundException({
8400
+ code: import_types22.CATALOG_ERROR_CODES.QUOTA_NOT_FOUND,
8401
+ message: `Quota '${quotaKey}' not found in project '${projectKey}'`,
8402
+ params: {
8403
+ quotaKey,
8404
+ projectKey
8405
+ }
8406
+ });
7825
8407
  }
7826
8408
  return this.repo.setQuotaReview(projectKey, quotaKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => {
7827
8409
  const discovered = snapshot.quotas.find((q) => q.quotaKey === quotaKey);
7828
8410
  if (!discovered) {
7829
- throw new import_common45.UnprocessableEntityException(`Quota '${quotaKey}' ist nicht im Discovery-Snapshot \u2014 eine nicht im Code deklarierte Quota kann nicht freigegeben werden`);
8411
+ throw new import_common45.UnprocessableEntityException({
8412
+ code: import_types22.CATALOG_ERROR_CODES.QUOTA_NOT_IN_DISCOVERY_SNAPSHOT,
8413
+ message: `Quota '${quotaKey}' is not in the discovery snapshot \u2014 a quota that is not declared in code cannot be approved`,
8414
+ params: {
8415
+ quotaKey
8416
+ }
8417
+ });
7830
8418
  }
7831
8419
  return quotaApprovalSignature(discovered);
7832
8420
  }));
7833
8421
  }
7834
8422
  resolveReviewUpdate(existing, target, reviewedBy, signatureOf) {
7835
8423
  if (!REVIEW_TRANSITIONS[existing.discoveryStatus].includes(target)) {
7836
- throw new import_common45.UnprocessableEntityException(`\xDCbergang '${existing.discoveryStatus}' \u2192 '${target}' ist nicht erlaubt`);
8424
+ throw new import_common45.UnprocessableEntityException({
8425
+ code: import_types22.CATALOG_ERROR_CODES.DISCOVERY_STATUS_TRANSITION_INVALID,
8426
+ message: `Transition '${existing.discoveryStatus}' \u2192 '${target}' is not allowed`,
8427
+ params: {
8428
+ from: existing.discoveryStatus,
8429
+ to: target
8430
+ }
8431
+ });
7837
8432
  }
7838
8433
  if (target === "approved") {
7839
8434
  const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
7840
8435
  if (!snapshot) {
7841
- throw new import_common45.UnprocessableEntityException("Freigabe braucht einen Discovery-Snapshot \u2014 Discovery ist nicht initialisiert (#25)");
8436
+ throw new import_common45.UnprocessableEntityException({
8437
+ code: import_types22.CATALOG_ERROR_CODES.DISCOVERY_NOT_INITIALIZED,
8438
+ message: "Approval requires a discovery snapshot \u2014 discovery is not initialised (#25)"
8439
+ });
7842
8440
  }
7843
8441
  return {
7844
8442
  discoveryStatus: "approved",
@@ -8031,7 +8629,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
8031
8629
  const claimants = claimantsByOldKey.get(entry.key);
8032
8630
  if (claimants && claimants.length > 0) {
8033
8631
  if (claimants.length > 1) {
8034
- this.logger.warn(`${type} '${entry.key}' wird von mehreren replaces-Deklarationen beansprucht (${claimants.join(", ")}); '${claimants[0]}' gewinnt \u2014 replaces im Code bereinigen (#39)`);
8632
+ 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)`);
8035
8633
  }
8036
8634
  target = claimants[0];
8037
8635
  }
@@ -8044,7 +8642,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
8044
8642
  if (changes.length === 0) return 0;
8045
8643
  const setSuccessor = type === "feature" ? this.repo.setFeatureSuccessor?.bind(this.repo) : this.repo.setQuotaSuccessor?.bind(this.repo);
8046
8644
  if (!setSuccessor) {
8047
- this.logger.warn(`CatalogEntryRepository implementiert set${type === "feature" ? "Feature" : "Quota"}Successor nicht \u2014 ${changes.length} Nachfolger-Pointer (#39) werden nicht persistiert`);
8645
+ this.logger.warn(`CatalogEntryRepository does not implement set${type === "feature" ? "Feature" : "Quota"}Successor \u2014 ${changes.length} successor pointers (#39) are not persisted`);
8048
8646
  return 0;
8049
8647
  }
8050
8648
  let replaced = 0;
@@ -8212,7 +8810,7 @@ var ListCatalogEntriesQueryDto = class {
8212
8810
  _ts_decorate48([
8213
8811
  (0, import_class_validator6.IsString)(),
8214
8812
  (0, import_class_validator6.Matches)(PROJECT_KEY_PATTERN, {
8215
- message: "projectKey muss kebab-case sein"
8813
+ message: "projectKey must be kebab-case"
8216
8814
  }),
8217
8815
  _ts_metadata35("design:type", String)
8218
8816
  ], ListCatalogEntriesQueryDto.prototype, "projectKey", void 0);
@@ -8237,7 +8835,7 @@ var ReviewCatalogEntryDto = class {
8237
8835
  _ts_decorate48([
8238
8836
  (0, import_class_validator6.IsString)(),
8239
8837
  (0, import_class_validator6.IsIn)(REVIEW_STATUSES, {
8240
- message: "discoveryStatus muss pending|approved|outdated|obsolete sein"
8838
+ message: "discoveryStatus must be pending|approved|outdated|obsolete"
8241
8839
  }),
8242
8840
  _ts_metadata35("design:type", typeof DiscoveryStatus === "undefined" ? Object : DiscoveryStatus)
8243
8841
  ], ReviewCatalogEntryDto.prototype, "discoveryStatus", void 0);
@@ -8538,6 +9136,7 @@ __name(buildCatalogEntriesController, "buildCatalogEntriesController");
8538
9136
 
8539
9137
  // src/catalog/promotions.service.ts
8540
9138
  var import_common48 = require("@nestjs/common");
9139
+ var import_types23 = require("@saasicat/types");
8541
9140
  function _ts_decorate50(decorators, target, key, desc) {
8542
9141
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
8543
9142
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -8571,7 +9170,13 @@ var PromotionsService = class {
8571
9170
  async getById(id) {
8572
9171
  const row = await this.repo.findById(id);
8573
9172
  if (!row) {
8574
- throw new import_common48.NotFoundException(`Promotion '${id}' nicht gefunden`);
9173
+ throw new import_common48.NotFoundException({
9174
+ code: import_types23.CATALOG_ERROR_CODES.PROMOTION_NOT_FOUND,
9175
+ message: `Promotion '${id}' not found`,
9176
+ params: {
9177
+ promotionId: id
9178
+ }
9179
+ });
8575
9180
  }
8576
9181
  return row;
8577
9182
  }
@@ -8581,14 +9186,26 @@ var PromotionsService = class {
8581
9186
  async update(id, data) {
8582
9187
  const existing = await this.repo.findById(id);
8583
9188
  if (!existing) {
8584
- throw new import_common48.NotFoundException(`Promotion '${id}' nicht gefunden`);
9189
+ throw new import_common48.NotFoundException({
9190
+ code: import_types23.CATALOG_ERROR_CODES.PROMOTION_NOT_FOUND,
9191
+ message: `Promotion '${id}' not found`,
9192
+ params: {
9193
+ promotionId: id
9194
+ }
9195
+ });
8585
9196
  }
8586
9197
  return this.repo.update(id, data);
8587
9198
  }
8588
9199
  async delete(id) {
8589
9200
  const existing = await this.repo.findById(id);
8590
9201
  if (!existing) {
8591
- throw new import_common48.NotFoundException(`Promotion '${id}' nicht gefunden`);
9202
+ throw new import_common48.NotFoundException({
9203
+ code: import_types23.CATALOG_ERROR_CODES.PROMOTION_NOT_FOUND,
9204
+ message: `Promotion '${id}' not found`,
9205
+ params: {
9206
+ promotionId: id
9207
+ }
9208
+ });
8592
9209
  }
8593
9210
  await this.repo.delete(id);
8594
9211
  }
@@ -8645,7 +9262,7 @@ var ListPromotionsQueryDto = class {
8645
9262
  _ts_decorate51([
8646
9263
  (0, import_class_validator7.IsString)(),
8647
9264
  (0, import_class_validator7.Matches)(PROJECT_KEY_PATTERN2, {
8648
- message: "projectKey muss kebab-case sein"
9265
+ message: "projectKey must be kebab-case"
8649
9266
  }),
8650
9267
  _ts_metadata38("design:type", String)
8651
9268
  ], ListPromotionsQueryDto.prototype, "projectKey", void 0);
@@ -8713,14 +9330,14 @@ _ts_decorate51([
8713
9330
  _ts_decorate51([
8714
9331
  (0, import_class_validator7.IsString)(),
8715
9332
  (0, import_class_validator7.Matches)(ISO_DATE_PATTERN2, {
8716
- message: "validFrom muss ISO-Datum sein"
9333
+ message: "validFrom must be an ISO date"
8717
9334
  }),
8718
9335
  _ts_metadata38("design:type", String)
8719
9336
  ], CreatePromotionDto.prototype, "validFrom", void 0);
8720
9337
  _ts_decorate51([
8721
9338
  (0, import_class_validator7.IsString)(),
8722
9339
  (0, import_class_validator7.Matches)(ISO_DATE_PATTERN2, {
8723
- message: "validTo muss ISO-Datum sein"
9340
+ message: "validTo must be an ISO date"
8724
9341
  }),
8725
9342
  _ts_metadata38("design:type", String)
8726
9343
  ], CreatePromotionDto.prototype, "validTo", void 0);
@@ -9041,7 +9658,7 @@ var ListMarketingSettingsQueryDto = class {
9041
9658
  _ts_decorate54([
9042
9659
  (0, import_class_validator8.IsString)(),
9043
9660
  (0, import_class_validator8.Matches)(PROJECT_KEY_PATTERN3, {
9044
- message: "projectKey muss kebab-case sein"
9661
+ message: "projectKey must be kebab-case"
9045
9662
  }),
9046
9663
  _ts_metadata41("design:type", String)
9047
9664
  ], ListMarketingSettingsQueryDto.prototype, "projectKey", void 0);
@@ -9055,7 +9672,7 @@ var UpdateMarketingSettingsDto = class {
9055
9672
  _ts_decorate54([
9056
9673
  (0, import_class_validator8.IsString)(),
9057
9674
  (0, import_class_validator8.Matches)(PROJECT_KEY_PATTERN3, {
9058
- message: "projectKey muss kebab-case sein"
9675
+ message: "projectKey must be kebab-case"
9059
9676
  }),
9060
9677
  _ts_metadata41("design:type", String)
9061
9678
  ], UpdateMarketingSettingsDto.prototype, "projectKey", void 0);
@@ -9067,7 +9684,7 @@ _ts_decorate54([
9067
9684
  }),
9068
9685
  (0, import_class_validator8.Matches)(LOCALE_PATTERN, {
9069
9686
  each: true,
9070
- message: "Locale muss ISO-639-1 sein"
9687
+ message: "Locale must be ISO-639-1"
9071
9688
  }),
9072
9689
  _ts_metadata41("design:type", Array)
9073
9690
  ], UpdateMarketingSettingsDto.prototype, "activeLocales", void 0);
@@ -9142,7 +9759,7 @@ __name(buildMarketingSettingsController, "buildMarketingSettingsController");
9142
9759
 
9143
9760
  // src/catalog/public-marketing-catalog.service.ts
9144
9761
  var import_common52 = require("@nestjs/common");
9145
- var import_types7 = require("@saasicat/types");
9762
+ var import_types24 = require("@saasicat/types");
9146
9763
  function _ts_decorate56(decorators, target, key, desc) {
9147
9764
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
9148
9765
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -9245,7 +9862,7 @@ var PublicMarketingCatalogService = class {
9245
9862
  });
9246
9863
  }
9247
9864
  out.sort((a, b) => b.priority - a.priority);
9248
- const requiresIndex = (0, import_types7.buildFeatureRequiresIndex)([
9865
+ const requiresIndex = (0, import_types24.buildFeatureRequiresIndex)([
9249
9866
  ...labelMeta.features
9250
9867
  ].map(([featureKey, meta]) => ({
9251
9868
  featureKey,
@@ -9299,7 +9916,7 @@ var PublicMarketingCatalogService = class {
9299
9916
  const features = [
9300
9917
  ...bv.features ?? []
9301
9918
  ];
9302
- const requiresFeatures = (0, import_types7.collectUnsatisfiedRequires)(features, requiresIndex);
9919
+ const requiresFeatures = (0, import_types24.collectUnsatisfiedRequires)(features, requiresIndex);
9303
9920
  return {
9304
9921
  bundleKey: bv.bundleKey,
9305
9922
  label: marketing?.displayLabel || bv.label,
@@ -9443,12 +10060,12 @@ var PublicMarketingCatalogService = class {
9443
10060
  };
9444
10061
  }
9445
10062
  resolvePromo(promotions, targetKey, targetType, locale, monthlyNet, yearlyNet, asOf) {
9446
- const monthlyPromo = (0, import_types7.pickActivePromo)(promotions, targetKey, locale, "monthly", asOf, targetType);
9447
- const yearlyPromo = (0, import_types7.pickActivePromo)(promotions, targetKey, locale, "yearly", asOf, targetType);
10063
+ const monthlyPromo = (0, import_types24.pickActivePromo)(promotions, targetKey, locale, "monthly", asOf, targetType);
10064
+ const yearlyPromo = (0, import_types24.pickActivePromo)(promotions, targetKey, locale, "yearly", asOf, targetType);
9448
10065
  const promo = monthlyPromo ?? yearlyPromo;
9449
10066
  if (!promo) return null;
9450
- const monthlyResult = (0, import_types7.applyPromo)(monthlyPromo, monthlyNet);
9451
- const yearlyResult = (0, import_types7.applyPromo)(yearlyPromo, yearlyNet);
10067
+ const monthlyResult = (0, import_types24.applyPromo)(monthlyPromo, monthlyNet);
10068
+ const yearlyResult = (0, import_types24.applyPromo)(yearlyPromo, yearlyNet);
9452
10069
  const i18n = promo.i18n?.[locale] ?? promo.i18n?.[DEFAULT_LOCALE] ?? {};
9453
10070
  return {
9454
10071
  type: promo.type,
@@ -9555,6 +10172,7 @@ __name(parseAsOf, "parseAsOf");
9555
10172
 
9556
10173
  // src/catalog/plans.service.ts
9557
10174
  var import_common54 = require("@nestjs/common");
10175
+ var import_types25 = require("@saasicat/types");
9558
10176
  function _ts_decorate58(decorators, target, key, desc) {
9559
10177
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
9560
10178
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -9601,24 +10219,55 @@ var PlansService = class {
9601
10219
  }
9602
10220
  async getPlan(planId) {
9603
10221
  const plan = await this.repo.findById(planId);
9604
- if (!plan) throw new import_common54.NotFoundException(`Plan '${planId}' nicht gefunden`);
10222
+ if (!plan) {
10223
+ throw new import_common54.NotFoundException({
10224
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10225
+ message: `Plan '${planId}' not found`,
10226
+ params: {
10227
+ planId
10228
+ }
10229
+ });
10230
+ }
9605
10231
  return plan;
9606
10232
  }
9607
10233
  async createPlan(data) {
9608
10234
  const existing = await this.repo.findByKey(data.projectKey, data.planKey);
9609
10235
  if (existing) {
9610
- throw new import_common54.UnprocessableEntityException(`Plan '${data.planKey}' existiert bereits in Projekt '${data.projectKey}'`);
10236
+ throw new import_common54.UnprocessableEntityException({
10237
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_ALREADY_EXISTS,
10238
+ message: `Plan '${data.planKey}' already exists in project '${data.projectKey}'`,
10239
+ params: {
10240
+ planKey: data.planKey,
10241
+ projectKey: data.projectKey
10242
+ }
10243
+ });
9611
10244
  }
9612
10245
  return this.repo.create(data);
9613
10246
  }
9614
10247
  async updatePlan(planId, data) {
9615
10248
  const existing = await this.repo.findById(planId);
9616
- if (!existing) throw new import_common54.NotFoundException(`Plan '${planId}' nicht gefunden`);
10249
+ if (!existing) {
10250
+ throw new import_common54.NotFoundException({
10251
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10252
+ message: `Plan '${planId}' not found`,
10253
+ params: {
10254
+ planId
10255
+ }
10256
+ });
10257
+ }
9617
10258
  return this.repo.update(planId, data);
9618
10259
  }
9619
10260
  async softDeletePlan(planId) {
9620
10261
  const existing = await this.repo.findById(planId);
9621
- if (!existing) throw new import_common54.NotFoundException(`Plan '${planId}' nicht gefunden`);
10262
+ if (!existing) {
10263
+ throw new import_common54.NotFoundException({
10264
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10265
+ message: `Plan '${planId}' not found`,
10266
+ params: {
10267
+ planId
10268
+ }
10269
+ });
10270
+ }
9622
10271
  if (existing.deletedAt !== null) return;
9623
10272
  await this.assertNoPublishedVersions(existing.planKey, "soft-delete");
9624
10273
  await this.repo.softDelete(planId);
@@ -9635,13 +10284,21 @@ var PlansService = class {
9635
10284
  */
9636
10285
  async hardDeletePlan(planId) {
9637
10286
  const existing = await this.repo.findById(planId);
9638
- if (!existing) throw new import_common54.NotFoundException(`Plan '${planId}' nicht gefunden`);
10287
+ if (!existing) {
10288
+ throw new import_common54.NotFoundException({
10289
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10290
+ message: `Plan '${planId}' not found`,
10291
+ params: {
10292
+ planId
10293
+ }
10294
+ });
10295
+ }
9639
10296
  await this.assertNoPublishedVersions(existing.planKey, "hard-delete");
9640
10297
  await this.assertNoDrafts(existing.planKey);
9641
10298
  if (typeof this.repo.hardDelete !== "function") {
9642
10299
  throw new import_common54.UnprocessableEntityException({
9643
- code: "PLAN_HARD_DELETE_NOT_IMPLEMENTED",
9644
- message: "Hartes L\xF6schen ist im aktuellen Repository nicht implementiert. Implementiere PlanRepository.hardDelete."
10300
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_HARD_DELETE_NOT_IMPLEMENTED,
10301
+ message: "Hard delete is not implemented in the current repository. Implement PlanRepository.hardDelete."
9645
10302
  });
9646
10303
  }
9647
10304
  await this.repo.hardDelete(planId);
@@ -9661,8 +10318,12 @@ var PlansService = class {
9661
10318
  const live = published.filter((v) => v.supersededAt === null).length;
9662
10319
  const superseded = published.length - live;
9663
10320
  throw new import_common54.UnprocessableEntityException({
9664
- code: "PLAN_HAS_PUBLISHED_VERSIONS",
9665
- 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.`,
10321
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_HAS_PUBLISHED_VERSIONS,
10322
+ 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.`,
10323
+ params: {
10324
+ planKey,
10325
+ operation: op
10326
+ },
9666
10327
  publishedCount: published.length,
9667
10328
  liveCount: live,
9668
10329
  supersededCount: superseded
@@ -9674,8 +10335,11 @@ var PlansService = class {
9674
10335
  const drafts = versions.filter((v) => v.publishedAt === null);
9675
10336
  if (drafts.length === 0) return;
9676
10337
  throw new import_common54.UnprocessableEntityException({
9677
- code: "PLAN_HAS_DRAFTS",
9678
- message: `Plan '${planKey}' hat noch ${drafts.length} offene Draft-Version(en). Verwerfe sie zuerst (DELETE /admin/catalog/plan-versions/:id) oder publishe sie.`,
10338
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_HAS_DRAFTS,
10339
+ message: `Plan '${planKey}' still has ${drafts.length} open draft version(s). Discard them first (DELETE /admin/catalog/plan-versions/:id) or publish them.`,
10340
+ params: {
10341
+ planKey
10342
+ },
9679
10343
  draftCount: drafts.length
9680
10344
  });
9681
10345
  }
@@ -9694,7 +10358,7 @@ PlansService = _ts_decorate58([
9694
10358
 
9695
10359
  // src/catalog/plan-versions.service.ts
9696
10360
  var import_common55 = require("@nestjs/common");
9697
- var import_types8 = require("@saasicat/types");
10361
+ var import_types26 = require("@saasicat/types");
9698
10362
  function _ts_decorate59(decorators, target, key, desc) {
9699
10363
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
9700
10364
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -9733,7 +10397,7 @@ var PlanVersionsService = class _PlanVersionsService {
9733
10397
  this.mode = config.strictModeCheckMode ?? "blocking";
9734
10398
  this.marketedOnly = new Set(config.marketedOnlyFeatures ?? []);
9735
10399
  if (this.mode === "blocking" && !hasDiscoverySnapshotSource(this.snapshot, this.scanner)) {
9736
- this.logger.error("PlanVersionsService: strictModeCheckMode=blocking ohne DiscoverySnapshot-Quelle \u2014 degradiere auf warn-only. DiscoveryModule wiren (#25).");
10400
+ this.logger.error("PlanVersionsService: strictModeCheckMode=blocking without a DiscoverySnapshot source \u2014 degrading to warn-only. Wire DiscoveryModule (#25).");
9737
10401
  this.mode = "warn-only";
9738
10402
  }
9739
10403
  const required = [
@@ -9747,7 +10411,7 @@ var PlanVersionsService = class _PlanVersionsService {
9747
10411
  ];
9748
10412
  for (const key of required) {
9749
10413
  if (typeof this.repo[key] !== "function") {
9750
- throw new Error(`PlanVersionsService: PlanRepository.${String(key)} fehlt \u2014 das Repository implementiert die Lifecycle-Methoden nicht.`);
10414
+ throw new Error(`PlanVersionsService: PlanRepository.${String(key)} is missing \u2014 the repository does not implement the lifecycle methods.`);
9751
10415
  }
9752
10416
  }
9753
10417
  }
@@ -9763,18 +10427,27 @@ var PlanVersionsService = class _PlanVersionsService {
9763
10427
  async discardPlanDraft(versionId) {
9764
10428
  const existing = await this.repo.findVersionById(versionId);
9765
10429
  if (!existing) {
9766
- throw new import_common55.NotFoundException(`PlanVersion '${versionId}' nicht gefunden`);
10430
+ throw new import_common55.NotFoundException({
10431
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10432
+ message: `PlanVersion '${versionId}' not found`,
10433
+ params: {
10434
+ versionId
10435
+ }
10436
+ });
9767
10437
  }
9768
10438
  if (existing.publishedAt !== null) {
9769
10439
  throw new import_common55.UnprocessableEntityException({
9770
- code: "PLAN_VERSION_ALREADY_PUBLISHED",
9771
- message: `PlanVersion '${versionId}' ist bereits published und kann nicht verworfen werden.`
10440
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_ALREADY_PUBLISHED,
10441
+ message: `PlanVersion '${versionId}' is already published and cannot be discarded.`,
10442
+ params: {
10443
+ versionId
10444
+ }
9772
10445
  });
9773
10446
  }
9774
10447
  if (typeof this.repo.deletePlanVersionDraft !== "function") {
9775
10448
  throw new import_common55.UnprocessableEntityException({
9776
- code: "PLAN_VERSION_DISCARD_NOT_IMPLEMENTED",
9777
- message: "Discard ist im aktuellen Repository nicht implementiert. Implementiere PlanRepository.deletePlanVersionDraft."
10449
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_DISCARD_NOT_IMPLEMENTED,
10450
+ message: "Discard is not implemented in the current repository. Implement PlanRepository.deletePlanVersionDraft."
9778
10451
  });
9779
10452
  }
9780
10453
  await this.repo.deletePlanVersionDraft(versionId);
@@ -9787,7 +10460,13 @@ var PlanVersionsService = class _PlanVersionsService {
9787
10460
  async listPlanVersions(planUuid) {
9788
10461
  const plan = await this.repo.findById(planUuid);
9789
10462
  if (!plan) {
9790
- throw new import_common55.NotFoundException(`Plan '${planUuid}' nicht gefunden`);
10463
+ throw new import_common55.NotFoundException({
10464
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10465
+ message: `Plan '${planUuid}' not found`,
10466
+ params: {
10467
+ planId: planUuid
10468
+ }
10469
+ });
9791
10470
  }
9792
10471
  const versions = await this.repo.listVersions(plan.planKey);
9793
10472
  return this.annotateEditability(versions);
@@ -9795,7 +10474,13 @@ var PlanVersionsService = class _PlanVersionsService {
9795
10474
  async getPlanVersion(versionId) {
9796
10475
  const version = await this.repo.findVersionById(versionId);
9797
10476
  if (!version) {
9798
- throw new import_common55.NotFoundException(`PlanVersion '${versionId}' nicht gefunden`);
10477
+ throw new import_common55.NotFoundException({
10478
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10479
+ message: `PlanVersion '${versionId}' not found`,
10480
+ params: {
10481
+ versionId
10482
+ }
10483
+ });
9799
10484
  }
9800
10485
  const [annotated] = await this.annotateEditability([
9801
10486
  version
@@ -9833,12 +10518,25 @@ var PlanVersionsService = class _PlanVersionsService {
9833
10518
  async createPlanDraft(data) {
9834
10519
  const plan = await this.repo.findById(data.planId);
9835
10520
  if (!plan) {
9836
- throw new import_common55.NotFoundException(`Plan '${data.planId}' nicht gefunden`);
10521
+ throw new import_common55.NotFoundException({
10522
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10523
+ message: `Plan '${data.planId}' not found`,
10524
+ params: {
10525
+ planId: data.planId
10526
+ }
10527
+ });
9837
10528
  }
9838
10529
  const planKey = plan.planKey;
9839
10530
  const existingDraft = await this.repo.findCurrentDraft(planKey);
9840
10531
  if (existingDraft) {
9841
- throw new import_common55.UnprocessableEntityException(`Plan '${planKey}' hat bereits eine Draft-Version v${existingDraft.version}; bitte erst publishen oder verwerfen`);
10532
+ throw new import_common55.UnprocessableEntityException({
10533
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_DRAFT_ALREADY_EXISTS,
10534
+ message: `Plan '${planKey}' already has a draft version v${existingDraft.version}; publish or discard it first`,
10535
+ params: {
10536
+ planKey,
10537
+ draftVersion: existingDraft.version
10538
+ }
10539
+ });
9842
10540
  }
9843
10541
  let baseVersionId = data.baseVersionId;
9844
10542
  if (baseVersionId === void 0) {
@@ -9863,16 +10561,25 @@ var PlanVersionsService = class _PlanVersionsService {
9863
10561
  async updatePlanDraft(versionId, data) {
9864
10562
  const existing = await this.repo.findVersionById(versionId);
9865
10563
  if (!existing) {
9866
- throw new import_common55.NotFoundException(`PlanVersion '${versionId}' nicht gefunden`);
10564
+ throw new import_common55.NotFoundException({
10565
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10566
+ message: `PlanVersion '${versionId}' not found`,
10567
+ params: {
10568
+ versionId
10569
+ }
10570
+ });
9867
10571
  }
9868
10572
  const [annotated] = await this.annotateEditability([
9869
10573
  existing
9870
10574
  ]);
9871
- const { editable } = (0, import_types8.isVersionEditable)(annotated);
10575
+ const { editable } = (0, import_types26.isVersionEditable)(annotated);
9872
10576
  if (!editable) {
9873
10577
  throw new import_common55.UnprocessableEntityException({
9874
- code: "PLAN_VERSION_NOT_EDITABLE",
9875
- 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.`
10578
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_EDITABLE,
10579
+ 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.`,
10580
+ params: {
10581
+ versionId
10582
+ }
9876
10583
  });
9877
10584
  }
9878
10585
  const merged = {
@@ -9890,10 +10597,22 @@ var PlanVersionsService = class _PlanVersionsService {
9890
10597
  async publishPlanVersion(versionId, publishMeta) {
9891
10598
  const draft = await this.repo.findVersionById(versionId);
9892
10599
  if (!draft) {
9893
- throw new import_common55.NotFoundException(`PlanVersion '${versionId}' nicht gefunden`);
10600
+ throw new import_common55.NotFoundException({
10601
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10602
+ message: `PlanVersion '${versionId}' not found`,
10603
+ params: {
10604
+ versionId
10605
+ }
10606
+ });
9894
10607
  }
9895
10608
  if (draft.publishedAt !== null) {
9896
- throw new import_common55.UnprocessableEntityException(`PlanVersion '${versionId}' ist bereits published`);
10609
+ throw new import_common55.UnprocessableEntityException({
10610
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_ALREADY_PUBLISHED,
10611
+ message: `PlanVersion '${versionId}' is already published`,
10612
+ params: {
10613
+ versionId
10614
+ }
10615
+ });
9897
10616
  }
9898
10617
  const warnings = await this.runStrictCheck({
9899
10618
  features: draft.features,
@@ -9905,8 +10624,8 @@ var PlanVersionsService = class _PlanVersionsService {
9905
10624
  const yearly = Number.parseFloat(String(draft.yearlyNet ?? "0"));
9906
10625
  if (monthly <= 0 || yearly <= 0) {
9907
10626
  throw new import_common55.UnprocessableEntityException({
9908
- code: "PLAN_VERSION_ZERO_PRICE",
9909
- message: "PlanVersion kann nicht mit Preis 0,00 published werden (Schutz gegen Seed-Platzhalter). F\xFCr bewusst kostenlose Sondervertr\xE4ge: allowZeroPrice setzen.",
10627
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_ZERO_PRICE,
10628
+ message: "A plan version cannot be published with a price of 0.00 (guard against seed placeholder). For deliberately free special contracts, set allowZeroPrice.",
9910
10629
  monthlyNet: String(draft.monthlyNet),
9911
10630
  yearlyNet: String(draft.yearlyNet)
9912
10631
  });
@@ -9916,23 +10635,30 @@ var PlanVersionsService = class _PlanVersionsService {
9916
10635
  const validFromInput = publishMeta.validFrom ?? draft.validFrom;
9917
10636
  if (!validFromInput) {
9918
10637
  throw new import_common55.UnprocessableEntityException({
9919
- code: "PLAN_VERSION_VALID_FROM_REQUIRED",
9920
- message: "Beim Publish muss validFrom gesetzt sein (auf Draft oder Publish-Aufruf). SPEC_V2 \xA74.2."
10638
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_REQUIRED,
10639
+ message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
9921
10640
  });
9922
10641
  }
9923
10642
  const validFrom = new Date(validFromInput);
9924
10643
  if (Number.isNaN(validFrom.getTime())) {
9925
10644
  throw new import_common55.UnprocessableEntityException({
9926
- code: "PLAN_VERSION_VALID_FROM_INVALID",
9927
- message: `validFrom '${validFromInput}' ist kein g\xFCltiges Datum`
10645
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_INVALID,
10646
+ message: `validFrom '${validFromInput}' is not a valid date`,
10647
+ params: {
10648
+ validFrom: validFromInput
10649
+ }
9928
10650
  });
9929
10651
  }
9930
10652
  if (previous?.validFrom) {
9931
10653
  const prevFrom = new Date(previous.validFrom);
9932
10654
  if (validFrom <= prevFrom) {
9933
10655
  throw new import_common55.UnprocessableEntityException({
9934
- code: "PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS",
9935
- message: `validFrom (${validFrom.toISOString()}) muss strikt nach validFrom der Vorg\xE4nger-Version (${previous.validFrom}) liegen.`
10656
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
10657
+ message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
10658
+ params: {
10659
+ validFrom: validFrom.toISOString(),
10660
+ previousValidFrom: previous.validFrom
10661
+ }
9936
10662
  });
9937
10663
  }
9938
10664
  if (previous.validUntil) {
@@ -9941,8 +10667,11 @@ var PlanVersionsService = class _PlanVersionsService {
9941
10667
  const requiredStart = new Date(prevUntil.getTime() + dayMs);
9942
10668
  if (validFrom.getTime() !== requiredStart.getTime()) {
9943
10669
  throw new import_common55.UnprocessableEntityException({
9944
- code: "PLAN_VERSION_VALID_FROM_NOT_GAPLESS",
9945
- 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)}.`,
10670
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_NOT_GAPLESS,
10671
+ 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)}.`,
10672
+ params: {
10673
+ received: validFrom.toISOString().slice(0, 10)
10674
+ },
9946
10675
  requiredValidFrom: requiredStart.toISOString().slice(0, 10),
9947
10676
  previousValidUntil: previous.validUntil
9948
10677
  });
@@ -9953,14 +10682,21 @@ var PlanVersionsService = class _PlanVersionsService {
9953
10682
  const validUntil = validUntilInput ? new Date(validUntilInput) : null;
9954
10683
  if (validUntil && Number.isNaN(validUntil.getTime())) {
9955
10684
  throw new import_common55.UnprocessableEntityException({
9956
- code: "PLAN_VERSION_VALID_UNTIL_INVALID",
9957
- message: `validUntil '${validUntilInput}' ist kein g\xFCltiges Datum`
10685
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_UNTIL_INVALID,
10686
+ message: `validUntil '${validUntilInput}' is not a valid date`,
10687
+ params: {
10688
+ validUntil: validUntilInput
10689
+ }
9958
10690
  });
9959
10691
  }
9960
10692
  if (validUntil && validUntil <= validFrom) {
9961
10693
  throw new import_common55.UnprocessableEntityException({
9962
- code: "PLAN_VERSION_VALID_UNTIL_BEFORE_FROM",
9963
- message: `validUntil (${validUntil.toISOString()}) muss strikt nach validFrom (${validFrom.toISOString()}) liegen.`
10694
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_UNTIL_BEFORE_FROM,
10695
+ message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
10696
+ params: {
10697
+ validUntil: validUntil.toISOString(),
10698
+ validFrom: validFrom.toISOString()
10699
+ }
9964
10700
  });
9965
10701
  }
9966
10702
  const fieldsFor = /* @__PURE__ */ __name((v) => ({
@@ -9971,14 +10707,14 @@ var PlanVersionsService = class _PlanVersionsService {
9971
10707
  monthlyNet: v.monthlyNet,
9972
10708
  yearlyNet: v.yearlyNet
9973
10709
  }), "fieldsFor");
9974
- const diff = previous ? (0, import_types.classifyPlanDiff)(fieldsFor(previous), fieldsFor(draft)) : {
10710
+ const diff = previous ? (0, import_types5.classifyPlanDiff)(fieldsFor(previous), fieldsFor(draft)) : {
9975
10711
  changes: [],
9976
10712
  nonRegressive: true
9977
10713
  };
9978
10714
  if (!diff.nonRegressive && !publishMeta.forceRegressive) {
9979
10715
  throw new import_common55.UnprocessableEntityException({
9980
- code: "PLAN_VERSION_REGRESSION",
9981
- message: "Diese PlanVersion ist regressiv (Feature entfernt / Quota gesenkt / Preis erh\xF6ht). Publishen erfordert explizites `forceRegressive: true` (UI-Best\xE4tigungsmodal mit MFA).",
10716
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_REGRESSION,
10717
+ message: "This plan version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
9982
10718
  changes: diff.changes
9983
10719
  });
9984
10720
  }
@@ -10013,36 +10749,51 @@ var PlanVersionsService = class _PlanVersionsService {
10013
10749
  async terminatePlanVersion(versionId, endsAt) {
10014
10750
  const existing = await this.repo.findVersionById(versionId);
10015
10751
  if (!existing) {
10016
- throw new import_common55.NotFoundException(`PlanVersion '${versionId}' nicht gefunden`);
10752
+ throw new import_common55.NotFoundException({
10753
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10754
+ message: `PlanVersion '${versionId}' not found`,
10755
+ params: {
10756
+ versionId
10757
+ }
10758
+ });
10017
10759
  }
10018
10760
  if (existing.publishedAt === null) {
10019
10761
  throw new import_common55.UnprocessableEntityException({
10020
- code: "PLAN_VERSION_NOT_LIVE",
10021
- message: `PlanVersion '${versionId}' ist nicht published und kann nicht terminiert werden.`
10762
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_PUBLISHED,
10763
+ message: `PlanVersion '${versionId}' is not published and cannot be terminated.`,
10764
+ params: {
10765
+ versionId
10766
+ }
10022
10767
  });
10023
10768
  }
10024
10769
  if (existing.supersededAt !== null) {
10025
10770
  throw new import_common55.UnprocessableEntityException({
10026
- code: "PLAN_VERSION_NOT_LIVE",
10027
- message: `PlanVersion '${versionId}' wurde bereits von einer Nachfolge-Version abgel\xF6st (supersededAt) und kann nicht terminiert werden.`
10771
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_SUPERSEDED,
10772
+ message: `PlanVersion '${versionId}' has already been superseded by a newer version (supersededAt) and cannot be terminated.`,
10773
+ params: {
10774
+ versionId
10775
+ }
10028
10776
  });
10029
10777
  }
10030
10778
  if (Number.isNaN(endsAt.getTime())) {
10031
10779
  throw new import_common55.UnprocessableEntityException({
10032
- code: "PLAN_TERMINATE_INVALID_DATE",
10033
- message: "endsAt ist kein g\xFCltiges Datum."
10780
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_TERMINATE_INVALID_DATE,
10781
+ message: "endsAt is not a valid date."
10034
10782
  });
10035
10783
  }
10036
10784
  if (endsAt.getTime() <= Date.now()) {
10037
10785
  throw new import_common55.UnprocessableEntityException({
10038
- code: "PLAN_TERMINATE_DATE_NOT_FUTURE",
10039
- message: `endsAt (${endsAt.toISOString()}) muss strikt in der Zukunft liegen.`
10786
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_TERMINATE_DATE_NOT_FUTURE,
10787
+ message: `endsAt (${endsAt.toISOString()}) must lie strictly in the future.`,
10788
+ params: {
10789
+ endsAt: endsAt.toISOString()
10790
+ }
10040
10791
  });
10041
10792
  }
10042
10793
  if (typeof this.repo.terminate !== "function") {
10043
10794
  throw new import_common55.UnprocessableEntityException({
10044
- code: "PLAN_TERMINATE_NOT_IMPLEMENTED",
10045
- message: "Terminate ist im aktuellen Repository nicht implementiert. Implementiere PlanRepository.terminate."
10795
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_TERMINATE_NOT_IMPLEMENTED,
10796
+ message: "Terminate is not implemented in the current repository. Implement PlanRepository.terminate."
10046
10797
  });
10047
10798
  }
10048
10799
  return this.repo.terminate(versionId, endsAt);
@@ -10060,8 +10811,8 @@ var PlanVersionsService = class _PlanVersionsService {
10060
10811
  if (blockingStrictModeWarnings(warnings).length === 0) return;
10061
10812
  if (this.mode === "blocking") {
10062
10813
  throw new import_common55.UnprocessableEntityException({
10063
- code: "STRICT_MODE_VIOLATIONS",
10064
- message: "Strict-Mode-Check hat Drift gegen den Discovery-Snapshot gefunden.",
10814
+ code: import_types26.CATALOG_ERROR_CODES.STRICT_MODE_VIOLATIONS,
10815
+ message: "The strict-mode check found drift against the discovery snapshot.",
10065
10816
  warnings
10066
10817
  });
10067
10818
  }
@@ -10128,7 +10879,7 @@ var CreateBundleDto = class {
10128
10879
  _ts_decorate60([
10129
10880
  (0, import_class_validator9.IsString)(),
10130
10881
  (0, import_class_validator9.Matches)(PROJECT_KEY_PATTERN4, {
10131
- message: 'projectKey muss kebab-case sein (z. B. "my-app")'
10882
+ message: 'projectKey must be kebab-case (e.g. "my-app")'
10132
10883
  }),
10133
10884
  (0, import_class_validator9.MaxLength)(64),
10134
10885
  _ts_metadata47("design:type", String)
@@ -10136,7 +10887,7 @@ _ts_decorate60([
10136
10887
  _ts_decorate60([
10137
10888
  (0, import_class_validator9.IsString)(),
10138
10889
  (0, import_class_validator9.Matches)(KEY_PATTERN, {
10139
- message: 'bundleKey muss SCREAMING_SNAKE_CASE sein (z. B. "BANKING")'
10890
+ message: 'bundleKey must be SCREAMING_SNAKE_CASE (e.g. "BANKING")'
10140
10891
  }),
10141
10892
  (0, import_class_validator9.MaxLength)(64),
10142
10893
  _ts_metadata47("design:type", String)
@@ -10236,7 +10987,7 @@ _ts_decorate60([
10236
10987
  }),
10237
10988
  (0, import_class_validator9.Matches)(FEATURE_KEY_PATTERN, {
10238
10989
  each: true,
10239
- message: "features-Eintr\xE4ge m\xFCssen SCREAMING_SNAKE_CASE sein"
10990
+ message: "features entries must be SCREAMING_SNAKE_CASE"
10240
10991
  }),
10241
10992
  _ts_metadata47("design:type", Array)
10242
10993
  ], CreateBundleVersionDraftDto.prototype, "features", void 0);
@@ -10262,7 +11013,7 @@ _ts_decorate60([
10262
11013
  no_symbols: false
10263
11014
  }),
10264
11015
  (0, import_class_validator9.Matches)(DECIMAL_PATTERN, {
10265
- message: 'monthlyNet muss Decimal mit max. 2 Nachkommastellen sein (z. B. "9.90")'
11016
+ message: 'monthlyNet must be a decimal with at most 2 fraction digits (e.g. "9.90")'
10266
11017
  }),
10267
11018
  _ts_metadata47("design:type", Object)
10268
11019
  ], CreateBundleVersionDraftDto.prototype, "monthlyNet", void 0);
@@ -10273,7 +11024,7 @@ _ts_decorate60([
10273
11024
  no_symbols: false
10274
11025
  }),
10275
11026
  (0, import_class_validator9.Matches)(DECIMAL_PATTERN, {
10276
- message: "yearlyNet muss Decimal mit max. 2 Nachkommastellen sein"
11027
+ message: "yearlyNet must be a decimal with at most 2 fraction digits"
10277
11028
  }),
10278
11029
  _ts_metadata47("design:type", Object)
10279
11030
  ], CreateBundleVersionDraftDto.prototype, "yearlyNet", void 0);
@@ -10297,7 +11048,7 @@ _ts_decorate60([
10297
11048
  (0, import_class_validator9.IsOptional)(),
10298
11049
  (0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
10299
11050
  (0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
10300
- message: "validFrom muss ISO-Datum sein (YYYY-MM-DD)"
11051
+ message: "validFrom must be an ISO date (YYYY-MM-DD)"
10301
11052
  }),
10302
11053
  _ts_metadata47("design:type", Object)
10303
11054
  ], CreateBundleVersionDraftDto.prototype, "validFrom", void 0);
@@ -10305,7 +11056,7 @@ _ts_decorate60([
10305
11056
  (0, import_class_validator9.IsOptional)(),
10306
11057
  (0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
10307
11058
  (0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
10308
- message: "validUntil muss ISO-Datum sein (YYYY-MM-DD)"
11059
+ message: "validUntil must be an ISO date (YYYY-MM-DD)"
10309
11060
  }),
10310
11061
  _ts_metadata47("design:type", Object)
10311
11062
  ], CreateBundleVersionDraftDto.prototype, "validUntil", void 0);
@@ -10377,7 +11128,7 @@ _ts_decorate60([
10377
11128
  (0, import_class_validator9.IsOptional)(),
10378
11129
  (0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
10379
11130
  (0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
10380
- message: "validFrom muss ISO-Datum sein (YYYY-MM-DD)"
11131
+ message: "validFrom must be an ISO date (YYYY-MM-DD)"
10381
11132
  }),
10382
11133
  _ts_metadata47("design:type", Object)
10383
11134
  ], UpdateBundleVersionDraftDto.prototype, "validFrom", void 0);
@@ -10385,7 +11136,7 @@ _ts_decorate60([
10385
11136
  (0, import_class_validator9.IsOptional)(),
10386
11137
  (0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
10387
11138
  (0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
10388
- message: "validUntil muss ISO-Datum sein (YYYY-MM-DD)"
11139
+ message: "validUntil must be an ISO date (YYYY-MM-DD)"
10389
11140
  }),
10390
11141
  _ts_metadata47("design:type", Object)
10391
11142
  ], UpdateBundleVersionDraftDto.prototype, "validUntil", void 0);
@@ -10422,7 +11173,7 @@ _ts_decorate60([
10422
11173
  (0, import_class_validator9.IsOptional)(),
10423
11174
  (0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
10424
11175
  (0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
10425
- message: "validFrom muss ISO-Datum sein (YYYY-MM-DD)"
11176
+ message: "validFrom must be an ISO date (YYYY-MM-DD)"
10426
11177
  }),
10427
11178
  _ts_metadata47("design:type", Object)
10428
11179
  ], PublishBundleVersionDto.prototype, "validFrom", void 0);
@@ -10430,7 +11181,7 @@ _ts_decorate60([
10430
11181
  (0, import_class_validator9.IsOptional)(),
10431
11182
  (0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
10432
11183
  (0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
10433
- message: "validUntil muss ISO-Datum sein (YYYY-MM-DD)"
11184
+ message: "validUntil must be an ISO date (YYYY-MM-DD)"
10434
11185
  }),
10435
11186
  _ts_metadata47("design:type", Object)
10436
11187
  ], PublishBundleVersionDto.prototype, "validUntil", void 0);
@@ -10734,7 +11485,7 @@ var CreateMarketingProjectionDto = class {
10734
11485
  _ts_decorate62([
10735
11486
  (0, import_class_validator10.IsString)(),
10736
11487
  (0, import_class_validator10.Matches)(PROJECT_KEY_PATTERN5, {
10737
- message: "projectKey muss kebab-case sein"
11488
+ message: "projectKey must be kebab-case"
10738
11489
  }),
10739
11490
  (0, import_class_validator10.MaxLength)(64),
10740
11491
  _ts_metadata49("design:type", String)
@@ -10752,7 +11503,7 @@ _ts_decorate62([
10752
11503
  (0, import_class_validator10.IsOptional)(),
10753
11504
  (0, import_class_validator10.IsString)(),
10754
11505
  (0, import_class_validator10.Matches)(LOCALE_PATTERN2, {
10755
- message: 'locale muss ISO-639-1 sein (z. B. "de", "en", "de-AT")'
11506
+ message: 'locale must be ISO-639-1 (e.g. "de", "en", "de-AT")'
10756
11507
  }),
10757
11508
  _ts_metadata49("design:type", String)
10758
11509
  ], CreateMarketingProjectionDto.prototype, "locale", void 0);
@@ -11083,7 +11834,7 @@ var CreatePlanDto = class {
11083
11834
  _ts_decorate64([
11084
11835
  (0, import_class_validator11.IsString)(),
11085
11836
  (0, import_class_validator11.Matches)(PROJECT_KEY_PATTERN6, {
11086
- message: 'projectKey muss kebab-case sein (z. B. "my-app")'
11837
+ message: 'projectKey must be kebab-case (e.g. "my-app")'
11087
11838
  }),
11088
11839
  (0, import_class_validator11.MaxLength)(64),
11089
11840
  _ts_metadata51("design:type", String)
@@ -11091,7 +11842,7 @@ _ts_decorate64([
11091
11842
  _ts_decorate64([
11092
11843
  (0, import_class_validator11.IsString)(),
11093
11844
  (0, import_class_validator11.Matches)(KEY_PATTERN2, {
11094
- message: 'planKey muss SCREAMING_SNAKE_CASE sein (z. B. "STARTER")'
11845
+ message: 'planKey must be SCREAMING_SNAKE_CASE (e.g. "STARTER")'
11095
11846
  }),
11096
11847
  (0, import_class_validator11.MaxLength)(64),
11097
11848
  _ts_metadata51("design:type", String)
@@ -11200,7 +11951,7 @@ _ts_decorate65([
11200
11951
  }),
11201
11952
  (0, import_class_validator12.Matches)(FEATURE_KEY_PATTERN2, {
11202
11953
  each: true,
11203
- message: "features-Eintr\xE4ge m\xFCssen SCREAMING_SNAKE_CASE sein"
11954
+ message: "features entries must be SCREAMING_SNAKE_CASE"
11204
11955
  }),
11205
11956
  _ts_metadata52("design:type", Array)
11206
11957
  ], CreatePlanVersionDraftDto.prototype, "features", void 0);
@@ -11212,7 +11963,7 @@ _ts_decorate65([
11212
11963
  }),
11213
11964
  (0, import_class_validator12.Matches)(FEATURE_KEY_PATTERN2, {
11214
11965
  each: true,
11215
- message: "bundles-Eintr\xE4ge m\xFCssen SCREAMING_SNAKE_CASE sein"
11966
+ message: "bundles entries must be SCREAMING_SNAKE_CASE"
11216
11967
  }),
11217
11968
  _ts_metadata52("design:type", Array)
11218
11969
  ], CreatePlanVersionDraftDto.prototype, "bundles", void 0);
@@ -11222,13 +11973,13 @@ _ts_decorate65([
11222
11973
  ], CreatePlanVersionDraftDto.prototype, "quotas", void 0);
11223
11974
  _ts_decorate65([
11224
11975
  (0, import_class_validator12.Matches)(DECIMAL_PATTERN2, {
11225
- message: 'monthlyNet muss Decimal mit max. 2 Nachkommastellen sein (z. B. "9.90")'
11976
+ message: 'monthlyNet must be a decimal with at most 2 fraction digits (e.g. "9.90")'
11226
11977
  }),
11227
11978
  _ts_metadata52("design:type", String)
11228
11979
  ], CreatePlanVersionDraftDto.prototype, "monthlyNet", void 0);
11229
11980
  _ts_decorate65([
11230
11981
  (0, import_class_validator12.Matches)(DECIMAL_PATTERN2, {
11231
- message: "yearlyNet muss Decimal mit max. 2 Nachkommastellen sein"
11982
+ message: "yearlyNet must be a decimal with at most 2 fraction digits"
11232
11983
  }),
11233
11984
  _ts_metadata52("design:type", String)
11234
11985
  ], CreatePlanVersionDraftDto.prototype, "yearlyNet", void 0);
@@ -11472,7 +12223,7 @@ function buildPlansController(guards) {
11472
12223
  }
11473
12224
  assertVersionsService() {
11474
12225
  if (!this.versionsService) {
11475
- throw new Error("PlanVersionsService nicht registriert \u2014 App hat den Plan-Lifecycle-Editor nicht aktiviert.");
12226
+ throw new Error("PlanVersionsService is not registered \u2014 the app has not enabled the plan lifecycle editor.");
11476
12227
  }
11477
12228
  }
11478
12229
  };
@@ -11837,9 +12588,9 @@ function runPreflight(input) {
11837
12588
  __name(runPreflight, "runPreflight");
11838
12589
  function formatPreflightReport(report) {
11839
12590
  const lines = [];
11840
- lines.push(`Preflight (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7 Gesamt: ${report.counts.total}`, "");
12591
+ lines.push(`Preflight (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7 Total: ${report.counts.total}`, "");
11841
12592
  if (report.findings.length === 0) {
11842
- lines.push(" \u2713 Keine Strict-Mode-Verst\xF6\xDFe gefunden.");
12593
+ lines.push(" \u2713 No strict-mode violations found.");
11843
12594
  return lines.join("\n");
11844
12595
  }
11845
12596
  let lastEntity = "";
@@ -11909,9 +12660,9 @@ function validateSeedAgainstSnapshot(input) {
11909
12660
  __name(validateSeedAgainstSnapshot, "validateSeedAgainstSnapshot");
11910
12661
  function formatSeedGateReport(report) {
11911
12662
  const lines = [];
11912
- lines.push(`Seed-Gate (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7 Gesamt: ${report.counts.total}`, "");
12663
+ lines.push(`Seed-Gate (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7 Total: ${report.counts.total}`, "");
11913
12664
  if (report.findings.length === 0) {
11914
- lines.push(" \u2713 Alle geseedeten Plan-/Bundle-Features sind discovered (und, sofern gepr\xFCft, approved).");
12665
+ lines.push(" \u2713 Every seeded plan/bundle feature is discovered (and approved where checked).");
11915
12666
  return lines.join("\n");
11916
12667
  }
11917
12668
  let lastEntity = "";
@@ -11941,7 +12692,7 @@ var DiscoverySnapshotNotFoundError = class extends Error {
11941
12692
  }
11942
12693
  path;
11943
12694
  constructor(path) {
11944
- super(`Discovery-Snapshot-Datei nicht gefunden: ${path}. Booten Sie zuerst die App mit DISCOVERY_SNAPSHOT_PATH oder DiscoveryModule.forRoot({ snapshotPath: '\u2026' }), damit der Scanner die Datei beim OnApplicationBootstrap-Hook schreibt.`), this.path = path;
12695
+ super(`Discovery snapshot file not found: ${path}. Boot the app first with DISCOVERY_SNAPSHOT_PATH or DiscoveryModule.forRoot({ snapshotPath: '\u2026' }) so that the scanner writes the file on the OnApplicationBootstrap hook.`), this.path = path;
11945
12696
  this.name = "DiscoverySnapshotNotFoundError";
11946
12697
  }
11947
12698
  };
@@ -11968,10 +12719,10 @@ function runSeedGateFromFile(options) {
11968
12719
  } catch (err) {
11969
12720
  if (err instanceof DiscoverySnapshotNotFoundError) {
11970
12721
  if (mode === "blocking") {
11971
- error(`[seed-gate] FEHLER: Kein Discovery-Snapshot unter ${options.snapshotPath} \u2014 im blocking-Modus Pflicht (Headless-Scan vor dem Seed ausf\xFChren, #23).`);
12722
+ error(`[seed-gate] ERROR: no discovery snapshot at ${options.snapshotPath} \u2014 required in blocking mode (run the headless scan before seeding, #23).`);
11972
12723
  return exit(4);
11973
12724
  }
11974
- warn(`[seed-gate] Kein Discovery-Snapshot unter ${options.snapshotPath} \u2014 \xFCbersprungen (report-only).`);
12725
+ warn(`[seed-gate] No discovery snapshot at ${options.snapshotPath} \u2014 skipped (report-only).`);
11975
12726
  return null;
11976
12727
  }
11977
12728
  throw err;
@@ -11985,10 +12736,10 @@ function runSeedGateFromFile(options) {
11985
12736
  log(formatSeedGateReport(report));
11986
12737
  if (report.overall === "error") {
11987
12738
  if (mode === "blocking") {
11988
- error("[seed-gate] blocking: Verst\xF6\xDFe gefunden \u2014 Seed wird abgebrochen (#23).");
12739
+ error("[seed-gate] blocking: violations found \u2014 aborting the seed (#23).");
11989
12740
  return exit(seedGateExitCode(report));
11990
12741
  }
11991
- warn("[seed-gate] report-only: Verst\xF6\xDFe gefunden, Seed l\xE4uft trotzdem weiter.");
12742
+ warn("[seed-gate] report-only: violations found, seeding continues anyway.");
11992
12743
  }
11993
12744
  return report;
11994
12745
  }
@@ -12007,7 +12758,7 @@ __export(checkout_offer_exports, {
12007
12758
 
12008
12759
  // src/checkout-offer/checkout-offer.service.ts
12009
12760
  var import_common60 = require("@nestjs/common");
12010
- var import_types9 = require("@saasicat/types");
12761
+ var import_types27 = require("@saasicat/types");
12011
12762
 
12012
12763
  // src/checkout-offer/tokens.ts
12013
12764
  var CHECKOUT_OFFER_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/CheckoutOfferRepository");
@@ -12050,7 +12801,13 @@ var CheckoutOfferService = class {
12050
12801
  async getById(id) {
12051
12802
  const row = await this.repo.findById(id);
12052
12803
  if (!row) {
12053
- throw new import_common60.NotFoundException(`CheckoutOffer '${id}' nicht gefunden`);
12804
+ throw new import_common60.NotFoundException({
12805
+ code: import_types27.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_NOT_FOUND,
12806
+ message: `CheckoutOffer '${id}' not found`,
12807
+ params: {
12808
+ offerId: id
12809
+ }
12810
+ });
12054
12811
  }
12055
12812
  return row;
12056
12813
  }
@@ -12068,7 +12825,7 @@ var CheckoutOfferService = class {
12068
12825
  /** Customization in onboarding — only while the offer is `open`. */
12069
12826
  async update(id, data) {
12070
12827
  const existing = await this.getById(id);
12071
- this.assertOpen(existing, "\xE4ndern");
12828
+ this.assertOpen(existing, "changed");
12072
12829
  const next = this.normalizeUpdateData(existing, data);
12073
12830
  await this.assertFeatureRequiresSatisfied({
12074
12831
  projectKey: existing.projectKey,
@@ -12086,7 +12843,7 @@ var CheckoutOfferService = class {
12086
12843
  */
12087
12844
  async consume(id) {
12088
12845
  const existing = await this.getById(id);
12089
- this.assertOpen(existing, "verbrauchen");
12846
+ this.assertOpen(existing, "consumed");
12090
12847
  await this.assertBundleVersionsStillBookable(existing);
12091
12848
  return this.repo.consume(id);
12092
12849
  }
@@ -12104,19 +12861,19 @@ var CheckoutOfferService = class {
12104
12861
  const entries = await this.catalogEntries.listFeatures({
12105
12862
  projectKey: input.projectKey
12106
12863
  });
12107
- const requiresIndex = (0, import_types9.buildFeatureRequiresIndex)(entries);
12864
+ const requiresIndex = (0, import_types27.buildFeatureRequiresIndex)(entries);
12108
12865
  if (requiresIndex.size === 0) return;
12109
12866
  const selected = /* @__PURE__ */ new Set([
12110
12867
  ...await this.resolvePlanFeatures(input),
12111
12868
  ...await this.resolveBundleFeatures(input)
12112
12869
  ]);
12113
- const missingRequires = (0, import_types9.collectUnsatisfiedRequires)([
12870
+ const missingRequires = (0, import_types27.collectUnsatisfiedRequires)([
12114
12871
  ...selected
12115
12872
  ], requiresIndex);
12116
12873
  if (missingRequires.length > 0) {
12117
12874
  throw new import_common60.UnprocessableEntityException({
12118
- code: "CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED",
12119
- message: `Die Paket-Auswahl deckt nicht alle Feature-Abh\xE4ngigkeiten: [${missingRequires.join(", ")}] fehlen in Plan + gew\xE4hlten Bundles.`,
12875
+ code: import_types27.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED,
12876
+ message: `The selected plan does not cover all feature dependencies: [${missingRequires.join(", ")}] are missing from the plan + selected bundles.`,
12120
12877
  missingRequires
12121
12878
  });
12122
12879
  }
@@ -12196,8 +12953,8 @@ var CheckoutOfferService = class {
12196
12953
  }
12197
12954
  if (violations.length > 0) {
12198
12955
  throw new import_common60.UnprocessableEntityException({
12199
- code: "CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE",
12200
- message: "Mindestens eine BundleVersion aus dem CheckoutOffer ist nicht mehr buchbar.",
12956
+ code: import_types27.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE,
12957
+ message: "At least one bundle version from the checkout offer is no longer bookable.",
12201
12958
  violations
12202
12959
  });
12203
12960
  }
@@ -12251,15 +13008,15 @@ var CheckoutOfferService = class {
12251
13008
  const hasPlan = lineItems.some((item) => item.kind === "plan");
12252
13009
  if (!hasPlan) {
12253
13010
  throw new import_common60.UnprocessableEntityException({
12254
- code: "CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED",
12255
- message: "CheckoutOffer ben\xF6tigt eine eingefrorene Plan-LineItem."
13011
+ code: import_types27.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED,
13012
+ message: "A checkout offer requires a frozen plan line item."
12256
13013
  });
12257
13014
  }
12258
13015
  const missingBundleVersionIds = input.bundleVersionIds.filter((versionId) => !lineItems.some((item) => item.kind === "bundle" && item.sourceVersionId === versionId));
12259
13016
  if (missingBundleVersionIds.length > 0) {
12260
13017
  throw new import_common60.UnprocessableEntityException({
12261
- code: "CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED",
12262
- message: "Jede ausgew\xE4hlte BundleVersion ben\xF6tigt eine eingefrorene Bundle-LineItem.",
13018
+ code: import_types27.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED,
13019
+ message: "Every selected bundle version requires a frozen bundle line item.",
12263
13020
  bundleVersionIds: missingBundleVersionIds
12264
13021
  });
12265
13022
  }
@@ -12291,13 +13048,36 @@ var CheckoutOfferService = class {
12291
13048
  }
12292
13049
  assertOpen(existing, action) {
12293
13050
  if (existing.status === "consumed") {
12294
- throw new import_common60.ConflictException(`CheckoutOffer '${existing.id}' ist bereits verbraucht und kann nicht ${action} werden`);
13051
+ throw new import_common60.ConflictException({
13052
+ code: import_types27.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_ALREADY_CONSUMED,
13053
+ message: `Checkout offer '${existing.id}' has already been consumed and cannot be ${action}`,
13054
+ params: {
13055
+ offerId: existing.id,
13056
+ action
13057
+ }
13058
+ });
12295
13059
  }
12296
13060
  if (existing.status === "expired") {
12297
- throw new import_common60.ConflictException(`CheckoutOffer '${existing.id}' ist abgelaufen und kann nicht ${action} werden`);
13061
+ throw new import_common60.ConflictException({
13062
+ code: import_types27.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_EXPIRED,
13063
+ message: `Checkout offer '${existing.id}' has expired and cannot be ${action}`,
13064
+ params: {
13065
+ offerId: existing.id,
13066
+ action,
13067
+ validUntil: existing.validUntil ?? null
13068
+ }
13069
+ });
12298
13070
  }
12299
13071
  if (this.isExpired(existing)) {
12300
- throw new import_common60.ConflictException(`CheckoutOffer '${existing.id}' ist abgelaufen und kann nicht ${action} werden`);
13072
+ throw new import_common60.ConflictException({
13073
+ code: import_types27.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_EXPIRED,
13074
+ message: `CheckoutOffer '${existing.id}' has expired and cannot be ${action}`,
13075
+ params: {
13076
+ offerId: existing.id,
13077
+ action,
13078
+ validUntil: existing.validUntil ?? null
13079
+ }
13080
+ });
12301
13081
  }
12302
13082
  }
12303
13083
  isExpired(row) {
@@ -12315,7 +13095,7 @@ var CheckoutOfferService = class {
12315
13095
  if (!value) return false;
12316
13096
  const validUntil = new Date(value).getTime();
12317
13097
  if (Number.isNaN(validUntil)) return true;
12318
- return validUntil < (0, import_types9.startOfUtcDay)(new Date(nowMs)).getTime();
13098
+ return validUntil < (0, import_types27.startOfUtcDay)(new Date(nowMs)).getTime();
12319
13099
  }
12320
13100
  };
12321
13101
  CheckoutOfferService = _ts_decorate68([
@@ -12379,7 +13159,7 @@ var CreateCheckoutOfferDto = class {
12379
13159
  _ts_decorate69([
12380
13160
  (0, import_class_validator13.IsString)(),
12381
13161
  (0, import_class_validator13.Matches)(PROJECT_KEY_PATTERN7, {
12382
- message: "projectKey muss kebab-case sein"
13162
+ message: "projectKey must be kebab-case"
12383
13163
  }),
12384
13164
  (0, import_class_validator13.MaxLength)(64),
12385
13165
  _ts_metadata55("design:type", String)
@@ -12867,7 +13647,7 @@ async function runHeadlessDiscoveryScan(appModule, options = {}) {
12867
13647
  await app.close();
12868
13648
  }
12869
13649
  } catch (err) {
12870
- console.error("[discovery-scan] FEHLER:", err instanceof Error ? err.message : err);
13650
+ console.error("[discovery-scan] ERROR:", err instanceof Error ? err.message : err);
12871
13651
  return exit(4);
12872
13652
  }
12873
13653
  }
@@ -12946,7 +13726,7 @@ __export(platform_exports, {
12946
13726
  // src/platform/saas-platform.module.ts
12947
13727
  var import_common79 = require("@nestjs/common");
12948
13728
  var import_core9 = require("@nestjs/core");
12949
- var import_types11 = require("@saasicat/types");
13729
+ var import_types31 = require("@saasicat/types");
12950
13730
 
12951
13731
  // src/promo/module.ts
12952
13732
  var import_common70 = require("@nestjs/common");
@@ -12987,7 +13767,7 @@ var PromoCodeExpirer = class _PromoCodeExpirer {
12987
13767
  const codes = await this.promoRepo.expireDueCodes(now);
12988
13768
  const redemptions = await this.redemptionRepo.expireDueRedemptions(now);
12989
13769
  if (codes > 0 || redemptions > 0) {
12990
- this.logger.log(`PromoCodeExpirer: ${codes} Codes, ${redemptions} Redemptions abgelaufen.`);
13770
+ this.logger.log(`PromoCodeExpirer: ${codes} Codes, ${redemptions} redemptions expired.`);
12991
13771
  }
12992
13772
  }
12993
13773
  };
@@ -13016,6 +13796,7 @@ var import_common68 = require("@nestjs/common");
13016
13796
 
13017
13797
  // src/promo/rate-limit.guard.ts
13018
13798
  var import_common67 = require("@nestjs/common");
13799
+ var import_types28 = require("@saasicat/types");
13019
13800
  function _ts_decorate75(decorators, target, key, desc) {
13020
13801
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
13021
13802
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -13027,6 +13808,18 @@ var IP_WINDOW_MS = 6e4;
13027
13808
  var IP_LIMIT = 20;
13028
13809
  var SESSION_WINDOW_MS = 60 * 6e4;
13029
13810
  var SESSION_LIMIT = 50;
13811
+ function rateLimited(windowMs) {
13812
+ return new import_common67.HttpException({
13813
+ ...codedError(import_types28.REGISTRATION_ERROR_CODES.RATE_LIMITED, {
13814
+ retryAfterSeconds: Math.ceil(windowMs / 1e3)
13815
+ }),
13816
+ retryAfterSeconds: Math.ceil(windowMs / 1e3),
13817
+ valid: false,
13818
+ // Superseded by `code` — kept until all consumers read `code`.
13819
+ reason: "RATE_LIMITED"
13820
+ }, import_common67.HttpStatus.TOO_MANY_REQUESTS);
13821
+ }
13822
+ __name(rateLimited, "rateLimited");
13030
13823
  var PromoCodeRateLimitGuard = class {
13031
13824
  static {
13032
13825
  __name(this, "PromoCodeRateLimitGuard");
@@ -13038,16 +13831,10 @@ var PromoCodeRateLimitGuard = class {
13038
13831
  const ipKey = ipFingerprint(req);
13039
13832
  const sessionKey = req.user?.id ?? null;
13040
13833
  if (this.exceeded(this.ipBuckets, ipKey, IP_WINDOW_MS, IP_LIMIT)) {
13041
- throw new import_common67.HttpException({
13042
- valid: false,
13043
- reason: "RATE_LIMITED"
13044
- }, import_common67.HttpStatus.TOO_MANY_REQUESTS);
13834
+ throw rateLimited(IP_WINDOW_MS);
13045
13835
  }
13046
13836
  if (sessionKey && this.exceeded(this.sessionBuckets, sessionKey, SESSION_WINDOW_MS, SESSION_LIMIT)) {
13047
- throw new import_common67.HttpException({
13048
- valid: false,
13049
- reason: "RATE_LIMITED"
13050
- }, import_common67.HttpStatus.TOO_MANY_REQUESTS);
13837
+ throw rateLimited(SESSION_WINDOW_MS);
13051
13838
  }
13052
13839
  return true;
13053
13840
  }
@@ -13111,21 +13898,21 @@ var PreviewPromoCodeDto = class {
13111
13898
  _ts_decorate76([
13112
13899
  (0, import_class_validator14.IsString)(),
13113
13900
  (0, import_class_validator14.Matches)(CODE_PATTERN2, {
13114
- message: 'code muss aus A\u2013Z, 0\u20139, "-" und "_" bestehen (4\u201332 Zeichen)'
13901
+ message: 'code must consist of A\u2013Z, 0\u20139, "-" and "_" (4\u201332 characters)'
13115
13902
  }),
13116
13903
  _ts_metadata59("design:type", String)
13117
13904
  ], PreviewPromoCodeDto.prototype, "code", void 0);
13118
13905
  _ts_decorate76([
13119
13906
  (0, import_class_validator14.IsString)(),
13120
13907
  (0, import_class_validator14.Matches)(PLAN_OR_CYCLE_PATTERN3, {
13121
- message: "plan muss SCREAMING_SNAKE_CASE sein"
13908
+ message: "plan must be SCREAMING_SNAKE_CASE"
13122
13909
  }),
13123
13910
  _ts_metadata59("design:type", String)
13124
13911
  ], PreviewPromoCodeDto.prototype, "plan", void 0);
13125
13912
  _ts_decorate76([
13126
13913
  (0, import_class_validator14.IsString)(),
13127
13914
  (0, import_class_validator14.Matches)(PLAN_OR_CYCLE_PATTERN3, {
13128
- message: "billingCycle muss SCREAMING_SNAKE_CASE sein (z. B. MONTHLY, YEARLY)"
13915
+ message: "billingCycle must be SCREAMING_SNAKE_CASE (e.g. MONTHLY, YEARLY)"
13129
13916
  }),
13130
13917
  _ts_metadata59("design:type", String)
13131
13918
  ], PreviewPromoCodeDto.prototype, "billingCycle", void 0);
@@ -13686,7 +14473,7 @@ var import_class_validator16 = require("class-validator");
13686
14473
 
13687
14474
  // src/setup/setup.service.ts
13688
14475
  var import_common71 = require("@nestjs/common");
13689
- var import_types10 = require("@saasicat/types");
14476
+ var import_types29 = require("@saasicat/types");
13690
14477
  var import_node_crypto3 = require("crypto");
13691
14478
  var import_qrcode = __toESM(require("qrcode"), 1);
13692
14479
 
@@ -13742,9 +14529,7 @@ var SetupService = class {
13742
14529
  await this.assertNoSuperAdmin();
13743
14530
  const email = req.email.trim().toLowerCase();
13744
14531
  if (!email.includes("@")) {
13745
- throw new import_common71.BadRequestException({
13746
- code: import_types10.SETUP_ERROR_CODES.INVALID_EMAIL
13747
- });
14532
+ throw new import_common71.BadRequestException(codedError(import_types29.SETUP_ERROR_CODES.INVALID_EMAIL));
13748
14533
  }
13749
14534
  const generated = !req.password;
13750
14535
  const password = req.password ?? (0, import_node_crypto3.randomBytes)(GENERATED_PASSWORD_BYTES).toString("base64url");
@@ -13755,9 +14540,9 @@ var SetupService = class {
13755
14540
  password
13756
14541
  });
13757
14542
  } catch (err) {
13758
- if ((0, import_types10.isPlatformUserExistsError)(err)) {
14543
+ if ((0, import_types29.isPlatformUserExistsError)(err)) {
13759
14544
  throw new import_common71.ConflictException({
13760
- code: import_types10.SETUP_ERROR_CODES.EMAIL_EXISTS,
14545
+ code: import_types29.SETUP_ERROR_CODES.EMAIL_EXISTS,
13761
14546
  message: err.message
13762
14547
  });
13763
14548
  }
@@ -13793,21 +14578,20 @@ var SetupService = class {
13793
14578
  const expected = process.env[this.config.setupTokenEnvVar]?.trim();
13794
14579
  if (!expected) {
13795
14580
  throw new import_common71.ForbiddenException({
13796
- code: import_types10.SETUP_ERROR_CODES.SETUP_DISABLED,
13797
- message: `${this.config.setupTokenEnvVar} ist nicht gesetzt \u2014 Setup ist deaktiviert.`
14581
+ code: import_types29.SETUP_ERROR_CODES.SETUP_DISABLED,
14582
+ message: `${this.config.setupTokenEnvVar} is not set \u2014 setup is disabled.`,
14583
+ params: {
14584
+ envVar: this.config.setupTokenEnvVar
14585
+ }
13798
14586
  });
13799
14587
  }
13800
14588
  if (!token || !timingSafeEqualStr(token, expected)) {
13801
- throw new import_common71.UnauthorizedException({
13802
- code: import_types10.SETUP_ERROR_CODES.INVALID_SETUP_TOKEN
13803
- });
14589
+ throw new import_common71.UnauthorizedException(codedError(import_types29.SETUP_ERROR_CODES.INVALID_SETUP_TOKEN));
13804
14590
  }
13805
14591
  }
13806
14592
  async assertNoSuperAdmin() {
13807
14593
  if (await this.users.countSuperAdmins() > 0) {
13808
- throw new import_common71.ConflictException({
13809
- code: import_types10.SETUP_ERROR_CODES.SETUP_ALREADY_DONE
13810
- });
14594
+ throw new import_common71.ConflictException(codedError(import_types29.SETUP_ERROR_CODES.SETUP_ALREADY_DONE));
13811
14595
  }
13812
14596
  }
13813
14597
  };
@@ -14082,6 +14866,7 @@ StaticEntitlementService = _ts_decorate84([
14082
14866
  // src/platform/static-feature.guard.ts
14083
14867
  var import_common75 = require("@nestjs/common");
14084
14868
  var import_core7 = require("@nestjs/core");
14869
+ var import_types30 = require("@saasicat/types");
14085
14870
  function _ts_decorate85(decorators, target, key, desc) {
14086
14871
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
14087
14872
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -14120,15 +14905,32 @@ var StaticFeatureGuard = class {
14120
14905
  if (!required || required.length === 0) return true;
14121
14906
  const request = context.switchToHttp().getRequest();
14122
14907
  const user = request.user;
14123
- if (!user) throw new import_common75.ForbiddenException("Nicht authentifiziert");
14908
+ if (!user) {
14909
+ throw new import_common75.ForbiddenException({
14910
+ code: import_types30.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
14911
+ message: "Not authenticated"
14912
+ });
14913
+ }
14124
14914
  const role = this.config?.userRoleResolver ? this.config.userRoleResolver(user) : user.role ?? user.platformRole;
14125
14915
  if (role === "SUPER_ADMIN") return true;
14126
14916
  const tenantId = this.config?.tenantIdResolver ? this.config.tenantIdResolver(request) : request.tenantId ?? user.tenantId;
14127
- if (!tenantId) throw new import_common75.ForbiddenException("Kein Mandant zugeordnet");
14917
+ if (!tenantId) {
14918
+ throw new import_common75.ForbiddenException({
14919
+ code: import_types30.AUTH_ERROR_CODES.NO_TENANT_ASSIGNED,
14920
+ message: "No tenant assigned"
14921
+ });
14922
+ }
14128
14923
  const snap = await this.entitlements.snapshot(tenantId);
14129
14924
  const allowed = required.some((f) => snap.features.includes(f));
14130
14925
  if (!allowed) {
14131
- throw new import_common75.ForbiddenException(`Feature ${required.join(" / ")} nicht im aktuellen Plan enthalten.`);
14926
+ const body = {
14927
+ code: import_types30.FEATURE_NOT_LICENSED,
14928
+ featureKey: required[0],
14929
+ featureKeys: required,
14930
+ offers: [],
14931
+ message: `Feature ${required.join(" / ")} is not included in the current plan.`
14932
+ };
14933
+ throw new import_common75.ForbiddenException(body);
14132
14934
  }
14133
14935
  return true;
14134
14936
  }
@@ -14371,7 +15173,7 @@ function buildTenantManifestController(options) {
14371
15173
  async get(req) {
14372
15174
  const tenantId = req.user?.tenantId ?? req.tenantId;
14373
15175
  if (!tenantId) {
14374
- throw new Error("Kein Mandant im Request \u2014 TenantManifest braucht user.tenantId.");
15176
+ throw new Error("No tenant on the request \u2014 TenantManifest requires user.tenantId.");
14375
15177
  }
14376
15178
  return this.svc.getManifest(tenantId);
14377
15179
  }
@@ -14608,7 +15410,7 @@ var SaasPlatformModule = class _SaasPlatformModule {
14608
15410
  throw new Error(`SaasPlatformModule.forRoot: entitlement active or required by the standard stack, but adapters are missing: ${missing.join(", ")}`);
14609
15411
  }
14610
15412
  if (persistence) {
14611
- (0, import_types11.assertPersistenceCapabilities)(persistence.capabilities, {
15413
+ (0, import_types31.assertPersistenceCapabilities)(persistence.capabilities, {
14612
15414
  transactions: true,
14613
15415
  pessimisticLocking: true
14614
15416
  }, "SaasPlatformModule entitlement (transactional enforceLimit)");
@@ -15064,8 +15866,8 @@ var REGISTRATION_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/Regist
15064
15866
 
15065
15867
  // src/registration/pending-registration.service.ts
15066
15868
  var import_common80 = require("@nestjs/common");
15067
- var import_types12 = require("@saasicat/types");
15068
- var import_types13 = require("@saasicat/types");
15869
+ var import_types32 = require("@saasicat/types");
15870
+ var import_types33 = require("@saasicat/types");
15069
15871
 
15070
15872
  // src/registration/pricing.ts
15071
15873
  function round24(value) {
@@ -15183,7 +15985,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15183
15985
  });
15184
15986
  } catch (error) {
15185
15987
  const message = error instanceof Error ? error.message : String(error);
15186
- this.logger.warn(`Audit-Log fehlgeschlagen (${eventType}): ${message}`);
15988
+ this.logger.warn(`Audit log write failed (${eventType}): ${message}`);
15187
15989
  }
15188
15990
  }
15189
15991
  /** Public signup plan list for step 3 (frontend `usePublicPlans()`). */
@@ -15193,28 +15995,20 @@ var PendingRegistrationService = class _PendingRegistrationService {
15193
15995
  async selectPlan(input, context) {
15194
15996
  const pending = await this.repo.findById(input.pendingRegistrationId);
15195
15997
  if (!pending) {
15196
- throw new import_common80.NotFoundException({
15197
- code: "PENDING_REGISTRATION_NOT_FOUND"
15198
- });
15998
+ throw new import_common80.NotFoundException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
15199
15999
  }
15200
16000
  if (pending.expiresAt.getTime() < Date.now()) {
15201
- throw new import_common80.BadRequestException({
15202
- code: "PENDING_REGISTRATION_EXPIRED"
15203
- });
16001
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
15204
16002
  }
15205
16003
  if (pending.status !== "EMAIL_VERIFIED" && pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
15206
- throw new import_common80.BadRequestException({
15207
- code: "INVALID_REGISTRATION_STATE"
15208
- });
16004
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE));
15209
16005
  }
15210
16006
  const plan = await this.planCatalog.findPublicSignupPlan(input.planId);
15211
16007
  if (!plan) {
15212
- throw new import_common80.BadRequestException({
15213
- code: "PLAN_NOT_AVAILABLE"
15214
- });
16008
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE));
15215
16009
  }
15216
16010
  const now = /* @__PURE__ */ new Date();
15217
- const expiresAt = new Date(now.getTime() + import_types13.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
16011
+ const expiresAt = new Date(now.getTime() + import_types33.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
15218
16012
  const updated = await this.repo.update(pending.id, {
15219
16013
  status: "PLAN_SELECTED",
15220
16014
  currentStep: 4,
@@ -15227,37 +16021,27 @@ var PendingRegistrationService = class _PendingRegistrationService {
15227
16021
  return {
15228
16022
  pendingRegistrationId: updated.id,
15229
16023
  status: updated.status,
15230
- nextStep: import_types13.REGISTRATION_STEP_BY_STATUS[updated.status],
16024
+ nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[updated.status],
15231
16025
  selectedPlanId: updated.selectedPlanId ?? plan.id
15232
16026
  };
15233
16027
  }
15234
16028
  async startCheckout(input, context) {
15235
16029
  const pending = await this.repo.findById(input.pendingRegistrationId);
15236
16030
  if (!pending) {
15237
- throw new import_common80.NotFoundException({
15238
- code: "PENDING_REGISTRATION_NOT_FOUND"
15239
- });
16031
+ throw new import_common80.NotFoundException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
15240
16032
  }
15241
16033
  if (pending.expiresAt.getTime() < Date.now()) {
15242
- throw new import_common80.BadRequestException({
15243
- code: "PENDING_REGISTRATION_EXPIRED"
15244
- });
16034
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
15245
16035
  }
15246
16036
  if (pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
15247
- throw new import_common80.BadRequestException({
15248
- code: "PLAN_NOT_SELECTED"
15249
- });
16037
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED));
15250
16038
  }
15251
16039
  if (!pending.selectedPlanId) {
15252
- throw new import_common80.BadRequestException({
15253
- code: "PLAN_NOT_SELECTED"
15254
- });
16040
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED));
15255
16041
  }
15256
16042
  const plan = await this.planCatalog.findPublicSignupPlan(pending.selectedPlanId);
15257
16043
  if (!plan) {
15258
- throw new import_common80.BadRequestException({
15259
- code: "PLAN_NOT_AVAILABLE"
15260
- });
16044
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE));
15261
16045
  }
15262
16046
  const session = await this.paymentProvider.createCheckoutSession({
15263
16047
  pendingRegistrationId: pending.id,
@@ -15267,7 +16051,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15267
16051
  cancelUrl: input.cancelUrl
15268
16052
  });
15269
16053
  const now = /* @__PURE__ */ new Date();
15270
- const expiresAt = new Date(now.getTime() + import_types13.PENDING_CHECKOUT_TTL_DAYS * 24 * 60 * 60 * 1e3);
16054
+ const expiresAt = new Date(now.getTime() + import_types33.PENDING_CHECKOUT_TTL_DAYS * 24 * 60 * 60 * 1e3);
15271
16055
  const updated = await this.repo.update(pending.id, {
15272
16056
  status: "CHECKOUT_STARTED",
15273
16057
  currentStep: 4,
@@ -15282,7 +16066,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15282
16066
  return {
15283
16067
  pendingRegistrationId: updated.id,
15284
16068
  status: updated.status,
15285
- nextStep: import_types13.REGISTRATION_STEP_BY_STATUS[updated.status],
16069
+ nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[updated.status],
15286
16070
  checkoutSessionId: session.sessionId,
15287
16071
  checkoutUrl: session.checkoutUrl
15288
16072
  };
@@ -15308,7 +16092,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15308
16092
  rawPayload: input.rawPayload
15309
16093
  });
15310
16094
  if (!claimed) {
15311
- this.logger.warn(`Idempotenz: PaymentEvent ${input.eventId} bereits verarbeitet \u2014 Duplikat verworfen.`);
16095
+ this.logger.warn(`Idempotency: payment event ${input.eventId} already processed \u2014 duplicate discarded.`);
15312
16096
  await this.record("PAYMENT_DUPLICATE_IGNORED", null, context, {
15313
16097
  eventId: input.eventId,
15314
16098
  sessionId: input.sessionId
@@ -15358,7 +16142,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15358
16142
  tenantId: result.tenantId,
15359
16143
  subscriptionId: result.subscriptionId
15360
16144
  });
15361
- this.logger.log(`Aktivierung erfolgreich: pending=${pending.id} \u2192 user=${result.userId} tenant=${result.tenantId} subscription=${result.subscriptionId}`);
16145
+ this.logger.log(`Activation succeeded: pending=${pending.id} \u2192 user=${result.userId} tenant=${result.tenantId} subscription=${result.subscriptionId}`);
15362
16146
  return {
15363
16147
  activated: true,
15364
16148
  result
@@ -15386,11 +16170,11 @@ var PendingRegistrationService = class _PendingRegistrationService {
15386
16170
  await this.repo.delete(pending.id);
15387
16171
  } catch (error) {
15388
16172
  const message = error instanceof Error ? error.message : String(error);
15389
- this.logger.warn(`Cleanup: PendingRegistration ${pending.id} konnte nicht geloescht werden (${message}).`);
16173
+ this.logger.warn(`Cleanup: pending registration ${pending.id} could not be deleted (${message}).`);
15390
16174
  }
15391
16175
  }
15392
16176
  if (expired.length > 0) {
15393
- this.logger.log(`Cleanup: ${expired.length} abgelaufene PendingRegistrations geloescht.`);
16177
+ this.logger.log(`Cleanup: ${expired.length} expired PendingRegistrations deleted.`);
15394
16178
  }
15395
16179
  return {
15396
16180
  deleted: expired.length,
@@ -15400,7 +16184,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15400
16184
  async start(input, context) {
15401
16185
  const email = normalizeEmail(input.email);
15402
16186
  if (await this.userLookup.hasActiveUser(email)) {
15403
- this.logger.log(`register/start fuer aktiven Account ignoriert (${email}).`);
16187
+ this.logger.log(`register/start ignored for an active account (${email}).`);
15404
16188
  await this.record("REGISTRATION_NEUTRAL_ACTIVE_USER", null, context);
15405
16189
  return {
15406
16190
  neutral: true
@@ -15438,8 +16222,8 @@ var PendingRegistrationService = class _PendingRegistrationService {
15438
16222
  const code = generateOtpCode();
15439
16223
  const otpHash = hashOtpCode(code);
15440
16224
  const now = /* @__PURE__ */ new Date();
15441
- const otpExpiresAt = new Date(now.getTime() + import_types13.OTP_TTL_MINUTES * 60 * 1e3);
15442
- const expiresAt = new Date(now.getTime() + import_types13.PENDING_EMAIL_TTL_HOURS * 60 * 60 * 1e3);
16225
+ const otpExpiresAt = new Date(now.getTime() + import_types33.OTP_TTL_MINUTES * 60 * 1e3);
16226
+ const expiresAt = new Date(now.getTime() + import_types33.PENDING_EMAIL_TTL_HOURS * 60 * 60 * 1e3);
15443
16227
  const pending = await this.repo.create({
15444
16228
  tenantName: input.tenantName.trim(),
15445
16229
  tenantSlug: slug,
@@ -15466,14 +16250,12 @@ var PendingRegistrationService = class _PendingRegistrationService {
15466
16250
  await this.record("OTP_VERIFY_FAILED", null, context, {
15467
16251
  reason: "unknown_email"
15468
16252
  });
15469
- throw new import_common80.BadRequestException({
15470
- code: "OTP_INVALID"
15471
- });
16253
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_INVALID));
15472
16254
  }
15473
16255
  if (pending.status !== "PENDING_EMAIL_VERIFICATION") {
15474
16256
  return {
15475
16257
  status: pending.status,
15476
- nextStep: import_types13.REGISTRATION_STEP_BY_STATUS[pending.status],
16258
+ nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[pending.status],
15477
16259
  pendingRegistrationId: pending.id
15478
16260
  };
15479
16261
  }
@@ -15481,43 +16263,33 @@ var PendingRegistrationService = class _PendingRegistrationService {
15481
16263
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
15482
16264
  reason: "no_otp_set"
15483
16265
  });
15484
- throw new import_common80.BadRequestException({
15485
- code: "OTP_INVALID"
15486
- });
16266
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_INVALID));
15487
16267
  }
15488
16268
  if (pending.otpExpiresAt.getTime() < Date.now()) {
15489
16269
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
15490
16270
  reason: "expired"
15491
16271
  });
15492
- throw new import_common80.BadRequestException({
15493
- code: "OTP_EXPIRED"
15494
- });
16272
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_EXPIRED));
15495
16273
  }
15496
16274
  const maxAttempts = resolveOtpVerifyMaxAttempts();
15497
16275
  if (pending.otpAttemptCount >= maxAttempts) {
15498
16276
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
15499
16277
  reason: "locked"
15500
16278
  });
15501
- throw new import_common80.BadRequestException({
15502
- code: "OTP_LOCKED"
15503
- });
16279
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_LOCKED));
15504
16280
  }
15505
16281
  const attemptCount = await this.repo.incrementOtpAttemptCount(pending.id);
15506
16282
  if (attemptCount > maxAttempts) {
15507
16283
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
15508
16284
  reason: "locked"
15509
16285
  });
15510
- throw new import_common80.BadRequestException({
15511
- code: "OTP_LOCKED"
15512
- });
16286
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_LOCKED));
15513
16287
  }
15514
16288
  if (!verifyOtpCode(pending.otpHash, code)) {
15515
16289
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
15516
16290
  reason: "wrong_code"
15517
16291
  });
15518
- throw new import_common80.BadRequestException({
15519
- code: "OTP_INVALID"
15520
- });
16292
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_INVALID));
15521
16293
  }
15522
16294
  const now = /* @__PURE__ */ new Date();
15523
16295
  const updated = await this.repo.update(pending.id, {
@@ -15526,12 +16298,12 @@ var PendingRegistrationService = class _PendingRegistrationService {
15526
16298
  emailVerifiedAt: now,
15527
16299
  otpHash: null,
15528
16300
  otpExpiresAt: null,
15529
- expiresAt: new Date(now.getTime() + import_types13.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3)
16301
+ expiresAt: new Date(now.getTime() + import_types33.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3)
15530
16302
  });
15531
16303
  await this.record("OTP_VERIFIED", updated.id, context);
15532
16304
  return {
15533
16305
  status: updated.status,
15534
- nextStep: import_types13.REGISTRATION_STEP_BY_STATUS[updated.status],
16306
+ nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[updated.status],
15535
16307
  pendingRegistrationId: updated.id
15536
16308
  };
15537
16309
  }
@@ -15556,15 +16328,15 @@ var PendingRegistrationService = class _PendingRegistrationService {
15556
16328
  }
15557
16329
  async regenerateOtp(pending) {
15558
16330
  const now = /* @__PURE__ */ new Date();
15559
- const windowMs = import_types13.OTP_RATE_LIMIT_WINDOW_MINUTES * 60 * 1e3;
16331
+ const windowMs = import_types33.OTP_RATE_LIMIT_WINDOW_MINUTES * 60 * 1e3;
15560
16332
  const withinWindow = pending.lastOtpSentAt !== null && now.getTime() - pending.lastOtpSentAt.getTime() < windowMs;
15561
- if (withinWindow && pending.otpSendCount >= import_types13.OTP_RATE_LIMIT_MAX_SENDS) {
15562
- this.logger.warn(`OTP-Rate-Limit erreicht fuer ${pending.email} \u2014 Send wird still verworfen.`);
16333
+ if (withinWindow && pending.otpSendCount >= import_types33.OTP_RATE_LIMIT_MAX_SENDS) {
16334
+ this.logger.warn(`OTP rate limit reached for ${pending.email} \u2014 send is being skipped.`);
15563
16335
  return;
15564
16336
  }
15565
16337
  const code = generateOtpCode();
15566
16338
  const otpHash = hashOtpCode(code);
15567
- const otpExpiresAt = new Date(now.getTime() + import_types13.OTP_TTL_MINUTES * 60 * 1e3);
16339
+ const otpExpiresAt = new Date(now.getTime() + import_types33.OTP_TTL_MINUTES * 60 * 1e3);
15568
16340
  const otpSendCount = withinWindow ? pending.otpSendCount + 1 : 1;
15569
16341
  await this.repo.update(pending.id, {
15570
16342
  otpHash,
@@ -15587,7 +16359,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15587
16359
  });
15588
16360
  } catch (error) {
15589
16361
  const message = error instanceof Error ? error.message : String(error);
15590
- this.logger.error(`OTP-Mail an ${pending.email} fehlgeschlagen: ${message}`);
16362
+ this.logger.error(`OTP email to ${pending.email} failed: ${message}`);
15591
16363
  }
15592
16364
  }
15593
16365
  /**
@@ -15601,7 +16373,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15601
16373
  }
15602
16374
  const token = await this.resumeTokenSigner.sign({
15603
16375
  pendingRegistrationId: pending.id,
15604
- ttlMinutes: import_types12.REGISTRATION_RESUME_TTL_MINUTES
16376
+ ttlMinutes: import_types32.REGISTRATION_RESUME_TTL_MINUTES
15605
16377
  });
15606
16378
  const base = (this.resumeBaseUrl ?? "http://localhost").replace(/\/+$/, "");
15607
16379
  const url = `${base}/login?resume=${encodeURIComponent(token)}`;
@@ -15614,7 +16386,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15614
16386
  });
15615
16387
  } catch (error) {
15616
16388
  const message = error instanceof Error ? error.message : String(error);
15617
- this.logger.error(`Resume-Mail an ${pending.email} fehlgeschlagen: ${message}`);
16389
+ this.logger.error(`Resume email to ${pending.email} failed: ${message}`);
15618
16390
  }
15619
16391
  }
15620
16392
  /**
@@ -15628,28 +16400,20 @@ var PendingRegistrationService = class _PendingRegistrationService {
15628
16400
  */
15629
16401
  async resumeWithToken(input, context) {
15630
16402
  if (!this.resumeTokenSigner) {
15631
- throw new import_common80.BadRequestException({
15632
- code: "RESUME_NOT_CONFIGURED"
15633
- });
16403
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.RESUME_NOT_CONFIGURED));
15634
16404
  }
15635
16405
  let decoded;
15636
16406
  try {
15637
16407
  decoded = await this.resumeTokenSigner.verify(input.token);
15638
16408
  } catch {
15639
- throw new import_common80.BadRequestException({
15640
- code: "RESUME_TOKEN_INVALID"
15641
- });
16409
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID));
15642
16410
  }
15643
16411
  const pending = await this.repo.findById(decoded.pendingRegistrationId);
15644
16412
  if (!pending) {
15645
- throw new import_common80.BadRequestException({
15646
- code: "RESUME_TOKEN_INVALID"
15647
- });
16413
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID));
15648
16414
  }
15649
16415
  if (pending.expiresAt.getTime() < Date.now()) {
15650
- throw new import_common80.BadRequestException({
15651
- code: "PENDING_REGISTRATION_EXPIRED"
15652
- });
16416
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
15653
16417
  }
15654
16418
  await this.record("REGISTRATION_NEUTRAL_REPLAY", pending.id, context, {
15655
16419
  via: "resume_token",
@@ -15658,7 +16422,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15658
16422
  return {
15659
16423
  pendingRegistrationId: pending.id,
15660
16424
  status: pending.status,
15661
- nextStep: import_types13.REGISTRATION_STEP_BY_STATUS[pending.status],
16425
+ nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[pending.status],
15662
16426
  snapshot: toSnapshot2(pending)
15663
16427
  };
15664
16428
  }
@@ -15677,9 +16441,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15677
16441
  /** Returns the (app-specific) configurator catalog. */
15678
16442
  async getConfiguratorCatalog() {
15679
16443
  if (!this.configuratorLookup) {
15680
- throw new import_common80.BadRequestException({
15681
- code: "CONFIGURATOR_NOT_CONFIGURED"
15682
- });
16444
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED));
15683
16445
  }
15684
16446
  return this.configuratorLookup.getCatalog();
15685
16447
  }
@@ -15696,32 +16458,22 @@ var PendingRegistrationService = class _PendingRegistrationService {
15696
16458
  */
15697
16459
  async saveConfiguration(input, context) {
15698
16460
  if (!this.configuratorLookup) {
15699
- throw new import_common80.BadRequestException({
15700
- code: "CONFIGURATOR_NOT_CONFIGURED"
15701
- });
16461
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED));
15702
16462
  }
15703
16463
  const pending = await this.repo.findById(input.pendingRegistrationId);
15704
16464
  if (!pending) {
15705
- throw new import_common80.NotFoundException({
15706
- code: "PENDING_REGISTRATION_NOT_FOUND"
15707
- });
16465
+ throw new import_common80.NotFoundException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
15708
16466
  }
15709
16467
  if (pending.expiresAt.getTime() < Date.now()) {
15710
- throw new import_common80.BadRequestException({
15711
- code: "PENDING_REGISTRATION_EXPIRED"
15712
- });
16468
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
15713
16469
  }
15714
16470
  if (pending.status !== "EMAIL_VERIFIED" && pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
15715
- throw new import_common80.BadRequestException({
15716
- code: "INVALID_REGISTRATION_STATE"
15717
- });
16471
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE));
15718
16472
  }
15719
16473
  const catalog = await this.configuratorLookup.getCatalog();
15720
16474
  const model = catalog.models.find((m) => m.id === input.selection.modelId);
15721
16475
  if (!model) {
15722
- throw new import_common80.BadRequestException({
15723
- code: "MODEL_NOT_AVAILABLE"
15724
- });
16476
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE));
15725
16477
  }
15726
16478
  let promoEval;
15727
16479
  if (input.selection.appliedPromoCode && this.promoPreview) {
@@ -15753,7 +16505,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15753
16505
  };
15754
16506
  }
15755
16507
  const now = /* @__PURE__ */ new Date();
15756
- const expiresAt = new Date(now.getTime() + import_types13.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
16508
+ const expiresAt = new Date(now.getTime() + import_types33.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
15757
16509
  const updated = await this.repo.update(pending.id, {
15758
16510
  status: "PLAN_SELECTED",
15759
16511
  currentStep: 4,
@@ -15772,7 +16524,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15772
16524
  return {
15773
16525
  pendingRegistrationId: updated.id,
15774
16526
  status: updated.status,
15775
- nextStep: import_types13.REGISTRATION_STEP_BY_STATUS[updated.status],
16527
+ nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[updated.status],
15776
16528
  selection: input.selection,
15777
16529
  breakdown
15778
16530
  };
@@ -15785,28 +16537,20 @@ var PendingRegistrationService = class _PendingRegistrationService {
15785
16537
  */
15786
16538
  async previewConfigPromo(pendingRegistrationId, code) {
15787
16539
  if (!this.configuratorLookup) {
15788
- throw new import_common80.BadRequestException({
15789
- code: "CONFIGURATOR_NOT_CONFIGURED"
15790
- });
16540
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED));
15791
16541
  }
15792
16542
  const pending = await this.repo.findById(pendingRegistrationId);
15793
16543
  if (!pending) {
15794
- throw new import_common80.NotFoundException({
15795
- code: "PENDING_REGISTRATION_NOT_FOUND"
15796
- });
16544
+ throw new import_common80.NotFoundException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
15797
16545
  }
15798
16546
  const config = pending.configJson;
15799
16547
  if (!config) {
15800
- throw new import_common80.BadRequestException({
15801
- code: "CONFIG_NOT_SAVED"
15802
- });
16548
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.CONFIG_NOT_SAVED));
15803
16549
  }
15804
16550
  const catalog = await this.configuratorLookup.getCatalog();
15805
16551
  const model = catalog.models.find((m) => m.id === config.modelId);
15806
16552
  if (!model) {
15807
- throw new import_common80.BadRequestException({
15808
- code: "MODEL_NOT_AVAILABLE"
15809
- });
16553
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE));
15810
16554
  }
15811
16555
  let promoEval;
15812
16556
  if (code && this.promoPreview) {
@@ -15864,7 +16608,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15864
16608
  return candidate;
15865
16609
  }
15866
16610
  }
15867
- throw new Error(`Konnte keinen freien Slug fuer "${tenantName}" finden.`);
16611
+ throw new Error(`Could not find a free slug for "${tenantName}".`);
15868
16612
  }
15869
16613
  };
15870
16614
  PendingRegistrationService = _ts_decorate90([
@@ -15914,9 +16658,9 @@ function normalizeEmail(value) {
15914
16658
  __name(normalizeEmail, "normalizeEmail");
15915
16659
  function resolveOtpVerifyMaxAttempts() {
15916
16660
  const raw = process.env.SAAS_PLATFORM_OTP_VERIFY_MAX_ATTEMPTS;
15917
- if (!raw) return import_types13.OTP_VERIFY_MAX_ATTEMPTS;
16661
+ if (!raw) return import_types33.OTP_VERIFY_MAX_ATTEMPTS;
15918
16662
  const parsed = Number.parseInt(raw, 10);
15919
- return Number.isInteger(parsed) && parsed > 0 ? parsed : import_types13.OTP_VERIFY_MAX_ATTEMPTS;
16663
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : import_types33.OTP_VERIFY_MAX_ATTEMPTS;
15920
16664
  }
15921
16665
  __name(resolveOtpVerifyMaxAttempts, "resolveOtpVerifyMaxAttempts");
15922
16666
  function toSnapshot2(pending) {
@@ -15989,7 +16733,7 @@ var RegistrationCleanupCron = class _RegistrationCleanupCron {
15989
16733
  }
15990
16734
  }
15991
16735
  if (totalDeleted > 0) {
15992
- this.logger.log(`PendingRegistration-Cleanup abgeschlossen: ${totalDeleted} geloescht in ${iterations} Batches.`);
16736
+ this.logger.log(`Pending registration cleanup finished: ${totalDeleted} deleted in ${iterations} batches.`);
15993
16737
  }
15994
16738
  }
15995
16739
  };
@@ -16011,6 +16755,7 @@ RegistrationCleanupCron = _ts_decorate91([
16011
16755
 
16012
16756
  // src/registration/ip-rate-limit.guard.ts
16013
16757
  var import_common82 = require("@nestjs/common");
16758
+ var import_types34 = require("@saasicat/types");
16014
16759
  function _ts_decorate92(decorators, target, key, desc) {
16015
16760
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
16016
16761
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -16039,9 +16784,14 @@ var BaseIpRateLimitGuard = class {
16039
16784
  const key = ipFingerprint2(req);
16040
16785
  if (this.exceeded(key)) {
16041
16786
  throw new import_common82.HttpException({
16042
- code: "RATE_LIMITED",
16043
- reason: this.options.name,
16044
- retryAfterSeconds: Math.ceil(this.options.windowMs / 1e3)
16787
+ ...codedError(import_types34.REGISTRATION_ERROR_CODES.RATE_LIMITED, {
16788
+ retryAfterSeconds: Math.ceil(this.options.windowMs / 1e3)
16789
+ }),
16790
+ retryAfterSeconds: Math.ceil(this.options.windowMs / 1e3),
16791
+ // Superseded by `code`. Note this carries the limiter name,
16792
+ // not the code — the two emitters of RATE_LIMITED disagree
16793
+ // on what `reason` means, which is why it is going away.
16794
+ reason: this.options.name
16045
16795
  }, import_common82.HttpStatus.TOO_MANY_REQUESTS);
16046
16796
  }
16047
16797
  return true;
@@ -16183,7 +16933,7 @@ _ts_decorate94([
16183
16933
  (0, import_class_validator17.IsOptional)(),
16184
16934
  (0, import_class_validator17.IsString)(),
16185
16935
  (0, import_class_validator17.Matches)(/^[a-z0-9-]+$/, {
16186
- message: "tenantSlug darf nur Kleinbuchstaben, Ziffern und Bindestriche enthalten."
16936
+ message: "tenantSlug may only contain lower-case letters, digits and hyphens."
16187
16937
  }),
16188
16938
  (0, import_class_validator17.MaxLength)(60),
16189
16939
  _ts_metadata73("design:type", String)
@@ -16219,7 +16969,7 @@ _ts_decorate94([
16219
16969
  ], RegisterStartDto.prototype, "password", void 0);
16220
16970
  _ts_decorate94([
16221
16971
  (0, import_class_validator17.Equals)(true, {
16222
- message: "AGB, Datenschutzerklaerung und Auftragsverarbeitung muessen zugestimmt werden."
16972
+ message: "The terms of service, privacy policy and data processing agreement must be accepted."
16223
16973
  }),
16224
16974
  _ts_metadata73("design:type", Boolean)
16225
16975
  ], RegisterStartDto.prototype, "termsAccepted", void 0);
@@ -16259,7 +17009,7 @@ _ts_decorate95([
16259
17009
  _ts_decorate95([
16260
17010
  (0, import_class_validator18.IsString)(),
16261
17011
  (0, import_class_validator18.Matches)(/^\d{6}$/, {
16262
- message: "OTP muss 6 Ziffern sein."
17012
+ message: "The OTP must be 6 digits."
16263
17013
  }),
16264
17014
  _ts_metadata74("design:type", String)
16265
17015
  ], VerifyRegistrationOtpDto.prototype, "otp", void 0);
@@ -16790,9 +17540,10 @@ __export(src_exports, {
16790
17540
  buildPublicMarketingCatalogController: () => buildPublicMarketingCatalogController,
16791
17541
  buildReplacedByIndex: () => buildReplacedByIndex,
16792
17542
  buildTenantSubscriptionBundlesController: () => buildTenantSubscriptionBundlesController,
16793
- classifyBundleVersionDiff: () => import_types.classifyBundleVersionDiff,
16794
- classifyPlanDiff: () => import_types.classifyPlanDiff,
17543
+ classifyBundleVersionDiff: () => import_types5.classifyBundleVersionDiff,
17544
+ classifyPlanDiff: () => import_types5.classifyPlanDiff,
16795
17545
  clearPendingPlanVersionFields: () => clearPendingPlanVersionFields,
17546
+ codedError: () => codedError,
16796
17547
  collectSubscriptionBundleFeatures: () => collectSubscriptionBundleFeatures,
16797
17548
  computeBreakdown: () => computeBreakdown,
16798
17549
  computeCarriedTrialEndsAt: () => computeCarriedTrialEndsAt,
@@ -16884,7 +17635,7 @@ __export(testing_exports, {
16884
17635
  });
16885
17636
 
16886
17637
  // src/testing/fake-repositories.ts
16887
- var import_types14 = require("@saasicat/types");
17638
+ var import_types35 = require("@saasicat/types");
16888
17639
  var FakeSubscriptionRepository = class {
16889
17640
  static {
16890
17641
  __name(this, "FakeSubscriptionRepository");
@@ -16975,10 +17726,10 @@ var FakeSubscriptionBundleRepository = class {
16975
17726
  async cancel(subscriptionBundleId, data) {
16976
17727
  const existing = this.byId.get(subscriptionBundleId);
16977
17728
  if (!existing) {
16978
- throw new Error(`SubscriptionBundle '${subscriptionBundleId}' nicht gefunden`);
17729
+ throw new Error(`SubscriptionBundle '${subscriptionBundleId}' not found`);
16979
17730
  }
16980
17731
  if (existing.canceledAt !== null) {
16981
- throw new Error(`SubscriptionBundle '${subscriptionBundleId}' ist bereits gek\xFCndigt`);
17732
+ throw new Error(`SubscriptionBundle '${subscriptionBundleId}' is already cancelled`);
16982
17733
  }
16983
17734
  const updated = {
16984
17735
  ...existing,
@@ -16992,7 +17743,7 @@ var FakeSubscriptionBundleRepository = class {
16992
17743
  async reactivate(subscriptionBundleId) {
16993
17744
  const existing = this.byId.get(subscriptionBundleId);
16994
17745
  if (!existing) {
16995
- throw new Error(`SubscriptionBundle '${subscriptionBundleId}' nicht gefunden`);
17746
+ throw new Error(`SubscriptionBundle '${subscriptionBundleId}' not found`);
16996
17747
  }
16997
17748
  const updated = {
16998
17749
  ...existing,
@@ -17102,7 +17853,7 @@ var FakeSubscriptionContractRepository = class {
17102
17853
  }
17103
17854
  async terminate(contractId, data) {
17104
17855
  const existing = this.byId.get(contractId);
17105
- if (!existing) throw new Error(`SubscriptionContract '${contractId}' nicht gefunden`);
17856
+ if (!existing) throw new Error(`SubscriptionContract '${contractId}' not found`);
17106
17857
  const updated = {
17107
17858
  ...existing,
17108
17859
  status: data.status,
@@ -17262,7 +18013,7 @@ var FakeBundleRepository = class {
17262
18013
  }
17263
18014
  async update(bundleId, data) {
17264
18015
  const existing = this.bundles.get(bundleId);
17265
- if (!existing) throw new Error(`Bundle '${bundleId}' nicht gefunden`);
18016
+ if (!existing) throw new Error(`Bundle '${bundleId}' not found`);
17266
18017
  const updated = {
17267
18018
  ...existing,
17268
18019
  label: data.label ?? existing.label,
@@ -17277,7 +18028,7 @@ var FakeBundleRepository = class {
17277
18028
  }
17278
18029
  async softDelete(bundleId) {
17279
18030
  const existing = this.bundles.get(bundleId);
17280
- if (!existing) throw new Error(`Bundle '${bundleId}' nicht gefunden`);
18031
+ if (!existing) throw new Error(`Bundle '${bundleId}' not found`);
17281
18032
  this.bundles.set(bundleId, {
17282
18033
  ...existing,
17283
18034
  deletedAt: this.nowIso()
@@ -17309,7 +18060,7 @@ var FakeBundleRepository = class {
17309
18060
  async createDraft(data) {
17310
18061
  const draft = await this.findCurrentDraft(data.bundleId);
17311
18062
  if (draft) {
17312
- throw new Error(`Bundle '${data.bundleId}' hat bereits eine Draft-Version v${draft.version}`);
18063
+ throw new Error(`Bundle '${data.bundleId}' already has a draft version v${draft.version}`);
17313
18064
  }
17314
18065
  const all = await this.listVersions(data.bundleId);
17315
18066
  const nextVersion = all.length === 0 ? 1 : Math.max(...all.map((v) => v.version)) + 1;
@@ -17345,7 +18096,7 @@ var FakeBundleRepository = class {
17345
18096
  }
17346
18097
  async updateDraft(versionId, data) {
17347
18098
  const existing = this.versions.get(versionId);
17348
- if (!existing) throw new Error(`BundleVersion '${versionId}' nicht gefunden`);
18099
+ if (!existing) throw new Error(`BundleVersion '${versionId}' not found`);
17349
18100
  const updated = {
17350
18101
  ...existing,
17351
18102
  features: data.features ?? existing.features,
@@ -17367,15 +18118,15 @@ var FakeBundleRepository = class {
17367
18118
  const existing = this.versions.get(versionId);
17368
18119
  if (!existing) return;
17369
18120
  if (existing.publishedAt !== null) {
17370
- throw new Error(`BundleVersion '${versionId}' ist bereits published`);
18121
+ throw new Error(`BundleVersion '${versionId}' is already published`);
17371
18122
  }
17372
18123
  this.versions.delete(versionId);
17373
18124
  }
17374
18125
  async publishDraft(versionId, publishMeta) {
17375
18126
  const draft = this.versions.get(versionId);
17376
- if (!draft) throw new Error(`BundleVersion '${versionId}' nicht gefunden`);
18127
+ if (!draft) throw new Error(`BundleVersion '${versionId}' not found`);
17377
18128
  if (draft.publishedAt !== null) {
17378
- throw new Error(`BundleVersion '${versionId}' ist bereits published`);
18129
+ throw new Error(`BundleVersion '${versionId}' is already published`);
17379
18130
  }
17380
18131
  const now = this.nowIso();
17381
18132
  const validFromIso = publishMeta.validFrom.toISOString();
@@ -17451,7 +18202,7 @@ var FakeMarketingProjectionRepository = class {
17451
18202
  const locale = data.locale ?? "de";
17452
18203
  const existing = await this.findByTarget(data.targetType, data.targetVersionId, locale);
17453
18204
  if (existing) {
17454
- throw new Error(`MarketingProjection f\xFCr ${data.targetType}/${data.targetVersionId}/${locale} existiert bereits`);
18205
+ throw new Error(`Marketing projection for ${data.targetType}/${data.targetVersionId}/${locale} already exists`);
17455
18206
  }
17456
18207
  const now = this.nowIso();
17457
18208
  const row = {
@@ -17479,7 +18230,7 @@ var FakeMarketingProjectionRepository = class {
17479
18230
  }
17480
18231
  async update(id, data) {
17481
18232
  const existing = this.rows.get(id);
17482
- if (!existing) throw new Error(`MarketingProjection '${id}' nicht gefunden`);
18233
+ if (!existing) throw new Error(`MarketingProjection '${id}' not found`);
17483
18234
  const updated = {
17484
18235
  ...existing,
17485
18236
  displayLabel: data.displayLabel ?? existing.displayLabel,
@@ -17499,7 +18250,7 @@ var FakeMarketingProjectionRepository = class {
17499
18250
  return updated;
17500
18251
  }
17501
18252
  async delete(id) {
17502
- if (!this.rows.has(id)) throw new Error(`MarketingProjection '${id}' nicht gefunden`);
18253
+ if (!this.rows.has(id)) throw new Error(`MarketingProjection '${id}' not found`);
17503
18254
  this.rows.delete(id);
17504
18255
  }
17505
18256
  };
@@ -17573,7 +18324,7 @@ var FakePlanRepository = class {
17573
18324
  }
17574
18325
  async update(planId, data) {
17575
18326
  const existing = this.plans.get(planId);
17576
- if (!existing) throw new Error(`Plan '${planId}' nicht gefunden`);
18327
+ if (!existing) throw new Error(`Plan '${planId}' not found`);
17577
18328
  const updated = {
17578
18329
  ...existing,
17579
18330
  label: data.label ?? existing.label,
@@ -17587,7 +18338,7 @@ var FakePlanRepository = class {
17587
18338
  }
17588
18339
  async softDelete(planId) {
17589
18340
  const existing = this.plans.get(planId);
17590
- if (!existing) throw new Error(`Plan '${planId}' nicht gefunden`);
18341
+ if (!existing) throw new Error(`Plan '${planId}' not found`);
17591
18342
  const updated = {
17592
18343
  ...existing,
17593
18344
  deletedAt: this.nowIso(),
@@ -17640,7 +18391,7 @@ var FakePlanRepository = class {
17640
18391
  }
17641
18392
  async findActivePlanVersion(planKey, asOf = /* @__PURE__ */ new Date()) {
17642
18393
  const t = asOf.getTime();
17643
- const dayStart = (0, import_types14.startOfUtcDay)(asOf).getTime();
18394
+ const dayStart = (0, import_types35.startOfUtcDay)(asOf).getTime();
17644
18395
  const matches = [
17645
18396
  ...this.versions.values()
17646
18397
  ].filter((v) => {
@@ -17704,7 +18455,7 @@ var FakePlanRepository = class {
17704
18455
  }
17705
18456
  async updatePlanVersionDraft(versionId, data) {
17706
18457
  const existing = this.versions.get(versionId);
17707
- if (!existing) throw new Error(`PlanVersion '${versionId}' nicht gefunden`);
18458
+ if (!existing) throw new Error(`PlanVersion '${versionId}' not found`);
17708
18459
  const updated = {
17709
18460
  ...existing,
17710
18461
  features: data.features ?? existing.features,
@@ -17721,7 +18472,7 @@ var FakePlanRepository = class {
17721
18472
  }
17722
18473
  async publishPlanVersionDraft(versionId, publishMeta) {
17723
18474
  const draft = this.versions.get(versionId);
17724
- if (!draft) throw new Error(`PlanVersion '${versionId}' nicht gefunden`);
18475
+ if (!draft) throw new Error(`PlanVersion '${versionId}' not found`);
17725
18476
  const planKey = draft.planId;
17726
18477
  const now = this.nowIso();
17727
18478
  const dayMs = 24 * 60 * 60 * 1e3;
@@ -17753,14 +18504,14 @@ var FakePlanRepository = class {
17753
18504
  const existing = this.versions.get(versionId);
17754
18505
  if (!existing) return;
17755
18506
  if (existing.publishedAt !== null) {
17756
- throw new Error(`FakePlanRepository: PlanVersion '${versionId}' ist bereits published \u2014 Discard nicht erlaubt`);
18507
+ throw new Error(`FakePlanRepository: plan version '${versionId}' is already published \u2014 discard is not allowed`);
17757
18508
  }
17758
18509
  this.versions.delete(versionId);
17759
18510
  }
17760
18511
  async terminate(versionId, endsAt) {
17761
18512
  const existing = this.versions.get(versionId);
17762
18513
  if (!existing) {
17763
- throw new Error(`FakePlanRepository: PlanVersion '${versionId}' nicht gefunden`);
18514
+ throw new Error(`FakePlanRepository: PlanVersion '${versionId}' not found`);
17764
18515
  }
17765
18516
  const updated = {
17766
18517
  ...existing,