@saasicat/nest 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/_entries.cjs +1309 -519
  2. package/dist/admin/index.js +1 -1
  3. package/dist/billing/index.d.cts +7 -4
  4. package/dist/billing/index.d.ts +7 -4
  5. package/dist/billing/index.js +6 -6
  6. package/dist/catalog/index.js +4 -4
  7. package/dist/checkout-offer/index.js +2 -2
  8. package/dist/{chunk-Z3FWN3HJ.js → chunk-5ZXBXYXU.js} +3 -3
  9. package/dist/{chunk-RZVSKKXX.js → chunk-6WA4X5U3.js} +46 -45
  10. package/dist/{chunk-V34AA7C7.js → chunk-6WL6VQ7Z.js} +1 -1
  11. package/dist/{chunk-ABB2CRAS.js → chunk-74SCJ4HF.js} +1 -1
  12. package/dist/{chunk-PLJEAUFU.js → chunk-7JIHG53V.js} +76 -39
  13. package/dist/{chunk-7RRWI5LH.js → chunk-AFK2IE2L.js} +10 -10
  14. package/dist/{chunk-32EMXPU2.js → chunk-B7NRY2LV.js} +48 -17
  15. package/dist/{chunk-MDIZUVIK.js → chunk-BSK6YBLI.js} +1 -1
  16. package/dist/{chunk-R2QOSVAA.js → chunk-DQKCK7DX.js} +1 -1
  17. package/dist/{chunk-GLLCB4BP.js → chunk-EUU3B6NB.js} +17 -13
  18. package/dist/{chunk-EXOLOJFZ.js → chunk-F6DCLVBB.js} +29 -8
  19. package/dist/{chunk-5KH2O4SF.js → chunk-I77VS3YH.js} +287 -105
  20. package/dist/{chunk-YITTE646.js → chunk-KUDKD7ZG.js} +34 -14
  21. package/dist/{chunk-P3DP6GJE.js → chunk-LXUBCLRK.js} +486 -160
  22. package/dist/{chunk-GJBXOCRZ.js → chunk-ORWIGSNN.js} +45 -17
  23. package/dist/{chunk-E7AJ42AB.js → chunk-QLJHIIOY.js} +168 -28
  24. package/dist/{chunk-BOADBIGB.js → chunk-YYUXKLXK.js} +2 -2
  25. package/dist/{define-saasicat-Rc0rXrYG.d.ts → define-saasicat-CqkhnHn-.d.ts} +1 -1
  26. package/dist/{define-saasicat-CeDBX9ji.d.cts → define-saasicat-D8c6Bs4T.d.cts} +1 -1
  27. package/dist/discovery/index.d.cts +2 -2
  28. package/dist/discovery/index.d.ts +2 -2
  29. package/dist/discovery/index.js +4 -4
  30. package/dist/{enforce-quota.interceptor-DMw-bPyk.d.cts → enforce-quota.interceptor-Dk0eEtzV.d.cts} +1 -1
  31. package/dist/{enforce-quota.interceptor-CmbWX39L.d.ts → enforce-quota.interceptor-Dp8wTYro.d.ts} +1 -1
  32. package/dist/entitlement/index.js +2 -2
  33. package/dist/index.d.cts +6 -6
  34. package/dist/index.d.ts +6 -6
  35. package/dist/index.js +17 -17
  36. package/dist/{module-DgYJur6f.d.ts → module-BwD-wAor.d.ts} +1 -1
  37. package/dist/{module-CSN8j8Tp.d.cts → module-DsDkbawm.d.cts} +1 -1
  38. package/dist/platform/index.d.cts +8 -8
  39. package/dist/platform/index.d.ts +8 -8
  40. package/dist/platform/index.js +13 -13
  41. package/dist/promo/index.d.cts +3 -3
  42. package/dist/promo/index.d.ts +3 -3
  43. package/dist/promo/index.js +3 -3
  44. package/dist/registration/index.js +1 -1
  45. package/dist/{saas-platform.module-DLhoqKNA.d.ts → saas-platform.module-BQmrDGVU.d.ts} +1 -1
  46. package/dist/{saas-platform.module-D3rUTiho.d.cts → saas-platform.module-DducnQ9s.d.cts} +1 -1
  47. package/dist/{service-DX8KbGXl.d.cts → service-DPZ2W1E-.d.cts} +2 -2
  48. package/dist/{service-DX8KbGXl.d.ts → service-DPZ2W1E-.d.ts} +2 -2
  49. package/dist/subscription-contract/index.js +2 -2
  50. package/dist/{tenant-billing.controller-BwZRnJ3q.d.ts → tenant-billing.controller-b1G1yB09.d.ts} +2 -2
  51. package/dist/{tenant-billing.controller-CG4CaFUP.d.cts → tenant-billing.controller-m42YQ_Ei.d.cts} +2 -2
  52. package/dist/testing/index.d.cts +5 -5
  53. package/dist/testing/index.d.ts +5 -5
  54. package/dist/testing/index.js +33 -33
  55. package/package.json +3 -3
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,7 @@ 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_types2 = require("@saasicat/types");
118
128
 
119
129
  // src/admin/mfa.ts
120
130
  var import_common2 = require("@nestjs/common");
@@ -194,7 +204,7 @@ var MfaService = class _MfaService {
194
204
  return false;
195
205
  }
196
206
  const message = error instanceof Error ? error.message : String(error);
197
- this.logger.warn(`TOTP-Verify f\xFCr User ${input.userId} unerwartet fehlgeschlagen: ${message}`);
207
+ this.logger.warn(`TOTP verification for user ${input.userId} failed unexpectedly: ${message}`);
198
208
  return false;
199
209
  }
200
210
  }
@@ -260,22 +270,30 @@ var MfaGuard = class _MfaGuard {
260
270
  }
261
271
  const request = context.switchToHttp().getRequest();
262
272
  const user = request.user;
263
- if (!user) throw new import_common3.UnauthorizedException({
264
- reason: "NOT_AUTHENTICATED"
265
- });
273
+ if (!user) {
274
+ throw new import_common3.UnauthorizedException({
275
+ code: import_types2.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
276
+ // `reason` is superseded by `code` and will be removed.
277
+ reason: "NOT_AUTHENTICATED"
278
+ });
279
+ }
266
280
  const enabled = await this.mfaService.isEnabled(user.id);
267
281
  if (!enabled) {
268
282
  throw new import_common3.UnauthorizedException({
283
+ code: import_types2.AUTH_ERROR_CODES.MFA_NOT_SET_UP,
284
+ // `reason` is superseded by `code` and will be removed.
269
285
  reason: "MFA_NOT_SET_UP",
270
- message: "Bitte zuerst MFA-Setup \xFCber das CLI ausf\xFChren."
286
+ message: "Run the MFA setup via the CLI first."
271
287
  });
272
288
  }
273
289
  const headerVal = request.headers?.["x-mfa-code"];
274
290
  const code = Array.isArray(headerVal) ? headerVal[0] : headerVal;
275
291
  if (!code) {
276
292
  throw new import_common3.UnauthorizedException({
293
+ code: import_types2.AUTH_ERROR_CODES.MFA_REQUIRED,
294
+ // `reason` is superseded by `code` and will be removed.
277
295
  reason: "MFA_REQUIRED",
278
- message: "TOTP-Code im Header X-Mfa-Code erforderlich."
296
+ message: "TOTP code required in the X-Mfa-Code header."
279
297
  });
280
298
  }
281
299
  const valid = await this.mfaService.verify({
@@ -284,8 +302,10 @@ var MfaGuard = class _MfaGuard {
284
302
  });
285
303
  if (!valid) {
286
304
  throw new import_common3.UnauthorizedException({
305
+ code: import_types2.AUTH_ERROR_CODES.MFA_FAILED,
306
+ // `reason` is superseded by `code` and will be removed.
287
307
  reason: "MFA_FAILED",
288
- message: "TOTP-Code ung\xFCltig."
308
+ message: "Invalid TOTP code."
289
309
  });
290
310
  }
291
311
  return true;
@@ -799,7 +819,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
799
819
  actions: [
800
820
  {
801
821
  id: "platform.tenants.suspend",
802
- label: "Mandant suspendieren",
822
+ label: "Suspend tenant",
803
823
  actionKey: "tenants.suspend",
804
824
  requiredCapability: "tenants.suspend",
805
825
  requiresMfa: true,
@@ -807,7 +827,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
807
827
  },
808
828
  {
809
829
  id: "platform.tenants.reactivate",
810
- label: "Mandant reaktivieren",
830
+ label: "Reactivate tenant",
811
831
  actionKey: "tenants.reactivate",
812
832
  requiredCapability: "tenants.reactivate",
813
833
  requiresMfa: true,
@@ -823,7 +843,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
823
843
  },
824
844
  {
825
845
  id: "platform.tenants.export",
826
- label: "DSGVO-Export",
846
+ label: "GDPR export",
827
847
  actionKey: "tenants.export",
828
848
  requiredCapability: "tenants.export",
829
849
  requiresMfa: true,
@@ -831,7 +851,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
831
851
  },
832
852
  {
833
853
  id: "platform.subscriptions.cancel",
834
- label: "Subscription k\xFCndigen",
854
+ label: "Cancel subscription",
835
855
  actionKey: "subscriptions.cancel",
836
856
  requiredCapability: "subscriptions.cancel",
837
857
  requiresMfa: true,
@@ -839,7 +859,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
839
859
  },
840
860
  {
841
861
  id: "platform.pilots.grant",
842
- label: "Pilot-Status setzen",
862
+ label: "Set pilot status",
843
863
  actionKey: "pilots.grant",
844
864
  requiredCapability: "pilots.grant",
845
865
  requiresMfa: true,
@@ -847,7 +867,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
847
867
  },
848
868
  {
849
869
  id: "platform.pilots.revoke",
850
- label: "Pilot-Status entziehen",
870
+ label: "Revoke pilot status",
851
871
  actionKey: "pilots.revoke",
852
872
  requiredCapability: "pilots.revoke",
853
873
  requiresMfa: true,
@@ -855,7 +875,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
855
875
  },
856
876
  {
857
877
  id: "platform.pilots.extend",
858
- label: "Pilot verl\xE4ngern",
878
+ label: "Extend pilot",
859
879
  actionKey: "pilots.extend",
860
880
  requiredCapability: "pilots.extend",
861
881
  requiresMfa: true,
@@ -867,102 +887,102 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
867
887
  actions: [
868
888
  {
869
889
  key: "TENANT_SUSPEND",
870
- label: "Mandant suspendiert",
890
+ label: "Tenant suspended",
871
891
  severity: "high"
872
892
  },
873
893
  {
874
894
  key: "TENANT_REACTIVATE",
875
- label: "Mandant reaktiviert",
895
+ label: "Tenant reactivated",
876
896
  severity: "medium"
877
897
  },
878
898
  {
879
899
  key: "TENANT_IMPERSONATE",
880
- label: "Impersonation gestartet",
900
+ label: "Impersonation started",
881
901
  severity: "high"
882
902
  },
883
903
  {
884
904
  key: "TENANT_EXPORT",
885
- label: "DSGVO-Export ausgel\xF6st",
905
+ label: "GDPR export triggered",
886
906
  severity: "medium"
887
907
  },
888
908
  {
889
909
  key: "PILOT_CREATE",
890
- label: "Pilot-Tenant angelegt",
910
+ label: "Pilot tenant created",
891
911
  severity: "medium"
892
912
  },
893
913
  {
894
914
  key: "PILOT_GRANT",
895
- label: "Pilot-Status gesetzt",
915
+ label: "Pilot status set",
896
916
  severity: "medium"
897
917
  },
898
918
  {
899
919
  key: "PILOT_REVOKE",
900
- label: "Pilot-Status entzogen",
920
+ label: "Pilot status revoked",
901
921
  severity: "medium"
902
922
  },
903
923
  {
904
924
  key: "PILOT_EXTEND",
905
- label: "Pilot verl\xE4ngert",
925
+ label: "Pilot extended",
906
926
  severity: "low"
907
927
  },
908
928
  {
909
929
  key: "PLAN_VERSION_PUBLISH",
910
- label: "Plan-Version freigegeben",
930
+ label: "Plan version approved",
911
931
  severity: "high"
912
932
  },
913
933
  {
914
934
  key: "PLAN_VERSION_TERMINATE",
915
- label: "Plan-Version terminiert",
935
+ label: "Plan version terminated",
916
936
  severity: "high"
917
937
  },
918
938
  {
919
939
  key: "SUBSCRIPTION_CANCEL",
920
- label: "Subscription gek\xFCndigt",
940
+ label: "Subscription cancelled",
921
941
  severity: "high"
922
942
  },
923
943
  {
924
944
  key: "USER_RESET_PASSWORD",
925
- label: "Passwort-Reset ausgel\xF6st",
945
+ label: "Password reset triggered",
926
946
  severity: "medium"
927
947
  },
928
948
  {
929
949
  key: "USER_DEACTIVATE",
930
- label: "User deaktiviert",
950
+ label: "User deactivated",
931
951
  severity: "high"
932
952
  },
933
953
  {
934
954
  key: "USER_REASSIGN_ADMIN",
935
- label: "Admin-Rolle \xFCbertragen",
955
+ label: "Admin role transferred",
936
956
  severity: "high"
937
957
  },
938
958
  {
939
959
  key: "PLATFORM_EMAIL_PROVIDER_CREATE",
940
- label: "Plattform-Absender angelegt",
960
+ label: "Platform sender created",
941
961
  severity: "medium"
942
962
  },
943
963
  {
944
964
  key: "PLATFORM_EMAIL_PROVIDER_UPDATE",
945
- label: "Plattform-Absender ge\xE4ndert",
965
+ label: "Platform sender changed",
946
966
  severity: "medium"
947
967
  },
948
968
  {
949
969
  key: "PLATFORM_EMAIL_PROVIDER_DELETE",
950
- label: "Plattform-Absender gel\xF6scht",
970
+ label: "Platform sender deleted",
951
971
  severity: "medium"
952
972
  },
953
973
  {
954
974
  key: "PLATFORM_EMAIL_PROVIDER_TEST",
955
- label: "Plattform-Absender Test-Versand",
975
+ label: "Platform sender test send",
956
976
  severity: "low"
957
977
  },
958
978
  {
959
979
  key: "EMAIL_HISTORY_DELETE",
960
- label: "Plattform-E-Mail aus Verlauf entfernt",
980
+ label: "Platform email removed from history",
961
981
  severity: "medium"
962
982
  },
963
983
  {
964
984
  key: "EMAIL_HISTORY_RESEND",
965
- label: "Plattform-E-Mail erneut versendet",
985
+ label: "Platform email resent",
966
986
  severity: "low"
967
987
  }
968
988
  ]
@@ -1081,7 +1101,7 @@ var AdminManifestModule = class _AdminManifestModule {
1081
1101
  ];
1082
1102
  if (includeManifestController) {
1083
1103
  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.");
1104
+ 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
1105
  }
1086
1106
  controllers.push(buildManifestController(options.guards, options.reloadGuards ?? []));
1087
1107
  }
@@ -1318,6 +1338,7 @@ AdminStatsModule = _ts_decorate12([
1318
1338
  // src/admin/admin-resources.module.ts
1319
1339
  var import_common14 = require("@nestjs/common");
1320
1340
  var import_class_validator = require("class-validator");
1341
+ var import_types3 = require("@saasicat/types");
1321
1342
  function _ts_decorate13(decorators, target, key, desc) {
1322
1343
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1323
1344
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1363,7 +1384,15 @@ var AdminResourcesService = class {
1363
1384
  }
1364
1385
  async getTenantDetail(slug) {
1365
1386
  const tenant = await this.resources.getTenantDetail(slug);
1366
- if (!tenant) throw new import_common14.NotFoundException(`Tenant ${slug} not found`);
1387
+ if (!tenant) {
1388
+ throw new import_common14.NotFoundException({
1389
+ code: import_types3.BILLING_ERROR_CODES.TENANT_NOT_FOUND,
1390
+ message: `Tenant ${slug} not found`,
1391
+ params: {
1392
+ slug
1393
+ }
1394
+ });
1395
+ }
1367
1396
  return tenant;
1368
1397
  }
1369
1398
  listUsers(filter) {
@@ -1389,7 +1418,15 @@ var AdminResourcesService = class {
1389
1418
  }
1390
1419
  async setTenantActive(slug, active, subscriptionStatus, actor, audit) {
1391
1420
  const result = await this.resources.setTenantActive(slug, active, subscriptionStatus);
1392
- if (!result) throw new import_common14.NotFoundException(`Tenant ${slug} not found`);
1421
+ if (!result) {
1422
+ throw new import_common14.NotFoundException({
1423
+ code: import_types3.BILLING_ERROR_CODES.TENANT_NOT_FOUND,
1424
+ message: `Tenant ${slug} not found`,
1425
+ params: {
1426
+ slug
1427
+ }
1428
+ });
1429
+ }
1393
1430
  await this.audit.log({
1394
1431
  actor,
1395
1432
  entity: "Tenant",
@@ -1631,8 +1668,8 @@ __export(billing_exports, {
1631
1668
  buildPlanCatalogFromSnapshot: () => buildPlanCatalogFromSnapshot,
1632
1669
  buildPlanCatalogImporterController: () => buildPlanCatalogImporterController,
1633
1670
  buildTenantSubscriptionBundlesController: () => buildTenantSubscriptionBundlesController,
1634
- classifyBundleVersionDiff: () => import_types.classifyBundleVersionDiff,
1635
- classifyPlanDiff: () => import_types.classifyPlanDiff,
1671
+ classifyBundleVersionDiff: () => import_types4.classifyBundleVersionDiff,
1672
+ classifyPlanDiff: () => import_types4.classifyPlanDiff,
1636
1673
  clearPendingPlanVersionFields: () => clearPendingPlanVersionFields,
1637
1674
  computeCarriedTrialEndsAt: () => computeCarriedTrialEndsAt,
1638
1675
  computeNextPeriod: () => computeNextPeriod,
@@ -1693,7 +1730,7 @@ function periodEndWithMinLead(startedAt, cycle, now, minLeadDays = 42) {
1693
1730
  __name(periodEndWithMinLead, "periodEndWithMinLead");
1694
1731
 
1695
1732
  // src/billing/version-diff.ts
1696
- var import_types = require("@saasicat/types");
1733
+ var import_types4 = require("@saasicat/types");
1697
1734
 
1698
1735
  // src/billing/version-publish.ts
1699
1736
  var PublishValidationError = class extends Error {
@@ -1709,35 +1746,35 @@ var PublishValidationError = class extends Error {
1709
1746
  function assertChangeNote(changeNote) {
1710
1747
  const trimmed = (changeNote ?? "").trim();
1711
1748
  if (trimmed.length === 0) {
1712
- throw new PublishValidationError("CHANGE_NOTE_REQUIRED", "changeNote ist Pflicht beim Ver\xF6ffentlichen einer Version.");
1749
+ throw new PublishValidationError("CHANGE_NOTE_REQUIRED", "changeNote is required when publishing a version.");
1713
1750
  }
1714
1751
  return trimmed;
1715
1752
  }
1716
1753
  __name(assertChangeNote, "assertChangeNote");
1717
1754
  function assertDraftPublishable(draft, draftId) {
1718
1755
  if (!draft) {
1719
- throw new PublishValidationError("NOT_FOUND", `PlanVersion ${draftId} nicht gefunden`);
1756
+ throw new PublishValidationError("NOT_FOUND", `PlanVersion ${draftId} not found`);
1720
1757
  }
1721
1758
  if (draft.publishedAt !== null) {
1722
- throw new PublishValidationError("ALREADY_PUBLISHED", `PlanVersion ${draftId} ist bereits ver\xF6ffentlicht.`);
1759
+ throw new PublishValidationError("ALREADY_PUBLISHED", `PlanVersion ${draftId} has already been published.`);
1723
1760
  }
1724
1761
  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.`);
1762
+ 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
1763
  }
1727
1764
  }
1728
1765
  __name(assertDraftPublishable, "assertDraftPublishable");
1729
1766
  function assertBaseVersionFresh(base, baseId, planContext) {
1730
1767
  if (!base) {
1731
- throw new PublishValidationError("BASE_NOT_FOUND", `baseVersion ${baseId} nicht mehr vorhanden.`);
1768
+ throw new PublishValidationError("BASE_NOT_FOUND", `baseVersion ${baseId} no longer exists.`);
1732
1769
  }
1733
1770
  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.`);
1771
+ 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
1772
  }
1736
1773
  }
1737
1774
  __name(assertBaseVersionFresh, "assertBaseVersionFresh");
1738
1775
  function assertOptimisticLockHeld(updateCount, draftId) {
1739
1776
  if (updateCount !== 1) {
1740
- throw new PublishValidationError("OPTIMISTIC_LOCK_CONFLICT", `Draft ${draftId} wurde zwischenzeitlich von einem anderen Admin ver\xF6ffentlicht.`);
1777
+ throw new PublishValidationError("OPTIMISTIC_LOCK_CONFLICT", `Draft ${draftId} has meanwhile been published by another admin.`);
1741
1778
  }
1742
1779
  }
1743
1780
  __name(assertOptimisticLockHeld, "assertOptimisticLockHeld");
@@ -1788,10 +1825,11 @@ var FEATURE_GUARD_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/Featu
1788
1825
  // src/billing/feature.guard.ts
1789
1826
  var import_common18 = require("@nestjs/common");
1790
1827
  var import_core2 = require("@nestjs/core");
1791
- var import_types2 = require("@saasicat/types");
1828
+ var import_types6 = require("@saasicat/types");
1792
1829
 
1793
1830
  // src/entitlement/service.ts
1794
1831
  var import_common17 = require("@nestjs/common");
1832
+ var import_types5 = require("@saasicat/types");
1795
1833
 
1796
1834
  // src/catalog/tokens.ts
1797
1835
  var PLAN_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/PlanRepository");
@@ -1966,7 +2004,7 @@ __name(findPlan, "findPlan");
1966
2004
  function getPlanOrThrow(catalog, planId) {
1967
2005
  const plan = findPlan(catalog, planId);
1968
2006
  if (!plan) {
1969
- throw new Error(`Plan "${planId}" nicht im Catalog (${catalog.projectKey}) gefunden`);
2007
+ throw new Error(`Plan "${planId}" not found in the catalog (${catalog.projectKey})`);
1970
2008
  }
1971
2009
  return plan;
1972
2010
  }
@@ -2236,7 +2274,7 @@ function assertNoReplacesCycle(index) {
2236
2274
  ...path.slice(cycleStart),
2237
2275
  key
2238
2276
  ].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).`);
2277
+ throw new Error(`replaces cycle in the discovery metadata: ${cycle}. Feature replacements must be acyclic \u2014 fix the replaces declarations in code (#39).`);
2240
2278
  }
2241
2279
  state.set(key, IN_PROGRESS);
2242
2280
  for (const successor of index.get(key) ?? []) {
@@ -2262,7 +2300,7 @@ var LimitExceededError = class extends Error {
2262
2300
  used;
2263
2301
  code = LIMIT_EXCEEDED;
2264
2302
  constructor(dimension, max, used) {
2265
- super(`Limit f\xFCr ${dimension} erreicht: ${used}/${max}.`), this.dimension = dimension, this.max = max, this.used = used;
2303
+ super(`Limit for ${dimension} reached: ${used}/${max}.`), this.dimension = dimension, this.max = max, this.used = used;
2266
2304
  this.name = "LimitExceededError";
2267
2305
  }
2268
2306
  };
@@ -2357,7 +2395,13 @@ var EntitlementService = class {
2357
2395
  if (cached) return cached;
2358
2396
  const sub = await this.subscriptions.findByTenantId(tenantId);
2359
2397
  if (!sub) {
2360
- throw new import_common17.NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
2398
+ throw new import_common17.NotFoundException({
2399
+ code: import_types5.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
2400
+ message: `No subscription for tenant ${tenantId}`,
2401
+ params: {
2402
+ tenantId
2403
+ }
2404
+ });
2361
2405
  }
2362
2406
  const limits = await this.deriveLimits(sub, now);
2363
2407
  this.writeCache(tenantId, limits, now.getTime());
@@ -2430,7 +2474,7 @@ var EntitlementService = class {
2430
2474
  return Promise.all(active.map(async (booking) => {
2431
2475
  const bv = await this.bundles.findVersionById(booking.bundleVersionId, tx);
2432
2476
  if (!bv) {
2433
- throw new Error(`BundleVersion '${booking.bundleVersionId}' aus aktiver SubscriptionBundle nicht gefunden`);
2477
+ throw new Error(`BundleVersion '${booking.bundleVersionId}' from an active subscription bundle not found`);
2434
2478
  }
2435
2479
  return {
2436
2480
  bundleKey: bv.bundleKey,
@@ -2452,12 +2496,18 @@ var EntitlementService = class {
2452
2496
  return this.tx.run(async (tx) => {
2453
2497
  const sub = await this.subscriptions.findByTenantIdLocked(input.tenantId, tx);
2454
2498
  if (!sub) {
2455
- throw new import_common17.NotFoundException(`Keine Subscription f\xFCr Tenant ${input.tenantId}`);
2499
+ throw new import_common17.NotFoundException({
2500
+ code: import_types5.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
2501
+ message: `No subscription for tenant ${input.tenantId}`,
2502
+ params: {
2503
+ tenantId: input.tenantId
2504
+ }
2505
+ });
2456
2506
  }
2457
2507
  const limits = await this.deriveLimits(sub, now, tx);
2458
2508
  const max = limits.quotas[input.dimension];
2459
2509
  if (max === void 0) {
2460
- throw new Error(`Quota-Dimension "${input.dimension}" nicht im PlanCatalog (${this.catalog.projectKey}).`);
2510
+ throw new Error(`Quota-Dimension "${input.dimension}" is not in the plan catalog (${this.catalog.projectKey}).`);
2461
2511
  }
2462
2512
  const used = await input.currentUsage(tx, input.tenantId);
2463
2513
  if (max !== -1 && used + delta > max) {
@@ -2478,7 +2528,15 @@ var EntitlementService = class {
2478
2528
  async findActivePlanVersionOrFallback(planId, asOf, tx) {
2479
2529
  const v = this.planVersions.findActive ? await this.planVersions.findActive(planId, asOf, tx) : await this.planVersions.findLatestLive(planId, tx);
2480
2530
  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.`);
2531
+ const asOfDate = asOf.toISOString().slice(0, 10);
2532
+ throw new import_common17.NotFoundException({
2533
+ code: import_types5.BILLING_ERROR_CODES.NO_ACTIVE_PLAN_VERSION,
2534
+ message: `No plan version for ${planId} active as of ${asOfDate} \u2014 neither the validFrom window is satisfied nor is a latest-live version available.`,
2535
+ params: {
2536
+ planId,
2537
+ asOf: asOfDate
2538
+ }
2539
+ });
2482
2540
  }
2483
2541
  return v;
2484
2542
  }
@@ -2581,11 +2639,21 @@ var FeatureGuard = class _FeatureGuard {
2581
2639
  if (!required || required.length === 0) return true;
2582
2640
  const request = context.switchToHttp().getRequest();
2583
2641
  const user = request.user;
2584
- if (!user) throw new import_common18.ForbiddenException("Nicht authentifiziert");
2642
+ if (!user) {
2643
+ throw new import_common18.ForbiddenException({
2644
+ code: import_types6.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
2645
+ message: "Not authenticated"
2646
+ });
2647
+ }
2585
2648
  const role = this.config?.userRoleResolver ? this.config.userRoleResolver(user) : user.role ?? user.platformRole;
2586
2649
  if (role === "SUPER_ADMIN") return true;
2587
2650
  const tenantId = this.config?.tenantIdResolver ? this.config.tenantIdResolver(request) : request.tenantId ?? user.tenantId;
2588
- if (!tenantId) throw new import_common18.ForbiddenException("Kein Mandant zugeordnet");
2651
+ if (!tenantId) {
2652
+ throw new import_common18.ForbiddenException({
2653
+ code: import_types6.AUTH_ERROR_CODES.NO_TENANT_ASSIGNED,
2654
+ message: "No tenant assigned"
2655
+ });
2656
+ }
2589
2657
  const compute = /* @__PURE__ */ __name(() => this.entitlements.computeLimits(tenantId), "compute");
2590
2658
  const limits = this.config?.tenantContextRunner ? await this.config.tenantContextRunner(tenantId, compute) : await compute();
2591
2659
  const allowed = required.some((f) => limits.features.has(f));
@@ -2597,19 +2665,30 @@ var FeatureGuard = class _FeatureGuard {
2597
2665
  /**
2598
2666
  * Upsell response (#36): with a registered `UpsellOfferResolver` the 403
2599
2667
  * becomes machine-readable (`FeatureNotLicensedBody`), so consumer UIs can
2600
- * render a purchase offer. Without a resolver the previous plain 403
2601
- * remains no breaking change.
2668
+ * render a purchase offer. Without a resolver the body carries the same
2669
+ * `code` but no offers.
2602
2670
  *
2603
2671
  * Deliberately 403 + `code` field instead of 402 — rationale in
2604
2672
  * `@saasicat/types` upsell.types.ts (402 is reserved / inconsistently
2605
2673
  * supported; SPA interceptors must not treat the 403 as an auth error, the
2606
2674
  * distinction runs through `code`).
2675
+ *
2676
+ * The message must keep starting with the word `Feature` — consumers
2677
+ * classify the response by that prefix.
2607
2678
  */
2608
2679
  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);
2680
+ const message = `Feature ${required.join(" / ")} is not included in the current plan.`;
2681
+ if (!this.upsellResolver) {
2682
+ return new import_common18.ForbiddenException({
2683
+ code: import_types6.FEATURE_NOT_LICENSED,
2684
+ message,
2685
+ params: {
2686
+ featureKeys: required
2687
+ }
2688
+ });
2689
+ }
2611
2690
  const body = {
2612
- code: import_types2.FEATURE_NOT_LICENSED,
2691
+ code: import_types6.FEATURE_NOT_LICENSED,
2613
2692
  featureKey: required[0],
2614
2693
  featureKeys: required,
2615
2694
  offers: await this.resolveOffersSafe(required, tenantId),
@@ -2625,7 +2704,7 @@ var FeatureGuard = class _FeatureGuard {
2625
2704
  try {
2626
2705
  return await this.upsellResolver.resolveOffers(required, tenantId);
2627
2706
  } catch (error) {
2628
- this.logger.warn(`UpsellOfferResolver fehlgeschlagen f\xFCr [${required.join(", ")}]: ${String(error)}`);
2707
+ this.logger.warn(`UpsellOfferResolver failed for [${required.join(", ")}]: ${String(error)}`);
2629
2708
  return [];
2630
2709
  }
2631
2710
  }
@@ -2649,7 +2728,7 @@ FeatureGuard = _ts_decorate16([
2649
2728
 
2650
2729
  // src/billing/catalog-bundle-upsell-resolver.ts
2651
2730
  var import_common19 = require("@nestjs/common");
2652
- var import_types3 = require("@saasicat/types");
2731
+ var import_types7 = require("@saasicat/types");
2653
2732
 
2654
2733
  // src/billing/public-catalog.tokens.ts
2655
2734
  var PUBLIC_CATALOG_PROJECT_KEY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/PublicCatalogProjectKey");
@@ -2719,7 +2798,7 @@ var CatalogBundleUpsellResolver = class {
2719
2798
  const rows = await this.catalogEntryRepo.listFeatures({
2720
2799
  projectKey: this.projectKey
2721
2800
  });
2722
- return (0, import_types3.collectUnsatisfiedRequires)(featureKeys, (0, import_types3.buildFeatureRequiresIndex)(rows));
2801
+ return (0, import_types7.collectUnsatisfiedRequires)(featureKeys, (0, import_types7.buildFeatureRequiresIndex)(rows));
2723
2802
  }
2724
2803
  /** Only published-and-live AND marketed — non-marketed bundles are not an offer. */
2725
2804
  async listLiveMarketedBundles() {
@@ -2778,6 +2857,7 @@ __name(compareRankedOffers, "compareRankedOffers");
2778
2857
  // src/billing/limit-exceeded.filter.ts
2779
2858
  var import_common20 = require("@nestjs/common");
2780
2859
  var import_core3 = require("@nestjs/core");
2860
+ var import_types8 = require("@saasicat/types");
2781
2861
  function _ts_decorate18(decorators, target, key, desc) {
2782
2862
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2783
2863
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2798,11 +2878,14 @@ var LimitExceededFilter = class _LimitExceededFilter extends import_core3.BaseEx
2798
2878
  const ctx = host.switchToHttp();
2799
2879
  const response = ctx.getResponse();
2800
2880
  const request = ctx.getRequest();
2801
- this.logger.warn(`Limit ${exception.dimension} \xFCberschritten: ${exception.used}/${exception.max} bei ${request.method ?? "?"} ${request.url ?? ""}`);
2881
+ this.logger.warn(`Limit ${exception.dimension} exceeded: ${exception.used}/${exception.max} on ${request.method ?? "?"} ${request.url ?? ""}`);
2802
2882
  response.status(import_common20.HttpStatus.PAYMENT_REQUIRED).send({
2803
2883
  statusCode: import_common20.HttpStatus.PAYMENT_REQUIRED,
2804
2884
  error: "PaymentRequired",
2805
- reason: "LIMIT_EXCEEDED",
2885
+ code: import_types8.BILLING_ERROR_CODES.LIMIT_EXCEEDED,
2886
+ // Deprecated: superseded by `code`, kept until consumers have
2887
+ // migrated. Same value, only under the older field name.
2888
+ reason: import_types8.BILLING_ERROR_CODES.LIMIT_EXCEEDED,
2806
2889
  dimension: exception.dimension,
2807
2890
  used: exception.used,
2808
2891
  max: exception.max,
@@ -2828,8 +2911,8 @@ var PlanCatalogValidationError = class extends Error {
2828
2911
  source;
2829
2912
  errors;
2830
2913
  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}:
2914
+ const messages = errors.map((e) => `${e.instancePath || "/"}: ${e.message ?? "unknown"}`).join("\n ");
2915
+ super(`Plan catalog validation failed for ${source}:
2833
2916
  ${messages}`), this.source = source, this.errors = errors;
2834
2917
  this.name = "PlanCatalogValidationError";
2835
2918
  }
@@ -2846,7 +2929,7 @@ __name(loadPlanCatalogFromFile, "loadPlanCatalogFromFile");
2846
2929
  function loadPlanCatalogFromString(yamlContent, opts) {
2847
2930
  const parsed = import_js_yaml.default.load(yamlContent);
2848
2931
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2849
- throw new Error(`YAML-Inhalt von ${opts.source} ist kein Objekt`);
2932
+ throw new Error(`The YAML content of ${opts.source} is not an object`);
2850
2933
  }
2851
2934
  const ajv = new import__.default({
2852
2935
  strict: false,
@@ -2870,19 +2953,19 @@ function validateConsistency(catalog, source) {
2870
2953
  const planIds = /* @__PURE__ */ new Set();
2871
2954
  for (const plan of catalog.plans ?? []) {
2872
2955
  if (planIds.has(plan.id)) {
2873
- errors.push(`plans[].id: Doppelte Plan-ID "${plan.id}"`);
2956
+ errors.push(`plans[].id: duplicate plan id "${plan.id}"`);
2874
2957
  }
2875
2958
  planIds.add(plan.id);
2876
2959
  if (catalog.features) {
2877
2960
  for (const fk of plan.features) {
2878
2961
  if (!declaredFeatureKeys.has(fk)) {
2879
- errors.push(`plans[id=${plan.id}].features: Unbekannter featureKey "${fk}" \u2014 nicht in catalog.features deklariert`);
2962
+ errors.push(`plans[id=${plan.id}].features: Unknown featureKey "${fk}" \u2014 not declared in catalog.features`);
2880
2963
  }
2881
2964
  }
2882
2965
  }
2883
2966
  }
2884
2967
  if (errors.length > 0) {
2885
- const err = new Error(`Plan-Catalog-Konsistenz-Fehler in ${source}:
2968
+ const err = new Error(`Plan catalog consistency error in ${source}:
2886
2969
  ${errors.join("\n ")}`);
2887
2970
  err.name = "PlanCatalogConsistencyError";
2888
2971
  throw err;
@@ -2965,7 +3048,7 @@ var PlanCatalogImporterService = class {
2965
3048
  if (planResult.created) report.plansCreated++;
2966
3049
  else report.plansSkipped++;
2967
3050
  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).`);
3051
+ report.warnings.push(`Plan '${plan.id}' has no monthlyNet \u2014 skipped (on-request plans need a manual plan version).`);
2969
3052
  continue;
2970
3053
  }
2971
3054
  const versionResult = await this.sink.upsertPlanVersion({
@@ -3114,7 +3197,7 @@ var FEATURE_UI_REGISTRY_TOKEN2 = /* @__PURE__ */ Symbol.for("saas-platform/Featu
3114
3197
 
3115
3198
  // src/billing/public-catalog.controller.ts
3116
3199
  var import_common24 = require("@nestjs/common");
3117
- var import_types4 = require("@saasicat/types");
3200
+ var import_types9 = require("@saasicat/types");
3118
3201
  function _ts_decorate22(decorators, target, key, desc) {
3119
3202
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3120
3203
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -3234,7 +3317,7 @@ var PublicCatalogController = class {
3234
3317
  const rows = await this.catalogEntryRepo.listFeatures({
3235
3318
  projectKey: this.projectKey
3236
3319
  });
3237
- return (0, import_types4.buildFeatureRequiresIndex)(rows);
3320
+ return (0, import_types9.buildFeatureRequiresIndex)(rows);
3238
3321
  } catch {
3239
3322
  return /* @__PURE__ */ new Map();
3240
3323
  }
@@ -3274,7 +3357,7 @@ var PublicCatalogController = class {
3274
3357
  quotas: live.quotas,
3275
3358
  monthlyNet: live.monthlyNet,
3276
3359
  yearlyNet: live.yearlyNet,
3277
- requiresFeatures: (0, import_types4.collectUnsatisfiedRequires)(features, requiresIndex),
3360
+ requiresFeatures: (0, import_types9.collectUnsatisfiedRequires)(features, requiresIndex),
3278
3361
  marketing
3279
3362
  };
3280
3363
  }
@@ -3398,6 +3481,7 @@ var PENDING_PLAN_QUERY_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/Pe
3398
3481
 
3399
3482
  // src/billing/composed-tenant-auth.guard.ts
3400
3483
  var import_common26 = require("@nestjs/common");
3484
+ var import_types10 = require("@saasicat/types");
3401
3485
  function _ts_decorate24(decorators, target, key, desc) {
3402
3486
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3403
3487
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -3425,7 +3509,10 @@ var ComposedTenantAuthGuard = class {
3425
3509
  }
3426
3510
  async canActivate(context) {
3427
3511
  if (!this.guards || this.guards.length === 0) {
3428
- throw new import_common26.ForbiddenException("TenantBillingModule.forRoot.authGuards ist nicht konfiguriert.");
3512
+ throw new import_common26.ForbiddenException({
3513
+ code: import_types10.AUTH_ERROR_CODES.AUTH_GUARDS_NOT_CONFIGURED,
3514
+ message: "TenantBillingModule.forRoot.authGuards is not configured."
3515
+ });
3429
3516
  }
3430
3517
  for (const guard of this.guards) {
3431
3518
  const result = await Promise.resolve(guard.canActivate(context));
@@ -3446,6 +3533,7 @@ ComposedTenantAuthGuard = _ts_decorate24([
3446
3533
 
3447
3534
  // src/billing/tenant-admin.guard.ts
3448
3535
  var import_common27 = require("@nestjs/common");
3536
+ var import_types11 = require("@saasicat/types");
3449
3537
  function _ts_decorate25(decorators, target, key, desc) {
3450
3538
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3451
3539
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -3464,11 +3552,17 @@ var TenantAdminGuard = class {
3464
3552
  canActivate(context) {
3465
3553
  const request = context.switchToHttp().getRequest();
3466
3554
  if (!request.user) {
3467
- throw new import_common27.ForbiddenException("Nicht authentifiziert");
3555
+ throw new import_common27.ForbiddenException({
3556
+ code: import_types11.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
3557
+ message: "Not authenticated"
3558
+ });
3468
3559
  }
3469
3560
  const role = request.user.platformRole ?? request.user.role;
3470
3561
  if (!role || !ADMIN_ROLES.has(role)) {
3471
- throw new import_common27.ForbiddenException("Diese Aktion erfordert die TENANT_ADMIN-Rolle.");
3562
+ throw new import_common27.ForbiddenException({
3563
+ code: import_types11.AUTH_ERROR_CODES.TENANT_ADMIN_REQUIRED,
3564
+ message: "This action requires the TENANT_ADMIN role."
3565
+ });
3472
3566
  }
3473
3567
  return true;
3474
3568
  }
@@ -3506,6 +3600,7 @@ __name(round22, "round2");
3506
3600
 
3507
3601
  // src/billing/plan-change-preview.service.ts
3508
3602
  var import_common29 = require("@nestjs/common");
3603
+ var import_types12 = require("@saasicat/types");
3509
3604
 
3510
3605
  // src/entitlement/index.ts
3511
3606
  var entitlement_exports = {};
@@ -3634,11 +3729,24 @@ var PlanChangePreviewService = class {
3634
3729
  async preview(tenantId, targetPlan, targetCycle, now = /* @__PURE__ */ new Date()) {
3635
3730
  const sub = await this.subscriptions.findForTenant(tenantId);
3636
3731
  if (!sub) {
3637
- throw new import_common29.NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
3732
+ throw new import_common29.NotFoundException({
3733
+ code: import_types12.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
3734
+ message: `No subscription for tenant ${tenantId}`,
3735
+ params: {
3736
+ tenantId
3737
+ }
3738
+ });
3638
3739
  }
3639
3740
  const targetPlanDef = findPlan(this.catalog, targetPlan);
3640
3741
  if (!targetPlanDef) {
3641
- throw new import_common29.NotFoundException(`Plan "${targetPlan}" nicht im Catalog (${this.catalog.projectKey})`);
3742
+ throw new import_common29.NotFoundException({
3743
+ code: import_types12.BILLING_ERROR_CODES.PLAN_NOT_IN_CATALOG,
3744
+ message: `Plan "${targetPlan}" is not in the catalog (${this.catalog.projectKey})`,
3745
+ params: {
3746
+ planKey: targetPlan,
3747
+ projectKey: this.catalog.projectKey
3748
+ }
3749
+ });
3642
3750
  }
3643
3751
  const ctx = {
3644
3752
  currentPeriodStart: sub.currentPeriodStart,
@@ -3703,13 +3811,13 @@ var PlanChangePreviewService = class {
3703
3811
  if (blockedTargets.includes(targetPlan)) {
3704
3812
  blockers.push({
3705
3813
  code: `${targetPlan}_NOT_SELF_SERVICE`,
3706
- message: `${targetSnap.name} wird nur per Sondervertrag aktiviert. Bitte den Vertragsbetreuer kontaktieren.`
3814
+ message: `${targetSnap.name} is only activated via a special contract. Please contact the contract manager.`
3707
3815
  });
3708
3816
  }
3709
3817
  if (blockedSources.includes(sub.plan)) {
3710
3818
  blockers.push({
3711
3819
  code: `${sub.plan}_LOCKED`,
3712
- message: `Aktiver ${currentSnap.name}-Sondervertrag \u2014 bitte den Vertragsbetreuer f\xFCr Plan-Wechsel kontaktieren.`
3820
+ message: `Active ${currentSnap.name} special contract \u2014 please contact the contract manager to change plans.`
3713
3821
  });
3714
3822
  }
3715
3823
  for (const [key, row] of Object.entries(limitsCheck)) {
@@ -3717,19 +3825,19 @@ var PlanChangePreviewService = class {
3717
3825
  const usedDisplay = isFloatQuota(key) ? row.used.toFixed(1) : row.used.toString();
3718
3826
  blockers.push({
3719
3827
  code: `${key.toUpperCase()}_OVER_TARGET`,
3720
- message: `Aktueller Verbrauch ${usedDisplay} \xFCberschreitet das Ziel-Limit ${row.targetMax} (${key}) im ${targetSnap.name}-Paket. Bitte Verbrauch reduzieren.`
3828
+ message: `Current usage ${usedDisplay} exceeds the target limit ${row.targetMax} (${key}) in the ${targetSnap.name} plan. Please reduce usage first.`
3721
3829
  });
3722
3830
  }
3723
3831
  if (featuresLost.length > 0) {
3724
3832
  warnings.push({
3725
3833
  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.`
3834
+ 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
3835
  });
3728
3836
  }
3729
3837
  if (changeType === "NOOP") {
3730
3838
  warnings.push({
3731
3839
  code: "NO_CHANGE",
3732
- message: "Ziel-Paket und Abrechnungszyklus stimmen mit dem aktuellen Stand \xFCberein."
3840
+ message: "Target plan and billing cycle already match the current state."
3733
3841
  });
3734
3842
  }
3735
3843
  const projectedTrialEndsAt = this.trialProjection && sub.status === "TRIAL" ? await this.trialProjection.projectTrialEndsAt({
@@ -3887,13 +3995,13 @@ var PendingPlanMaterializationService = class _PendingPlanMaterializationService
3887
3995
  this.entitlements.invalidateTenant(change.tenantId);
3888
3996
  applied += 1;
3889
3997
  } catch (err) {
3890
- this.logger.error(`Pending-Plan-Materialisierung fehlgeschlagen (tenant ${change.tenantId}): ${String(err)}`);
3998
+ this.logger.error(`Pending plan materialisation failed (tenant ${change.tenantId}): ${String(err)}`);
3891
3999
  continue;
3892
4000
  }
3893
4001
  await this.tryFreeze(change.tenantId, change.pendingPlan, cycle, now);
3894
4002
  }
3895
4003
  if (applied > 0) {
3896
- this.logger.log(`Pending-Plan-Materialisierung: ${applied} geplante(r) Plan-Wechsel angewandt.`);
4004
+ this.logger.log(`Pending plan materialisation: applied ${applied} scheduled plan change(s).`);
3897
4005
  }
3898
4006
  return {
3899
4007
  applied
@@ -3904,7 +4012,7 @@ var PendingPlanMaterializationService = class _PendingPlanMaterializationService
3904
4012
  try {
3905
4013
  await this.contractFreeze.freezeOnPlanChange(tenantId, plan, cycle, now);
3906
4014
  } catch (err) {
3907
- this.logger.error(`Contract-Freeze nach Materialisierung fehlgeschlagen (tenant ${tenantId}): ${String(err)}`);
4015
+ this.logger.error(`Contract freeze after materialisation failed (tenant ${tenantId}): ${String(err)}`);
3908
4016
  }
3909
4017
  }
3910
4018
  };
@@ -3978,7 +4086,7 @@ function createDiscountLineItem(input, discountNet) {
3978
4086
  kind: "discount",
3979
4087
  sourceKey: promoCode?.code ?? firstPromotion?.id ?? "price-discount",
3980
4088
  sourceVersionId: null,
3981
- titleSnapshot: promoCode?.label ?? firstPromotion?.label ?? "Rabatt",
4089
+ titleSnapshot: promoCode?.label ?? firstPromotion?.label ?? "Discount",
3982
4090
  descriptionSnapshot: promoCode ? `Promo-Code ${promoCode.code}` : null,
3983
4091
  quantity: 1,
3984
4092
  unit: null,
@@ -4021,6 +4129,7 @@ function cloneJsonValue(value) {
4021
4129
  __name(cloneJsonValue, "cloneJsonValue");
4022
4130
 
4023
4131
  // src/subscription-contract/subscription-contract.service.ts
4132
+ var import_types13 = require("@saasicat/types");
4024
4133
  function _ts_decorate29(decorators, target, key, desc) {
4025
4134
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4026
4135
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -4052,7 +4161,13 @@ var SubscriptionContractService = class {
4052
4161
  async getById(contractId) {
4053
4162
  const row = await this.repo.findById(contractId);
4054
4163
  if (!row) {
4055
- throw new import_common31.NotFoundException(`SubscriptionContract '${contractId}' nicht gefunden`);
4164
+ throw new import_common31.NotFoundException({
4165
+ code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_NOT_FOUND,
4166
+ message: `SubscriptionContract '${contractId}' not found`,
4167
+ params: {
4168
+ contractId
4169
+ }
4170
+ });
4056
4171
  }
4057
4172
  return row;
4058
4173
  }
@@ -4062,7 +4177,14 @@ var SubscriptionContractService = class {
4062
4177
  async getActiveInvoiceSnapshotForTenant(tenantId, asOf = /* @__PURE__ */ new Date()) {
4063
4178
  const contract = await this.repo.findActiveByTenantId(tenantId, asOf);
4064
4179
  if (!contract) {
4065
- throw new import_common31.NotFoundException(`Kein aktiver SubscriptionContract f\xFCr Tenant ${tenantId}`);
4180
+ throw new import_common31.NotFoundException({
4181
+ code: import_types13.CONTRACT_ERROR_CODES.NO_ACTIVE_SUBSCRIPTION_CONTRACT,
4182
+ message: `No active subscription contract for tenant ${tenantId}`,
4183
+ params: {
4184
+ tenantId,
4185
+ asOf: asOf.toISOString()
4186
+ }
4187
+ });
4066
4188
  }
4067
4189
  return subscriptionContractToInvoiceSnapshot(contract);
4068
4190
  }
@@ -4095,7 +4217,14 @@ var SubscriptionContractService = class {
4095
4217
  }
4096
4218
  createDataFromOffer(offer, options) {
4097
4219
  if (offer.status !== "consumed") {
4098
- throw new import_common31.ConflictException(`CheckoutOffer '${offer.id}' muss vor Contract-Erstellung consumed sein`);
4220
+ throw new import_common31.ConflictException({
4221
+ code: import_types13.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_NOT_CONSUMED,
4222
+ message: `CheckoutOffer '${offer.id}' must be consumed before the contract is created`,
4223
+ params: {
4224
+ offerId: offer.id,
4225
+ status: offer.status
4226
+ }
4227
+ });
4099
4228
  }
4100
4229
  const lineItems = this.lineItemsFromOffer(offer);
4101
4230
  return {
@@ -4128,8 +4257,8 @@ var SubscriptionContractService = class {
4128
4257
  const source = offer.lineItems ?? [];
4129
4258
  if (source.length === 0) {
4130
4259
  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."
4260
+ code: import_types13.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_LINE_ITEMS_REQUIRED,
4261
+ message: "A checkout offer can yield only one contract, and only once its line items are frozen."
4133
4262
  });
4134
4263
  }
4135
4264
  return appendImplicitDiscountLineItem({
@@ -4167,8 +4296,11 @@ var SubscriptionContractService = class {
4167
4296
  const date = value instanceof Date ? new Date(value) : new Date(value);
4168
4297
  if (Number.isNaN(date.getTime())) {
4169
4298
  throw new import_common31.UnprocessableEntityException({
4170
- code: "SUBSCRIPTION_CONTRACT_INVALID_DATE",
4171
- message: `${field} muss ein g\xFCltiges Datum sein.`
4299
+ code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_DATE,
4300
+ message: `${field} must be a valid date.`,
4301
+ params: {
4302
+ field
4303
+ }
4172
4304
  });
4173
4305
  }
4174
4306
  return date;
@@ -4188,32 +4320,39 @@ var SubscriptionContractService = class {
4188
4320
  assertCreateData(data) {
4189
4321
  if (data.lineItems.length === 0) {
4190
4322
  throw new import_common31.UnprocessableEntityException({
4191
- code: "SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED",
4192
- message: "SubscriptionContract ben\xF6tigt mindestens eine LineItem."
4323
+ code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED,
4324
+ message: "A subscription contract requires at least one line item."
4193
4325
  });
4194
4326
  }
4195
4327
  if (data.effectiveUntil && data.effectiveUntil <= data.effectiveFrom) {
4196
4328
  throw new import_common31.UnprocessableEntityException({
4197
- code: "SUBSCRIPTION_CONTRACT_INVALID_WINDOW",
4198
- message: "effectiveUntil muss nach effectiveFrom liegen."
4329
+ code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_WINDOW,
4330
+ message: "effectiveUntil must be after effectiveFrom."
4199
4331
  });
4200
4332
  }
4201
4333
  const planLineItems = data.lineItems.filter((item) => item.kind === "plan");
4202
4334
  if (planLineItems.length !== 1) {
4203
4335
  throw new import_common31.UnprocessableEntityException({
4204
- code: "SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED",
4205
- message: "SubscriptionContract ben\xF6tigt genau eine Plan-Grundposition."
4336
+ code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED,
4337
+ message: "A subscription contract requires exactly one plan base item."
4206
4338
  });
4207
4339
  }
4208
4340
  }
4209
4341
  assertTerminable(existing, data) {
4210
4342
  if (existing.status === "terminated" || existing.status === "superseded") {
4211
- throw new import_common31.ConflictException(`SubscriptionContract '${existing.id}' ist bereits geschlossen`);
4343
+ throw new import_common31.ConflictException({
4344
+ code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_ALREADY_CLOSED,
4345
+ message: `SubscriptionContract '${existing.id}' is already closed`,
4346
+ params: {
4347
+ contractId: existing.id,
4348
+ status: existing.status
4349
+ }
4350
+ });
4212
4351
  }
4213
4352
  if (data.effectiveUntil <= existing.effectiveFrom) {
4214
4353
  throw new import_common31.UnprocessableEntityException({
4215
- code: "SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START",
4216
- message: "effectiveUntil muss nach effectiveFrom des Contracts liegen."
4354
+ code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START,
4355
+ message: "effectiveUntil must be after the effectiveFrom of the contract."
4217
4356
  });
4218
4357
  }
4219
4358
  }
@@ -4505,14 +4644,14 @@ var PreviewPlanChangeDto = class {
4505
4644
  _ts_decorate32([
4506
4645
  (0, import_class_validator3.IsString)(),
4507
4646
  (0, import_class_validator3.Matches)(PLAN_OR_CYCLE_PATTERN, {
4508
- message: "plan muss SCREAMING_SNAKE_CASE sein"
4647
+ message: "plan must be SCREAMING_SNAKE_CASE"
4509
4648
  }),
4510
4649
  _ts_metadata22("design:type", String)
4511
4650
  ], PreviewPlanChangeDto.prototype, "plan", void 0);
4512
4651
  _ts_decorate32([
4513
4652
  (0, import_class_validator3.IsString)(),
4514
4653
  (0, import_class_validator3.Matches)(PLAN_OR_CYCLE_PATTERN, {
4515
- message: "billingCycle muss SCREAMING_SNAKE_CASE sein (z. B. MONTHLY, YEARLY)"
4654
+ message: "billingCycle must be SCREAMING_SNAKE_CASE (e.g. MONTHLY, YEARLY)"
4516
4655
  }),
4517
4656
  _ts_metadata22("design:type", String)
4518
4657
  ], PreviewPlanChangeDto.prototype, "billingCycle", void 0);
@@ -4576,14 +4715,14 @@ var CompleteOnboardingSubscriptionDto = class {
4576
4715
  _ts_decorate33([
4577
4716
  (0, import_class_validator4.IsString)(),
4578
4717
  (0, import_class_validator4.Matches)(PLAN_OR_CYCLE_PATTERN2, {
4579
- message: "plan muss SCREAMING_SNAKE_CASE sein"
4718
+ message: "plan must be SCREAMING_SNAKE_CASE"
4580
4719
  }),
4581
4720
  _ts_metadata23("design:type", String)
4582
4721
  ], CompleteOnboardingSubscriptionDto.prototype, "plan", void 0);
4583
4722
  _ts_decorate33([
4584
4723
  (0, import_class_validator4.IsString)(),
4585
4724
  (0, import_class_validator4.Matches)(PLAN_OR_CYCLE_PATTERN2, {
4586
- message: "billingCycle muss SCREAMING_SNAKE_CASE sein (z. B. MONTHLY, YEARLY)"
4725
+ message: "billingCycle must be SCREAMING_SNAKE_CASE (e.g. MONTHLY, YEARLY)"
4587
4726
  }),
4588
4727
  _ts_metadata23("design:type", String)
4589
4728
  ], CompleteOnboardingSubscriptionDto.prototype, "billingCycle", void 0);
@@ -4591,7 +4730,7 @@ _ts_decorate33([
4591
4730
  (0, import_class_validator4.IsOptional)(),
4592
4731
  (0, import_class_validator4.IsString)(),
4593
4732
  (0, import_class_validator4.Matches)(PROMO_CODE_PATTERN, {
4594
- message: 'promoCode muss aus A\u2013Z, 0\u20139, "-", "_" bestehen (4\u201332 Zeichen)'
4733
+ message: 'promoCode must consist of A\u2013Z, 0\u20139, "-", "_" (4\u201332 characters)'
4595
4734
  }),
4596
4735
  _ts_metadata23("design:type", String)
4597
4736
  ], CompleteOnboardingSubscriptionDto.prototype, "promoCode", void 0);
@@ -4607,9 +4746,11 @@ _ts_decorate33([
4607
4746
 
4608
4747
  // src/billing/tenant-billing.controller.ts
4609
4748
  var import_common36 = require("@nestjs/common");
4749
+ var import_types16 = require("@saasicat/types");
4610
4750
 
4611
4751
  // src/promo/service.ts
4612
4752
  var import_common34 = require("@nestjs/common");
4753
+ var import_types14 = require("@saasicat/types");
4613
4754
 
4614
4755
  // src/promo/tokens.ts
4615
4756
  var PROMO_CODE_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/PromoCodeRepository");
@@ -4663,15 +4804,15 @@ function buildLabel(code, cycle) {
4663
4804
  maximumFractionDigits: 2
4664
4805
  })} \u20AC`;
4665
4806
  if (code.durationType === "ONCE") {
4666
- return `${valueStr} einmalig`;
4807
+ return `${valueStr} once`;
4667
4808
  }
4668
4809
  if (code.durationType === "MONTHS") {
4669
4810
  const m = code.durationValue ?? 0;
4670
- return m === 1 ? `${valueStr} im ersten Monat` : `${valueStr} f\xFCr ${m} Monate`;
4811
+ return m === 1 ? `${valueStr} for the first month` : `${valueStr} for ${m} months`;
4671
4812
  }
4672
4813
  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`;
4814
+ if (n === 1) return cycle === "YEARLY" ? `${valueStr} for the first year` : `${valueStr} for the first month`;
4815
+ return cycle === "YEARLY" ? `${valueStr} for the first ${n} years` : `${valueStr} for the first ${n} months`;
4675
4816
  }
4676
4817
  __name(buildLabel, "buildLabel");
4677
4818
 
@@ -4693,7 +4834,23 @@ function _ts_param22(paramIndex, decorator) {
4693
4834
  };
4694
4835
  }
4695
4836
  __name(_ts_param22, "_ts_param");
4696
- var CODE_PATTERN = /^[A-Z0-9_-]{4,32}$/;
4837
+ var CODE_MIN_LENGTH = 4;
4838
+ var CODE_MAX_LENGTH = 32;
4839
+ var CODE_PATTERN = new RegExp(`^[A-Z0-9_-]{${CODE_MIN_LENGTH},${CODE_MAX_LENGTH}}$`);
4840
+ var PERCENT_MIN_EXCLUSIVE = 0;
4841
+ var PERCENT_MAX = 100;
4842
+ var DURATION_MIN = 1;
4843
+ var DURATION_MAX = 24;
4844
+ function notRedeemable(reason) {
4845
+ return new import_common34.BadRequestException({
4846
+ code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_NOT_REDEEMABLE,
4847
+ message: `Code cannot be redeemed: ${reason}`,
4848
+ params: {
4849
+ reason
4850
+ }
4851
+ });
4852
+ }
4853
+ __name(notRedeemable, "notRedeemable");
4697
4854
  var PromoCodesService = class {
4698
4855
  static {
4699
4856
  __name(this, "PromoCodesService");
@@ -4722,46 +4879,118 @@ var PromoCodesService = class {
4722
4879
  async create(input) {
4723
4880
  const code = input.code.trim().toUpperCase();
4724
4881
  if (!CODE_PATTERN.test(code)) {
4725
- throw new import_common34.BadRequestException('Code darf nur Gro\xDFbuchstaben, Ziffern, \u201E-" und \u201E_" enthalten (4\u201332 Zeichen).');
4882
+ throw new import_common34.BadRequestException({
4883
+ code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_FORMAT_INVALID,
4884
+ message: 'The code may only contain upper-case letters, digits, "-" and "_" (4\u201332 characters).',
4885
+ params: {
4886
+ promoCode: code,
4887
+ minLength: CODE_MIN_LENGTH,
4888
+ maxLength: CODE_MAX_LENGTH
4889
+ }
4890
+ });
4726
4891
  }
4727
4892
  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.");
4893
+ if (!(input.value > PERCENT_MIN_EXCLUSIVE && input.value <= PERCENT_MAX)) {
4894
+ throw new import_common34.BadRequestException({
4895
+ code: import_types14.PROMO_ERROR_CODES.PROMO_PERCENT_OUT_OF_RANGE,
4896
+ message: "The percentage must be between 0 and 100.",
4897
+ params: {
4898
+ value: input.value,
4899
+ min: PERCENT_MIN_EXCLUSIVE,
4900
+ max: PERCENT_MAX
4901
+ }
4902
+ });
4730
4903
  }
4731
4904
  } else if (!(input.value > 0)) {
4732
- throw new import_common34.BadRequestException("Betrag muss positiv sein.");
4905
+ throw new import_common34.BadRequestException({
4906
+ code: import_types14.PROMO_ERROR_CODES.PROMO_AMOUNT_NOT_POSITIVE,
4907
+ message: "The amount must be positive.",
4908
+ params: {
4909
+ value: input.value,
4910
+ min: 0
4911
+ }
4912
+ });
4733
4913
  }
4734
4914
  if (input.durationType === "ONCE") {
4735
4915
  if (input.durationValue != null) {
4736
- throw new import_common34.BadRequestException("Bei einmaligem Rabatt darf keine Laufzeit gesetzt sein.");
4916
+ throw new import_common34.BadRequestException({
4917
+ code: import_types14.PROMO_ERROR_CODES.PROMO_ONE_OFF_WITH_DURATION,
4918
+ message: "A one-off discount must not set a duration.",
4919
+ params: {
4920
+ durationValue: input.durationValue
4921
+ }
4922
+ });
4737
4923
  }
4738
4924
  } else {
4739
4925
  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).");
4926
+ if (v == null || v < DURATION_MIN || v > DURATION_MAX) {
4927
+ throw new import_common34.BadRequestException({
4928
+ code: import_types14.PROMO_ERROR_CODES.PROMO_DURATION_INVALID,
4929
+ message: "Invalid duration (at most 24 months or billing periods).",
4930
+ params: {
4931
+ durationValue: v ?? null,
4932
+ min: DURATION_MIN,
4933
+ max: DURATION_MAX
4934
+ }
4935
+ });
4742
4936
  }
4743
4937
  }
4744
4938
  if (input.validFrom && input.validUntil && input.validFrom >= input.validUntil) {
4745
- throw new import_common34.BadRequestException("G\xFCltigkeits-Fenster ung\xFCltig.");
4939
+ throw new import_common34.BadRequestException({
4940
+ code: import_types14.PROMO_ERROR_CODES.PROMO_VALIDITY_WINDOW_INVALID,
4941
+ message: "Invalid validity window.",
4942
+ params: {
4943
+ validFrom: input.validFrom.toISOString(),
4944
+ validUntil: input.validUntil.toISOString()
4945
+ }
4946
+ });
4746
4947
  }
4747
4948
  const plans = input.appliesToPlans ?? [];
4748
4949
  const blocked = this.config.nonRedeemablePlans ?? [];
4749
4950
  for (const p of plans) {
4750
4951
  if (blocked.includes(p)) {
4751
- throw new import_common34.BadRequestException(`${p}-Plan kann nicht rabattiert werden.`);
4952
+ throw new import_common34.BadRequestException({
4953
+ code: import_types14.PROMO_ERROR_CODES.PROMO_PLAN_NOT_DISCOUNTABLE,
4954
+ message: `${p}-Plan cannot be discounted.`,
4955
+ params: {
4956
+ plan: p
4957
+ }
4958
+ });
4752
4959
  }
4753
4960
  }
4754
4961
  if (input.minimumPlanAmountGross != null && input.minimumPlanAmountGross <= 0) {
4755
- throw new import_common34.BadRequestException("Mindest-Plan-Brutto muss positiv sein.");
4962
+ throw new import_common34.BadRequestException({
4963
+ code: import_types14.PROMO_ERROR_CODES.PROMO_MIN_AMOUNT_NOT_POSITIVE,
4964
+ message: "The minimum plan gross amount must be positive.",
4965
+ params: {
4966
+ value: input.minimumPlanAmountGross,
4967
+ min: 0
4968
+ }
4969
+ });
4756
4970
  }
4757
4971
  if (input.valueType === "ABSOLUTE" && !input.allowZeroInvoice) {
4758
4972
  const lowestApplicableGross = this.lowestApplicablePlanGross(plans);
4759
4973
  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.");
4974
+ throw new import_common34.BadRequestException({
4975
+ code: import_types14.PROMO_ERROR_CODES.PROMO_WOULD_PRODUCE_ZERO_INVOICE,
4976
+ message: "For absolute amounts the discount must stay below the lowest applicable plan price, or allowZeroInvoice must be enabled.",
4977
+ params: {
4978
+ value: input.value,
4979
+ lowestApplicablePlanGross: lowestApplicableGross
4980
+ }
4981
+ });
4761
4982
  }
4762
4983
  }
4763
4984
  const exists = await this.promoRepo.findByCode(code);
4764
- if (exists) throw new import_common34.BadRequestException("Code existiert bereits.");
4985
+ if (exists) {
4986
+ throw new import_common34.BadRequestException({
4987
+ code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_ALREADY_EXISTS,
4988
+ message: "The code already exists.",
4989
+ params: {
4990
+ promoCode: code
4991
+ }
4992
+ });
4993
+ }
4765
4994
  return this.promoRepo.create({
4766
4995
  ...input,
4767
4996
  code
@@ -4769,10 +4998,25 @@ var PromoCodesService = class {
4769
4998
  }
4770
4999
  async update(id, input) {
4771
5000
  const existing = await this.promoRepo.findById(id);
4772
- if (!existing) throw new import_common34.NotFoundException("Code nicht gefunden");
5001
+ if (!existing) {
5002
+ throw new import_common34.NotFoundException({
5003
+ code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
5004
+ message: "Code not found",
5005
+ params: {
5006
+ promoCodeId: id
5007
+ }
5008
+ });
5009
+ }
4773
5010
  if (input.maxRedemptions != null && existing.maxRedemptions != null) {
4774
5011
  if (input.maxRedemptions < existing.maxRedemptions) {
4775
- throw new import_common34.BadRequestException("maxRedemptions kann nicht gesenkt werden.");
5012
+ throw new import_common34.BadRequestException({
5013
+ code: import_types14.PROMO_ERROR_CODES.PROMO_MAX_REDEMPTIONS_LOWERED,
5014
+ message: "maxRedemptions cannot be lowered.",
5015
+ params: {
5016
+ current: existing.maxRedemptions,
5017
+ requested: input.maxRedemptions
5018
+ }
5019
+ });
4776
5020
  }
4777
5021
  }
4778
5022
  return this.promoRepo.update(id, input);
@@ -4780,7 +5024,14 @@ var PromoCodesService = class {
4780
5024
  async softDelete(id) {
4781
5025
  const redemptions = await this.redemptionRepo.countByPromoCode(id);
4782
5026
  if (redemptions > 0) {
4783
- throw new import_common34.BadRequestException("Code hat bereits Einl\xF6sungen \u2014 Soft-Delete nicht m\xF6glich. Stattdessen pausieren.");
5027
+ throw new import_common34.BadRequestException({
5028
+ code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_HAS_REDEMPTIONS,
5029
+ message: "The code already has redemptions \u2014 it cannot be soft-deleted. Pause it instead.",
5030
+ params: {
5031
+ promoCodeId: id,
5032
+ redemptions
5033
+ }
5034
+ });
4784
5035
  }
4785
5036
  await this.promoRepo.softDelete(id);
4786
5037
  }
@@ -4791,7 +5042,15 @@ var PromoCodesService = class {
4791
5042
  async findOne(id) {
4792
5043
  await this.lazyExpire();
4793
5044
  const code = await this.promoRepo.findById(id);
4794
- if (!code || code.deletedAt) throw new import_common34.NotFoundException("Code nicht gefunden");
5045
+ if (!code || code.deletedAt) {
5046
+ throw new import_common34.NotFoundException({
5047
+ code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
5048
+ message: "Code not found",
5049
+ params: {
5050
+ promoCodeId: id
5051
+ }
5052
+ });
5053
+ }
4795
5054
  return code;
4796
5055
  }
4797
5056
  async stats(id) {
@@ -4919,28 +5178,49 @@ var PromoCodesService = class {
4919
5178
  const code = input.code.trim().toUpperCase();
4920
5179
  const promo = await this.promoRepo.findByCode(code, tx);
4921
5180
  if (!promo || promo.deletedAt) {
4922
- throw new import_common34.BadRequestException("Code nicht gefunden");
5181
+ throw new import_common34.BadRequestException({
5182
+ code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
5183
+ message: "Code not found",
5184
+ params: {
5185
+ promoCode: code
5186
+ }
5187
+ });
4923
5188
  }
4924
5189
  const sub = await this.subscriptionLookup.findById(input.subscriptionId, tx);
4925
- if (!sub) throw new import_common34.NotFoundException("Subscription nicht gefunden");
5190
+ if (!sub) {
5191
+ throw new import_common34.NotFoundException({
5192
+ code: import_types14.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5193
+ message: "Subscription not found",
5194
+ params: {
5195
+ subscriptionId: input.subscriptionId
5196
+ }
5197
+ });
5198
+ }
4926
5199
  if (sub.tenantId !== input.tenantId) {
4927
- throw new import_common34.BadRequestException("Subscription geh\xF6rt nicht zum Tenant");
5200
+ throw new import_common34.BadRequestException({
5201
+ code: import_types14.BILLING_ERROR_CODES.SUBSCRIPTION_TENANT_MISMATCH,
5202
+ message: "Subscription does not belong to the tenant",
5203
+ params: {
5204
+ subscriptionId: sub.id,
5205
+ tenantId: input.tenantId
5206
+ }
5207
+ });
4928
5208
  }
4929
5209
  const reason = this.checkEligibility(promo, {
4930
5210
  code,
4931
5211
  planId: sub.plan,
4932
5212
  billingCycle: sub.billingCycle
4933
5213
  });
4934
- if (reason) throw new import_common34.BadRequestException(`Code nicht einl\xF6sbar: ${reason}`);
5214
+ if (reason) throw notRedeemable(reason);
4935
5215
  const firstTimeReason = await this.checkFirstTimeCustomer(promo, input.email, {
4936
5216
  requireEmail: true
4937
5217
  });
4938
5218
  if (firstTimeReason) {
4939
- throw new import_common34.BadRequestException(`Code nicht einl\xF6sbar: ${firstTimeReason}`);
5219
+ throw notRedeemable(firstTimeReason);
4940
5220
  }
4941
5221
  const claimed = await this.promoRepo.claimSlot(promo.id, tx);
4942
5222
  if (!claimed) {
4943
- throw new import_common34.BadRequestException("Code nicht einl\xF6sbar: EXHAUSTED");
5223
+ throw notRedeemable("EXHAUSTED");
4944
5224
  }
4945
5225
  await this.promoRepo.markExhaustedIfFull(promo.id, tx);
4946
5226
  const startsAt = sub.startedAt ?? /* @__PURE__ */ new Date();
@@ -5051,6 +5331,7 @@ PromoCodesService = _ts_decorate34([
5051
5331
 
5052
5332
  // src/billing/subscription-bundles.service.ts
5053
5333
  var import_common35 = require("@nestjs/common");
5334
+ var import_types15 = require("@saasicat/types");
5054
5335
  function _ts_decorate35(decorators, target, key, desc) {
5055
5336
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5056
5337
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -5098,38 +5379,61 @@ var SubscriptionBundlesService = class {
5098
5379
  async addBundleToSubscription(input) {
5099
5380
  const bundleVersion = await this.bundles.findVersionById(input.bundleVersionId);
5100
5381
  if (!bundleVersion) {
5101
- throw new import_common35.NotFoundException(`BundleVersion '${input.bundleVersionId}' nicht gefunden`);
5382
+ throw new import_common35.NotFoundException({
5383
+ code: import_types15.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
5384
+ message: `BundleVersion '${input.bundleVersionId}' not found`,
5385
+ params: {
5386
+ bundleVersionId: input.bundleVersionId
5387
+ }
5388
+ });
5102
5389
  }
5103
5390
  if (bundleVersion.publishedAt === null) {
5104
5391
  throw new import_common35.UnprocessableEntityException({
5105
- code: "BUNDLE_VERSION_NOT_PUBLISHED",
5106
- message: `BundleVersion '${input.bundleVersionId}' ist nicht published und kann nicht gebucht werden.`
5392
+ code: import_types15.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_PUBLISHED,
5393
+ message: `BundleVersion '${input.bundleVersionId}' is not published and cannot be booked.`,
5394
+ params: {
5395
+ bundleVersionId: input.bundleVersionId
5396
+ }
5107
5397
  });
5108
5398
  }
5109
5399
  if (bundleVersion.supersededAt !== null) {
5110
5400
  throw new import_common35.UnprocessableEntityException({
5111
- code: "BUNDLE_VERSION_SUPERSEDED",
5112
- message: `BundleVersion '${input.bundleVersionId}' wurde durch eine Nachfolge-Version abgel\xF6st.`
5401
+ code: import_types15.CATALOG_ERROR_CODES.BUNDLE_VERSION_SUPERSEDED,
5402
+ message: `BundleVersion '${input.bundleVersionId}' has been superseded by a newer version.`,
5403
+ params: {
5404
+ bundleVersionId: input.bundleVersionId
5405
+ }
5113
5406
  });
5114
5407
  }
5115
5408
  if (this.blockedBundles?.bundleKeys?.includes(bundleVersion.bundleKey)) {
5116
5409
  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.`
5410
+ code: import_types15.BILLING_ERROR_CODES.BUNDLE_NOT_SELF_SERVICE,
5411
+ message: `Bundle '${bundleVersion.bundleKey}' is only activated via a special contract. Please contact the contract manager.`,
5412
+ params: {
5413
+ bundleKey: bundleVersion.bundleKey
5414
+ }
5119
5415
  });
5120
5416
  }
5121
5417
  const planIds = bundleVersion.compatibility?.planIds ?? [];
5122
5418
  if (planIds.length > 0 && !planIds.includes(input.currentPlanKey)) {
5123
5419
  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(", ")}].`
5420
+ code: import_types15.BILLING_ERROR_CODES.BUNDLE_INCOMPATIBLE_WITH_PLAN,
5421
+ message: `BundleVersion '${input.bundleVersionId}' is not compatible with plan '${input.currentPlanKey}'. Allowed: [${planIds.join(", ")}].`,
5422
+ params: {
5423
+ bundleVersionId: input.bundleVersionId,
5424
+ planKey: input.currentPlanKey,
5425
+ allowedPlanKeys: planIds
5426
+ }
5126
5427
  });
5127
5428
  }
5128
5429
  const active = await this.repo.listActiveBySubscription(input.subscriptionId);
5129
5430
  if (active.some((b) => b.bundleVersionId === input.bundleVersionId)) {
5130
5431
  throw new import_common35.UnprocessableEntityException({
5131
- code: "BUNDLE_ALREADY_SUBSCRIBED",
5132
- message: `Subscription '${input.subscriptionId}' hat dieses Bundle bereits aktiv gebucht.`
5432
+ code: import_types15.BILLING_ERROR_CODES.BUNDLE_ALREADY_SUBSCRIBED,
5433
+ message: `Subscription '${input.subscriptionId}' has already actively booked this bundle.`,
5434
+ params: {
5435
+ subscriptionId: input.subscriptionId
5436
+ }
5133
5437
  });
5134
5438
  }
5135
5439
  const startedAt = input.startedAt ?? /* @__PURE__ */ new Date();
@@ -5145,12 +5449,21 @@ var SubscriptionBundlesService = class {
5145
5449
  async cancelBundleFromSubscription(input) {
5146
5450
  const existing = await this.repo.findById(input.subscriptionBundleId);
5147
5451
  if (!existing) {
5148
- throw new import_common35.NotFoundException(`SubscriptionBundle '${input.subscriptionBundleId}' nicht gefunden`);
5452
+ throw new import_common35.NotFoundException({
5453
+ code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_FOUND,
5454
+ message: `SubscriptionBundle '${input.subscriptionBundleId}' not found`,
5455
+ params: {
5456
+ subscriptionBundleId: input.subscriptionBundleId
5457
+ }
5458
+ });
5149
5459
  }
5150
5460
  if (existing.canceledAt !== null) {
5151
5461
  throw new import_common35.UnprocessableEntityException({
5152
- code: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELED",
5153
- message: `SubscriptionBundle '${input.subscriptionBundleId}' ist bereits gek\xFCndigt.`
5462
+ code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_ALREADY_CANCELED,
5463
+ message: `SubscriptionBundle '${input.subscriptionBundleId}' is already cancelled.`,
5464
+ params: {
5465
+ subscriptionBundleId: input.subscriptionBundleId
5466
+ }
5154
5467
  });
5155
5468
  }
5156
5469
  const canceledAt = input.canceledAt ?? /* @__PURE__ */ new Date();
@@ -5171,18 +5484,27 @@ var SubscriptionBundlesService = class {
5171
5484
  async reactivateBundle(subscriptionBundleId) {
5172
5485
  const existing = await this.repo.findById(subscriptionBundleId);
5173
5486
  if (!existing) {
5174
- throw new import_common35.NotFoundException(`SubscriptionBundle '${subscriptionBundleId}' nicht gefunden`);
5487
+ throw new import_common35.NotFoundException({
5488
+ code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_FOUND,
5489
+ message: `SubscriptionBundle '${subscriptionBundleId}' not found`,
5490
+ params: {
5491
+ subscriptionBundleId
5492
+ }
5493
+ });
5175
5494
  }
5176
5495
  if (existing.canceledAt === null) {
5177
5496
  throw new import_common35.UnprocessableEntityException({
5178
- code: "SUBSCRIPTION_BUNDLE_NOT_CANCELED",
5179
- message: `SubscriptionBundle '${subscriptionBundleId}' ist nicht gek\xFCndigt.`
5497
+ code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_CANCELED,
5498
+ message: `SubscriptionBundle '${subscriptionBundleId}' is not cancelled.`,
5499
+ params: {
5500
+ subscriptionBundleId
5501
+ }
5180
5502
  });
5181
5503
  }
5182
5504
  if (existing.canceledEffectiveAt !== null && existing.canceledEffectiveAt <= /* @__PURE__ */ new Date()) {
5183
5505
  throw new import_common35.UnprocessableEntityException({
5184
- code: "SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE",
5185
- message: "K\xFCndigung bereits wirksam \u2014 bitte das Bundle neu buchen."
5506
+ code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE,
5507
+ message: "Cancellation already in effect \u2014 book the bundle again."
5186
5508
  });
5187
5509
  }
5188
5510
  return this.repo.reactivate(subscriptionBundleId);
@@ -5288,7 +5610,13 @@ var TenantBillingController = class _TenantBillingController {
5288
5610
  this.usageSnapshot.snapshot(tenantId)
5289
5611
  ]);
5290
5612
  if (!sub) {
5291
- throw new import_common36.NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
5613
+ throw new import_common36.NotFoundException({
5614
+ code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5615
+ message: `No subscription for tenant ${tenantId}`,
5616
+ params: {
5617
+ tenantId
5618
+ }
5619
+ });
5292
5620
  }
5293
5621
  const usage = {};
5294
5622
  for (const key of /* @__PURE__ */ new Set([
@@ -5337,14 +5665,29 @@ var TenantBillingController = class _TenantBillingController {
5337
5665
  const userId = this.requireUserId(req);
5338
5666
  const blockedTargets = this.blockedPlans?.asTarget ?? [];
5339
5667
  if (blockedTargets.includes(dto.plan)) {
5340
- throw new import_common36.ForbiddenException(`${dto.plan} wird nicht per Self-Service aktiviert.`);
5668
+ throw new import_common36.ForbiddenException({
5669
+ code: import_types16.BILLING_ERROR_CODES.PLAN_NOT_SELF_SERVICE,
5670
+ message: `${dto.plan} is not activated via self-service.`,
5671
+ params: {
5672
+ planKey: dto.plan
5673
+ }
5674
+ });
5341
5675
  }
5342
5676
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
5343
- if (!sub) throw new import_common36.NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
5677
+ if (!sub) {
5678
+ throw new import_common36.NotFoundException({
5679
+ code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5680
+ message: `No subscription for tenant ${tenantId}`,
5681
+ params: {
5682
+ tenantId
5683
+ }
5684
+ });
5685
+ }
5344
5686
  const blockers = await this.planPreview.assertChangeAllowed(tenantId, dto.plan, dto.billingCycle);
5345
5687
  if (blockers.length > 0) {
5346
5688
  throw new import_common36.BadRequestException({
5347
- message: "Plan-Wechsel blockiert.",
5689
+ code: import_types16.BILLING_ERROR_CODES.PLAN_CHANGE_BLOCKED,
5690
+ message: "Plan change is blocked.",
5348
5691
  blockers
5349
5692
  });
5350
5693
  }
@@ -5414,14 +5757,29 @@ var TenantBillingController = class _TenantBillingController {
5414
5757
  const warnings = [];
5415
5758
  const blockedTargets = this.blockedPlans?.asTarget ?? [];
5416
5759
  if (blockedTargets.includes(dto.plan)) {
5417
- throw new import_common36.ForbiddenException(`${dto.plan} wird nicht per Self-Service aktiviert.`);
5760
+ throw new import_common36.ForbiddenException({
5761
+ code: import_types16.BILLING_ERROR_CODES.PLAN_NOT_SELF_SERVICE,
5762
+ message: `${dto.plan} is not activated via self-service.`,
5763
+ params: {
5764
+ planKey: dto.plan
5765
+ }
5766
+ });
5418
5767
  }
5419
5768
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
5420
- if (!sub) throw new import_common36.NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
5769
+ if (!sub) {
5770
+ throw new import_common36.NotFoundException({
5771
+ code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5772
+ message: `No subscription for tenant ${tenantId}`,
5773
+ params: {
5774
+ tenantId
5775
+ }
5776
+ });
5777
+ }
5421
5778
  const blockers = await this.planPreview.assertChangeAllowed(tenantId, dto.plan, dto.billingCycle);
5422
5779
  if (blockers.length > 0) {
5423
5780
  throw new import_common36.BadRequestException({
5424
- message: "Plan-Wechsel im Onboarding blockiert.",
5781
+ code: import_types16.BILLING_ERROR_CODES.PLAN_CHANGE_BLOCKED,
5782
+ message: "Plan change during onboarding is blocked.",
5425
5783
  blockers
5426
5784
  });
5427
5785
  }
@@ -5455,8 +5813,13 @@ var TenantBillingController = class _TenantBillingController {
5455
5813
  warnings.push(...this.collectPromoSkipReasons(dto.promoCode, sub));
5456
5814
  }
5457
5815
  } catch (err) {
5816
+ const reason = err instanceof Error ? err.message : null;
5458
5817
  throw new import_common36.BadRequestException({
5459
- message: err instanceof Error ? `Onboarding-Anlage fehlgeschlagen: ${err.message}` : "Onboarding-Anlage fehlgeschlagen"
5818
+ code: import_types16.BILLING_ERROR_CODES.ONBOARDING_CREATE_FAILED,
5819
+ message: reason ? `Onboarding creation failed: ${reason}` : "Onboarding creation failed",
5820
+ params: {
5821
+ reason
5822
+ }
5460
5823
  });
5461
5824
  }
5462
5825
  } else {
@@ -5469,9 +5832,9 @@ var TenantBillingController = class _TenantBillingController {
5469
5832
  });
5470
5833
  if (dto.promoCode) {
5471
5834
  if (!this.promoCodes) {
5472
- warnings.push("Promo-Code wurde gesendet, aber PromoCodesModule ist nicht geladen \u2014 der Code wurde NICHT eingel\xF6st.");
5835
+ warnings.push("A promo code was sent, but PromoCodesModule is not loaded \u2014 the code was NOT redeemed.");
5473
5836
  } 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.");
5837
+ warnings.push("A promo code was sent, but the adapter returns no SubscriptionUsageRecord.id \u2014 the code was NOT redeemed.");
5475
5838
  } else {
5476
5839
  try {
5477
5840
  const redemption = await this.promoCodes.redeem({
@@ -5482,8 +5845,8 @@ var TenantBillingController = class _TenantBillingController {
5482
5845
  });
5483
5846
  promoRedemption = this.toResponseRedemption(redemption, dto.promoCode);
5484
5847
  } catch (err) {
5485
- const message = err instanceof Error ? err.message : "Unbekannter Fehler";
5486
- warnings.push(`Promo-Code konnte nicht eingel\xF6st werden: ${message}`);
5848
+ const message = err instanceof Error ? err.message : "Unknown error";
5849
+ warnings.push(`The promo code could not be redeemed: ${message}`);
5487
5850
  }
5488
5851
  }
5489
5852
  }
@@ -5509,11 +5872,11 @@ var TenantBillingController = class _TenantBillingController {
5509
5872
  });
5510
5873
  bundlesAdded += 1;
5511
5874
  } catch (err) {
5512
- warnings.push(`Bundle '${bundleVersionId}' konnte nicht gebucht werden: ` + (err instanceof Error ? err.message : String(err)));
5875
+ warnings.push(`Bundle '${bundleVersionId}' could not be booked: ` + (err instanceof Error ? err.message : String(err)));
5513
5876
  }
5514
5877
  }
5515
5878
  } 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.");
5879
+ warnings.push("Bundle bookings were requested during onboarding, but SubscriptionBundleModule is not registered in the consumer. No bundles were created.");
5517
5880
  }
5518
5881
  return {
5519
5882
  plan: planResult.plan,
@@ -5533,9 +5896,20 @@ var TenantBillingController = class _TenantBillingController {
5533
5896
  const tenantId = this.requireTenantId(req);
5534
5897
  const userId = this.requireUserId(req);
5535
5898
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
5536
- if (!sub) throw new import_common36.NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
5899
+ if (!sub) {
5900
+ throw new import_common36.NotFoundException({
5901
+ code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5902
+ message: `No subscription for tenant ${tenantId}`,
5903
+ params: {
5904
+ tenantId
5905
+ }
5906
+ });
5907
+ }
5537
5908
  if (!sub.pendingPlanVersion) {
5538
- throw new import_common36.BadRequestException("Es liegt aktuell keine Pending-Plan-Version zur Best\xE4tigung vor.");
5909
+ throw new import_common36.BadRequestException({
5910
+ code: import_types16.BILLING_ERROR_CODES.NO_PENDING_PLAN_VERSION,
5911
+ message: "There is no pending plan version awaiting confirmation."
5912
+ });
5539
5913
  }
5540
5914
  const result = await this.subscriptionWrite.acceptPendingPlanVersion(tenantId, userId, /* @__PURE__ */ new Date());
5541
5915
  if (!result.alreadyAccepted) {
@@ -5580,7 +5954,10 @@ var TenantBillingController = class _TenantBillingController {
5580
5954
  const resolver = this.tenantIdResolver ?? ((r) => r.user?.tenantId ?? null);
5581
5955
  const tenantId = resolver(req);
5582
5956
  if (!tenantId) {
5583
- throw new import_common36.NotFoundException("Tenant-ID nicht im Request gefunden");
5957
+ throw new import_common36.NotFoundException({
5958
+ code: import_types16.AUTH_ERROR_CODES.TENANT_CONTEXT_MISSING,
5959
+ message: "No tenant ID found on the request"
5960
+ });
5584
5961
  }
5585
5962
  return tenantId;
5586
5963
  }
@@ -5588,7 +5965,10 @@ var TenantBillingController = class _TenantBillingController {
5588
5965
  const resolver = this.userIdResolver ?? ((r) => r.user?.sub ?? r.user?.id ?? null);
5589
5966
  const userId = resolver(req);
5590
5967
  if (!userId) {
5591
- throw new import_common36.NotFoundException("User-ID nicht im Request gefunden");
5968
+ throw new import_common36.NotFoundException({
5969
+ code: import_types16.AUTH_ERROR_CODES.TENANT_CONTEXT_MISSING,
5970
+ message: "No user ID found on the request"
5971
+ });
5592
5972
  }
5593
5973
  return userId;
5594
5974
  }
@@ -5628,10 +6008,10 @@ var TenantBillingController = class _TenantBillingController {
5628
6008
  collectPromoSkipReasons(promoCode, sub) {
5629
6009
  const reasons = [];
5630
6010
  if (!this.promoCodes) {
5631
- reasons.push(`Promo-Code "${promoCode}" wurde gesendet, aber PromoCodesModule ist nicht geladen \u2014 der Code wurde NICHT eingel\xF6st.`);
6011
+ reasons.push(`Promo-Code "${promoCode}" was sent, but PromoCodesModule is not loaded \u2014 the code was NOT redeemed.`);
5632
6012
  }
5633
6013
  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.`);
6014
+ reasons.push(`Promo-Code "${promoCode}" was sent, but the adapter returns no SubscriptionUsageRecord.id \u2014 the code was NOT redeemed.`);
5635
6015
  }
5636
6016
  return reasons;
5637
6017
  }
@@ -5646,7 +6026,7 @@ var TenantBillingController = class _TenantBillingController {
5646
6026
  try {
5647
6027
  await this.contractFreeze.freezeOnPlanChange(tenantId, plan, cycle, /* @__PURE__ */ new Date());
5648
6028
  } catch (err) {
5649
- this.logger.error(`Contract-Freeze nach Plan-Wechsel fehlgeschlagen (tenant ${tenantId}): ${String(err)}`);
6029
+ this.logger.error(`Contract freeze after plan change failed (tenant ${tenantId}): ${String(err)}`);
5650
6030
  }
5651
6031
  }
5652
6032
  /**
@@ -5957,7 +6337,7 @@ __name(normalizeQuotas, "normalizeQuotas");
5957
6337
 
5958
6338
  // src/billing/subscription-bundle-preview.service.ts
5959
6339
  var import_common39 = require("@nestjs/common");
5960
- var import_types5 = require("@saasicat/types");
6340
+ var import_types17 = require("@saasicat/types");
5961
6341
  function _ts_decorate39(decorators, target, key, desc) {
5962
6342
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5963
6343
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -5996,7 +6376,13 @@ var SubscriptionBundlePreviewService = class {
5996
6376
  async previewAdd(ctx, input, now = /* @__PURE__ */ new Date()) {
5997
6377
  const bundleVersion = await this.bundles.findVersionById(input.bundleVersionId);
5998
6378
  if (!bundleVersion) {
5999
- throw new import_common39.NotFoundException(`BundleVersion '${input.bundleVersionId}' nicht gefunden`);
6379
+ throw new import_common39.NotFoundException({
6380
+ code: import_types17.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
6381
+ message: `BundleVersion '${input.bundleVersionId}' not found`,
6382
+ params: {
6383
+ bundleVersionId: input.bundleVersionId
6384
+ }
6385
+ });
6000
6386
  }
6001
6387
  const blockers = [];
6002
6388
  const warnings = [];
@@ -6005,7 +6391,7 @@ var SubscriptionBundlePreviewService = class {
6005
6391
  if (activeBundleVersions.some((bv) => bv.id === bundleVersion.id)) {
6006
6392
  blockers.push({
6007
6393
  code: "BUNDLE_ALREADY_SUBSCRIBED",
6008
- message: "Dieses Bundle ist bereits aktiv gebucht."
6394
+ message: "This bundle is already actively booked."
6009
6395
  });
6010
6396
  }
6011
6397
  const planFeatures = new Set(await this.resolvePlanFeatures(ctx.currentPlanKey, now));
@@ -6013,14 +6399,14 @@ var SubscriptionBundlePreviewService = class {
6013
6399
  if (redundantFeatures.length > 0) {
6014
6400
  warnings.push({
6015
6401
  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.`
6402
+ 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
6403
  });
6018
6404
  }
6019
6405
  const missingRequires = await this.collectMissingRequires(bundleVersion, planFeatures, activeBundleVersions);
6020
6406
  if (missingRequires.length > 0) {
6021
6407
  blockers.push({
6022
6408
  code: "BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED",
6023
- message: `Das Bundle ben\xF6tigt [${missingRequires.join(", ")}] \u2014 weder im Plan noch in den aktiven Bundles enthalten.`
6409
+ message: `The bundle requires [${missingRequires.join(", ")}] \u2014 present neither in the plan nor in the active bundles.`
6024
6410
  });
6025
6411
  }
6026
6412
  const priceNet = resolveBundlePriceNet(bundleVersion, ctx.currentPlanKey, ctx.billingCycle);
@@ -6049,18 +6435,30 @@ var SubscriptionBundlePreviewService = class {
6049
6435
  async previewCancel(ctx, input, now = /* @__PURE__ */ new Date()) {
6050
6436
  const existing = await this.subscriptionBundles.findById(input.subscriptionBundleId);
6051
6437
  if (!existing || existing.subscriptionId !== ctx.subscriptionId) {
6052
- throw new import_common39.NotFoundException(`SubscriptionBundle '${input.subscriptionBundleId}' nicht gefunden`);
6438
+ throw new import_common39.NotFoundException({
6439
+ code: import_types17.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_FOUND,
6440
+ message: `SubscriptionBundle '${input.subscriptionBundleId}' not found`,
6441
+ params: {
6442
+ subscriptionBundleId: input.subscriptionBundleId
6443
+ }
6444
+ });
6053
6445
  }
6054
6446
  const bundleVersion = await this.bundles.findVersionById(existing.bundleVersionId);
6055
6447
  if (!bundleVersion) {
6056
- throw new import_common39.NotFoundException(`BundleVersion '${existing.bundleVersionId}' nicht gefunden`);
6448
+ throw new import_common39.NotFoundException({
6449
+ code: import_types17.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
6450
+ message: `BundleVersion '${existing.bundleVersionId}' not found`,
6451
+ params: {
6452
+ bundleVersionId: existing.bundleVersionId
6453
+ }
6454
+ });
6057
6455
  }
6058
6456
  const blockers = [];
6059
6457
  const warnings = [];
6060
6458
  if (existing.canceledAt !== null) {
6061
6459
  blockers.push({
6062
6460
  code: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELED",
6063
- message: "Diese Bundle-Buchung ist bereits gek\xFCndigt."
6461
+ message: "This bundle booking is already cancelled."
6064
6462
  });
6065
6463
  }
6066
6464
  const effectiveAt = resolveBundleCancelEffectiveAt({
@@ -6072,7 +6470,7 @@ var SubscriptionBundlePreviewService = class {
6072
6470
  if (existing.minimumTermEndsAt && existing.minimumTermEndsAt.getTime() > periodEnd.getTime()) {
6073
6471
  warnings.push({
6074
6472
  code: "MINIMUM_TERM_BINDS",
6075
- message: "Die Mindestlaufzeit bindet \xFCber das Periodenende hinaus \u2014 die K\xFCndigung wirkt erst zum Mindestlaufzeit-Ende."
6473
+ message: "The minimum term extends beyond the end of the period \u2014 the cancellation only takes effect when the minimum term ends."
6076
6474
  });
6077
6475
  }
6078
6476
  return {
@@ -6091,26 +6489,26 @@ var SubscriptionBundlePreviewService = class {
6091
6489
  if (bundleVersion.publishedAt === null) {
6092
6490
  blockers.push({
6093
6491
  code: "BUNDLE_VERSION_NOT_PUBLISHED",
6094
- message: "Diese BundleVersion ist nicht ver\xF6ffentlicht und nicht buchbar."
6492
+ message: "This bundle version is not published and cannot be booked."
6095
6493
  });
6096
6494
  }
6097
6495
  if (bundleVersion.supersededAt !== null) {
6098
6496
  blockers.push({
6099
6497
  code: "BUNDLE_VERSION_SUPERSEDED",
6100
- message: "Diese BundleVersion wurde durch eine Nachfolge-Version abgel\xF6st."
6498
+ message: "This bundle version has been superseded by a newer one."
6101
6499
  });
6102
6500
  }
6103
6501
  const planIds = bundleVersion.compatibility?.planIds ?? [];
6104
6502
  if (planIds.length > 0 && !planIds.includes(currentPlanKey)) {
6105
6503
  blockers.push({
6106
6504
  code: "BUNDLE_INCOMPATIBLE_WITH_PLAN",
6107
- message: `Das Bundle ist nicht mit Plan '${currentPlanKey}' kompatibel. Erlaubt: [${planIds.join(", ")}].`
6505
+ message: `The bundle is not compatible with plan '${currentPlanKey}'. Allowed: [${planIds.join(", ")}].`
6108
6506
  });
6109
6507
  }
6110
6508
  if (this.blockedBundles?.bundleKeys?.includes(bundleVersion.bundleKey)) {
6111
6509
  blockers.push({
6112
6510
  code: "BUNDLE_NOT_SELF_SERVICE",
6113
- message: `Bundle '${bundleVersion.bundleKey}' wird nur per Sondervertrag aktiviert. Bitte den Vertragsbetreuer kontaktieren.`
6511
+ message: `Bundle '${bundleVersion.bundleKey}' is only activated via a special contract. Please contact the contract manager.`
6114
6512
  });
6115
6513
  }
6116
6514
  }
@@ -6159,12 +6557,12 @@ var SubscriptionBundlePreviewService = class {
6159
6557
  const entries = await this.catalogEntries.listFeatures({
6160
6558
  projectKey
6161
6559
  });
6162
- const requiresIndex = (0, import_types5.buildFeatureRequiresIndex)(entries);
6560
+ const requiresIndex = (0, import_types17.buildFeatureRequiresIndex)(entries);
6163
6561
  const covered = /* @__PURE__ */ new Set([
6164
6562
  ...planFeatures,
6165
6563
  ...activeBundleVersions.flatMap((bv) => bv.features ?? [])
6166
6564
  ]);
6167
- return (0, import_types5.collectUnsatisfiedRequires)(bundleVersion.features ?? [], requiresIndex).filter((key) => !covered.has(key));
6565
+ return (0, import_types17.collectUnsatisfiedRequires)(bundleVersion.features ?? [], requiresIndex).filter((key) => !covered.has(key));
6168
6566
  }
6169
6567
  /** `projectKey` lives on the bundle stem, not on the version. */
6170
6568
  async resolveProjectKey(bundleVersion) {
@@ -6223,6 +6621,7 @@ var import_common41 = require("@nestjs/common");
6223
6621
 
6224
6622
  // src/billing/tenant-subscription-bundles.controller.ts
6225
6623
  var import_common40 = require("@nestjs/common");
6624
+ var import_types18 = require("@saasicat/types");
6226
6625
 
6227
6626
  // src/billing/dto/subscription-bundles.dto.ts
6228
6627
  var import_class_validator5 = require("class-validator");
@@ -6303,7 +6702,7 @@ var CancelSubscriptionBundleDto = class {
6303
6702
  _ts_decorate40([
6304
6703
  (0, import_class_validator5.IsOptional)(),
6305
6704
  (0, import_class_validator5.Matches)(ISO_DATE_PATTERN, {
6306
- message: "canceledAt muss ISO-Datum sein (YYYY-MM-DD)"
6705
+ message: "canceledAt must be an ISO date (YYYY-MM-DD)"
6307
6706
  }),
6308
6707
  _ts_metadata28("design:type", String)
6309
6708
  ], CancelSubscriptionBundleDto.prototype, "canceledAt", void 0);
@@ -6367,7 +6766,10 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
6367
6766
  const hasAdd = Boolean(dto.bundleVersionId);
6368
6767
  const hasCancel = Boolean(dto.subscriptionBundleId);
6369
6768
  if (hasAdd === hasCancel) {
6370
- throw new import_common40.BadRequestException("Genau eines von bundleVersionId (Add-Preview) oder subscriptionBundleId (Cancel-Preview) angeben.");
6769
+ throw new import_common40.BadRequestException({
6770
+ code: import_types18.BILLING_ERROR_CODES.BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS,
6771
+ message: "Exactly one of bundleVersionId (add preview) or subscriptionBundleId (cancel preview) must be given."
6772
+ });
6371
6773
  }
6372
6774
  const sub = await this.requireSubscription(this.requireTenantId(req));
6373
6775
  const ctx = {
@@ -6407,21 +6809,30 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
6407
6809
  }
6408
6810
  requireSubscriptionPk(sub) {
6409
6811
  if (!sub.id) {
6410
- throw new import_common40.NotFoundException("SubscriptionUsageRecord ohne `id` \u2014 Adapter muss die Subscription-PK durchreichen (siehe SubscriptionUsageRecord.id).");
6812
+ throw new import_common40.NotFoundException("SubscriptionUsageRecord without `id` \u2014 the adapter must return the subscription primary key (see SubscriptionUsageRecord.id).");
6411
6813
  }
6412
6814
  return sub.id;
6413
6815
  }
6414
6816
  requireTenantId(req) {
6415
6817
  const tenantId = this.tenantIdResolver(req);
6416
6818
  if (!tenantId) {
6417
- throw new import_common40.NotFoundException("Tenant-ID nicht im Request gefunden");
6819
+ throw new import_common40.NotFoundException({
6820
+ code: import_types18.AUTH_ERROR_CODES.TENANT_CONTEXT_MISSING,
6821
+ message: "No tenant ID found on the request"
6822
+ });
6418
6823
  }
6419
6824
  return tenantId;
6420
6825
  }
6421
6826
  async requireSubscription(tenantId) {
6422
6827
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
6423
6828
  if (!sub) {
6424
- throw new import_common40.NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
6829
+ throw new import_common40.NotFoundException({
6830
+ code: import_types18.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
6831
+ message: `No subscription for tenant ${tenantId}`,
6832
+ params: {
6833
+ tenantId
6834
+ }
6835
+ });
6425
6836
  }
6426
6837
  return sub;
6427
6838
  }
@@ -6440,7 +6851,7 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
6440
6851
  try {
6441
6852
  await this.contractFreeze.freezeOnPlanChange(tenantId, sub.planVersion.planId, sub.billingCycle, /* @__PURE__ */ new Date());
6442
6853
  } catch (err) {
6443
- this.logger.error(`Contract-Freeze nach Bundle-Mutation fehlgeschlagen (tenant ${tenantId}): ${String(err)}`);
6854
+ this.logger.error(`Contract freeze after bundle mutation failed (tenant ${tenantId}): ${String(err)}`);
6444
6855
  }
6445
6856
  }
6446
6857
  };
@@ -6659,7 +7070,7 @@ __export(catalog_exports, {
6659
7070
 
6660
7071
  // src/catalog/bundles.service.ts
6661
7072
  var import_common43 = require("@nestjs/common");
6662
- var import_types6 = require("@saasicat/types");
7073
+ var import_types19 = require("@saasicat/types");
6663
7074
 
6664
7075
  // src/discovery/discovery.scanner.ts
6665
7076
  var import_node_crypto2 = require("crypto");
@@ -6728,7 +7139,7 @@ var DiscoveryScanner = class _DiscoveryScanner {
6728
7139
  (0, import_node_fs2.writeFileSync)(path, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
6729
7140
  this.logger.log(`Discovery snapshot persisted to ${path}`);
6730
7141
  } catch (err) {
6731
- this.logger.warn(`Discovery snapshot konnte nicht nach ${path} geschrieben werden: ${err.message}`);
7142
+ this.logger.warn(`The discovery snapshot could not be written to ${path}: ${err.message}`);
6732
7143
  }
6733
7144
  }
6734
7145
  /**
@@ -6813,7 +7224,7 @@ var DiscoveryScanner = class _DiscoveryScanner {
6813
7224
  registerCapability(target, meta, declaredAt) {
6814
7225
  const existing = target.get(meta.capabilityKey);
6815
7226
  if (existing) {
6816
- this.logger.warn(`Capability '${meta.capabilityKey}' wird an mehreren Stellen deklariert (${existing.declaredAt} und ${declaredAt}); erste Deklaration gewinnt`);
7227
+ this.logger.warn(`Capability '${meta.capabilityKey}' is declared in several places (${existing.declaredAt} and ${declaredAt}); the first declaration wins`);
6817
7228
  return;
6818
7229
  }
6819
7230
  target.set(meta.capabilityKey, {
@@ -6834,7 +7245,7 @@ var DiscoveryScanner = class _DiscoveryScanner {
6834
7245
  registerQuota(target, opts, declaredAt) {
6835
7246
  const existing = target.get(opts.key);
6836
7247
  if (existing) {
6837
- this.logger.warn(`Quota '${opts.key}' wird an mehreren Stellen definiert (${existing.declaredAt} und ${declaredAt}); erste Definition gewinnt`);
7248
+ this.logger.warn(`Quota '${opts.key}' is defined in several places (${existing.declaredAt} and ${declaredAt}); the first definition wins`);
6838
7249
  return;
6839
7250
  }
6840
7251
  target.set(opts.key, {
@@ -7013,7 +7424,7 @@ function dependencyWarnings(features, snapshot, code) {
7013
7424
  if (selected.has(required)) continue;
7014
7425
  warnings.push({
7015
7426
  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.`,
7427
+ 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
7428
  field: `features[${index}]`,
7018
7429
  value: required
7019
7430
  });
@@ -7032,7 +7443,7 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
7032
7443
  if (approved && !approved.features.has(feature)) {
7033
7444
  warnings.push({
7034
7445
  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).`,
7446
+ message: `Feature '${feature}' is not approved (status != approved). Only approved features are sellable \u2014 approve it in the discovery review (#20).`,
7036
7447
  field: `features[${index}]`,
7037
7448
  value: feature
7038
7449
  });
@@ -7041,7 +7452,7 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
7041
7452
  }
7042
7453
  warnings.push({
7043
7454
  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.`,
7455
+ 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
7456
  field: `features[${index}]`,
7046
7457
  value: feature
7047
7458
  });
@@ -7050,14 +7461,14 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
7050
7461
  if (!knownQuotas.has(quotaKey)) {
7051
7462
  warnings.push({
7052
7463
  code: "QUOTA_MISSING",
7053
- message: `Quota '${quotaKey}' wurde im Discovery-Snapshot nicht gefunden. Im Code muss eine Klasse mit @DefinesQuota({ key: '${quotaKey}', ... }) annotiert sein.`,
7464
+ message: `Quota '${quotaKey}' was not found in the discovery snapshot. In code, a class must be annotated with @DefinesQuota({ key: '${quotaKey}', ... }).`,
7054
7465
  field: `quotas.${quotaKey}`,
7055
7466
  value: quotaKey
7056
7467
  });
7057
7468
  } else if (approved && !approved.quotas.has(quotaKey)) {
7058
7469
  warnings.push({
7059
7470
  code: "QUOTA_NOT_APPROVED",
7060
- message: `Quota '${quotaKey}' ist nicht freigegeben (Status != approved). Nur freigegebene Quotas sind verkaufbar \u2014 im Discovery-Review freigeben (#20).`,
7471
+ message: `Quota '${quotaKey}' is not approved (status != approved). Only approved quotas are sellable \u2014 approve it in the discovery review (#20).`,
7061
7472
  field: `quotas.${quotaKey}`,
7062
7473
  value: quotaKey
7063
7474
  });
@@ -7069,7 +7480,7 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
7069
7480
  if (knownPlanKeys.has(planKey)) return;
7070
7481
  warnings.push({
7071
7482
  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.`,
7483
+ message: `Plan '${planKey}' from compatibility.planIds does not exist in the project. Either fix the planKey, or create the plan record first.`,
7073
7484
  field: `compatibility.planIds[${index}]`,
7074
7485
  value: planKey
7075
7486
  });
@@ -7089,7 +7500,7 @@ function validatePlanDraft(draft, snapshot, marketedOnlyFeatures = /* @__PURE__
7089
7500
  if (approved && !approved.features.has(feature)) {
7090
7501
  warnings.push({
7091
7502
  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).`,
7503
+ message: `Feature '${feature}' is not approved (status != approved). Only approved features are sellable \u2014 approve it in the discovery review (#20).`,
7093
7504
  field: `features[${index}]`,
7094
7505
  value: feature
7095
7506
  });
@@ -7098,7 +7509,7 @@ function validatePlanDraft(draft, snapshot, marketedOnlyFeatures = /* @__PURE__
7098
7509
  }
7099
7510
  warnings.push({
7100
7511
  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.`,
7512
+ 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
7513
  field: `features[${index}]`,
7103
7514
  value: feature
7104
7515
  });
@@ -7107,14 +7518,14 @@ function validatePlanDraft(draft, snapshot, marketedOnlyFeatures = /* @__PURE__
7107
7518
  if (!knownQuotas.has(quotaKey)) {
7108
7519
  warnings.push({
7109
7520
  code: "QUOTA_MISSING",
7110
- message: `Quota '${quotaKey}' wurde im Discovery-Snapshot nicht gefunden. Im Code muss eine Klasse mit @DefinesQuota({ key: '${quotaKey}', ... }) annotiert sein.`,
7521
+ message: `Quota '${quotaKey}' was not found in the discovery snapshot. In code, a class must be annotated with @DefinesQuota({ key: '${quotaKey}', ... }).`,
7111
7522
  field: `quotas.${quotaKey}`,
7112
7523
  value: quotaKey
7113
7524
  });
7114
7525
  } else if (approved && !approved.quotas.has(quotaKey)) {
7115
7526
  warnings.push({
7116
7527
  code: "QUOTA_NOT_APPROVED",
7117
- message: `Quota '${quotaKey}' ist nicht freigegeben (Status != approved). Nur freigegebene Quotas sind verkaufbar \u2014 im Discovery-Review freigeben (#20).`,
7528
+ message: `Quota '${quotaKey}' is not approved (status != approved). Only approved quotas are sellable \u2014 approve it in the discovery review (#20).`,
7118
7529
  field: `quotas.${quotaKey}`,
7119
7530
  value: quotaKey
7120
7531
  });
@@ -7166,7 +7577,7 @@ var BundlesService = class _BundlesService {
7166
7577
  this.mode = config.strictModeCheckMode ?? "blocking";
7167
7578
  this.marketedOnly = new Set(config.marketedOnlyFeatures ?? []);
7168
7579
  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).");
7580
+ this.logger.error("BundlesService: strictModeCheckMode=blocking without a DiscoverySnapshot source \u2014 degrading to warn-only. Wire DiscoveryModule so that enforcement takes effect (#25).");
7170
7581
  this.mode = "warn-only";
7171
7582
  }
7172
7583
  }
@@ -7181,7 +7592,15 @@ var BundlesService = class _BundlesService {
7181
7592
  }
7182
7593
  async getBundle(bundleId) {
7183
7594
  const bundle = await this.repo.findById(bundleId);
7184
- if (!bundle) throw new import_common43.NotFoundException(`Bundle '${bundleId}' nicht gefunden`);
7595
+ if (!bundle) {
7596
+ throw new import_common43.NotFoundException({
7597
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
7598
+ message: `Bundle '${bundleId}' not found`,
7599
+ params: {
7600
+ bundleId
7601
+ }
7602
+ });
7603
+ }
7185
7604
  const versions = await this.annotateEditability(await this.repo.listVersions(bundleId));
7186
7605
  return {
7187
7606
  bundle,
@@ -7191,18 +7610,41 @@ var BundlesService = class _BundlesService {
7191
7610
  async createBundle(data) {
7192
7611
  const existing = await this.repo.findByKey(data.projectKey, data.bundleKey);
7193
7612
  if (existing) {
7194
- throw new import_common43.UnprocessableEntityException(`Bundle '${data.bundleKey}' existiert bereits in Projekt '${data.projectKey}'`);
7613
+ throw new import_common43.UnprocessableEntityException({
7614
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_ALREADY_EXISTS,
7615
+ message: `Bundle '${data.bundleKey}' already exists in project '${data.projectKey}'`,
7616
+ params: {
7617
+ bundleKey: data.bundleKey,
7618
+ projectKey: data.projectKey
7619
+ }
7620
+ });
7195
7621
  }
7196
7622
  return this.repo.create(data);
7197
7623
  }
7198
7624
  async updateBundle(bundleId, data) {
7199
7625
  const existing = await this.repo.findById(bundleId);
7200
- if (!existing) throw new import_common43.NotFoundException(`Bundle '${bundleId}' nicht gefunden`);
7626
+ if (!existing) {
7627
+ throw new import_common43.NotFoundException({
7628
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
7629
+ message: `Bundle '${bundleId}' not found`,
7630
+ params: {
7631
+ bundleId
7632
+ }
7633
+ });
7634
+ }
7201
7635
  return this.repo.update(bundleId, data);
7202
7636
  }
7203
7637
  async softDeleteBundle(bundleId) {
7204
7638
  const existing = await this.repo.findById(bundleId);
7205
- if (!existing) throw new import_common43.NotFoundException(`Bundle '${bundleId}' nicht gefunden`);
7639
+ if (!existing) {
7640
+ throw new import_common43.NotFoundException({
7641
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
7642
+ message: `Bundle '${bundleId}' not found`,
7643
+ params: {
7644
+ bundleId
7645
+ }
7646
+ });
7647
+ }
7206
7648
  if (existing.deletedAt !== null) return;
7207
7649
  await this.repo.softDelete(bundleId);
7208
7650
  }
@@ -7214,18 +7656,27 @@ var BundlesService = class _BundlesService {
7214
7656
  async discardBundleDraft(versionId) {
7215
7657
  const existing = await this.repo.findVersionById(versionId);
7216
7658
  if (!existing) {
7217
- throw new import_common43.NotFoundException(`BundleVersion '${versionId}' nicht gefunden`);
7659
+ throw new import_common43.NotFoundException({
7660
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
7661
+ message: `BundleVersion '${versionId}' not found`,
7662
+ params: {
7663
+ versionId
7664
+ }
7665
+ });
7218
7666
  }
7219
7667
  if (existing.publishedAt !== null) {
7220
7668
  throw new import_common43.UnprocessableEntityException({
7221
- code: "BUNDLE_VERSION_ALREADY_PUBLISHED",
7222
- message: `BundleVersion '${versionId}' ist bereits published und kann nicht verworfen werden.`
7669
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_ALREADY_PUBLISHED,
7670
+ message: `BundleVersion '${versionId}' is already published and cannot be discarded.`,
7671
+ params: {
7672
+ versionId
7673
+ }
7223
7674
  });
7224
7675
  }
7225
7676
  if (typeof this.repo.deleteDraft !== "function") {
7226
7677
  throw new import_common43.UnprocessableEntityException({
7227
- code: "BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED",
7228
- message: "Discard ist im aktuellen Repository nicht implementiert. Implementiere BundleRepository.deleteDraft."
7678
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED,
7679
+ message: "Discard is not implemented in the current repository. Implement BundleRepository.deleteDraft."
7229
7680
  });
7230
7681
  }
7231
7682
  await this.repo.deleteDraft(versionId);
@@ -7239,7 +7690,13 @@ var BundlesService = class _BundlesService {
7239
7690
  async getBundleVersion(versionId) {
7240
7691
  const version = await this.repo.findVersionById(versionId);
7241
7692
  if (!version) {
7242
- throw new import_common43.NotFoundException(`BundleVersion '${versionId}' nicht gefunden`);
7693
+ throw new import_common43.NotFoundException({
7694
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
7695
+ message: `BundleVersion '${versionId}' not found`,
7696
+ params: {
7697
+ versionId
7698
+ }
7699
+ });
7243
7700
  }
7244
7701
  const [annotated] = await this.annotateEditability([
7245
7702
  version
@@ -7281,11 +7738,24 @@ var BundlesService = class _BundlesService {
7281
7738
  async createBundleDraft(data) {
7282
7739
  const bundle = await this.repo.findById(data.bundleId);
7283
7740
  if (!bundle) {
7284
- throw new import_common43.NotFoundException(`Bundle '${data.bundleId}' nicht gefunden`);
7741
+ throw new import_common43.NotFoundException({
7742
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
7743
+ message: `Bundle '${data.bundleId}' not found`,
7744
+ params: {
7745
+ bundleId: data.bundleId
7746
+ }
7747
+ });
7285
7748
  }
7286
7749
  const existingDraft = await this.repo.findCurrentDraft(data.bundleId);
7287
7750
  if (existingDraft) {
7288
- throw new import_common43.UnprocessableEntityException(`Bundle '${bundle.bundleKey}' hat bereits eine Draft-Version v${existingDraft.version}; bitte erst publishen oder verwerfen`);
7751
+ throw new import_common43.UnprocessableEntityException({
7752
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_DRAFT_ALREADY_EXISTS,
7753
+ message: `Bundle '${bundle.bundleKey}' already has a draft version v${existingDraft.version}; publish or discard it first`,
7754
+ params: {
7755
+ bundleKey: bundle.bundleKey,
7756
+ draftVersion: existingDraft.version
7757
+ }
7758
+ });
7289
7759
  }
7290
7760
  if (data.baseVersionId === void 0) {
7291
7761
  const latestLive = await this.repo.findLatestLive(data.bundleId);
@@ -7317,16 +7787,25 @@ var BundlesService = class _BundlesService {
7317
7787
  async updateBundleDraft(versionId, data) {
7318
7788
  const existing = await this.repo.findVersionById(versionId);
7319
7789
  if (!existing) {
7320
- throw new import_common43.NotFoundException(`BundleVersion '${versionId}' nicht gefunden`);
7790
+ throw new import_common43.NotFoundException({
7791
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
7792
+ message: `BundleVersion '${versionId}' not found`,
7793
+ params: {
7794
+ versionId
7795
+ }
7796
+ });
7321
7797
  }
7322
7798
  const [annotated] = await this.annotateEditability([
7323
7799
  existing
7324
7800
  ]);
7325
- const { editable } = (0, import_types6.isVersionEditable)(annotated);
7801
+ const { editable } = (0, import_types19.isVersionEditable)(annotated);
7326
7802
  if (!editable) {
7327
7803
  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.`
7804
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_EDITABLE,
7805
+ 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.`,
7806
+ params: {
7807
+ versionId
7808
+ }
7330
7809
  });
7331
7810
  }
7332
7811
  const merged = {
@@ -7357,10 +7836,22 @@ var BundlesService = class _BundlesService {
7357
7836
  async publishBundleVersion(versionId, publishMeta) {
7358
7837
  const draft = await this.repo.findVersionById(versionId);
7359
7838
  if (!draft) {
7360
- throw new import_common43.NotFoundException(`BundleVersion '${versionId}' nicht gefunden`);
7839
+ throw new import_common43.NotFoundException({
7840
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
7841
+ message: `BundleVersion '${versionId}' not found`,
7842
+ params: {
7843
+ versionId
7844
+ }
7845
+ });
7361
7846
  }
7362
7847
  if (draft.publishedAt !== null) {
7363
- throw new import_common43.UnprocessableEntityException(`BundleVersion '${versionId}' ist bereits published`);
7848
+ throw new import_common43.UnprocessableEntityException({
7849
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_ALREADY_PUBLISHED,
7850
+ message: `BundleVersion '${versionId}' is already published`,
7851
+ params: {
7852
+ versionId
7853
+ }
7854
+ });
7364
7855
  }
7365
7856
  const bundle = await this.repo.findById(draft.bundleId);
7366
7857
  const warnings = await this.runStrictCheck({
@@ -7373,8 +7864,8 @@ var BundlesService = class _BundlesService {
7373
7864
  const explicitZero = /* @__PURE__ */ __name((v) => v !== null && v !== void 0 && Number.parseFloat(String(v)) <= 0, "explicitZero");
7374
7865
  if (explicitZero(draft.monthlyNet) || explicitZero(draft.yearlyNet)) {
7375
7866
  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.",
7867
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_ZERO_PRICE,
7868
+ 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
7869
  monthlyNet: draft.monthlyNet,
7379
7870
  yearlyNet: draft.yearlyNet
7380
7871
  });
@@ -7384,23 +7875,30 @@ var BundlesService = class _BundlesService {
7384
7875
  const validFromInput = publishMeta.validFrom ?? draft.validFrom;
7385
7876
  if (!validFromInput) {
7386
7877
  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."
7878
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
7879
+ message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
7389
7880
  });
7390
7881
  }
7391
7882
  const validFrom = new Date(validFromInput);
7392
7883
  if (Number.isNaN(validFrom.getTime())) {
7393
7884
  throw new import_common43.UnprocessableEntityException({
7394
- code: "BUNDLE_VERSION_VALID_FROM_INVALID",
7395
- message: `validFrom '${validFromInput}' ist kein g\xFCltiges Datum`
7885
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_INVALID,
7886
+ message: `validFrom '${validFromInput}' is not a valid date`,
7887
+ params: {
7888
+ validFrom: validFromInput
7889
+ }
7396
7890
  });
7397
7891
  }
7398
7892
  if (previous?.validFrom) {
7399
7893
  const prevFrom = new Date(previous.validFrom);
7400
7894
  if (validFrom <= prevFrom) {
7401
7895
  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.`
7896
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
7897
+ message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
7898
+ params: {
7899
+ validFrom: validFrom.toISOString(),
7900
+ previousValidFrom: previous.validFrom
7901
+ }
7404
7902
  });
7405
7903
  }
7406
7904
  if (previous.validUntil) {
@@ -7409,8 +7907,11 @@ var BundlesService = class _BundlesService {
7409
7907
  const requiredStart = new Date(prevUntil.getTime() + dayMs);
7410
7908
  if (validFrom.getTime() !== requiredStart.getTime()) {
7411
7909
  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)}.`,
7910
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
7911
+ 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)}.`,
7912
+ params: {
7913
+ received: validFrom.toISOString().slice(0, 10)
7914
+ },
7414
7915
  requiredValidFrom: requiredStart.toISOString().slice(0, 10),
7415
7916
  previousValidUntil: previous.validUntil
7416
7917
  });
@@ -7421,17 +7922,24 @@ var BundlesService = class _BundlesService {
7421
7922
  const validUntil = validUntilInput ? new Date(validUntilInput) : null;
7422
7923
  if (validUntil && Number.isNaN(validUntil.getTime())) {
7423
7924
  throw new import_common43.UnprocessableEntityException({
7424
- code: "BUNDLE_VERSION_VALID_UNTIL_INVALID",
7425
- message: `validUntil '${validUntilInput}' ist kein g\xFCltiges Datum`
7925
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_INVALID,
7926
+ message: `validUntil '${validUntilInput}' is not a valid date`,
7927
+ params: {
7928
+ validUntil: validUntilInput
7929
+ }
7426
7930
  });
7427
7931
  }
7428
7932
  if (validUntil && validUntil <= validFrom) {
7429
7933
  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.`
7934
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM,
7935
+ message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
7936
+ params: {
7937
+ validUntil: validUntil.toISOString(),
7938
+ validFrom: validFrom.toISOString()
7939
+ }
7432
7940
  });
7433
7941
  }
7434
- const diff = previous ? (0, import_types.classifyBundleVersionDiff)({
7942
+ const diff = previous ? (0, import_types4.classifyBundleVersionDiff)({
7435
7943
  features: previous.features,
7436
7944
  quotas: previous.quotas,
7437
7945
  monthlyNet: previous.monthlyNet,
@@ -7447,8 +7955,8 @@ var BundlesService = class _BundlesService {
7447
7955
  };
7448
7956
  if (!diff.nonRegressive && !publishMeta.forceRegressive) {
7449
7957
  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).",
7958
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_REGRESSION,
7959
+ message: "This bundle version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
7452
7960
  changes: diff.changes
7453
7961
  });
7454
7962
  }
@@ -7487,8 +7995,8 @@ var BundlesService = class _BundlesService {
7487
7995
  const hasHardBlockingWarning = blocking.some((w) => w.code === "BUNDLE_PLAN_KEY_UNKNOWN");
7488
7996
  if (this.mode === "blocking" || hasHardBlockingWarning) {
7489
7997
  throw new import_common43.UnprocessableEntityException({
7490
- code: "STRICT_MODE_VIOLATIONS",
7491
- message: "Strict-Mode-Check hat Drift gegen den Discovery-Snapshot gefunden.",
7998
+ code: import_types19.CATALOG_ERROR_CODES.STRICT_MODE_VIOLATIONS,
7999
+ message: "The strict-mode check found drift against the discovery snapshot.",
7492
8000
  warnings
7493
8001
  });
7494
8002
  }
@@ -7501,8 +8009,8 @@ var BundlesService = class _BundlesService {
7501
8009
  if (!validFromInput) {
7502
8010
  if (existing.publishedAt !== null) {
7503
8011
  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."
8012
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
8013
+ message: "Published bundle versions must keep a validFrom. Set a new future date, or edit the draft before publishing."
7506
8014
  });
7507
8015
  }
7508
8016
  return;
@@ -7510,14 +8018,21 @@ var BundlesService = class _BundlesService {
7510
8018
  const validFrom = this.parseRequiredBundleDate(validFromInput, "validFrom", "BUNDLE_VERSION_VALID_FROM_INVALID");
7511
8019
  if (existing.publishedAt !== null && validFrom.getTime() <= Date.now()) {
7512
8020
  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.`
8021
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_FUTURE,
8022
+ message: `validFrom (${validFrom.toISOString()}) must, for a published-but-future bundle version, still lie in the future.`,
8023
+ params: {
8024
+ validFrom: validFrom.toISOString()
8025
+ }
7515
8026
  });
7516
8027
  }
7517
8028
  if (validUntil && validUntil <= validFrom) {
7518
8029
  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.`
8030
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM,
8031
+ message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
8032
+ params: {
8033
+ validUntil: validUntil.toISOString(),
8034
+ validFrom: validFrom.toISOString()
8035
+ }
7521
8036
  });
7522
8037
  }
7523
8038
  const previous = await this.findPreviousBundleVersion(existing);
@@ -7526,8 +8041,12 @@ var BundlesService = class _BundlesService {
7526
8041
  if (Number.isNaN(previousValidFrom.getTime())) return;
7527
8042
  if (validFrom <= previousValidFrom) {
7528
8043
  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.`
8044
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
8045
+ message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
8046
+ params: {
8047
+ validFrom: validFrom.toISOString(),
8048
+ previousValidFrom: previous.validFrom
8049
+ }
7531
8050
  });
7532
8051
  }
7533
8052
  if (!previous.validUntil) return;
@@ -7537,8 +8056,11 @@ var BundlesService = class _BundlesService {
7537
8056
  const requiredStart = new Date(previousValidUntil.getTime() + dayMs);
7538
8057
  if (validFrom.getTime() !== requiredStart.getTime()) {
7539
8058
  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)}.`,
8059
+ code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
8060
+ 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)}.`,
8061
+ params: {
8062
+ received: validFrom.toISOString().slice(0, 10)
8063
+ },
7542
8064
  requiredValidFrom: requiredStart.toISOString().slice(0, 10),
7543
8065
  previousValidUntil: previous.validUntil
7544
8066
  });
@@ -7557,7 +8079,11 @@ var BundlesService = class _BundlesService {
7557
8079
  if (Number.isNaN(date.getTime())) {
7558
8080
  throw new import_common43.UnprocessableEntityException({
7559
8081
  code,
7560
- message: `${field} '${input}' ist kein g\xFCltiges Datum`
8082
+ message: `${field} '${input}' is not a valid date`,
8083
+ params: {
8084
+ field,
8085
+ value: input
8086
+ }
7561
8087
  });
7562
8088
  }
7563
8089
  return date;
@@ -7592,6 +8118,7 @@ BundlesService = _ts_decorate44([
7592
8118
 
7593
8119
  // src/catalog/marketing-projections.service.ts
7594
8120
  var import_common44 = require("@nestjs/common");
8121
+ var import_types20 = require("@saasicat/types");
7595
8122
  function _ts_decorate45(decorators, target, key, desc) {
7596
8123
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
7597
8124
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -7623,7 +8150,13 @@ var MarketingProjectionsService = class {
7623
8150
  async getById(id) {
7624
8151
  const row = await this.repo.findById(id);
7625
8152
  if (!row) {
7626
- throw new import_common44.NotFoundException(`MarketingProjection '${id}' nicht gefunden`);
8153
+ throw new import_common44.NotFoundException({
8154
+ code: import_types20.CATALOG_ERROR_CODES.MARKETING_PROJECTION_NOT_FOUND,
8155
+ message: `MarketingProjection '${id}' not found`,
8156
+ params: {
8157
+ projectionId: id
8158
+ }
8159
+ });
7627
8160
  }
7628
8161
  return row;
7629
8162
  }
@@ -7631,7 +8164,15 @@ var MarketingProjectionsService = class {
7631
8164
  const locale = data.locale ?? "de";
7632
8165
  const existing = await this.repo.findByTarget(data.targetType, data.targetVersionId, locale);
7633
8166
  if (existing) {
7634
- throw new import_common44.ConflictException(`MarketingProjection f\xFCr ${data.targetType}/${data.targetVersionId}/${locale} existiert bereits \u2014 nutze PATCH zum Editieren`);
8167
+ throw new import_common44.ConflictException({
8168
+ code: import_types20.CATALOG_ERROR_CODES.MARKETING_PROJECTION_ALREADY_EXISTS,
8169
+ message: `Marketing projection for ${data.targetType}/${data.targetVersionId}/${locale} already exists \u2014 use PATCH to edit it`,
8170
+ params: {
8171
+ targetType: data.targetType,
8172
+ targetVersionId: data.targetVersionId,
8173
+ locale
8174
+ }
8175
+ });
7635
8176
  }
7636
8177
  return this.repo.create({
7637
8178
  ...data,
@@ -7641,14 +8182,26 @@ var MarketingProjectionsService = class {
7641
8182
  async update(id, data) {
7642
8183
  const existing = await this.repo.findById(id);
7643
8184
  if (!existing) {
7644
- throw new import_common44.NotFoundException(`MarketingProjection '${id}' nicht gefunden`);
8185
+ throw new import_common44.NotFoundException({
8186
+ code: import_types20.CATALOG_ERROR_CODES.MARKETING_PROJECTION_NOT_FOUND,
8187
+ message: `MarketingProjection '${id}' not found`,
8188
+ params: {
8189
+ projectionId: id
8190
+ }
8191
+ });
7645
8192
  }
7646
8193
  return this.repo.update(id, data);
7647
8194
  }
7648
8195
  async delete(id) {
7649
8196
  const existing = await this.repo.findById(id);
7650
8197
  if (!existing) {
7651
- throw new import_common44.NotFoundException(`MarketingProjection '${id}' nicht gefunden`);
8198
+ throw new import_common44.NotFoundException({
8199
+ code: import_types20.CATALOG_ERROR_CODES.MARKETING_PROJECTION_NOT_FOUND,
8200
+ message: `MarketingProjection '${id}' not found`,
8201
+ params: {
8202
+ projectionId: id
8203
+ }
8204
+ });
7652
8205
  }
7653
8206
  await this.repo.delete(id);
7654
8207
  }
@@ -7664,6 +8217,7 @@ MarketingProjectionsService = _ts_decorate45([
7664
8217
 
7665
8218
  // src/catalog/catalog-entries.service.ts
7666
8219
  var import_common45 = require("@nestjs/common");
8220
+ var import_types21 = require("@saasicat/types");
7667
8221
 
7668
8222
  // src/catalog/approval-signature.ts
7669
8223
  function featureApprovalSignature(featureKey, snapshot) {
@@ -7776,14 +8330,14 @@ var CatalogEntriesService = class _CatalogEntriesService {
7776
8330
  if (!this.autoSyncAtBoot) return;
7777
8331
  const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
7778
8332
  if (!snapshot) {
7779
- this.logger.warn("Discovery-Auto-Sync \xFCbersprungen: kein Snapshot (weder Token noch DiscoveryScanner injiziert). DiscoveryModule wiren (#25).");
8333
+ this.logger.warn("Discovery auto-sync skipped: no snapshot (neither token nor DiscoveryScanner injected). Wire up DiscoveryModule (#25).");
7780
8334
  return;
7781
8335
  }
7782
8336
  try {
7783
8337
  const result = await this.syncFromSnapshot(snapshot);
7784
8338
  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
8339
  } 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));
8340
+ 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
8341
  }
7788
8342
  }
7789
8343
  listCapabilities(projectKey, codeStatus) {
@@ -7813,7 +8367,14 @@ var CatalogEntriesService = class _CatalogEntriesService {
7813
8367
  async reviewFeature(projectKey, featureKey, data, reviewedBy) {
7814
8368
  const existing = await this.repo.findFeature(projectKey, featureKey);
7815
8369
  if (!existing) {
7816
- throw new import_common45.NotFoundException(`Feature '${featureKey}' in Projekt '${projectKey}' nicht gefunden`);
8370
+ throw new import_common45.NotFoundException({
8371
+ code: import_types21.CATALOG_ERROR_CODES.FEATURE_NOT_FOUND,
8372
+ message: `Feature '${featureKey}' not found in project '${projectKey}'`,
8373
+ params: {
8374
+ featureKey,
8375
+ projectKey
8376
+ }
8377
+ });
7817
8378
  }
7818
8379
  return this.repo.setFeatureReview(projectKey, featureKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => featureApprovalSignature(featureKey, snapshot)));
7819
8380
  }
@@ -7821,24 +8382,47 @@ var CatalogEntriesService = class _CatalogEntriesService {
7821
8382
  async reviewQuota(projectKey, quotaKey, data, reviewedBy) {
7822
8383
  const existing = await this.repo.findQuota(projectKey, quotaKey);
7823
8384
  if (!existing) {
7824
- throw new import_common45.NotFoundException(`Quota '${quotaKey}' in Projekt '${projectKey}' nicht gefunden`);
8385
+ throw new import_common45.NotFoundException({
8386
+ code: import_types21.CATALOG_ERROR_CODES.QUOTA_NOT_FOUND,
8387
+ message: `Quota '${quotaKey}' not found in project '${projectKey}'`,
8388
+ params: {
8389
+ quotaKey,
8390
+ projectKey
8391
+ }
8392
+ });
7825
8393
  }
7826
8394
  return this.repo.setQuotaReview(projectKey, quotaKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => {
7827
8395
  const discovered = snapshot.quotas.find((q) => q.quotaKey === quotaKey);
7828
8396
  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`);
8397
+ throw new import_common45.UnprocessableEntityException({
8398
+ code: import_types21.CATALOG_ERROR_CODES.QUOTA_NOT_IN_DISCOVERY_SNAPSHOT,
8399
+ message: `Quota '${quotaKey}' is not in the discovery snapshot \u2014 a quota that is not declared in code cannot be approved`,
8400
+ params: {
8401
+ quotaKey
8402
+ }
8403
+ });
7830
8404
  }
7831
8405
  return quotaApprovalSignature(discovered);
7832
8406
  }));
7833
8407
  }
7834
8408
  resolveReviewUpdate(existing, target, reviewedBy, signatureOf) {
7835
8409
  if (!REVIEW_TRANSITIONS[existing.discoveryStatus].includes(target)) {
7836
- throw new import_common45.UnprocessableEntityException(`\xDCbergang '${existing.discoveryStatus}' \u2192 '${target}' ist nicht erlaubt`);
8410
+ throw new import_common45.UnprocessableEntityException({
8411
+ code: import_types21.CATALOG_ERROR_CODES.DISCOVERY_STATUS_TRANSITION_INVALID,
8412
+ message: `Transition '${existing.discoveryStatus}' \u2192 '${target}' is not allowed`,
8413
+ params: {
8414
+ from: existing.discoveryStatus,
8415
+ to: target
8416
+ }
8417
+ });
7837
8418
  }
7838
8419
  if (target === "approved") {
7839
8420
  const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
7840
8421
  if (!snapshot) {
7841
- throw new import_common45.UnprocessableEntityException("Freigabe braucht einen Discovery-Snapshot \u2014 Discovery ist nicht initialisiert (#25)");
8422
+ throw new import_common45.UnprocessableEntityException({
8423
+ code: import_types21.CATALOG_ERROR_CODES.DISCOVERY_NOT_INITIALIZED,
8424
+ message: "Approval requires a discovery snapshot \u2014 discovery is not initialised (#25)"
8425
+ });
7842
8426
  }
7843
8427
  return {
7844
8428
  discoveryStatus: "approved",
@@ -8031,7 +8615,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
8031
8615
  const claimants = claimantsByOldKey.get(entry.key);
8032
8616
  if (claimants && claimants.length > 0) {
8033
8617
  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)`);
8618
+ 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
8619
  }
8036
8620
  target = claimants[0];
8037
8621
  }
@@ -8044,7 +8628,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
8044
8628
  if (changes.length === 0) return 0;
8045
8629
  const setSuccessor = type === "feature" ? this.repo.setFeatureSuccessor?.bind(this.repo) : this.repo.setQuotaSuccessor?.bind(this.repo);
8046
8630
  if (!setSuccessor) {
8047
- this.logger.warn(`CatalogEntryRepository implementiert set${type === "feature" ? "Feature" : "Quota"}Successor nicht \u2014 ${changes.length} Nachfolger-Pointer (#39) werden nicht persistiert`);
8631
+ this.logger.warn(`CatalogEntryRepository does not implement set${type === "feature" ? "Feature" : "Quota"}Successor \u2014 ${changes.length} successor pointers (#39) are not persisted`);
8048
8632
  return 0;
8049
8633
  }
8050
8634
  let replaced = 0;
@@ -8212,7 +8796,7 @@ var ListCatalogEntriesQueryDto = class {
8212
8796
  _ts_decorate48([
8213
8797
  (0, import_class_validator6.IsString)(),
8214
8798
  (0, import_class_validator6.Matches)(PROJECT_KEY_PATTERN, {
8215
- message: "projectKey muss kebab-case sein"
8799
+ message: "projectKey must be kebab-case"
8216
8800
  }),
8217
8801
  _ts_metadata35("design:type", String)
8218
8802
  ], ListCatalogEntriesQueryDto.prototype, "projectKey", void 0);
@@ -8237,7 +8821,7 @@ var ReviewCatalogEntryDto = class {
8237
8821
  _ts_decorate48([
8238
8822
  (0, import_class_validator6.IsString)(),
8239
8823
  (0, import_class_validator6.IsIn)(REVIEW_STATUSES, {
8240
- message: "discoveryStatus muss pending|approved|outdated|obsolete sein"
8824
+ message: "discoveryStatus must be pending|approved|outdated|obsolete"
8241
8825
  }),
8242
8826
  _ts_metadata35("design:type", typeof DiscoveryStatus === "undefined" ? Object : DiscoveryStatus)
8243
8827
  ], ReviewCatalogEntryDto.prototype, "discoveryStatus", void 0);
@@ -8538,6 +9122,7 @@ __name(buildCatalogEntriesController, "buildCatalogEntriesController");
8538
9122
 
8539
9123
  // src/catalog/promotions.service.ts
8540
9124
  var import_common48 = require("@nestjs/common");
9125
+ var import_types22 = require("@saasicat/types");
8541
9126
  function _ts_decorate50(decorators, target, key, desc) {
8542
9127
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
8543
9128
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -8571,7 +9156,13 @@ var PromotionsService = class {
8571
9156
  async getById(id) {
8572
9157
  const row = await this.repo.findById(id);
8573
9158
  if (!row) {
8574
- throw new import_common48.NotFoundException(`Promotion '${id}' nicht gefunden`);
9159
+ throw new import_common48.NotFoundException({
9160
+ code: import_types22.CATALOG_ERROR_CODES.PROMOTION_NOT_FOUND,
9161
+ message: `Promotion '${id}' not found`,
9162
+ params: {
9163
+ promotionId: id
9164
+ }
9165
+ });
8575
9166
  }
8576
9167
  return row;
8577
9168
  }
@@ -8581,14 +9172,26 @@ var PromotionsService = class {
8581
9172
  async update(id, data) {
8582
9173
  const existing = await this.repo.findById(id);
8583
9174
  if (!existing) {
8584
- throw new import_common48.NotFoundException(`Promotion '${id}' nicht gefunden`);
9175
+ throw new import_common48.NotFoundException({
9176
+ code: import_types22.CATALOG_ERROR_CODES.PROMOTION_NOT_FOUND,
9177
+ message: `Promotion '${id}' not found`,
9178
+ params: {
9179
+ promotionId: id
9180
+ }
9181
+ });
8585
9182
  }
8586
9183
  return this.repo.update(id, data);
8587
9184
  }
8588
9185
  async delete(id) {
8589
9186
  const existing = await this.repo.findById(id);
8590
9187
  if (!existing) {
8591
- throw new import_common48.NotFoundException(`Promotion '${id}' nicht gefunden`);
9188
+ throw new import_common48.NotFoundException({
9189
+ code: import_types22.CATALOG_ERROR_CODES.PROMOTION_NOT_FOUND,
9190
+ message: `Promotion '${id}' not found`,
9191
+ params: {
9192
+ promotionId: id
9193
+ }
9194
+ });
8592
9195
  }
8593
9196
  await this.repo.delete(id);
8594
9197
  }
@@ -8645,7 +9248,7 @@ var ListPromotionsQueryDto = class {
8645
9248
  _ts_decorate51([
8646
9249
  (0, import_class_validator7.IsString)(),
8647
9250
  (0, import_class_validator7.Matches)(PROJECT_KEY_PATTERN2, {
8648
- message: "projectKey muss kebab-case sein"
9251
+ message: "projectKey must be kebab-case"
8649
9252
  }),
8650
9253
  _ts_metadata38("design:type", String)
8651
9254
  ], ListPromotionsQueryDto.prototype, "projectKey", void 0);
@@ -8713,14 +9316,14 @@ _ts_decorate51([
8713
9316
  _ts_decorate51([
8714
9317
  (0, import_class_validator7.IsString)(),
8715
9318
  (0, import_class_validator7.Matches)(ISO_DATE_PATTERN2, {
8716
- message: "validFrom muss ISO-Datum sein"
9319
+ message: "validFrom must be an ISO date"
8717
9320
  }),
8718
9321
  _ts_metadata38("design:type", String)
8719
9322
  ], CreatePromotionDto.prototype, "validFrom", void 0);
8720
9323
  _ts_decorate51([
8721
9324
  (0, import_class_validator7.IsString)(),
8722
9325
  (0, import_class_validator7.Matches)(ISO_DATE_PATTERN2, {
8723
- message: "validTo muss ISO-Datum sein"
9326
+ message: "validTo must be an ISO date"
8724
9327
  }),
8725
9328
  _ts_metadata38("design:type", String)
8726
9329
  ], CreatePromotionDto.prototype, "validTo", void 0);
@@ -9041,7 +9644,7 @@ var ListMarketingSettingsQueryDto = class {
9041
9644
  _ts_decorate54([
9042
9645
  (0, import_class_validator8.IsString)(),
9043
9646
  (0, import_class_validator8.Matches)(PROJECT_KEY_PATTERN3, {
9044
- message: "projectKey muss kebab-case sein"
9647
+ message: "projectKey must be kebab-case"
9045
9648
  }),
9046
9649
  _ts_metadata41("design:type", String)
9047
9650
  ], ListMarketingSettingsQueryDto.prototype, "projectKey", void 0);
@@ -9055,7 +9658,7 @@ var UpdateMarketingSettingsDto = class {
9055
9658
  _ts_decorate54([
9056
9659
  (0, import_class_validator8.IsString)(),
9057
9660
  (0, import_class_validator8.Matches)(PROJECT_KEY_PATTERN3, {
9058
- message: "projectKey muss kebab-case sein"
9661
+ message: "projectKey must be kebab-case"
9059
9662
  }),
9060
9663
  _ts_metadata41("design:type", String)
9061
9664
  ], UpdateMarketingSettingsDto.prototype, "projectKey", void 0);
@@ -9067,7 +9670,7 @@ _ts_decorate54([
9067
9670
  }),
9068
9671
  (0, import_class_validator8.Matches)(LOCALE_PATTERN, {
9069
9672
  each: true,
9070
- message: "Locale muss ISO-639-1 sein"
9673
+ message: "Locale must be ISO-639-1"
9071
9674
  }),
9072
9675
  _ts_metadata41("design:type", Array)
9073
9676
  ], UpdateMarketingSettingsDto.prototype, "activeLocales", void 0);
@@ -9142,7 +9745,7 @@ __name(buildMarketingSettingsController, "buildMarketingSettingsController");
9142
9745
 
9143
9746
  // src/catalog/public-marketing-catalog.service.ts
9144
9747
  var import_common52 = require("@nestjs/common");
9145
- var import_types7 = require("@saasicat/types");
9748
+ var import_types23 = require("@saasicat/types");
9146
9749
  function _ts_decorate56(decorators, target, key, desc) {
9147
9750
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
9148
9751
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -9245,7 +9848,7 @@ var PublicMarketingCatalogService = class {
9245
9848
  });
9246
9849
  }
9247
9850
  out.sort((a, b) => b.priority - a.priority);
9248
- const requiresIndex = (0, import_types7.buildFeatureRequiresIndex)([
9851
+ const requiresIndex = (0, import_types23.buildFeatureRequiresIndex)([
9249
9852
  ...labelMeta.features
9250
9853
  ].map(([featureKey, meta]) => ({
9251
9854
  featureKey,
@@ -9299,7 +9902,7 @@ var PublicMarketingCatalogService = class {
9299
9902
  const features = [
9300
9903
  ...bv.features ?? []
9301
9904
  ];
9302
- const requiresFeatures = (0, import_types7.collectUnsatisfiedRequires)(features, requiresIndex);
9905
+ const requiresFeatures = (0, import_types23.collectUnsatisfiedRequires)(features, requiresIndex);
9303
9906
  return {
9304
9907
  bundleKey: bv.bundleKey,
9305
9908
  label: marketing?.displayLabel || bv.label,
@@ -9443,12 +10046,12 @@ var PublicMarketingCatalogService = class {
9443
10046
  };
9444
10047
  }
9445
10048
  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);
10049
+ const monthlyPromo = (0, import_types23.pickActivePromo)(promotions, targetKey, locale, "monthly", asOf, targetType);
10050
+ const yearlyPromo = (0, import_types23.pickActivePromo)(promotions, targetKey, locale, "yearly", asOf, targetType);
9448
10051
  const promo = monthlyPromo ?? yearlyPromo;
9449
10052
  if (!promo) return null;
9450
- const monthlyResult = (0, import_types7.applyPromo)(monthlyPromo, monthlyNet);
9451
- const yearlyResult = (0, import_types7.applyPromo)(yearlyPromo, yearlyNet);
10053
+ const monthlyResult = (0, import_types23.applyPromo)(monthlyPromo, monthlyNet);
10054
+ const yearlyResult = (0, import_types23.applyPromo)(yearlyPromo, yearlyNet);
9452
10055
  const i18n = promo.i18n?.[locale] ?? promo.i18n?.[DEFAULT_LOCALE] ?? {};
9453
10056
  return {
9454
10057
  type: promo.type,
@@ -9555,6 +10158,7 @@ __name(parseAsOf, "parseAsOf");
9555
10158
 
9556
10159
  // src/catalog/plans.service.ts
9557
10160
  var import_common54 = require("@nestjs/common");
10161
+ var import_types24 = require("@saasicat/types");
9558
10162
  function _ts_decorate58(decorators, target, key, desc) {
9559
10163
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
9560
10164
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -9601,24 +10205,55 @@ var PlansService = class {
9601
10205
  }
9602
10206
  async getPlan(planId) {
9603
10207
  const plan = await this.repo.findById(planId);
9604
- if (!plan) throw new import_common54.NotFoundException(`Plan '${planId}' nicht gefunden`);
10208
+ if (!plan) {
10209
+ throw new import_common54.NotFoundException({
10210
+ code: import_types24.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10211
+ message: `Plan '${planId}' not found`,
10212
+ params: {
10213
+ planId
10214
+ }
10215
+ });
10216
+ }
9605
10217
  return plan;
9606
10218
  }
9607
10219
  async createPlan(data) {
9608
10220
  const existing = await this.repo.findByKey(data.projectKey, data.planKey);
9609
10221
  if (existing) {
9610
- throw new import_common54.UnprocessableEntityException(`Plan '${data.planKey}' existiert bereits in Projekt '${data.projectKey}'`);
10222
+ throw new import_common54.UnprocessableEntityException({
10223
+ code: import_types24.CATALOG_ERROR_CODES.PLAN_ALREADY_EXISTS,
10224
+ message: `Plan '${data.planKey}' already exists in project '${data.projectKey}'`,
10225
+ params: {
10226
+ planKey: data.planKey,
10227
+ projectKey: data.projectKey
10228
+ }
10229
+ });
9611
10230
  }
9612
10231
  return this.repo.create(data);
9613
10232
  }
9614
10233
  async updatePlan(planId, data) {
9615
10234
  const existing = await this.repo.findById(planId);
9616
- if (!existing) throw new import_common54.NotFoundException(`Plan '${planId}' nicht gefunden`);
10235
+ if (!existing) {
10236
+ throw new import_common54.NotFoundException({
10237
+ code: import_types24.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10238
+ message: `Plan '${planId}' not found`,
10239
+ params: {
10240
+ planId
10241
+ }
10242
+ });
10243
+ }
9617
10244
  return this.repo.update(planId, data);
9618
10245
  }
9619
10246
  async softDeletePlan(planId) {
9620
10247
  const existing = await this.repo.findById(planId);
9621
- if (!existing) throw new import_common54.NotFoundException(`Plan '${planId}' nicht gefunden`);
10248
+ if (!existing) {
10249
+ throw new import_common54.NotFoundException({
10250
+ code: import_types24.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10251
+ message: `Plan '${planId}' not found`,
10252
+ params: {
10253
+ planId
10254
+ }
10255
+ });
10256
+ }
9622
10257
  if (existing.deletedAt !== null) return;
9623
10258
  await this.assertNoPublishedVersions(existing.planKey, "soft-delete");
9624
10259
  await this.repo.softDelete(planId);
@@ -9635,13 +10270,21 @@ var PlansService = class {
9635
10270
  */
9636
10271
  async hardDeletePlan(planId) {
9637
10272
  const existing = await this.repo.findById(planId);
9638
- if (!existing) throw new import_common54.NotFoundException(`Plan '${planId}' nicht gefunden`);
10273
+ if (!existing) {
10274
+ throw new import_common54.NotFoundException({
10275
+ code: import_types24.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10276
+ message: `Plan '${planId}' not found`,
10277
+ params: {
10278
+ planId
10279
+ }
10280
+ });
10281
+ }
9639
10282
  await this.assertNoPublishedVersions(existing.planKey, "hard-delete");
9640
10283
  await this.assertNoDrafts(existing.planKey);
9641
10284
  if (typeof this.repo.hardDelete !== "function") {
9642
10285
  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."
10286
+ code: import_types24.CATALOG_ERROR_CODES.PLAN_HARD_DELETE_NOT_IMPLEMENTED,
10287
+ message: "Hard delete is not implemented in the current repository. Implement PlanRepository.hardDelete."
9645
10288
  });
9646
10289
  }
9647
10290
  await this.repo.hardDelete(planId);
@@ -9661,8 +10304,12 @@ var PlansService = class {
9661
10304
  const live = published.filter((v) => v.supersededAt === null).length;
9662
10305
  const superseded = published.length - live;
9663
10306
  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.`,
10307
+ code: import_types24.CATALOG_ERROR_CODES.PLAN_HAS_PUBLISHED_VERSIONS,
10308
+ 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.`,
10309
+ params: {
10310
+ planKey,
10311
+ operation: op
10312
+ },
9666
10313
  publishedCount: published.length,
9667
10314
  liveCount: live,
9668
10315
  supersededCount: superseded
@@ -9674,8 +10321,11 @@ var PlansService = class {
9674
10321
  const drafts = versions.filter((v) => v.publishedAt === null);
9675
10322
  if (drafts.length === 0) return;
9676
10323
  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.`,
10324
+ code: import_types24.CATALOG_ERROR_CODES.PLAN_HAS_DRAFTS,
10325
+ message: `Plan '${planKey}' still has ${drafts.length} open draft version(s). Discard them first (DELETE /admin/catalog/plan-versions/:id) or publish them.`,
10326
+ params: {
10327
+ planKey
10328
+ },
9679
10329
  draftCount: drafts.length
9680
10330
  });
9681
10331
  }
@@ -9694,7 +10344,7 @@ PlansService = _ts_decorate58([
9694
10344
 
9695
10345
  // src/catalog/plan-versions.service.ts
9696
10346
  var import_common55 = require("@nestjs/common");
9697
- var import_types8 = require("@saasicat/types");
10347
+ var import_types25 = require("@saasicat/types");
9698
10348
  function _ts_decorate59(decorators, target, key, desc) {
9699
10349
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
9700
10350
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -9733,7 +10383,7 @@ var PlanVersionsService = class _PlanVersionsService {
9733
10383
  this.mode = config.strictModeCheckMode ?? "blocking";
9734
10384
  this.marketedOnly = new Set(config.marketedOnlyFeatures ?? []);
9735
10385
  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).");
10386
+ this.logger.error("PlanVersionsService: strictModeCheckMode=blocking without a DiscoverySnapshot source \u2014 degrading to warn-only. Wire DiscoveryModule (#25).");
9737
10387
  this.mode = "warn-only";
9738
10388
  }
9739
10389
  const required = [
@@ -9747,7 +10397,7 @@ var PlanVersionsService = class _PlanVersionsService {
9747
10397
  ];
9748
10398
  for (const key of required) {
9749
10399
  if (typeof this.repo[key] !== "function") {
9750
- throw new Error(`PlanVersionsService: PlanRepository.${String(key)} fehlt \u2014 das Repository implementiert die Lifecycle-Methoden nicht.`);
10400
+ throw new Error(`PlanVersionsService: PlanRepository.${String(key)} is missing \u2014 the repository does not implement the lifecycle methods.`);
9751
10401
  }
9752
10402
  }
9753
10403
  }
@@ -9763,18 +10413,27 @@ var PlanVersionsService = class _PlanVersionsService {
9763
10413
  async discardPlanDraft(versionId) {
9764
10414
  const existing = await this.repo.findVersionById(versionId);
9765
10415
  if (!existing) {
9766
- throw new import_common55.NotFoundException(`PlanVersion '${versionId}' nicht gefunden`);
10416
+ throw new import_common55.NotFoundException({
10417
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10418
+ message: `PlanVersion '${versionId}' not found`,
10419
+ params: {
10420
+ versionId
10421
+ }
10422
+ });
9767
10423
  }
9768
10424
  if (existing.publishedAt !== null) {
9769
10425
  throw new import_common55.UnprocessableEntityException({
9770
- code: "PLAN_VERSION_ALREADY_PUBLISHED",
9771
- message: `PlanVersion '${versionId}' ist bereits published und kann nicht verworfen werden.`
10426
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_ALREADY_PUBLISHED,
10427
+ message: `PlanVersion '${versionId}' is already published and cannot be discarded.`,
10428
+ params: {
10429
+ versionId
10430
+ }
9772
10431
  });
9773
10432
  }
9774
10433
  if (typeof this.repo.deletePlanVersionDraft !== "function") {
9775
10434
  throw new import_common55.UnprocessableEntityException({
9776
- code: "PLAN_VERSION_DISCARD_NOT_IMPLEMENTED",
9777
- message: "Discard ist im aktuellen Repository nicht implementiert. Implementiere PlanRepository.deletePlanVersionDraft."
10435
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_DISCARD_NOT_IMPLEMENTED,
10436
+ message: "Discard is not implemented in the current repository. Implement PlanRepository.deletePlanVersionDraft."
9778
10437
  });
9779
10438
  }
9780
10439
  await this.repo.deletePlanVersionDraft(versionId);
@@ -9787,7 +10446,13 @@ var PlanVersionsService = class _PlanVersionsService {
9787
10446
  async listPlanVersions(planUuid) {
9788
10447
  const plan = await this.repo.findById(planUuid);
9789
10448
  if (!plan) {
9790
- throw new import_common55.NotFoundException(`Plan '${planUuid}' nicht gefunden`);
10449
+ throw new import_common55.NotFoundException({
10450
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10451
+ message: `Plan '${planUuid}' not found`,
10452
+ params: {
10453
+ planId: planUuid
10454
+ }
10455
+ });
9791
10456
  }
9792
10457
  const versions = await this.repo.listVersions(plan.planKey);
9793
10458
  return this.annotateEditability(versions);
@@ -9795,7 +10460,13 @@ var PlanVersionsService = class _PlanVersionsService {
9795
10460
  async getPlanVersion(versionId) {
9796
10461
  const version = await this.repo.findVersionById(versionId);
9797
10462
  if (!version) {
9798
- throw new import_common55.NotFoundException(`PlanVersion '${versionId}' nicht gefunden`);
10463
+ throw new import_common55.NotFoundException({
10464
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10465
+ message: `PlanVersion '${versionId}' not found`,
10466
+ params: {
10467
+ versionId
10468
+ }
10469
+ });
9799
10470
  }
9800
10471
  const [annotated] = await this.annotateEditability([
9801
10472
  version
@@ -9833,12 +10504,25 @@ var PlanVersionsService = class _PlanVersionsService {
9833
10504
  async createPlanDraft(data) {
9834
10505
  const plan = await this.repo.findById(data.planId);
9835
10506
  if (!plan) {
9836
- throw new import_common55.NotFoundException(`Plan '${data.planId}' nicht gefunden`);
10507
+ throw new import_common55.NotFoundException({
10508
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10509
+ message: `Plan '${data.planId}' not found`,
10510
+ params: {
10511
+ planId: data.planId
10512
+ }
10513
+ });
9837
10514
  }
9838
10515
  const planKey = plan.planKey;
9839
10516
  const existingDraft = await this.repo.findCurrentDraft(planKey);
9840
10517
  if (existingDraft) {
9841
- throw new import_common55.UnprocessableEntityException(`Plan '${planKey}' hat bereits eine Draft-Version v${existingDraft.version}; bitte erst publishen oder verwerfen`);
10518
+ throw new import_common55.UnprocessableEntityException({
10519
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_DRAFT_ALREADY_EXISTS,
10520
+ message: `Plan '${planKey}' already has a draft version v${existingDraft.version}; publish or discard it first`,
10521
+ params: {
10522
+ planKey,
10523
+ draftVersion: existingDraft.version
10524
+ }
10525
+ });
9842
10526
  }
9843
10527
  let baseVersionId = data.baseVersionId;
9844
10528
  if (baseVersionId === void 0) {
@@ -9863,16 +10547,25 @@ var PlanVersionsService = class _PlanVersionsService {
9863
10547
  async updatePlanDraft(versionId, data) {
9864
10548
  const existing = await this.repo.findVersionById(versionId);
9865
10549
  if (!existing) {
9866
- throw new import_common55.NotFoundException(`PlanVersion '${versionId}' nicht gefunden`);
10550
+ throw new import_common55.NotFoundException({
10551
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10552
+ message: `PlanVersion '${versionId}' not found`,
10553
+ params: {
10554
+ versionId
10555
+ }
10556
+ });
9867
10557
  }
9868
10558
  const [annotated] = await this.annotateEditability([
9869
10559
  existing
9870
10560
  ]);
9871
- const { editable } = (0, import_types8.isVersionEditable)(annotated);
10561
+ const { editable } = (0, import_types25.isVersionEditable)(annotated);
9872
10562
  if (!editable) {
9873
10563
  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.`
10564
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_EDITABLE,
10565
+ 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.`,
10566
+ params: {
10567
+ versionId
10568
+ }
9876
10569
  });
9877
10570
  }
9878
10571
  const merged = {
@@ -9890,10 +10583,22 @@ var PlanVersionsService = class _PlanVersionsService {
9890
10583
  async publishPlanVersion(versionId, publishMeta) {
9891
10584
  const draft = await this.repo.findVersionById(versionId);
9892
10585
  if (!draft) {
9893
- throw new import_common55.NotFoundException(`PlanVersion '${versionId}' nicht gefunden`);
10586
+ throw new import_common55.NotFoundException({
10587
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10588
+ message: `PlanVersion '${versionId}' not found`,
10589
+ params: {
10590
+ versionId
10591
+ }
10592
+ });
9894
10593
  }
9895
10594
  if (draft.publishedAt !== null) {
9896
- throw new import_common55.UnprocessableEntityException(`PlanVersion '${versionId}' ist bereits published`);
10595
+ throw new import_common55.UnprocessableEntityException({
10596
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_ALREADY_PUBLISHED,
10597
+ message: `PlanVersion '${versionId}' is already published`,
10598
+ params: {
10599
+ versionId
10600
+ }
10601
+ });
9897
10602
  }
9898
10603
  const warnings = await this.runStrictCheck({
9899
10604
  features: draft.features,
@@ -9905,8 +10610,8 @@ var PlanVersionsService = class _PlanVersionsService {
9905
10610
  const yearly = Number.parseFloat(String(draft.yearlyNet ?? "0"));
9906
10611
  if (monthly <= 0 || yearly <= 0) {
9907
10612
  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.",
10613
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_ZERO_PRICE,
10614
+ 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
10615
  monthlyNet: String(draft.monthlyNet),
9911
10616
  yearlyNet: String(draft.yearlyNet)
9912
10617
  });
@@ -9916,23 +10621,30 @@ var PlanVersionsService = class _PlanVersionsService {
9916
10621
  const validFromInput = publishMeta.validFrom ?? draft.validFrom;
9917
10622
  if (!validFromInput) {
9918
10623
  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."
10624
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_REQUIRED,
10625
+ message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
9921
10626
  });
9922
10627
  }
9923
10628
  const validFrom = new Date(validFromInput);
9924
10629
  if (Number.isNaN(validFrom.getTime())) {
9925
10630
  throw new import_common55.UnprocessableEntityException({
9926
- code: "PLAN_VERSION_VALID_FROM_INVALID",
9927
- message: `validFrom '${validFromInput}' ist kein g\xFCltiges Datum`
10631
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_INVALID,
10632
+ message: `validFrom '${validFromInput}' is not a valid date`,
10633
+ params: {
10634
+ validFrom: validFromInput
10635
+ }
9928
10636
  });
9929
10637
  }
9930
10638
  if (previous?.validFrom) {
9931
10639
  const prevFrom = new Date(previous.validFrom);
9932
10640
  if (validFrom <= prevFrom) {
9933
10641
  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.`
10642
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
10643
+ message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
10644
+ params: {
10645
+ validFrom: validFrom.toISOString(),
10646
+ previousValidFrom: previous.validFrom
10647
+ }
9936
10648
  });
9937
10649
  }
9938
10650
  if (previous.validUntil) {
@@ -9941,8 +10653,11 @@ var PlanVersionsService = class _PlanVersionsService {
9941
10653
  const requiredStart = new Date(prevUntil.getTime() + dayMs);
9942
10654
  if (validFrom.getTime() !== requiredStart.getTime()) {
9943
10655
  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)}.`,
10656
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_NOT_GAPLESS,
10657
+ 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)}.`,
10658
+ params: {
10659
+ received: validFrom.toISOString().slice(0, 10)
10660
+ },
9946
10661
  requiredValidFrom: requiredStart.toISOString().slice(0, 10),
9947
10662
  previousValidUntil: previous.validUntil
9948
10663
  });
@@ -9953,14 +10668,21 @@ var PlanVersionsService = class _PlanVersionsService {
9953
10668
  const validUntil = validUntilInput ? new Date(validUntilInput) : null;
9954
10669
  if (validUntil && Number.isNaN(validUntil.getTime())) {
9955
10670
  throw new import_common55.UnprocessableEntityException({
9956
- code: "PLAN_VERSION_VALID_UNTIL_INVALID",
9957
- message: `validUntil '${validUntilInput}' ist kein g\xFCltiges Datum`
10671
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_UNTIL_INVALID,
10672
+ message: `validUntil '${validUntilInput}' is not a valid date`,
10673
+ params: {
10674
+ validUntil: validUntilInput
10675
+ }
9958
10676
  });
9959
10677
  }
9960
10678
  if (validUntil && validUntil <= validFrom) {
9961
10679
  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.`
10680
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_UNTIL_BEFORE_FROM,
10681
+ message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
10682
+ params: {
10683
+ validUntil: validUntil.toISOString(),
10684
+ validFrom: validFrom.toISOString()
10685
+ }
9964
10686
  });
9965
10687
  }
9966
10688
  const fieldsFor = /* @__PURE__ */ __name((v) => ({
@@ -9971,14 +10693,14 @@ var PlanVersionsService = class _PlanVersionsService {
9971
10693
  monthlyNet: v.monthlyNet,
9972
10694
  yearlyNet: v.yearlyNet
9973
10695
  }), "fieldsFor");
9974
- const diff = previous ? (0, import_types.classifyPlanDiff)(fieldsFor(previous), fieldsFor(draft)) : {
10696
+ const diff = previous ? (0, import_types4.classifyPlanDiff)(fieldsFor(previous), fieldsFor(draft)) : {
9975
10697
  changes: [],
9976
10698
  nonRegressive: true
9977
10699
  };
9978
10700
  if (!diff.nonRegressive && !publishMeta.forceRegressive) {
9979
10701
  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).",
10702
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_REGRESSION,
10703
+ message: "This plan version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
9982
10704
  changes: diff.changes
9983
10705
  });
9984
10706
  }
@@ -10013,36 +10735,51 @@ var PlanVersionsService = class _PlanVersionsService {
10013
10735
  async terminatePlanVersion(versionId, endsAt) {
10014
10736
  const existing = await this.repo.findVersionById(versionId);
10015
10737
  if (!existing) {
10016
- throw new import_common55.NotFoundException(`PlanVersion '${versionId}' nicht gefunden`);
10738
+ throw new import_common55.NotFoundException({
10739
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10740
+ message: `PlanVersion '${versionId}' not found`,
10741
+ params: {
10742
+ versionId
10743
+ }
10744
+ });
10017
10745
  }
10018
10746
  if (existing.publishedAt === null) {
10019
10747
  throw new import_common55.UnprocessableEntityException({
10020
- code: "PLAN_VERSION_NOT_LIVE",
10021
- message: `PlanVersion '${versionId}' ist nicht published und kann nicht terminiert werden.`
10748
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_LIVE,
10749
+ message: `PlanVersion '${versionId}' is not published and cannot be terminated.`,
10750
+ params: {
10751
+ versionId
10752
+ }
10022
10753
  });
10023
10754
  }
10024
10755
  if (existing.supersededAt !== null) {
10025
10756
  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.`
10757
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_LIVE,
10758
+ message: `PlanVersion '${versionId}' has already been superseded by a newer version (supersededAt) and cannot be terminated.`,
10759
+ params: {
10760
+ versionId
10761
+ }
10028
10762
  });
10029
10763
  }
10030
10764
  if (Number.isNaN(endsAt.getTime())) {
10031
10765
  throw new import_common55.UnprocessableEntityException({
10032
- code: "PLAN_TERMINATE_INVALID_DATE",
10033
- message: "endsAt ist kein g\xFCltiges Datum."
10766
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_TERMINATE_INVALID_DATE,
10767
+ message: "endsAt is not a valid date."
10034
10768
  });
10035
10769
  }
10036
10770
  if (endsAt.getTime() <= Date.now()) {
10037
10771
  throw new import_common55.UnprocessableEntityException({
10038
- code: "PLAN_TERMINATE_DATE_NOT_FUTURE",
10039
- message: `endsAt (${endsAt.toISOString()}) muss strikt in der Zukunft liegen.`
10772
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_TERMINATE_DATE_NOT_FUTURE,
10773
+ message: `endsAt (${endsAt.toISOString()}) must lie strictly in the future.`,
10774
+ params: {
10775
+ endsAt: endsAt.toISOString()
10776
+ }
10040
10777
  });
10041
10778
  }
10042
10779
  if (typeof this.repo.terminate !== "function") {
10043
10780
  throw new import_common55.UnprocessableEntityException({
10044
- code: "PLAN_TERMINATE_NOT_IMPLEMENTED",
10045
- message: "Terminate ist im aktuellen Repository nicht implementiert. Implementiere PlanRepository.terminate."
10781
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_TERMINATE_NOT_IMPLEMENTED,
10782
+ message: "Terminate is not implemented in the current repository. Implement PlanRepository.terminate."
10046
10783
  });
10047
10784
  }
10048
10785
  return this.repo.terminate(versionId, endsAt);
@@ -10060,8 +10797,8 @@ var PlanVersionsService = class _PlanVersionsService {
10060
10797
  if (blockingStrictModeWarnings(warnings).length === 0) return;
10061
10798
  if (this.mode === "blocking") {
10062
10799
  throw new import_common55.UnprocessableEntityException({
10063
- code: "STRICT_MODE_VIOLATIONS",
10064
- message: "Strict-Mode-Check hat Drift gegen den Discovery-Snapshot gefunden.",
10800
+ code: import_types25.CATALOG_ERROR_CODES.STRICT_MODE_VIOLATIONS,
10801
+ message: "The strict-mode check found drift against the discovery snapshot.",
10065
10802
  warnings
10066
10803
  });
10067
10804
  }
@@ -10128,7 +10865,7 @@ var CreateBundleDto = class {
10128
10865
  _ts_decorate60([
10129
10866
  (0, import_class_validator9.IsString)(),
10130
10867
  (0, import_class_validator9.Matches)(PROJECT_KEY_PATTERN4, {
10131
- message: 'projectKey muss kebab-case sein (z. B. "my-app")'
10868
+ message: 'projectKey must be kebab-case (e.g. "my-app")'
10132
10869
  }),
10133
10870
  (0, import_class_validator9.MaxLength)(64),
10134
10871
  _ts_metadata47("design:type", String)
@@ -10136,7 +10873,7 @@ _ts_decorate60([
10136
10873
  _ts_decorate60([
10137
10874
  (0, import_class_validator9.IsString)(),
10138
10875
  (0, import_class_validator9.Matches)(KEY_PATTERN, {
10139
- message: 'bundleKey muss SCREAMING_SNAKE_CASE sein (z. B. "BANKING")'
10876
+ message: 'bundleKey must be SCREAMING_SNAKE_CASE (e.g. "BANKING")'
10140
10877
  }),
10141
10878
  (0, import_class_validator9.MaxLength)(64),
10142
10879
  _ts_metadata47("design:type", String)
@@ -10236,7 +10973,7 @@ _ts_decorate60([
10236
10973
  }),
10237
10974
  (0, import_class_validator9.Matches)(FEATURE_KEY_PATTERN, {
10238
10975
  each: true,
10239
- message: "features-Eintr\xE4ge m\xFCssen SCREAMING_SNAKE_CASE sein"
10976
+ message: "features entries must be SCREAMING_SNAKE_CASE"
10240
10977
  }),
10241
10978
  _ts_metadata47("design:type", Array)
10242
10979
  ], CreateBundleVersionDraftDto.prototype, "features", void 0);
@@ -10262,7 +10999,7 @@ _ts_decorate60([
10262
10999
  no_symbols: false
10263
11000
  }),
10264
11001
  (0, import_class_validator9.Matches)(DECIMAL_PATTERN, {
10265
- message: 'monthlyNet muss Decimal mit max. 2 Nachkommastellen sein (z. B. "9.90")'
11002
+ message: 'monthlyNet must be a decimal with at most 2 fraction digits (e.g. "9.90")'
10266
11003
  }),
10267
11004
  _ts_metadata47("design:type", Object)
10268
11005
  ], CreateBundleVersionDraftDto.prototype, "monthlyNet", void 0);
@@ -10273,7 +11010,7 @@ _ts_decorate60([
10273
11010
  no_symbols: false
10274
11011
  }),
10275
11012
  (0, import_class_validator9.Matches)(DECIMAL_PATTERN, {
10276
- message: "yearlyNet muss Decimal mit max. 2 Nachkommastellen sein"
11013
+ message: "yearlyNet must be a decimal with at most 2 fraction digits"
10277
11014
  }),
10278
11015
  _ts_metadata47("design:type", Object)
10279
11016
  ], CreateBundleVersionDraftDto.prototype, "yearlyNet", void 0);
@@ -10297,7 +11034,7 @@ _ts_decorate60([
10297
11034
  (0, import_class_validator9.IsOptional)(),
10298
11035
  (0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
10299
11036
  (0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
10300
- message: "validFrom muss ISO-Datum sein (YYYY-MM-DD)"
11037
+ message: "validFrom must be an ISO date (YYYY-MM-DD)"
10301
11038
  }),
10302
11039
  _ts_metadata47("design:type", Object)
10303
11040
  ], CreateBundleVersionDraftDto.prototype, "validFrom", void 0);
@@ -10305,7 +11042,7 @@ _ts_decorate60([
10305
11042
  (0, import_class_validator9.IsOptional)(),
10306
11043
  (0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
10307
11044
  (0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
10308
- message: "validUntil muss ISO-Datum sein (YYYY-MM-DD)"
11045
+ message: "validUntil must be an ISO date (YYYY-MM-DD)"
10309
11046
  }),
10310
11047
  _ts_metadata47("design:type", Object)
10311
11048
  ], CreateBundleVersionDraftDto.prototype, "validUntil", void 0);
@@ -10377,7 +11114,7 @@ _ts_decorate60([
10377
11114
  (0, import_class_validator9.IsOptional)(),
10378
11115
  (0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
10379
11116
  (0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
10380
- message: "validFrom muss ISO-Datum sein (YYYY-MM-DD)"
11117
+ message: "validFrom must be an ISO date (YYYY-MM-DD)"
10381
11118
  }),
10382
11119
  _ts_metadata47("design:type", Object)
10383
11120
  ], UpdateBundleVersionDraftDto.prototype, "validFrom", void 0);
@@ -10385,7 +11122,7 @@ _ts_decorate60([
10385
11122
  (0, import_class_validator9.IsOptional)(),
10386
11123
  (0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
10387
11124
  (0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
10388
- message: "validUntil muss ISO-Datum sein (YYYY-MM-DD)"
11125
+ message: "validUntil must be an ISO date (YYYY-MM-DD)"
10389
11126
  }),
10390
11127
  _ts_metadata47("design:type", Object)
10391
11128
  ], UpdateBundleVersionDraftDto.prototype, "validUntil", void 0);
@@ -10422,7 +11159,7 @@ _ts_decorate60([
10422
11159
  (0, import_class_validator9.IsOptional)(),
10423
11160
  (0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
10424
11161
  (0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
10425
- message: "validFrom muss ISO-Datum sein (YYYY-MM-DD)"
11162
+ message: "validFrom must be an ISO date (YYYY-MM-DD)"
10426
11163
  }),
10427
11164
  _ts_metadata47("design:type", Object)
10428
11165
  ], PublishBundleVersionDto.prototype, "validFrom", void 0);
@@ -10430,7 +11167,7 @@ _ts_decorate60([
10430
11167
  (0, import_class_validator9.IsOptional)(),
10431
11168
  (0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
10432
11169
  (0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
10433
- message: "validUntil muss ISO-Datum sein (YYYY-MM-DD)"
11170
+ message: "validUntil must be an ISO date (YYYY-MM-DD)"
10434
11171
  }),
10435
11172
  _ts_metadata47("design:type", Object)
10436
11173
  ], PublishBundleVersionDto.prototype, "validUntil", void 0);
@@ -10734,7 +11471,7 @@ var CreateMarketingProjectionDto = class {
10734
11471
  _ts_decorate62([
10735
11472
  (0, import_class_validator10.IsString)(),
10736
11473
  (0, import_class_validator10.Matches)(PROJECT_KEY_PATTERN5, {
10737
- message: "projectKey muss kebab-case sein"
11474
+ message: "projectKey must be kebab-case"
10738
11475
  }),
10739
11476
  (0, import_class_validator10.MaxLength)(64),
10740
11477
  _ts_metadata49("design:type", String)
@@ -10752,7 +11489,7 @@ _ts_decorate62([
10752
11489
  (0, import_class_validator10.IsOptional)(),
10753
11490
  (0, import_class_validator10.IsString)(),
10754
11491
  (0, import_class_validator10.Matches)(LOCALE_PATTERN2, {
10755
- message: 'locale muss ISO-639-1 sein (z. B. "de", "en", "de-AT")'
11492
+ message: 'locale must be ISO-639-1 (e.g. "de", "en", "de-AT")'
10756
11493
  }),
10757
11494
  _ts_metadata49("design:type", String)
10758
11495
  ], CreateMarketingProjectionDto.prototype, "locale", void 0);
@@ -11083,7 +11820,7 @@ var CreatePlanDto = class {
11083
11820
  _ts_decorate64([
11084
11821
  (0, import_class_validator11.IsString)(),
11085
11822
  (0, import_class_validator11.Matches)(PROJECT_KEY_PATTERN6, {
11086
- message: 'projectKey muss kebab-case sein (z. B. "my-app")'
11823
+ message: 'projectKey must be kebab-case (e.g. "my-app")'
11087
11824
  }),
11088
11825
  (0, import_class_validator11.MaxLength)(64),
11089
11826
  _ts_metadata51("design:type", String)
@@ -11091,7 +11828,7 @@ _ts_decorate64([
11091
11828
  _ts_decorate64([
11092
11829
  (0, import_class_validator11.IsString)(),
11093
11830
  (0, import_class_validator11.Matches)(KEY_PATTERN2, {
11094
- message: 'planKey muss SCREAMING_SNAKE_CASE sein (z. B. "STARTER")'
11831
+ message: 'planKey must be SCREAMING_SNAKE_CASE (e.g. "STARTER")'
11095
11832
  }),
11096
11833
  (0, import_class_validator11.MaxLength)(64),
11097
11834
  _ts_metadata51("design:type", String)
@@ -11200,7 +11937,7 @@ _ts_decorate65([
11200
11937
  }),
11201
11938
  (0, import_class_validator12.Matches)(FEATURE_KEY_PATTERN2, {
11202
11939
  each: true,
11203
- message: "features-Eintr\xE4ge m\xFCssen SCREAMING_SNAKE_CASE sein"
11940
+ message: "features entries must be SCREAMING_SNAKE_CASE"
11204
11941
  }),
11205
11942
  _ts_metadata52("design:type", Array)
11206
11943
  ], CreatePlanVersionDraftDto.prototype, "features", void 0);
@@ -11212,7 +11949,7 @@ _ts_decorate65([
11212
11949
  }),
11213
11950
  (0, import_class_validator12.Matches)(FEATURE_KEY_PATTERN2, {
11214
11951
  each: true,
11215
- message: "bundles-Eintr\xE4ge m\xFCssen SCREAMING_SNAKE_CASE sein"
11952
+ message: "bundles entries must be SCREAMING_SNAKE_CASE"
11216
11953
  }),
11217
11954
  _ts_metadata52("design:type", Array)
11218
11955
  ], CreatePlanVersionDraftDto.prototype, "bundles", void 0);
@@ -11222,13 +11959,13 @@ _ts_decorate65([
11222
11959
  ], CreatePlanVersionDraftDto.prototype, "quotas", void 0);
11223
11960
  _ts_decorate65([
11224
11961
  (0, import_class_validator12.Matches)(DECIMAL_PATTERN2, {
11225
- message: 'monthlyNet muss Decimal mit max. 2 Nachkommastellen sein (z. B. "9.90")'
11962
+ message: 'monthlyNet must be a decimal with at most 2 fraction digits (e.g. "9.90")'
11226
11963
  }),
11227
11964
  _ts_metadata52("design:type", String)
11228
11965
  ], CreatePlanVersionDraftDto.prototype, "monthlyNet", void 0);
11229
11966
  _ts_decorate65([
11230
11967
  (0, import_class_validator12.Matches)(DECIMAL_PATTERN2, {
11231
- message: "yearlyNet muss Decimal mit max. 2 Nachkommastellen sein"
11968
+ message: "yearlyNet must be a decimal with at most 2 fraction digits"
11232
11969
  }),
11233
11970
  _ts_metadata52("design:type", String)
11234
11971
  ], CreatePlanVersionDraftDto.prototype, "yearlyNet", void 0);
@@ -11472,7 +12209,7 @@ function buildPlansController(guards) {
11472
12209
  }
11473
12210
  assertVersionsService() {
11474
12211
  if (!this.versionsService) {
11475
- throw new Error("PlanVersionsService nicht registriert \u2014 App hat den Plan-Lifecycle-Editor nicht aktiviert.");
12212
+ throw new Error("PlanVersionsService is not registered \u2014 the app has not enabled the plan lifecycle editor.");
11476
12213
  }
11477
12214
  }
11478
12215
  };
@@ -11837,9 +12574,9 @@ function runPreflight(input) {
11837
12574
  __name(runPreflight, "runPreflight");
11838
12575
  function formatPreflightReport(report) {
11839
12576
  const lines = [];
11840
- lines.push(`Preflight (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7 Gesamt: ${report.counts.total}`, "");
12577
+ lines.push(`Preflight (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7 Total: ${report.counts.total}`, "");
11841
12578
  if (report.findings.length === 0) {
11842
- lines.push(" \u2713 Keine Strict-Mode-Verst\xF6\xDFe gefunden.");
12579
+ lines.push(" \u2713 No strict-mode violations found.");
11843
12580
  return lines.join("\n");
11844
12581
  }
11845
12582
  let lastEntity = "";
@@ -11909,9 +12646,9 @@ function validateSeedAgainstSnapshot(input) {
11909
12646
  __name(validateSeedAgainstSnapshot, "validateSeedAgainstSnapshot");
11910
12647
  function formatSeedGateReport(report) {
11911
12648
  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}`, "");
12649
+ lines.push(`Seed-Gate (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7 Total: ${report.counts.total}`, "");
11913
12650
  if (report.findings.length === 0) {
11914
- lines.push(" \u2713 Alle geseedeten Plan-/Bundle-Features sind discovered (und, sofern gepr\xFCft, approved).");
12651
+ lines.push(" \u2713 Every seeded plan/bundle feature is discovered (and approved where checked).");
11915
12652
  return lines.join("\n");
11916
12653
  }
11917
12654
  let lastEntity = "";
@@ -11941,7 +12678,7 @@ var DiscoverySnapshotNotFoundError = class extends Error {
11941
12678
  }
11942
12679
  path;
11943
12680
  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;
12681
+ 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
12682
  this.name = "DiscoverySnapshotNotFoundError";
11946
12683
  }
11947
12684
  };
@@ -11968,10 +12705,10 @@ function runSeedGateFromFile(options) {
11968
12705
  } catch (err) {
11969
12706
  if (err instanceof DiscoverySnapshotNotFoundError) {
11970
12707
  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).`);
12708
+ error(`[seed-gate] ERROR: no discovery snapshot at ${options.snapshotPath} \u2014 required in blocking mode (run the headless scan before seeding, #23).`);
11972
12709
  return exit(4);
11973
12710
  }
11974
- warn(`[seed-gate] Kein Discovery-Snapshot unter ${options.snapshotPath} \u2014 \xFCbersprungen (report-only).`);
12711
+ warn(`[seed-gate] No discovery snapshot at ${options.snapshotPath} \u2014 skipped (report-only).`);
11975
12712
  return null;
11976
12713
  }
11977
12714
  throw err;
@@ -11985,10 +12722,10 @@ function runSeedGateFromFile(options) {
11985
12722
  log(formatSeedGateReport(report));
11986
12723
  if (report.overall === "error") {
11987
12724
  if (mode === "blocking") {
11988
- error("[seed-gate] blocking: Verst\xF6\xDFe gefunden \u2014 Seed wird abgebrochen (#23).");
12725
+ error("[seed-gate] blocking: violations found \u2014 aborting the seed (#23).");
11989
12726
  return exit(seedGateExitCode(report));
11990
12727
  }
11991
- warn("[seed-gate] report-only: Verst\xF6\xDFe gefunden, Seed l\xE4uft trotzdem weiter.");
12728
+ warn("[seed-gate] report-only: violations found, seeding continues anyway.");
11992
12729
  }
11993
12730
  return report;
11994
12731
  }
@@ -12007,7 +12744,7 @@ __export(checkout_offer_exports, {
12007
12744
 
12008
12745
  // src/checkout-offer/checkout-offer.service.ts
12009
12746
  var import_common60 = require("@nestjs/common");
12010
- var import_types9 = require("@saasicat/types");
12747
+ var import_types26 = require("@saasicat/types");
12011
12748
 
12012
12749
  // src/checkout-offer/tokens.ts
12013
12750
  var CHECKOUT_OFFER_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/CheckoutOfferRepository");
@@ -12050,7 +12787,13 @@ var CheckoutOfferService = class {
12050
12787
  async getById(id) {
12051
12788
  const row = await this.repo.findById(id);
12052
12789
  if (!row) {
12053
- throw new import_common60.NotFoundException(`CheckoutOffer '${id}' nicht gefunden`);
12790
+ throw new import_common60.NotFoundException({
12791
+ code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_NOT_FOUND,
12792
+ message: `CheckoutOffer '${id}' not found`,
12793
+ params: {
12794
+ offerId: id
12795
+ }
12796
+ });
12054
12797
  }
12055
12798
  return row;
12056
12799
  }
@@ -12068,7 +12811,7 @@ var CheckoutOfferService = class {
12068
12811
  /** Customization in onboarding — only while the offer is `open`. */
12069
12812
  async update(id, data) {
12070
12813
  const existing = await this.getById(id);
12071
- this.assertOpen(existing, "\xE4ndern");
12814
+ this.assertOpen(existing, "changed");
12072
12815
  const next = this.normalizeUpdateData(existing, data);
12073
12816
  await this.assertFeatureRequiresSatisfied({
12074
12817
  projectKey: existing.projectKey,
@@ -12086,7 +12829,7 @@ var CheckoutOfferService = class {
12086
12829
  */
12087
12830
  async consume(id) {
12088
12831
  const existing = await this.getById(id);
12089
- this.assertOpen(existing, "verbrauchen");
12832
+ this.assertOpen(existing, "consumed");
12090
12833
  await this.assertBundleVersionsStillBookable(existing);
12091
12834
  return this.repo.consume(id);
12092
12835
  }
@@ -12104,19 +12847,19 @@ var CheckoutOfferService = class {
12104
12847
  const entries = await this.catalogEntries.listFeatures({
12105
12848
  projectKey: input.projectKey
12106
12849
  });
12107
- const requiresIndex = (0, import_types9.buildFeatureRequiresIndex)(entries);
12850
+ const requiresIndex = (0, import_types26.buildFeatureRequiresIndex)(entries);
12108
12851
  if (requiresIndex.size === 0) return;
12109
12852
  const selected = /* @__PURE__ */ new Set([
12110
12853
  ...await this.resolvePlanFeatures(input),
12111
12854
  ...await this.resolveBundleFeatures(input)
12112
12855
  ]);
12113
- const missingRequires = (0, import_types9.collectUnsatisfiedRequires)([
12856
+ const missingRequires = (0, import_types26.collectUnsatisfiedRequires)([
12114
12857
  ...selected
12115
12858
  ], requiresIndex);
12116
12859
  if (missingRequires.length > 0) {
12117
12860
  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.`,
12861
+ code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED,
12862
+ message: `The selected plan does not cover all feature dependencies: [${missingRequires.join(", ")}] are missing from the plan + selected bundles.`,
12120
12863
  missingRequires
12121
12864
  });
12122
12865
  }
@@ -12196,8 +12939,8 @@ var CheckoutOfferService = class {
12196
12939
  }
12197
12940
  if (violations.length > 0) {
12198
12941
  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.",
12942
+ code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE,
12943
+ message: "At least one bundle version from the checkout offer is no longer bookable.",
12201
12944
  violations
12202
12945
  });
12203
12946
  }
@@ -12251,15 +12994,15 @@ var CheckoutOfferService = class {
12251
12994
  const hasPlan = lineItems.some((item) => item.kind === "plan");
12252
12995
  if (!hasPlan) {
12253
12996
  throw new import_common60.UnprocessableEntityException({
12254
- code: "CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED",
12255
- message: "CheckoutOffer ben\xF6tigt eine eingefrorene Plan-LineItem."
12997
+ code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED,
12998
+ message: "A checkout offer requires a frozen plan line item."
12256
12999
  });
12257
13000
  }
12258
13001
  const missingBundleVersionIds = input.bundleVersionIds.filter((versionId) => !lineItems.some((item) => item.kind === "bundle" && item.sourceVersionId === versionId));
12259
13002
  if (missingBundleVersionIds.length > 0) {
12260
13003
  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.",
13004
+ code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED,
13005
+ message: "Every selected bundle version requires a frozen bundle line item.",
12263
13006
  bundleVersionIds: missingBundleVersionIds
12264
13007
  });
12265
13008
  }
@@ -12291,13 +13034,35 @@ var CheckoutOfferService = class {
12291
13034
  }
12292
13035
  assertOpen(existing, action) {
12293
13036
  if (existing.status === "consumed") {
12294
- throw new import_common60.ConflictException(`CheckoutOffer '${existing.id}' ist bereits verbraucht und kann nicht ${action} werden`);
13037
+ throw new import_common60.ConflictException({
13038
+ code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_ALREADY_CONSUMED,
13039
+ message: `Checkout offer '${existing.id}' has already been consumed and cannot be ${action}`,
13040
+ params: {
13041
+ offerId: existing.id,
13042
+ action
13043
+ }
13044
+ });
12295
13045
  }
12296
13046
  if (existing.status === "expired") {
12297
- throw new import_common60.ConflictException(`CheckoutOffer '${existing.id}' ist abgelaufen und kann nicht ${action} werden`);
13047
+ throw new import_common60.ConflictException({
13048
+ code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_EXPIRED,
13049
+ message: `Checkout offer '${existing.id}' has expired and cannot be ${action}`,
13050
+ params: {
13051
+ offerId: existing.id,
13052
+ action
13053
+ }
13054
+ });
12298
13055
  }
12299
13056
  if (this.isExpired(existing)) {
12300
- throw new import_common60.ConflictException(`CheckoutOffer '${existing.id}' ist abgelaufen und kann nicht ${action} werden`);
13057
+ throw new import_common60.ConflictException({
13058
+ code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_EXPIRED,
13059
+ message: `CheckoutOffer '${existing.id}' has expired and cannot be ${action}`,
13060
+ params: {
13061
+ offerId: existing.id,
13062
+ action,
13063
+ validUntil: existing.validUntil ?? null
13064
+ }
13065
+ });
12301
13066
  }
12302
13067
  }
12303
13068
  isExpired(row) {
@@ -12315,7 +13080,7 @@ var CheckoutOfferService = class {
12315
13080
  if (!value) return false;
12316
13081
  const validUntil = new Date(value).getTime();
12317
13082
  if (Number.isNaN(validUntil)) return true;
12318
- return validUntil < (0, import_types9.startOfUtcDay)(new Date(nowMs)).getTime();
13083
+ return validUntil < (0, import_types26.startOfUtcDay)(new Date(nowMs)).getTime();
12319
13084
  }
12320
13085
  };
12321
13086
  CheckoutOfferService = _ts_decorate68([
@@ -12379,7 +13144,7 @@ var CreateCheckoutOfferDto = class {
12379
13144
  _ts_decorate69([
12380
13145
  (0, import_class_validator13.IsString)(),
12381
13146
  (0, import_class_validator13.Matches)(PROJECT_KEY_PATTERN7, {
12382
- message: "projectKey muss kebab-case sein"
13147
+ message: "projectKey must be kebab-case"
12383
13148
  }),
12384
13149
  (0, import_class_validator13.MaxLength)(64),
12385
13150
  _ts_metadata55("design:type", String)
@@ -12867,7 +13632,7 @@ async function runHeadlessDiscoveryScan(appModule, options = {}) {
12867
13632
  await app.close();
12868
13633
  }
12869
13634
  } catch (err) {
12870
- console.error("[discovery-scan] FEHLER:", err instanceof Error ? err.message : err);
13635
+ console.error("[discovery-scan] ERROR:", err instanceof Error ? err.message : err);
12871
13636
  return exit(4);
12872
13637
  }
12873
13638
  }
@@ -12946,7 +13711,7 @@ __export(platform_exports, {
12946
13711
  // src/platform/saas-platform.module.ts
12947
13712
  var import_common79 = require("@nestjs/common");
12948
13713
  var import_core9 = require("@nestjs/core");
12949
- var import_types11 = require("@saasicat/types");
13714
+ var import_types30 = require("@saasicat/types");
12950
13715
 
12951
13716
  // src/promo/module.ts
12952
13717
  var import_common70 = require("@nestjs/common");
@@ -12987,7 +13752,7 @@ var PromoCodeExpirer = class _PromoCodeExpirer {
12987
13752
  const codes = await this.promoRepo.expireDueCodes(now);
12988
13753
  const redemptions = await this.redemptionRepo.expireDueRedemptions(now);
12989
13754
  if (codes > 0 || redemptions > 0) {
12990
- this.logger.log(`PromoCodeExpirer: ${codes} Codes, ${redemptions} Redemptions abgelaufen.`);
13755
+ this.logger.log(`PromoCodeExpirer: ${codes} Codes, ${redemptions} redemptions expired.`);
12991
13756
  }
12992
13757
  }
12993
13758
  };
@@ -13016,6 +13781,7 @@ var import_common68 = require("@nestjs/common");
13016
13781
 
13017
13782
  // src/promo/rate-limit.guard.ts
13018
13783
  var import_common67 = require("@nestjs/common");
13784
+ var import_types27 = require("@saasicat/types");
13019
13785
  function _ts_decorate75(decorators, target, key, desc) {
13020
13786
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
13021
13787
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -13027,6 +13793,15 @@ var IP_WINDOW_MS = 6e4;
13027
13793
  var IP_LIMIT = 20;
13028
13794
  var SESSION_WINDOW_MS = 60 * 6e4;
13029
13795
  var SESSION_LIMIT = 50;
13796
+ function rateLimited() {
13797
+ return new import_common67.HttpException({
13798
+ valid: false,
13799
+ // Superseded by `code` — kept until all consumers read `code`.
13800
+ reason: "RATE_LIMITED",
13801
+ code: import_types27.REGISTRATION_ERROR_CODES.RATE_LIMITED
13802
+ }, import_common67.HttpStatus.TOO_MANY_REQUESTS);
13803
+ }
13804
+ __name(rateLimited, "rateLimited");
13030
13805
  var PromoCodeRateLimitGuard = class {
13031
13806
  static {
13032
13807
  __name(this, "PromoCodeRateLimitGuard");
@@ -13038,16 +13813,10 @@ var PromoCodeRateLimitGuard = class {
13038
13813
  const ipKey = ipFingerprint(req);
13039
13814
  const sessionKey = req.user?.id ?? null;
13040
13815
  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);
13816
+ throw rateLimited();
13045
13817
  }
13046
13818
  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);
13819
+ throw rateLimited();
13051
13820
  }
13052
13821
  return true;
13053
13822
  }
@@ -13111,21 +13880,21 @@ var PreviewPromoCodeDto = class {
13111
13880
  _ts_decorate76([
13112
13881
  (0, import_class_validator14.IsString)(),
13113
13882
  (0, import_class_validator14.Matches)(CODE_PATTERN2, {
13114
- message: 'code muss aus A\u2013Z, 0\u20139, "-" und "_" bestehen (4\u201332 Zeichen)'
13883
+ message: 'code must consist of A\u2013Z, 0\u20139, "-" and "_" (4\u201332 characters)'
13115
13884
  }),
13116
13885
  _ts_metadata59("design:type", String)
13117
13886
  ], PreviewPromoCodeDto.prototype, "code", void 0);
13118
13887
  _ts_decorate76([
13119
13888
  (0, import_class_validator14.IsString)(),
13120
13889
  (0, import_class_validator14.Matches)(PLAN_OR_CYCLE_PATTERN3, {
13121
- message: "plan muss SCREAMING_SNAKE_CASE sein"
13890
+ message: "plan must be SCREAMING_SNAKE_CASE"
13122
13891
  }),
13123
13892
  _ts_metadata59("design:type", String)
13124
13893
  ], PreviewPromoCodeDto.prototype, "plan", void 0);
13125
13894
  _ts_decorate76([
13126
13895
  (0, import_class_validator14.IsString)(),
13127
13896
  (0, import_class_validator14.Matches)(PLAN_OR_CYCLE_PATTERN3, {
13128
- message: "billingCycle muss SCREAMING_SNAKE_CASE sein (z. B. MONTHLY, YEARLY)"
13897
+ message: "billingCycle must be SCREAMING_SNAKE_CASE (e.g. MONTHLY, YEARLY)"
13129
13898
  }),
13130
13899
  _ts_metadata59("design:type", String)
13131
13900
  ], PreviewPromoCodeDto.prototype, "billingCycle", void 0);
@@ -13686,7 +14455,7 @@ var import_class_validator16 = require("class-validator");
13686
14455
 
13687
14456
  // src/setup/setup.service.ts
13688
14457
  var import_common71 = require("@nestjs/common");
13689
- var import_types10 = require("@saasicat/types");
14458
+ var import_types28 = require("@saasicat/types");
13690
14459
  var import_node_crypto3 = require("crypto");
13691
14460
  var import_qrcode = __toESM(require("qrcode"), 1);
13692
14461
 
@@ -13743,7 +14512,7 @@ var SetupService = class {
13743
14512
  const email = req.email.trim().toLowerCase();
13744
14513
  if (!email.includes("@")) {
13745
14514
  throw new import_common71.BadRequestException({
13746
- code: import_types10.SETUP_ERROR_CODES.INVALID_EMAIL
14515
+ code: import_types28.SETUP_ERROR_CODES.INVALID_EMAIL
13747
14516
  });
13748
14517
  }
13749
14518
  const generated = !req.password;
@@ -13755,9 +14524,9 @@ var SetupService = class {
13755
14524
  password
13756
14525
  });
13757
14526
  } catch (err) {
13758
- if ((0, import_types10.isPlatformUserExistsError)(err)) {
14527
+ if ((0, import_types28.isPlatformUserExistsError)(err)) {
13759
14528
  throw new import_common71.ConflictException({
13760
- code: import_types10.SETUP_ERROR_CODES.EMAIL_EXISTS,
14529
+ code: import_types28.SETUP_ERROR_CODES.EMAIL_EXISTS,
13761
14530
  message: err.message
13762
14531
  });
13763
14532
  }
@@ -13793,20 +14562,23 @@ var SetupService = class {
13793
14562
  const expected = process.env[this.config.setupTokenEnvVar]?.trim();
13794
14563
  if (!expected) {
13795
14564
  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.`
14565
+ code: import_types28.SETUP_ERROR_CODES.SETUP_DISABLED,
14566
+ message: `${this.config.setupTokenEnvVar} is not set \u2014 setup is disabled.`,
14567
+ params: {
14568
+ envVar: this.config.setupTokenEnvVar
14569
+ }
13798
14570
  });
13799
14571
  }
13800
14572
  if (!token || !timingSafeEqualStr(token, expected)) {
13801
14573
  throw new import_common71.UnauthorizedException({
13802
- code: import_types10.SETUP_ERROR_CODES.INVALID_SETUP_TOKEN
14574
+ code: import_types28.SETUP_ERROR_CODES.INVALID_SETUP_TOKEN
13803
14575
  });
13804
14576
  }
13805
14577
  }
13806
14578
  async assertNoSuperAdmin() {
13807
14579
  if (await this.users.countSuperAdmins() > 0) {
13808
14580
  throw new import_common71.ConflictException({
13809
- code: import_types10.SETUP_ERROR_CODES.SETUP_ALREADY_DONE
14581
+ code: import_types28.SETUP_ERROR_CODES.SETUP_ALREADY_DONE
13810
14582
  });
13811
14583
  }
13812
14584
  }
@@ -14082,6 +14854,7 @@ StaticEntitlementService = _ts_decorate84([
14082
14854
  // src/platform/static-feature.guard.ts
14083
14855
  var import_common75 = require("@nestjs/common");
14084
14856
  var import_core7 = require("@nestjs/core");
14857
+ var import_types29 = require("@saasicat/types");
14085
14858
  function _ts_decorate85(decorators, target, key, desc) {
14086
14859
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
14087
14860
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -14120,15 +14893,31 @@ var StaticFeatureGuard = class {
14120
14893
  if (!required || required.length === 0) return true;
14121
14894
  const request = context.switchToHttp().getRequest();
14122
14895
  const user = request.user;
14123
- if (!user) throw new import_common75.ForbiddenException("Nicht authentifiziert");
14896
+ if (!user) {
14897
+ throw new import_common75.ForbiddenException({
14898
+ code: import_types29.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
14899
+ message: "Not authenticated"
14900
+ });
14901
+ }
14124
14902
  const role = this.config?.userRoleResolver ? this.config.userRoleResolver(user) : user.role ?? user.platformRole;
14125
14903
  if (role === "SUPER_ADMIN") return true;
14126
14904
  const tenantId = this.config?.tenantIdResolver ? this.config.tenantIdResolver(request) : request.tenantId ?? user.tenantId;
14127
- if (!tenantId) throw new import_common75.ForbiddenException("Kein Mandant zugeordnet");
14905
+ if (!tenantId) {
14906
+ throw new import_common75.ForbiddenException({
14907
+ code: import_types29.AUTH_ERROR_CODES.NO_TENANT_ASSIGNED,
14908
+ message: "No tenant assigned"
14909
+ });
14910
+ }
14128
14911
  const snap = await this.entitlements.snapshot(tenantId);
14129
14912
  const allowed = required.some((f) => snap.features.includes(f));
14130
14913
  if (!allowed) {
14131
- throw new import_common75.ForbiddenException(`Feature ${required.join(" / ")} nicht im aktuellen Plan enthalten.`);
14914
+ throw new import_common75.ForbiddenException({
14915
+ code: import_types29.FEATURE_NOT_LICENSED,
14916
+ message: `Feature ${required.join(" / ")} is not included in the current plan.`,
14917
+ params: {
14918
+ featureKeys: required
14919
+ }
14920
+ });
14132
14921
  }
14133
14922
  return true;
14134
14923
  }
@@ -14371,7 +15160,7 @@ function buildTenantManifestController(options) {
14371
15160
  async get(req) {
14372
15161
  const tenantId = req.user?.tenantId ?? req.tenantId;
14373
15162
  if (!tenantId) {
14374
- throw new Error("Kein Mandant im Request \u2014 TenantManifest braucht user.tenantId.");
15163
+ throw new Error("No tenant on the request \u2014 TenantManifest requires user.tenantId.");
14375
15164
  }
14376
15165
  return this.svc.getManifest(tenantId);
14377
15166
  }
@@ -14608,7 +15397,7 @@ var SaasPlatformModule = class _SaasPlatformModule {
14608
15397
  throw new Error(`SaasPlatformModule.forRoot: entitlement active or required by the standard stack, but adapters are missing: ${missing.join(", ")}`);
14609
15398
  }
14610
15399
  if (persistence) {
14611
- (0, import_types11.assertPersistenceCapabilities)(persistence.capabilities, {
15400
+ (0, import_types30.assertPersistenceCapabilities)(persistence.capabilities, {
14612
15401
  transactions: true,
14613
15402
  pessimisticLocking: true
14614
15403
  }, "SaasPlatformModule entitlement (transactional enforceLimit)");
@@ -15064,8 +15853,8 @@ var REGISTRATION_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/Regist
15064
15853
 
15065
15854
  // src/registration/pending-registration.service.ts
15066
15855
  var import_common80 = require("@nestjs/common");
15067
- var import_types12 = require("@saasicat/types");
15068
- var import_types13 = require("@saasicat/types");
15856
+ var import_types31 = require("@saasicat/types");
15857
+ var import_types32 = require("@saasicat/types");
15069
15858
 
15070
15859
  // src/registration/pricing.ts
15071
15860
  function round24(value) {
@@ -15183,7 +15972,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15183
15972
  });
15184
15973
  } catch (error) {
15185
15974
  const message = error instanceof Error ? error.message : String(error);
15186
- this.logger.warn(`Audit-Log fehlgeschlagen (${eventType}): ${message}`);
15975
+ this.logger.warn(`Audit log write failed (${eventType}): ${message}`);
15187
15976
  }
15188
15977
  }
15189
15978
  /** Public signup plan list for step 3 (frontend `usePublicPlans()`). */
@@ -15194,27 +15983,27 @@ var PendingRegistrationService = class _PendingRegistrationService {
15194
15983
  const pending = await this.repo.findById(input.pendingRegistrationId);
15195
15984
  if (!pending) {
15196
15985
  throw new import_common80.NotFoundException({
15197
- code: "PENDING_REGISTRATION_NOT_FOUND"
15986
+ code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
15198
15987
  });
15199
15988
  }
15200
15989
  if (pending.expiresAt.getTime() < Date.now()) {
15201
15990
  throw new import_common80.BadRequestException({
15202
- code: "PENDING_REGISTRATION_EXPIRED"
15991
+ code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
15203
15992
  });
15204
15993
  }
15205
15994
  if (pending.status !== "EMAIL_VERIFIED" && pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
15206
15995
  throw new import_common80.BadRequestException({
15207
- code: "INVALID_REGISTRATION_STATE"
15996
+ code: import_types31.REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE
15208
15997
  });
15209
15998
  }
15210
15999
  const plan = await this.planCatalog.findPublicSignupPlan(input.planId);
15211
16000
  if (!plan) {
15212
16001
  throw new import_common80.BadRequestException({
15213
- code: "PLAN_NOT_AVAILABLE"
16002
+ code: import_types31.REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE
15214
16003
  });
15215
16004
  }
15216
16005
  const now = /* @__PURE__ */ new Date();
15217
- const expiresAt = new Date(now.getTime() + import_types13.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
16006
+ const expiresAt = new Date(now.getTime() + import_types32.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
15218
16007
  const updated = await this.repo.update(pending.id, {
15219
16008
  status: "PLAN_SELECTED",
15220
16009
  currentStep: 4,
@@ -15227,7 +16016,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15227
16016
  return {
15228
16017
  pendingRegistrationId: updated.id,
15229
16018
  status: updated.status,
15230
- nextStep: import_types13.REGISTRATION_STEP_BY_STATUS[updated.status],
16019
+ nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[updated.status],
15231
16020
  selectedPlanId: updated.selectedPlanId ?? plan.id
15232
16021
  };
15233
16022
  }
@@ -15235,28 +16024,28 @@ var PendingRegistrationService = class _PendingRegistrationService {
15235
16024
  const pending = await this.repo.findById(input.pendingRegistrationId);
15236
16025
  if (!pending) {
15237
16026
  throw new import_common80.NotFoundException({
15238
- code: "PENDING_REGISTRATION_NOT_FOUND"
16027
+ code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
15239
16028
  });
15240
16029
  }
15241
16030
  if (pending.expiresAt.getTime() < Date.now()) {
15242
16031
  throw new import_common80.BadRequestException({
15243
- code: "PENDING_REGISTRATION_EXPIRED"
16032
+ code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
15244
16033
  });
15245
16034
  }
15246
16035
  if (pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
15247
16036
  throw new import_common80.BadRequestException({
15248
- code: "PLAN_NOT_SELECTED"
16037
+ code: import_types31.REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED
15249
16038
  });
15250
16039
  }
15251
16040
  if (!pending.selectedPlanId) {
15252
16041
  throw new import_common80.BadRequestException({
15253
- code: "PLAN_NOT_SELECTED"
16042
+ code: import_types31.REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED
15254
16043
  });
15255
16044
  }
15256
16045
  const plan = await this.planCatalog.findPublicSignupPlan(pending.selectedPlanId);
15257
16046
  if (!plan) {
15258
16047
  throw new import_common80.BadRequestException({
15259
- code: "PLAN_NOT_AVAILABLE"
16048
+ code: import_types31.REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE
15260
16049
  });
15261
16050
  }
15262
16051
  const session = await this.paymentProvider.createCheckoutSession({
@@ -15267,7 +16056,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15267
16056
  cancelUrl: input.cancelUrl
15268
16057
  });
15269
16058
  const now = /* @__PURE__ */ new Date();
15270
- const expiresAt = new Date(now.getTime() + import_types13.PENDING_CHECKOUT_TTL_DAYS * 24 * 60 * 60 * 1e3);
16059
+ const expiresAt = new Date(now.getTime() + import_types32.PENDING_CHECKOUT_TTL_DAYS * 24 * 60 * 60 * 1e3);
15271
16060
  const updated = await this.repo.update(pending.id, {
15272
16061
  status: "CHECKOUT_STARTED",
15273
16062
  currentStep: 4,
@@ -15282,7 +16071,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15282
16071
  return {
15283
16072
  pendingRegistrationId: updated.id,
15284
16073
  status: updated.status,
15285
- nextStep: import_types13.REGISTRATION_STEP_BY_STATUS[updated.status],
16074
+ nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[updated.status],
15286
16075
  checkoutSessionId: session.sessionId,
15287
16076
  checkoutUrl: session.checkoutUrl
15288
16077
  };
@@ -15308,7 +16097,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15308
16097
  rawPayload: input.rawPayload
15309
16098
  });
15310
16099
  if (!claimed) {
15311
- this.logger.warn(`Idempotenz: PaymentEvent ${input.eventId} bereits verarbeitet \u2014 Duplikat verworfen.`);
16100
+ this.logger.warn(`Idempotency: payment event ${input.eventId} already processed \u2014 duplicate discarded.`);
15312
16101
  await this.record("PAYMENT_DUPLICATE_IGNORED", null, context, {
15313
16102
  eventId: input.eventId,
15314
16103
  sessionId: input.sessionId
@@ -15358,7 +16147,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15358
16147
  tenantId: result.tenantId,
15359
16148
  subscriptionId: result.subscriptionId
15360
16149
  });
15361
- this.logger.log(`Aktivierung erfolgreich: pending=${pending.id} \u2192 user=${result.userId} tenant=${result.tenantId} subscription=${result.subscriptionId}`);
16150
+ this.logger.log(`Activation succeeded: pending=${pending.id} \u2192 user=${result.userId} tenant=${result.tenantId} subscription=${result.subscriptionId}`);
15362
16151
  return {
15363
16152
  activated: true,
15364
16153
  result
@@ -15386,11 +16175,11 @@ var PendingRegistrationService = class _PendingRegistrationService {
15386
16175
  await this.repo.delete(pending.id);
15387
16176
  } catch (error) {
15388
16177
  const message = error instanceof Error ? error.message : String(error);
15389
- this.logger.warn(`Cleanup: PendingRegistration ${pending.id} konnte nicht geloescht werden (${message}).`);
16178
+ this.logger.warn(`Cleanup: pending registration ${pending.id} could not be deleted (${message}).`);
15390
16179
  }
15391
16180
  }
15392
16181
  if (expired.length > 0) {
15393
- this.logger.log(`Cleanup: ${expired.length} abgelaufene PendingRegistrations geloescht.`);
16182
+ this.logger.log(`Cleanup: ${expired.length} expired PendingRegistrations deleted.`);
15394
16183
  }
15395
16184
  return {
15396
16185
  deleted: expired.length,
@@ -15400,7 +16189,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15400
16189
  async start(input, context) {
15401
16190
  const email = normalizeEmail(input.email);
15402
16191
  if (await this.userLookup.hasActiveUser(email)) {
15403
- this.logger.log(`register/start fuer aktiven Account ignoriert (${email}).`);
16192
+ this.logger.log(`register/start ignored for an active account (${email}).`);
15404
16193
  await this.record("REGISTRATION_NEUTRAL_ACTIVE_USER", null, context);
15405
16194
  return {
15406
16195
  neutral: true
@@ -15438,8 +16227,8 @@ var PendingRegistrationService = class _PendingRegistrationService {
15438
16227
  const code = generateOtpCode();
15439
16228
  const otpHash = hashOtpCode(code);
15440
16229
  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);
16230
+ const otpExpiresAt = new Date(now.getTime() + import_types32.OTP_TTL_MINUTES * 60 * 1e3);
16231
+ const expiresAt = new Date(now.getTime() + import_types32.PENDING_EMAIL_TTL_HOURS * 60 * 60 * 1e3);
15443
16232
  const pending = await this.repo.create({
15444
16233
  tenantName: input.tenantName.trim(),
15445
16234
  tenantSlug: slug,
@@ -15467,13 +16256,13 @@ var PendingRegistrationService = class _PendingRegistrationService {
15467
16256
  reason: "unknown_email"
15468
16257
  });
15469
16258
  throw new import_common80.BadRequestException({
15470
- code: "OTP_INVALID"
16259
+ code: import_types31.REGISTRATION_ERROR_CODES.OTP_INVALID
15471
16260
  });
15472
16261
  }
15473
16262
  if (pending.status !== "PENDING_EMAIL_VERIFICATION") {
15474
16263
  return {
15475
16264
  status: pending.status,
15476
- nextStep: import_types13.REGISTRATION_STEP_BY_STATUS[pending.status],
16265
+ nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[pending.status],
15477
16266
  pendingRegistrationId: pending.id
15478
16267
  };
15479
16268
  }
@@ -15482,7 +16271,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15482
16271
  reason: "no_otp_set"
15483
16272
  });
15484
16273
  throw new import_common80.BadRequestException({
15485
- code: "OTP_INVALID"
16274
+ code: import_types31.REGISTRATION_ERROR_CODES.OTP_INVALID
15486
16275
  });
15487
16276
  }
15488
16277
  if (pending.otpExpiresAt.getTime() < Date.now()) {
@@ -15490,7 +16279,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15490
16279
  reason: "expired"
15491
16280
  });
15492
16281
  throw new import_common80.BadRequestException({
15493
- code: "OTP_EXPIRED"
16282
+ code: import_types31.REGISTRATION_ERROR_CODES.OTP_EXPIRED
15494
16283
  });
15495
16284
  }
15496
16285
  const maxAttempts = resolveOtpVerifyMaxAttempts();
@@ -15499,7 +16288,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15499
16288
  reason: "locked"
15500
16289
  });
15501
16290
  throw new import_common80.BadRequestException({
15502
- code: "OTP_LOCKED"
16291
+ code: import_types31.REGISTRATION_ERROR_CODES.OTP_LOCKED
15503
16292
  });
15504
16293
  }
15505
16294
  const attemptCount = await this.repo.incrementOtpAttemptCount(pending.id);
@@ -15508,7 +16297,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15508
16297
  reason: "locked"
15509
16298
  });
15510
16299
  throw new import_common80.BadRequestException({
15511
- code: "OTP_LOCKED"
16300
+ code: import_types31.REGISTRATION_ERROR_CODES.OTP_LOCKED
15512
16301
  });
15513
16302
  }
15514
16303
  if (!verifyOtpCode(pending.otpHash, code)) {
@@ -15516,7 +16305,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15516
16305
  reason: "wrong_code"
15517
16306
  });
15518
16307
  throw new import_common80.BadRequestException({
15519
- code: "OTP_INVALID"
16308
+ code: import_types31.REGISTRATION_ERROR_CODES.OTP_INVALID
15520
16309
  });
15521
16310
  }
15522
16311
  const now = /* @__PURE__ */ new Date();
@@ -15526,12 +16315,12 @@ var PendingRegistrationService = class _PendingRegistrationService {
15526
16315
  emailVerifiedAt: now,
15527
16316
  otpHash: null,
15528
16317
  otpExpiresAt: null,
15529
- expiresAt: new Date(now.getTime() + import_types13.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3)
16318
+ expiresAt: new Date(now.getTime() + import_types32.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3)
15530
16319
  });
15531
16320
  await this.record("OTP_VERIFIED", updated.id, context);
15532
16321
  return {
15533
16322
  status: updated.status,
15534
- nextStep: import_types13.REGISTRATION_STEP_BY_STATUS[updated.status],
16323
+ nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[updated.status],
15535
16324
  pendingRegistrationId: updated.id
15536
16325
  };
15537
16326
  }
@@ -15556,15 +16345,15 @@ var PendingRegistrationService = class _PendingRegistrationService {
15556
16345
  }
15557
16346
  async regenerateOtp(pending) {
15558
16347
  const now = /* @__PURE__ */ new Date();
15559
- const windowMs = import_types13.OTP_RATE_LIMIT_WINDOW_MINUTES * 60 * 1e3;
16348
+ const windowMs = import_types32.OTP_RATE_LIMIT_WINDOW_MINUTES * 60 * 1e3;
15560
16349
  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.`);
16350
+ if (withinWindow && pending.otpSendCount >= import_types32.OTP_RATE_LIMIT_MAX_SENDS) {
16351
+ this.logger.warn(`OTP rate limit reached for ${pending.email} \u2014 send is being skipped.`);
15563
16352
  return;
15564
16353
  }
15565
16354
  const code = generateOtpCode();
15566
16355
  const otpHash = hashOtpCode(code);
15567
- const otpExpiresAt = new Date(now.getTime() + import_types13.OTP_TTL_MINUTES * 60 * 1e3);
16356
+ const otpExpiresAt = new Date(now.getTime() + import_types32.OTP_TTL_MINUTES * 60 * 1e3);
15568
16357
  const otpSendCount = withinWindow ? pending.otpSendCount + 1 : 1;
15569
16358
  await this.repo.update(pending.id, {
15570
16359
  otpHash,
@@ -15587,7 +16376,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15587
16376
  });
15588
16377
  } catch (error) {
15589
16378
  const message = error instanceof Error ? error.message : String(error);
15590
- this.logger.error(`OTP-Mail an ${pending.email} fehlgeschlagen: ${message}`);
16379
+ this.logger.error(`OTP email to ${pending.email} failed: ${message}`);
15591
16380
  }
15592
16381
  }
15593
16382
  /**
@@ -15601,7 +16390,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15601
16390
  }
15602
16391
  const token = await this.resumeTokenSigner.sign({
15603
16392
  pendingRegistrationId: pending.id,
15604
- ttlMinutes: import_types12.REGISTRATION_RESUME_TTL_MINUTES
16393
+ ttlMinutes: import_types31.REGISTRATION_RESUME_TTL_MINUTES
15605
16394
  });
15606
16395
  const base = (this.resumeBaseUrl ?? "http://localhost").replace(/\/+$/, "");
15607
16396
  const url = `${base}/login?resume=${encodeURIComponent(token)}`;
@@ -15614,7 +16403,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15614
16403
  });
15615
16404
  } catch (error) {
15616
16405
  const message = error instanceof Error ? error.message : String(error);
15617
- this.logger.error(`Resume-Mail an ${pending.email} fehlgeschlagen: ${message}`);
16406
+ this.logger.error(`Resume email to ${pending.email} failed: ${message}`);
15618
16407
  }
15619
16408
  }
15620
16409
  /**
@@ -15629,7 +16418,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15629
16418
  async resumeWithToken(input, context) {
15630
16419
  if (!this.resumeTokenSigner) {
15631
16420
  throw new import_common80.BadRequestException({
15632
- code: "RESUME_NOT_CONFIGURED"
16421
+ code: import_types31.REGISTRATION_ERROR_CODES.RESUME_NOT_CONFIGURED
15633
16422
  });
15634
16423
  }
15635
16424
  let decoded;
@@ -15637,18 +16426,18 @@ var PendingRegistrationService = class _PendingRegistrationService {
15637
16426
  decoded = await this.resumeTokenSigner.verify(input.token);
15638
16427
  } catch {
15639
16428
  throw new import_common80.BadRequestException({
15640
- code: "RESUME_TOKEN_INVALID"
16429
+ code: import_types31.REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID
15641
16430
  });
15642
16431
  }
15643
16432
  const pending = await this.repo.findById(decoded.pendingRegistrationId);
15644
16433
  if (!pending) {
15645
16434
  throw new import_common80.BadRequestException({
15646
- code: "RESUME_TOKEN_INVALID"
16435
+ code: import_types31.REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID
15647
16436
  });
15648
16437
  }
15649
16438
  if (pending.expiresAt.getTime() < Date.now()) {
15650
16439
  throw new import_common80.BadRequestException({
15651
- code: "PENDING_REGISTRATION_EXPIRED"
16440
+ code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
15652
16441
  });
15653
16442
  }
15654
16443
  await this.record("REGISTRATION_NEUTRAL_REPLAY", pending.id, context, {
@@ -15658,7 +16447,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15658
16447
  return {
15659
16448
  pendingRegistrationId: pending.id,
15660
16449
  status: pending.status,
15661
- nextStep: import_types13.REGISTRATION_STEP_BY_STATUS[pending.status],
16450
+ nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[pending.status],
15662
16451
  snapshot: toSnapshot2(pending)
15663
16452
  };
15664
16453
  }
@@ -15678,7 +16467,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15678
16467
  async getConfiguratorCatalog() {
15679
16468
  if (!this.configuratorLookup) {
15680
16469
  throw new import_common80.BadRequestException({
15681
- code: "CONFIGURATOR_NOT_CONFIGURED"
16470
+ code: import_types31.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
15682
16471
  });
15683
16472
  }
15684
16473
  return this.configuratorLookup.getCatalog();
@@ -15697,30 +16486,30 @@ var PendingRegistrationService = class _PendingRegistrationService {
15697
16486
  async saveConfiguration(input, context) {
15698
16487
  if (!this.configuratorLookup) {
15699
16488
  throw new import_common80.BadRequestException({
15700
- code: "CONFIGURATOR_NOT_CONFIGURED"
16489
+ code: import_types31.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
15701
16490
  });
15702
16491
  }
15703
16492
  const pending = await this.repo.findById(input.pendingRegistrationId);
15704
16493
  if (!pending) {
15705
16494
  throw new import_common80.NotFoundException({
15706
- code: "PENDING_REGISTRATION_NOT_FOUND"
16495
+ code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
15707
16496
  });
15708
16497
  }
15709
16498
  if (pending.expiresAt.getTime() < Date.now()) {
15710
16499
  throw new import_common80.BadRequestException({
15711
- code: "PENDING_REGISTRATION_EXPIRED"
16500
+ code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
15712
16501
  });
15713
16502
  }
15714
16503
  if (pending.status !== "EMAIL_VERIFIED" && pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
15715
16504
  throw new import_common80.BadRequestException({
15716
- code: "INVALID_REGISTRATION_STATE"
16505
+ code: import_types31.REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE
15717
16506
  });
15718
16507
  }
15719
16508
  const catalog = await this.configuratorLookup.getCatalog();
15720
16509
  const model = catalog.models.find((m) => m.id === input.selection.modelId);
15721
16510
  if (!model) {
15722
16511
  throw new import_common80.BadRequestException({
15723
- code: "MODEL_NOT_AVAILABLE"
16512
+ code: import_types31.REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE
15724
16513
  });
15725
16514
  }
15726
16515
  let promoEval;
@@ -15753,7 +16542,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15753
16542
  };
15754
16543
  }
15755
16544
  const now = /* @__PURE__ */ new Date();
15756
- const expiresAt = new Date(now.getTime() + import_types13.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
16545
+ const expiresAt = new Date(now.getTime() + import_types32.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
15757
16546
  const updated = await this.repo.update(pending.id, {
15758
16547
  status: "PLAN_SELECTED",
15759
16548
  currentStep: 4,
@@ -15772,7 +16561,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15772
16561
  return {
15773
16562
  pendingRegistrationId: updated.id,
15774
16563
  status: updated.status,
15775
- nextStep: import_types13.REGISTRATION_STEP_BY_STATUS[updated.status],
16564
+ nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[updated.status],
15776
16565
  selection: input.selection,
15777
16566
  breakdown
15778
16567
  };
@@ -15786,26 +16575,26 @@ var PendingRegistrationService = class _PendingRegistrationService {
15786
16575
  async previewConfigPromo(pendingRegistrationId, code) {
15787
16576
  if (!this.configuratorLookup) {
15788
16577
  throw new import_common80.BadRequestException({
15789
- code: "CONFIGURATOR_NOT_CONFIGURED"
16578
+ code: import_types31.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
15790
16579
  });
15791
16580
  }
15792
16581
  const pending = await this.repo.findById(pendingRegistrationId);
15793
16582
  if (!pending) {
15794
16583
  throw new import_common80.NotFoundException({
15795
- code: "PENDING_REGISTRATION_NOT_FOUND"
16584
+ code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
15796
16585
  });
15797
16586
  }
15798
16587
  const config = pending.configJson;
15799
16588
  if (!config) {
15800
16589
  throw new import_common80.BadRequestException({
15801
- code: "CONFIG_NOT_SAVED"
16590
+ code: import_types31.REGISTRATION_ERROR_CODES.CONFIG_NOT_SAVED
15802
16591
  });
15803
16592
  }
15804
16593
  const catalog = await this.configuratorLookup.getCatalog();
15805
16594
  const model = catalog.models.find((m) => m.id === config.modelId);
15806
16595
  if (!model) {
15807
16596
  throw new import_common80.BadRequestException({
15808
- code: "MODEL_NOT_AVAILABLE"
16597
+ code: import_types31.REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE
15809
16598
  });
15810
16599
  }
15811
16600
  let promoEval;
@@ -15864,7 +16653,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
15864
16653
  return candidate;
15865
16654
  }
15866
16655
  }
15867
- throw new Error(`Konnte keinen freien Slug fuer "${tenantName}" finden.`);
16656
+ throw new Error(`Could not find a free slug for "${tenantName}".`);
15868
16657
  }
15869
16658
  };
15870
16659
  PendingRegistrationService = _ts_decorate90([
@@ -15914,9 +16703,9 @@ function normalizeEmail(value) {
15914
16703
  __name(normalizeEmail, "normalizeEmail");
15915
16704
  function resolveOtpVerifyMaxAttempts() {
15916
16705
  const raw = process.env.SAAS_PLATFORM_OTP_VERIFY_MAX_ATTEMPTS;
15917
- if (!raw) return import_types13.OTP_VERIFY_MAX_ATTEMPTS;
16706
+ if (!raw) return import_types32.OTP_VERIFY_MAX_ATTEMPTS;
15918
16707
  const parsed = Number.parseInt(raw, 10);
15919
- return Number.isInteger(parsed) && parsed > 0 ? parsed : import_types13.OTP_VERIFY_MAX_ATTEMPTS;
16708
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : import_types32.OTP_VERIFY_MAX_ATTEMPTS;
15920
16709
  }
15921
16710
  __name(resolveOtpVerifyMaxAttempts, "resolveOtpVerifyMaxAttempts");
15922
16711
  function toSnapshot2(pending) {
@@ -15989,7 +16778,7 @@ var RegistrationCleanupCron = class _RegistrationCleanupCron {
15989
16778
  }
15990
16779
  }
15991
16780
  if (totalDeleted > 0) {
15992
- this.logger.log(`PendingRegistration-Cleanup abgeschlossen: ${totalDeleted} geloescht in ${iterations} Batches.`);
16781
+ this.logger.log(`Pending registration cleanup finished: ${totalDeleted} deleted in ${iterations} batches.`);
15993
16782
  }
15994
16783
  }
15995
16784
  };
@@ -16011,6 +16800,7 @@ RegistrationCleanupCron = _ts_decorate91([
16011
16800
 
16012
16801
  // src/registration/ip-rate-limit.guard.ts
16013
16802
  var import_common82 = require("@nestjs/common");
16803
+ var import_types33 = require("@saasicat/types");
16014
16804
  function _ts_decorate92(decorators, target, key, desc) {
16015
16805
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
16016
16806
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -16039,7 +16829,7 @@ var BaseIpRateLimitGuard = class {
16039
16829
  const key = ipFingerprint2(req);
16040
16830
  if (this.exceeded(key)) {
16041
16831
  throw new import_common82.HttpException({
16042
- code: "RATE_LIMITED",
16832
+ code: import_types33.REGISTRATION_ERROR_CODES.RATE_LIMITED,
16043
16833
  reason: this.options.name,
16044
16834
  retryAfterSeconds: Math.ceil(this.options.windowMs / 1e3)
16045
16835
  }, import_common82.HttpStatus.TOO_MANY_REQUESTS);
@@ -16183,7 +16973,7 @@ _ts_decorate94([
16183
16973
  (0, import_class_validator17.IsOptional)(),
16184
16974
  (0, import_class_validator17.IsString)(),
16185
16975
  (0, import_class_validator17.Matches)(/^[a-z0-9-]+$/, {
16186
- message: "tenantSlug darf nur Kleinbuchstaben, Ziffern und Bindestriche enthalten."
16976
+ message: "tenantSlug may only contain lower-case letters, digits and hyphens."
16187
16977
  }),
16188
16978
  (0, import_class_validator17.MaxLength)(60),
16189
16979
  _ts_metadata73("design:type", String)
@@ -16219,7 +17009,7 @@ _ts_decorate94([
16219
17009
  ], RegisterStartDto.prototype, "password", void 0);
16220
17010
  _ts_decorate94([
16221
17011
  (0, import_class_validator17.Equals)(true, {
16222
- message: "AGB, Datenschutzerklaerung und Auftragsverarbeitung muessen zugestimmt werden."
17012
+ message: "The terms of service, privacy policy and data processing agreement must be accepted."
16223
17013
  }),
16224
17014
  _ts_metadata73("design:type", Boolean)
16225
17015
  ], RegisterStartDto.prototype, "termsAccepted", void 0);
@@ -16259,7 +17049,7 @@ _ts_decorate95([
16259
17049
  _ts_decorate95([
16260
17050
  (0, import_class_validator18.IsString)(),
16261
17051
  (0, import_class_validator18.Matches)(/^\d{6}$/, {
16262
- message: "OTP muss 6 Ziffern sein."
17052
+ message: "The OTP must be 6 digits."
16263
17053
  }),
16264
17054
  _ts_metadata74("design:type", String)
16265
17055
  ], VerifyRegistrationOtpDto.prototype, "otp", void 0);
@@ -16790,8 +17580,8 @@ __export(src_exports, {
16790
17580
  buildPublicMarketingCatalogController: () => buildPublicMarketingCatalogController,
16791
17581
  buildReplacedByIndex: () => buildReplacedByIndex,
16792
17582
  buildTenantSubscriptionBundlesController: () => buildTenantSubscriptionBundlesController,
16793
- classifyBundleVersionDiff: () => import_types.classifyBundleVersionDiff,
16794
- classifyPlanDiff: () => import_types.classifyPlanDiff,
17583
+ classifyBundleVersionDiff: () => import_types4.classifyBundleVersionDiff,
17584
+ classifyPlanDiff: () => import_types4.classifyPlanDiff,
16795
17585
  clearPendingPlanVersionFields: () => clearPendingPlanVersionFields,
16796
17586
  collectSubscriptionBundleFeatures: () => collectSubscriptionBundleFeatures,
16797
17587
  computeBreakdown: () => computeBreakdown,
@@ -16884,7 +17674,7 @@ __export(testing_exports, {
16884
17674
  });
16885
17675
 
16886
17676
  // src/testing/fake-repositories.ts
16887
- var import_types14 = require("@saasicat/types");
17677
+ var import_types34 = require("@saasicat/types");
16888
17678
  var FakeSubscriptionRepository = class {
16889
17679
  static {
16890
17680
  __name(this, "FakeSubscriptionRepository");
@@ -16975,10 +17765,10 @@ var FakeSubscriptionBundleRepository = class {
16975
17765
  async cancel(subscriptionBundleId, data) {
16976
17766
  const existing = this.byId.get(subscriptionBundleId);
16977
17767
  if (!existing) {
16978
- throw new Error(`SubscriptionBundle '${subscriptionBundleId}' nicht gefunden`);
17768
+ throw new Error(`SubscriptionBundle '${subscriptionBundleId}' not found`);
16979
17769
  }
16980
17770
  if (existing.canceledAt !== null) {
16981
- throw new Error(`SubscriptionBundle '${subscriptionBundleId}' ist bereits gek\xFCndigt`);
17771
+ throw new Error(`SubscriptionBundle '${subscriptionBundleId}' is already cancelled`);
16982
17772
  }
16983
17773
  const updated = {
16984
17774
  ...existing,
@@ -16992,7 +17782,7 @@ var FakeSubscriptionBundleRepository = class {
16992
17782
  async reactivate(subscriptionBundleId) {
16993
17783
  const existing = this.byId.get(subscriptionBundleId);
16994
17784
  if (!existing) {
16995
- throw new Error(`SubscriptionBundle '${subscriptionBundleId}' nicht gefunden`);
17785
+ throw new Error(`SubscriptionBundle '${subscriptionBundleId}' not found`);
16996
17786
  }
16997
17787
  const updated = {
16998
17788
  ...existing,
@@ -17102,7 +17892,7 @@ var FakeSubscriptionContractRepository = class {
17102
17892
  }
17103
17893
  async terminate(contractId, data) {
17104
17894
  const existing = this.byId.get(contractId);
17105
- if (!existing) throw new Error(`SubscriptionContract '${contractId}' nicht gefunden`);
17895
+ if (!existing) throw new Error(`SubscriptionContract '${contractId}' not found`);
17106
17896
  const updated = {
17107
17897
  ...existing,
17108
17898
  status: data.status,
@@ -17262,7 +18052,7 @@ var FakeBundleRepository = class {
17262
18052
  }
17263
18053
  async update(bundleId, data) {
17264
18054
  const existing = this.bundles.get(bundleId);
17265
- if (!existing) throw new Error(`Bundle '${bundleId}' nicht gefunden`);
18055
+ if (!existing) throw new Error(`Bundle '${bundleId}' not found`);
17266
18056
  const updated = {
17267
18057
  ...existing,
17268
18058
  label: data.label ?? existing.label,
@@ -17277,7 +18067,7 @@ var FakeBundleRepository = class {
17277
18067
  }
17278
18068
  async softDelete(bundleId) {
17279
18069
  const existing = this.bundles.get(bundleId);
17280
- if (!existing) throw new Error(`Bundle '${bundleId}' nicht gefunden`);
18070
+ if (!existing) throw new Error(`Bundle '${bundleId}' not found`);
17281
18071
  this.bundles.set(bundleId, {
17282
18072
  ...existing,
17283
18073
  deletedAt: this.nowIso()
@@ -17309,7 +18099,7 @@ var FakeBundleRepository = class {
17309
18099
  async createDraft(data) {
17310
18100
  const draft = await this.findCurrentDraft(data.bundleId);
17311
18101
  if (draft) {
17312
- throw new Error(`Bundle '${data.bundleId}' hat bereits eine Draft-Version v${draft.version}`);
18102
+ throw new Error(`Bundle '${data.bundleId}' already has a draft version v${draft.version}`);
17313
18103
  }
17314
18104
  const all = await this.listVersions(data.bundleId);
17315
18105
  const nextVersion = all.length === 0 ? 1 : Math.max(...all.map((v) => v.version)) + 1;
@@ -17345,7 +18135,7 @@ var FakeBundleRepository = class {
17345
18135
  }
17346
18136
  async updateDraft(versionId, data) {
17347
18137
  const existing = this.versions.get(versionId);
17348
- if (!existing) throw new Error(`BundleVersion '${versionId}' nicht gefunden`);
18138
+ if (!existing) throw new Error(`BundleVersion '${versionId}' not found`);
17349
18139
  const updated = {
17350
18140
  ...existing,
17351
18141
  features: data.features ?? existing.features,
@@ -17367,15 +18157,15 @@ var FakeBundleRepository = class {
17367
18157
  const existing = this.versions.get(versionId);
17368
18158
  if (!existing) return;
17369
18159
  if (existing.publishedAt !== null) {
17370
- throw new Error(`BundleVersion '${versionId}' ist bereits published`);
18160
+ throw new Error(`BundleVersion '${versionId}' is already published`);
17371
18161
  }
17372
18162
  this.versions.delete(versionId);
17373
18163
  }
17374
18164
  async publishDraft(versionId, publishMeta) {
17375
18165
  const draft = this.versions.get(versionId);
17376
- if (!draft) throw new Error(`BundleVersion '${versionId}' nicht gefunden`);
18166
+ if (!draft) throw new Error(`BundleVersion '${versionId}' not found`);
17377
18167
  if (draft.publishedAt !== null) {
17378
- throw new Error(`BundleVersion '${versionId}' ist bereits published`);
18168
+ throw new Error(`BundleVersion '${versionId}' is already published`);
17379
18169
  }
17380
18170
  const now = this.nowIso();
17381
18171
  const validFromIso = publishMeta.validFrom.toISOString();
@@ -17451,7 +18241,7 @@ var FakeMarketingProjectionRepository = class {
17451
18241
  const locale = data.locale ?? "de";
17452
18242
  const existing = await this.findByTarget(data.targetType, data.targetVersionId, locale);
17453
18243
  if (existing) {
17454
- throw new Error(`MarketingProjection f\xFCr ${data.targetType}/${data.targetVersionId}/${locale} existiert bereits`);
18244
+ throw new Error(`Marketing projection for ${data.targetType}/${data.targetVersionId}/${locale} already exists`);
17455
18245
  }
17456
18246
  const now = this.nowIso();
17457
18247
  const row = {
@@ -17479,7 +18269,7 @@ var FakeMarketingProjectionRepository = class {
17479
18269
  }
17480
18270
  async update(id, data) {
17481
18271
  const existing = this.rows.get(id);
17482
- if (!existing) throw new Error(`MarketingProjection '${id}' nicht gefunden`);
18272
+ if (!existing) throw new Error(`MarketingProjection '${id}' not found`);
17483
18273
  const updated = {
17484
18274
  ...existing,
17485
18275
  displayLabel: data.displayLabel ?? existing.displayLabel,
@@ -17499,7 +18289,7 @@ var FakeMarketingProjectionRepository = class {
17499
18289
  return updated;
17500
18290
  }
17501
18291
  async delete(id) {
17502
- if (!this.rows.has(id)) throw new Error(`MarketingProjection '${id}' nicht gefunden`);
18292
+ if (!this.rows.has(id)) throw new Error(`MarketingProjection '${id}' not found`);
17503
18293
  this.rows.delete(id);
17504
18294
  }
17505
18295
  };
@@ -17573,7 +18363,7 @@ var FakePlanRepository = class {
17573
18363
  }
17574
18364
  async update(planId, data) {
17575
18365
  const existing = this.plans.get(planId);
17576
- if (!existing) throw new Error(`Plan '${planId}' nicht gefunden`);
18366
+ if (!existing) throw new Error(`Plan '${planId}' not found`);
17577
18367
  const updated = {
17578
18368
  ...existing,
17579
18369
  label: data.label ?? existing.label,
@@ -17587,7 +18377,7 @@ var FakePlanRepository = class {
17587
18377
  }
17588
18378
  async softDelete(planId) {
17589
18379
  const existing = this.plans.get(planId);
17590
- if (!existing) throw new Error(`Plan '${planId}' nicht gefunden`);
18380
+ if (!existing) throw new Error(`Plan '${planId}' not found`);
17591
18381
  const updated = {
17592
18382
  ...existing,
17593
18383
  deletedAt: this.nowIso(),
@@ -17640,7 +18430,7 @@ var FakePlanRepository = class {
17640
18430
  }
17641
18431
  async findActivePlanVersion(planKey, asOf = /* @__PURE__ */ new Date()) {
17642
18432
  const t = asOf.getTime();
17643
- const dayStart = (0, import_types14.startOfUtcDay)(asOf).getTime();
18433
+ const dayStart = (0, import_types34.startOfUtcDay)(asOf).getTime();
17644
18434
  const matches = [
17645
18435
  ...this.versions.values()
17646
18436
  ].filter((v) => {
@@ -17704,7 +18494,7 @@ var FakePlanRepository = class {
17704
18494
  }
17705
18495
  async updatePlanVersionDraft(versionId, data) {
17706
18496
  const existing = this.versions.get(versionId);
17707
- if (!existing) throw new Error(`PlanVersion '${versionId}' nicht gefunden`);
18497
+ if (!existing) throw new Error(`PlanVersion '${versionId}' not found`);
17708
18498
  const updated = {
17709
18499
  ...existing,
17710
18500
  features: data.features ?? existing.features,
@@ -17721,7 +18511,7 @@ var FakePlanRepository = class {
17721
18511
  }
17722
18512
  async publishPlanVersionDraft(versionId, publishMeta) {
17723
18513
  const draft = this.versions.get(versionId);
17724
- if (!draft) throw new Error(`PlanVersion '${versionId}' nicht gefunden`);
18514
+ if (!draft) throw new Error(`PlanVersion '${versionId}' not found`);
17725
18515
  const planKey = draft.planId;
17726
18516
  const now = this.nowIso();
17727
18517
  const dayMs = 24 * 60 * 60 * 1e3;
@@ -17753,14 +18543,14 @@ var FakePlanRepository = class {
17753
18543
  const existing = this.versions.get(versionId);
17754
18544
  if (!existing) return;
17755
18545
  if (existing.publishedAt !== null) {
17756
- throw new Error(`FakePlanRepository: PlanVersion '${versionId}' ist bereits published \u2014 Discard nicht erlaubt`);
18546
+ throw new Error(`FakePlanRepository: plan version '${versionId}' is already published \u2014 discard is not allowed`);
17757
18547
  }
17758
18548
  this.versions.delete(versionId);
17759
18549
  }
17760
18550
  async terminate(versionId, endsAt) {
17761
18551
  const existing = this.versions.get(versionId);
17762
18552
  if (!existing) {
17763
- throw new Error(`FakePlanRepository: PlanVersion '${versionId}' nicht gefunden`);
18553
+ throw new Error(`FakePlanRepository: PlanVersion '${versionId}' not found`);
17764
18554
  }
17765
18555
  const updated = {
17766
18556
  ...existing,