@saasicat/nest 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/_entries.cjs +1309 -519
  2. package/dist/admin/index.js +1 -1
  3. package/dist/billing/index.d.cts +7 -4
  4. package/dist/billing/index.d.ts +7 -4
  5. package/dist/billing/index.js +6 -6
  6. package/dist/catalog/index.js +4 -4
  7. package/dist/checkout-offer/index.js +2 -2
  8. package/dist/{chunk-Z3FWN3HJ.js → chunk-5ZXBXYXU.js} +3 -3
  9. package/dist/{chunk-RZVSKKXX.js → chunk-6WA4X5U3.js} +46 -45
  10. package/dist/{chunk-V34AA7C7.js → chunk-6WL6VQ7Z.js} +1 -1
  11. package/dist/{chunk-ABB2CRAS.js → chunk-74SCJ4HF.js} +1 -1
  12. package/dist/{chunk-PLJEAUFU.js → chunk-7JIHG53V.js} +76 -39
  13. package/dist/{chunk-7RRWI5LH.js → chunk-AFK2IE2L.js} +10 -10
  14. package/dist/{chunk-32EMXPU2.js → chunk-B7NRY2LV.js} +48 -17
  15. package/dist/{chunk-MDIZUVIK.js → chunk-BSK6YBLI.js} +1 -1
  16. package/dist/{chunk-R2QOSVAA.js → chunk-DQKCK7DX.js} +1 -1
  17. package/dist/{chunk-GLLCB4BP.js → chunk-EUU3B6NB.js} +17 -13
  18. package/dist/{chunk-EXOLOJFZ.js → chunk-F6DCLVBB.js} +29 -8
  19. package/dist/{chunk-5KH2O4SF.js → chunk-I77VS3YH.js} +287 -105
  20. package/dist/{chunk-YITTE646.js → chunk-KUDKD7ZG.js} +34 -14
  21. package/dist/{chunk-P3DP6GJE.js → chunk-LXUBCLRK.js} +486 -160
  22. package/dist/{chunk-GJBXOCRZ.js → chunk-ORWIGSNN.js} +45 -17
  23. package/dist/{chunk-E7AJ42AB.js → chunk-QLJHIIOY.js} +168 -28
  24. package/dist/{chunk-BOADBIGB.js → chunk-YYUXKLXK.js} +2 -2
  25. package/dist/{define-saasicat-Rc0rXrYG.d.ts → define-saasicat-CqkhnHn-.d.ts} +1 -1
  26. package/dist/{define-saasicat-CeDBX9ji.d.cts → define-saasicat-D8c6Bs4T.d.cts} +1 -1
  27. package/dist/discovery/index.d.cts +2 -2
  28. package/dist/discovery/index.d.ts +2 -2
  29. package/dist/discovery/index.js +4 -4
  30. package/dist/{enforce-quota.interceptor-DMw-bPyk.d.cts → enforce-quota.interceptor-Dk0eEtzV.d.cts} +1 -1
  31. package/dist/{enforce-quota.interceptor-CmbWX39L.d.ts → enforce-quota.interceptor-Dp8wTYro.d.ts} +1 -1
  32. package/dist/entitlement/index.js +2 -2
  33. package/dist/index.d.cts +6 -6
  34. package/dist/index.d.ts +6 -6
  35. package/dist/index.js +17 -17
  36. package/dist/{module-DgYJur6f.d.ts → module-BwD-wAor.d.ts} +1 -1
  37. package/dist/{module-CSN8j8Tp.d.cts → module-DsDkbawm.d.cts} +1 -1
  38. package/dist/platform/index.d.cts +8 -8
  39. package/dist/platform/index.d.ts +8 -8
  40. package/dist/platform/index.js +13 -13
  41. package/dist/promo/index.d.cts +3 -3
  42. package/dist/promo/index.d.ts +3 -3
  43. package/dist/promo/index.js +3 -3
  44. package/dist/registration/index.js +1 -1
  45. package/dist/{saas-platform.module-DLhoqKNA.d.ts → saas-platform.module-BQmrDGVU.d.ts} +1 -1
  46. package/dist/{saas-platform.module-D3rUTiho.d.cts → saas-platform.module-DducnQ9s.d.cts} +1 -1
  47. package/dist/{service-DX8KbGXl.d.cts → service-DPZ2W1E-.d.cts} +2 -2
  48. package/dist/{service-DX8KbGXl.d.ts → service-DPZ2W1E-.d.ts} +2 -2
  49. package/dist/subscription-contract/index.js +2 -2
  50. package/dist/{tenant-billing.controller-BwZRnJ3q.d.ts → tenant-billing.controller-b1G1yB09.d.ts} +2 -2
  51. package/dist/{tenant-billing.controller-CG4CaFUP.d.cts → tenant-billing.controller-m42YQ_Ei.d.cts} +2 -2
  52. package/dist/testing/index.d.cts +5 -5
  53. package/dist/testing/index.d.ts +5 -5
  54. package/dist/testing/index.js +33 -33
  55. package/package.json +3 -3
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  PromoCodesService
3
- } from "./chunk-E7AJ42AB.js";
3
+ } from "./chunk-QLJHIIOY.js";
4
4
  import {
5
5
  SubscriptionContractService
6
- } from "./chunk-32EMXPU2.js";
6
+ } from "./chunk-B7NRY2LV.js";
7
7
  import {
8
8
  AUDIT_CONTEXT_RESOLVER_TOKEN,
9
9
  PENDING_PLAN_QUERY_PORT_TOKEN,
@@ -28,13 +28,13 @@ import {
28
28
  SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN,
29
29
  isLimitExceededError,
30
30
  toEffectiveLimitsSnapshot
31
- } from "./chunk-EXOLOJFZ.js";
31
+ } from "./chunk-F6DCLVBB.js";
32
32
  import {
33
33
  PLAN_CATALOG_TOKEN,
34
34
  findPlan,
35
35
  getMarketedPlans,
36
36
  getPlanPriceNet
37
- } from "./chunk-R2QOSVAA.js";
37
+ } from "./chunk-DQKCK7DX.js";
38
38
  import {
39
39
  SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN
40
40
  } from "./chunk-NYLON2VC.js";
@@ -103,35 +103,35 @@ var PublishValidationError = class extends Error {
103
103
  function assertChangeNote(changeNote) {
104
104
  const trimmed = (changeNote ?? "").trim();
105
105
  if (trimmed.length === 0) {
106
- throw new PublishValidationError("CHANGE_NOTE_REQUIRED", "changeNote ist Pflicht beim Ver\xF6ffentlichen einer Version.");
106
+ throw new PublishValidationError("CHANGE_NOTE_REQUIRED", "changeNote is required when publishing a version.");
107
107
  }
108
108
  return trimmed;
109
109
  }
110
110
  __name(assertChangeNote, "assertChangeNote");
111
111
  function assertDraftPublishable(draft, draftId) {
112
112
  if (!draft) {
113
- throw new PublishValidationError("NOT_FOUND", `PlanVersion ${draftId} nicht gefunden`);
113
+ throw new PublishValidationError("NOT_FOUND", `PlanVersion ${draftId} not found`);
114
114
  }
115
115
  if (draft.publishedAt !== null) {
116
- throw new PublishValidationError("ALREADY_PUBLISHED", `PlanVersion ${draftId} ist bereits ver\xF6ffentlicht.`);
116
+ throw new PublishValidationError("ALREADY_PUBLISHED", `PlanVersion ${draftId} has already been published.`);
117
117
  }
118
118
  if (!draft.baseVersionId) {
119
- throw new PublishValidationError("NO_BASE_VERSION", `Draft ${draftId} hat keine baseVersion \u2014 wahrscheinlich manuell angelegtes v1, das nur per Migration ver\xF6ffentlicht werden darf.`);
119
+ 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.`);
120
120
  }
121
121
  }
122
122
  __name(assertDraftPublishable, "assertDraftPublishable");
123
123
  function assertBaseVersionFresh(base, baseId, planContext) {
124
124
  if (!base) {
125
- throw new PublishValidationError("BASE_NOT_FOUND", `baseVersion ${baseId} nicht mehr vorhanden.`);
125
+ throw new PublishValidationError("BASE_NOT_FOUND", `baseVersion ${baseId} no longer exists.`);
126
126
  }
127
127
  if (base.supersededAt !== null) {
128
- 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.`);
128
+ 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.`);
129
129
  }
130
130
  }
131
131
  __name(assertBaseVersionFresh, "assertBaseVersionFresh");
132
132
  function assertOptimisticLockHeld(updateCount, draftId) {
133
133
  if (updateCount !== 1) {
134
- throw new PublishValidationError("OPTIMISTIC_LOCK_CONFLICT", `Draft ${draftId} wurde zwischenzeitlich von einem anderen Admin ver\xF6ffentlicht.`);
134
+ throw new PublishValidationError("OPTIMISTIC_LOCK_CONFLICT", `Draft ${draftId} has meanwhile been published by another admin.`);
135
135
  }
136
136
  }
137
137
  __name(assertOptimisticLockHeld, "assertOptimisticLockHeld");
@@ -182,7 +182,7 @@ var FEATURE_GUARD_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/Featu
182
182
  // src/billing/feature.guard.ts
183
183
  import { ForbiddenException, Inject, Injectable, Logger, Optional } from "@nestjs/common";
184
184
  import { Reflector } from "@nestjs/core";
185
- import { FEATURE_NOT_LICENSED } from "@saasicat/types";
185
+ import { AUTH_ERROR_CODES, FEATURE_NOT_LICENSED } from "@saasicat/types";
186
186
 
187
187
  // src/billing/upsell.tokens.ts
188
188
  var UPSELL_OFFER_RESOLVER_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/UpsellOfferResolver");
@@ -229,11 +229,21 @@ var FeatureGuard = class _FeatureGuard {
229
229
  if (!required || required.length === 0) return true;
230
230
  const request = context.switchToHttp().getRequest();
231
231
  const user = request.user;
232
- if (!user) throw new ForbiddenException("Nicht authentifiziert");
232
+ if (!user) {
233
+ throw new ForbiddenException({
234
+ code: AUTH_ERROR_CODES.NOT_AUTHENTICATED,
235
+ message: "Not authenticated"
236
+ });
237
+ }
233
238
  const role = this.config?.userRoleResolver ? this.config.userRoleResolver(user) : user.role ?? user.platformRole;
234
239
  if (role === "SUPER_ADMIN") return true;
235
240
  const tenantId = this.config?.tenantIdResolver ? this.config.tenantIdResolver(request) : request.tenantId ?? user.tenantId;
236
- if (!tenantId) throw new ForbiddenException("Kein Mandant zugeordnet");
241
+ if (!tenantId) {
242
+ throw new ForbiddenException({
243
+ code: AUTH_ERROR_CODES.NO_TENANT_ASSIGNED,
244
+ message: "No tenant assigned"
245
+ });
246
+ }
237
247
  const compute = /* @__PURE__ */ __name(() => this.entitlements.computeLimits(tenantId), "compute");
238
248
  const limits = this.config?.tenantContextRunner ? await this.config.tenantContextRunner(tenantId, compute) : await compute();
239
249
  const allowed = required.some((f) => limits.features.has(f));
@@ -245,17 +255,28 @@ var FeatureGuard = class _FeatureGuard {
245
255
  /**
246
256
  * Upsell response (#36): with a registered `UpsellOfferResolver` the 403
247
257
  * becomes machine-readable (`FeatureNotLicensedBody`), so consumer UIs can
248
- * render a purchase offer. Without a resolver the previous plain 403
249
- * remains no breaking change.
258
+ * render a purchase offer. Without a resolver the body carries the same
259
+ * `code` but no offers.
250
260
  *
251
261
  * Deliberately 403 + `code` field instead of 402 — rationale in
252
262
  * `@saasicat/types` upsell.types.ts (402 is reserved / inconsistently
253
263
  * supported; SPA interceptors must not treat the 403 as an auth error, the
254
264
  * distinction runs through `code`).
265
+ *
266
+ * The message must keep starting with the word `Feature` — consumers
267
+ * classify the response by that prefix.
255
268
  */
256
269
  async buildNotLicensedException(required, tenantId) {
257
- const message = `Feature ${required.join(" / ")} nicht im aktuellen Paket enthalten.`;
258
- if (!this.upsellResolver) return new ForbiddenException(message);
270
+ const message = `Feature ${required.join(" / ")} is not included in the current plan.`;
271
+ if (!this.upsellResolver) {
272
+ return new ForbiddenException({
273
+ code: FEATURE_NOT_LICENSED,
274
+ message,
275
+ params: {
276
+ featureKeys: required
277
+ }
278
+ });
279
+ }
259
280
  const body = {
260
281
  code: FEATURE_NOT_LICENSED,
261
282
  featureKey: required[0],
@@ -273,7 +294,7 @@ var FeatureGuard = class _FeatureGuard {
273
294
  try {
274
295
  return await this.upsellResolver.resolveOffers(required, tenantId);
275
296
  } catch (error) {
276
- this.logger.warn(`UpsellOfferResolver fehlgeschlagen f\xFCr [${required.join(", ")}]: ${String(error)}`);
297
+ this.logger.warn(`UpsellOfferResolver failed for [${required.join(", ")}]: ${String(error)}`);
277
298
  return [];
278
299
  }
279
300
  }
@@ -426,6 +447,7 @@ __name(compareRankedOffers, "compareRankedOffers");
426
447
  // src/billing/limit-exceeded.filter.ts
427
448
  import { Catch, HttpStatus, Logger as Logger2 } from "@nestjs/common";
428
449
  import { BaseExceptionFilter } from "@nestjs/core";
450
+ import { BILLING_ERROR_CODES } from "@saasicat/types";
429
451
  function _ts_decorate3(decorators, target, key, desc) {
430
452
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
431
453
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -446,11 +468,14 @@ var LimitExceededFilter = class _LimitExceededFilter extends BaseExceptionFilter
446
468
  const ctx = host.switchToHttp();
447
469
  const response = ctx.getResponse();
448
470
  const request = ctx.getRequest();
449
- this.logger.warn(`Limit ${exception.dimension} \xFCberschritten: ${exception.used}/${exception.max} bei ${request.method ?? "?"} ${request.url ?? ""}`);
471
+ this.logger.warn(`Limit ${exception.dimension} exceeded: ${exception.used}/${exception.max} on ${request.method ?? "?"} ${request.url ?? ""}`);
450
472
  response.status(HttpStatus.PAYMENT_REQUIRED).send({
451
473
  statusCode: HttpStatus.PAYMENT_REQUIRED,
452
474
  error: "PaymentRequired",
453
- reason: "LIMIT_EXCEEDED",
475
+ code: BILLING_ERROR_CODES.LIMIT_EXCEEDED,
476
+ // Deprecated: superseded by `code`, kept until consumers have
477
+ // migrated. Same value, only under the older field name.
478
+ reason: BILLING_ERROR_CODES.LIMIT_EXCEEDED,
454
479
  dimension: exception.dimension,
455
480
  used: exception.used,
456
481
  max: exception.max,
@@ -476,8 +501,8 @@ var PlanCatalogValidationError = class extends Error {
476
501
  source;
477
502
  errors;
478
503
  constructor(source, errors) {
479
- const messages = errors.map((e) => `${e.instancePath || "/"}: ${e.message ?? "unbekannt"}`).join("\n ");
480
- super(`Plan-Catalog-Validierung fehlgeschlagen f\xFCr ${source}:
504
+ const messages = errors.map((e) => `${e.instancePath || "/"}: ${e.message ?? "unknown"}`).join("\n ");
505
+ super(`Plan catalog validation failed for ${source}:
481
506
  ${messages}`), this.source = source, this.errors = errors;
482
507
  this.name = "PlanCatalogValidationError";
483
508
  }
@@ -494,7 +519,7 @@ __name(loadPlanCatalogFromFile, "loadPlanCatalogFromFile");
494
519
  function loadPlanCatalogFromString(yamlContent, opts) {
495
520
  const parsed = yaml.load(yamlContent);
496
521
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
497
- throw new Error(`YAML-Inhalt von ${opts.source} ist kein Objekt`);
522
+ throw new Error(`The YAML content of ${opts.source} is not an object`);
498
523
  }
499
524
  const ajv = new Ajv2020({
500
525
  strict: false,
@@ -518,19 +543,19 @@ function validateConsistency(catalog, source) {
518
543
  const planIds = /* @__PURE__ */ new Set();
519
544
  for (const plan of catalog.plans ?? []) {
520
545
  if (planIds.has(plan.id)) {
521
- errors.push(`plans[].id: Doppelte Plan-ID "${plan.id}"`);
546
+ errors.push(`plans[].id: duplicate plan id "${plan.id}"`);
522
547
  }
523
548
  planIds.add(plan.id);
524
549
  if (catalog.features) {
525
550
  for (const fk of plan.features) {
526
551
  if (!declaredFeatureKeys.has(fk)) {
527
- errors.push(`plans[id=${plan.id}].features: Unbekannter featureKey "${fk}" \u2014 nicht in catalog.features deklariert`);
552
+ errors.push(`plans[id=${plan.id}].features: Unknown featureKey "${fk}" \u2014 not declared in catalog.features`);
528
553
  }
529
554
  }
530
555
  }
531
556
  }
532
557
  if (errors.length > 0) {
533
- const err = new Error(`Plan-Catalog-Konsistenz-Fehler in ${source}:
558
+ const err = new Error(`Plan catalog consistency error in ${source}:
534
559
  ${errors.join("\n ")}`);
535
560
  err.name = "PlanCatalogConsistencyError";
536
561
  throw err;
@@ -613,7 +638,7 @@ var PlanCatalogImporterService = class {
613
638
  if (planResult.created) report.plansCreated++;
614
639
  else report.plansSkipped++;
615
640
  if (plan.monthlyNet === null || plan.monthlyNet === void 0) {
616
- report.warnings.push(`Plan '${plan.id}' hat keinen monthlyNet \u2014 \xFCbersprungen (auf-Anfrage-Plans brauchen manuelle PlanVersion).`);
641
+ report.warnings.push(`Plan '${plan.id}' has no monthlyNet \u2014 skipped (on-request plans need a manual plan version).`);
617
642
  continue;
618
643
  }
619
644
  const versionResult = await this.sink.upsertPlanVersion({
@@ -1034,6 +1059,7 @@ PublicCatalogModule = _ts_decorate8([
1034
1059
 
1035
1060
  // src/billing/composed-tenant-auth.guard.ts
1036
1061
  import { ForbiddenException as ForbiddenException2, Inject as Inject6, Injectable as Injectable4, Optional as Optional4 } from "@nestjs/common";
1062
+ import { AUTH_ERROR_CODES as AUTH_ERROR_CODES2 } from "@saasicat/types";
1037
1063
  function _ts_decorate9(decorators, target, key, desc) {
1038
1064
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1039
1065
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1061,7 +1087,10 @@ var ComposedTenantAuthGuard = class {
1061
1087
  }
1062
1088
  async canActivate(context) {
1063
1089
  if (!this.guards || this.guards.length === 0) {
1064
- throw new ForbiddenException2("TenantBillingModule.forRoot.authGuards ist nicht konfiguriert.");
1090
+ throw new ForbiddenException2({
1091
+ code: AUTH_ERROR_CODES2.AUTH_GUARDS_NOT_CONFIGURED,
1092
+ message: "TenantBillingModule.forRoot.authGuards is not configured."
1093
+ });
1065
1094
  }
1066
1095
  for (const guard of this.guards) {
1067
1096
  const result = await Promise.resolve(guard.canActivate(context));
@@ -1082,6 +1111,7 @@ ComposedTenantAuthGuard = _ts_decorate9([
1082
1111
 
1083
1112
  // src/billing/tenant-admin.guard.ts
1084
1113
  import { ForbiddenException as ForbiddenException3, Injectable as Injectable5 } from "@nestjs/common";
1114
+ import { AUTH_ERROR_CODES as AUTH_ERROR_CODES3 } from "@saasicat/types";
1085
1115
  function _ts_decorate10(decorators, target, key, desc) {
1086
1116
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1087
1117
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1100,11 +1130,17 @@ var TenantAdminGuard = class {
1100
1130
  canActivate(context) {
1101
1131
  const request = context.switchToHttp().getRequest();
1102
1132
  if (!request.user) {
1103
- throw new ForbiddenException3("Nicht authentifiziert");
1133
+ throw new ForbiddenException3({
1134
+ code: AUTH_ERROR_CODES3.NOT_AUTHENTICATED,
1135
+ message: "Not authenticated"
1136
+ });
1104
1137
  }
1105
1138
  const role = request.user.platformRole ?? request.user.role;
1106
1139
  if (!role || !ADMIN_ROLES.has(role)) {
1107
- throw new ForbiddenException3("Diese Aktion erfordert die TENANT_ADMIN-Rolle.");
1140
+ throw new ForbiddenException3({
1141
+ code: AUTH_ERROR_CODES3.TENANT_ADMIN_REQUIRED,
1142
+ message: "This action requires the TENANT_ADMIN role."
1143
+ });
1108
1144
  }
1109
1145
  return true;
1110
1146
  }
@@ -1142,6 +1178,7 @@ __name(round2, "round2");
1142
1178
 
1143
1179
  // src/billing/plan-change-preview.service.ts
1144
1180
  import { Inject as Inject7, Injectable as Injectable6, NotFoundException, Optional as Optional5 } from "@nestjs/common";
1181
+ import { BILLING_ERROR_CODES as BILLING_ERROR_CODES2 } from "@saasicat/types";
1145
1182
  function _ts_decorate11(decorators, target, key, desc) {
1146
1183
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1147
1184
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1180,11 +1217,24 @@ var PlanChangePreviewService = class {
1180
1217
  async preview(tenantId, targetPlan, targetCycle, now = /* @__PURE__ */ new Date()) {
1181
1218
  const sub = await this.subscriptions.findForTenant(tenantId);
1182
1219
  if (!sub) {
1183
- throw new NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
1220
+ throw new NotFoundException({
1221
+ code: BILLING_ERROR_CODES2.SUBSCRIPTION_NOT_FOUND,
1222
+ message: `No subscription for tenant ${tenantId}`,
1223
+ params: {
1224
+ tenantId
1225
+ }
1226
+ });
1184
1227
  }
1185
1228
  const targetPlanDef = findPlan(this.catalog, targetPlan);
1186
1229
  if (!targetPlanDef) {
1187
- throw new NotFoundException(`Plan "${targetPlan}" nicht im Catalog (${this.catalog.projectKey})`);
1230
+ throw new NotFoundException({
1231
+ code: BILLING_ERROR_CODES2.PLAN_NOT_IN_CATALOG,
1232
+ message: `Plan "${targetPlan}" is not in the catalog (${this.catalog.projectKey})`,
1233
+ params: {
1234
+ planKey: targetPlan,
1235
+ projectKey: this.catalog.projectKey
1236
+ }
1237
+ });
1188
1238
  }
1189
1239
  const ctx = {
1190
1240
  currentPeriodStart: sub.currentPeriodStart,
@@ -1249,13 +1299,13 @@ var PlanChangePreviewService = class {
1249
1299
  if (blockedTargets.includes(targetPlan)) {
1250
1300
  blockers.push({
1251
1301
  code: `${targetPlan}_NOT_SELF_SERVICE`,
1252
- message: `${targetSnap.name} wird nur per Sondervertrag aktiviert. Bitte den Vertragsbetreuer kontaktieren.`
1302
+ message: `${targetSnap.name} is only activated via a special contract. Please contact the contract manager.`
1253
1303
  });
1254
1304
  }
1255
1305
  if (blockedSources.includes(sub.plan)) {
1256
1306
  blockers.push({
1257
1307
  code: `${sub.plan}_LOCKED`,
1258
- message: `Aktiver ${currentSnap.name}-Sondervertrag \u2014 bitte den Vertragsbetreuer f\xFCr Plan-Wechsel kontaktieren.`
1308
+ message: `Active ${currentSnap.name} special contract \u2014 please contact the contract manager to change plans.`
1259
1309
  });
1260
1310
  }
1261
1311
  for (const [key, row] of Object.entries(limitsCheck)) {
@@ -1263,19 +1313,19 @@ var PlanChangePreviewService = class {
1263
1313
  const usedDisplay = isFloatQuota(key) ? row.used.toFixed(1) : row.used.toString();
1264
1314
  blockers.push({
1265
1315
  code: `${key.toUpperCase()}_OVER_TARGET`,
1266
- message: `Aktueller Verbrauch ${usedDisplay} \xFCberschreitet das Ziel-Limit ${row.targetMax} (${key}) im ${targetSnap.name}-Paket. Bitte Verbrauch reduzieren.`
1316
+ message: `Current usage ${usedDisplay} exceeds the target limit ${row.targetMax} (${key}) in the ${targetSnap.name} plan. Please reduce usage first.`
1267
1317
  });
1268
1318
  }
1269
1319
  if (featuresLost.length > 0) {
1270
1320
  warnings.push({
1271
1321
  code: "FEATURES_LOST",
1272
- 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.`
1322
+ 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.`
1273
1323
  });
1274
1324
  }
1275
1325
  if (changeType === "NOOP") {
1276
1326
  warnings.push({
1277
1327
  code: "NO_CHANGE",
1278
- message: "Ziel-Paket und Abrechnungszyklus stimmen mit dem aktuellen Stand \xFCberein."
1328
+ message: "Target plan and billing cycle already match the current state."
1279
1329
  });
1280
1330
  }
1281
1331
  const projectedTrialEndsAt = this.trialProjection && sub.status === "TRIAL" ? await this.trialProjection.projectTrialEndsAt({
@@ -1433,13 +1483,13 @@ var PendingPlanMaterializationService = class _PendingPlanMaterializationService
1433
1483
  this.entitlements.invalidateTenant(change.tenantId);
1434
1484
  applied += 1;
1435
1485
  } catch (err) {
1436
- this.logger.error(`Pending-Plan-Materialisierung fehlgeschlagen (tenant ${change.tenantId}): ${String(err)}`);
1486
+ this.logger.error(`Pending plan materialisation failed (tenant ${change.tenantId}): ${String(err)}`);
1437
1487
  continue;
1438
1488
  }
1439
1489
  await this.tryFreeze(change.tenantId, change.pendingPlan, cycle, now);
1440
1490
  }
1441
1491
  if (applied > 0) {
1442
- this.logger.log(`Pending-Plan-Materialisierung: ${applied} geplante(r) Plan-Wechsel angewandt.`);
1492
+ this.logger.log(`Pending plan materialisation: applied ${applied} scheduled plan change(s).`);
1443
1493
  }
1444
1494
  return {
1445
1495
  applied
@@ -1450,7 +1500,7 @@ var PendingPlanMaterializationService = class _PendingPlanMaterializationService
1450
1500
  try {
1451
1501
  await this.contractFreeze.freezeOnPlanChange(tenantId, plan, cycle, now);
1452
1502
  } catch (err) {
1453
- this.logger.error(`Contract-Freeze nach Materialisierung fehlgeschlagen (tenant ${tenantId}): ${String(err)}`);
1503
+ this.logger.error(`Contract freeze after materialisation failed (tenant ${tenantId}): ${String(err)}`);
1454
1504
  }
1455
1505
  }
1456
1506
  };
@@ -1628,14 +1678,14 @@ var PreviewPlanChangeDto = class {
1628
1678
  _ts_decorate14([
1629
1679
  IsString2(),
1630
1680
  Matches(PLAN_OR_CYCLE_PATTERN, {
1631
- message: "plan muss SCREAMING_SNAKE_CASE sein"
1681
+ message: "plan must be SCREAMING_SNAKE_CASE"
1632
1682
  }),
1633
1683
  _ts_metadata10("design:type", String)
1634
1684
  ], PreviewPlanChangeDto.prototype, "plan", void 0);
1635
1685
  _ts_decorate14([
1636
1686
  IsString2(),
1637
1687
  Matches(PLAN_OR_CYCLE_PATTERN, {
1638
- message: "billingCycle muss SCREAMING_SNAKE_CASE sein (z. B. MONTHLY, YEARLY)"
1688
+ message: "billingCycle must be SCREAMING_SNAKE_CASE (e.g. MONTHLY, YEARLY)"
1639
1689
  }),
1640
1690
  _ts_metadata10("design:type", String)
1641
1691
  ], PreviewPlanChangeDto.prototype, "billingCycle", void 0);
@@ -1699,14 +1749,14 @@ var CompleteOnboardingSubscriptionDto = class {
1699
1749
  _ts_decorate15([
1700
1750
  IsString3(),
1701
1751
  Matches2(PLAN_OR_CYCLE_PATTERN2, {
1702
- message: "plan muss SCREAMING_SNAKE_CASE sein"
1752
+ message: "plan must be SCREAMING_SNAKE_CASE"
1703
1753
  }),
1704
1754
  _ts_metadata11("design:type", String)
1705
1755
  ], CompleteOnboardingSubscriptionDto.prototype, "plan", void 0);
1706
1756
  _ts_decorate15([
1707
1757
  IsString3(),
1708
1758
  Matches2(PLAN_OR_CYCLE_PATTERN2, {
1709
- message: "billingCycle muss SCREAMING_SNAKE_CASE sein (z. B. MONTHLY, YEARLY)"
1759
+ message: "billingCycle must be SCREAMING_SNAKE_CASE (e.g. MONTHLY, YEARLY)"
1710
1760
  }),
1711
1761
  _ts_metadata11("design:type", String)
1712
1762
  ], CompleteOnboardingSubscriptionDto.prototype, "billingCycle", void 0);
@@ -1714,7 +1764,7 @@ _ts_decorate15([
1714
1764
  IsOptional3(),
1715
1765
  IsString3(),
1716
1766
  Matches2(PROMO_CODE_PATTERN, {
1717
- message: 'promoCode muss aus A\u2013Z, 0\u20139, "-", "_" bestehen (4\u201332 Zeichen)'
1767
+ message: 'promoCode must consist of A\u2013Z, 0\u20139, "-", "_" (4\u201332 characters)'
1718
1768
  }),
1719
1769
  _ts_metadata11("design:type", String)
1720
1770
  ], CompleteOnboardingSubscriptionDto.prototype, "promoCode", void 0);
@@ -1730,9 +1780,11 @@ _ts_decorate15([
1730
1780
 
1731
1781
  // src/billing/tenant-billing.controller.ts
1732
1782
  import { BadRequestException, Body as Body2, Controller as Controller3, ForbiddenException as ForbiddenException4, Get as Get2, HttpCode, HttpStatus as HttpStatus2, Inject as Inject11, Logger as Logger4, NotFoundException as NotFoundException3, Optional as Optional8, Post as Post2, Req, UseGuards as UseGuards2 } from "@nestjs/common";
1783
+ import { AUTH_ERROR_CODES as AUTH_ERROR_CODES4, BILLING_ERROR_CODES as BILLING_ERROR_CODES4 } from "@saasicat/types";
1733
1784
 
1734
1785
  // src/billing/subscription-bundles.service.ts
1735
1786
  import { Inject as Inject10, Injectable as Injectable9, NotFoundException as NotFoundException2, Optional as Optional7, UnprocessableEntityException } from "@nestjs/common";
1787
+ import { BILLING_ERROR_CODES as BILLING_ERROR_CODES3, CATALOG_ERROR_CODES } from "@saasicat/types";
1736
1788
  function _ts_decorate16(decorators, target, key, desc) {
1737
1789
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1738
1790
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1780,38 +1832,61 @@ var SubscriptionBundlesService = class {
1780
1832
  async addBundleToSubscription(input) {
1781
1833
  const bundleVersion = await this.bundles.findVersionById(input.bundleVersionId);
1782
1834
  if (!bundleVersion) {
1783
- throw new NotFoundException2(`BundleVersion '${input.bundleVersionId}' nicht gefunden`);
1835
+ throw new NotFoundException2({
1836
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
1837
+ message: `BundleVersion '${input.bundleVersionId}' not found`,
1838
+ params: {
1839
+ bundleVersionId: input.bundleVersionId
1840
+ }
1841
+ });
1784
1842
  }
1785
1843
  if (bundleVersion.publishedAt === null) {
1786
1844
  throw new UnprocessableEntityException({
1787
- code: "BUNDLE_VERSION_NOT_PUBLISHED",
1788
- message: `BundleVersion '${input.bundleVersionId}' ist nicht published und kann nicht gebucht werden.`
1845
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_PUBLISHED,
1846
+ message: `BundleVersion '${input.bundleVersionId}' is not published and cannot be booked.`,
1847
+ params: {
1848
+ bundleVersionId: input.bundleVersionId
1849
+ }
1789
1850
  });
1790
1851
  }
1791
1852
  if (bundleVersion.supersededAt !== null) {
1792
1853
  throw new UnprocessableEntityException({
1793
- code: "BUNDLE_VERSION_SUPERSEDED",
1794
- message: `BundleVersion '${input.bundleVersionId}' wurde durch eine Nachfolge-Version abgel\xF6st.`
1854
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_SUPERSEDED,
1855
+ message: `BundleVersion '${input.bundleVersionId}' has been superseded by a newer version.`,
1856
+ params: {
1857
+ bundleVersionId: input.bundleVersionId
1858
+ }
1795
1859
  });
1796
1860
  }
1797
1861
  if (this.blockedBundles?.bundleKeys?.includes(bundleVersion.bundleKey)) {
1798
1862
  throw new UnprocessableEntityException({
1799
- code: "BUNDLE_NOT_SELF_SERVICE",
1800
- message: `Bundle '${bundleVersion.bundleKey}' wird nur per Sondervertrag aktiviert. Bitte den Vertragsbetreuer kontaktieren.`
1863
+ code: BILLING_ERROR_CODES3.BUNDLE_NOT_SELF_SERVICE,
1864
+ message: `Bundle '${bundleVersion.bundleKey}' is only activated via a special contract. Please contact the contract manager.`,
1865
+ params: {
1866
+ bundleKey: bundleVersion.bundleKey
1867
+ }
1801
1868
  });
1802
1869
  }
1803
1870
  const planIds = bundleVersion.compatibility?.planIds ?? [];
1804
1871
  if (planIds.length > 0 && !planIds.includes(input.currentPlanKey)) {
1805
1872
  throw new UnprocessableEntityException({
1806
- code: "BUNDLE_INCOMPATIBLE_WITH_PLAN",
1807
- message: `BundleVersion '${input.bundleVersionId}' ist nicht mit Plan '${input.currentPlanKey}' kompatibel. Erlaubt: [${planIds.join(", ")}].`
1873
+ code: BILLING_ERROR_CODES3.BUNDLE_INCOMPATIBLE_WITH_PLAN,
1874
+ message: `BundleVersion '${input.bundleVersionId}' is not compatible with plan '${input.currentPlanKey}'. Allowed: [${planIds.join(", ")}].`,
1875
+ params: {
1876
+ bundleVersionId: input.bundleVersionId,
1877
+ planKey: input.currentPlanKey,
1878
+ allowedPlanKeys: planIds
1879
+ }
1808
1880
  });
1809
1881
  }
1810
1882
  const active = await this.repo.listActiveBySubscription(input.subscriptionId);
1811
1883
  if (active.some((b) => b.bundleVersionId === input.bundleVersionId)) {
1812
1884
  throw new UnprocessableEntityException({
1813
- code: "BUNDLE_ALREADY_SUBSCRIBED",
1814
- message: `Subscription '${input.subscriptionId}' hat dieses Bundle bereits aktiv gebucht.`
1885
+ code: BILLING_ERROR_CODES3.BUNDLE_ALREADY_SUBSCRIBED,
1886
+ message: `Subscription '${input.subscriptionId}' has already actively booked this bundle.`,
1887
+ params: {
1888
+ subscriptionId: input.subscriptionId
1889
+ }
1815
1890
  });
1816
1891
  }
1817
1892
  const startedAt = input.startedAt ?? /* @__PURE__ */ new Date();
@@ -1827,12 +1902,21 @@ var SubscriptionBundlesService = class {
1827
1902
  async cancelBundleFromSubscription(input) {
1828
1903
  const existing = await this.repo.findById(input.subscriptionBundleId);
1829
1904
  if (!existing) {
1830
- throw new NotFoundException2(`SubscriptionBundle '${input.subscriptionBundleId}' nicht gefunden`);
1905
+ throw new NotFoundException2({
1906
+ code: BILLING_ERROR_CODES3.SUBSCRIPTION_BUNDLE_NOT_FOUND,
1907
+ message: `SubscriptionBundle '${input.subscriptionBundleId}' not found`,
1908
+ params: {
1909
+ subscriptionBundleId: input.subscriptionBundleId
1910
+ }
1911
+ });
1831
1912
  }
1832
1913
  if (existing.canceledAt !== null) {
1833
1914
  throw new UnprocessableEntityException({
1834
- code: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELED",
1835
- message: `SubscriptionBundle '${input.subscriptionBundleId}' ist bereits gek\xFCndigt.`
1915
+ code: BILLING_ERROR_CODES3.SUBSCRIPTION_BUNDLE_ALREADY_CANCELED,
1916
+ message: `SubscriptionBundle '${input.subscriptionBundleId}' is already cancelled.`,
1917
+ params: {
1918
+ subscriptionBundleId: input.subscriptionBundleId
1919
+ }
1836
1920
  });
1837
1921
  }
1838
1922
  const canceledAt = input.canceledAt ?? /* @__PURE__ */ new Date();
@@ -1853,18 +1937,27 @@ var SubscriptionBundlesService = class {
1853
1937
  async reactivateBundle(subscriptionBundleId) {
1854
1938
  const existing = await this.repo.findById(subscriptionBundleId);
1855
1939
  if (!existing) {
1856
- throw new NotFoundException2(`SubscriptionBundle '${subscriptionBundleId}' nicht gefunden`);
1940
+ throw new NotFoundException2({
1941
+ code: BILLING_ERROR_CODES3.SUBSCRIPTION_BUNDLE_NOT_FOUND,
1942
+ message: `SubscriptionBundle '${subscriptionBundleId}' not found`,
1943
+ params: {
1944
+ subscriptionBundleId
1945
+ }
1946
+ });
1857
1947
  }
1858
1948
  if (existing.canceledAt === null) {
1859
1949
  throw new UnprocessableEntityException({
1860
- code: "SUBSCRIPTION_BUNDLE_NOT_CANCELED",
1861
- message: `SubscriptionBundle '${subscriptionBundleId}' ist nicht gek\xFCndigt.`
1950
+ code: BILLING_ERROR_CODES3.SUBSCRIPTION_BUNDLE_NOT_CANCELED,
1951
+ message: `SubscriptionBundle '${subscriptionBundleId}' is not cancelled.`,
1952
+ params: {
1953
+ subscriptionBundleId
1954
+ }
1862
1955
  });
1863
1956
  }
1864
1957
  if (existing.canceledEffectiveAt !== null && existing.canceledEffectiveAt <= /* @__PURE__ */ new Date()) {
1865
1958
  throw new UnprocessableEntityException({
1866
- code: "SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE",
1867
- message: "K\xFCndigung bereits wirksam \u2014 bitte das Bundle neu buchen."
1959
+ code: BILLING_ERROR_CODES3.SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE,
1960
+ message: "Cancellation already in effect \u2014 book the bundle again."
1868
1961
  });
1869
1962
  }
1870
1963
  return this.repo.reactivate(subscriptionBundleId);
@@ -1970,7 +2063,13 @@ var TenantBillingController = class _TenantBillingController {
1970
2063
  this.usageSnapshot.snapshot(tenantId)
1971
2064
  ]);
1972
2065
  if (!sub) {
1973
- throw new NotFoundException3(`Keine Subscription f\xFCr Tenant ${tenantId}`);
2066
+ throw new NotFoundException3({
2067
+ code: BILLING_ERROR_CODES4.SUBSCRIPTION_NOT_FOUND,
2068
+ message: `No subscription for tenant ${tenantId}`,
2069
+ params: {
2070
+ tenantId
2071
+ }
2072
+ });
1974
2073
  }
1975
2074
  const usage = {};
1976
2075
  for (const key of /* @__PURE__ */ new Set([
@@ -2019,14 +2118,29 @@ var TenantBillingController = class _TenantBillingController {
2019
2118
  const userId = this.requireUserId(req);
2020
2119
  const blockedTargets = this.blockedPlans?.asTarget ?? [];
2021
2120
  if (blockedTargets.includes(dto.plan)) {
2022
- throw new ForbiddenException4(`${dto.plan} wird nicht per Self-Service aktiviert.`);
2121
+ throw new ForbiddenException4({
2122
+ code: BILLING_ERROR_CODES4.PLAN_NOT_SELF_SERVICE,
2123
+ message: `${dto.plan} is not activated via self-service.`,
2124
+ params: {
2125
+ planKey: dto.plan
2126
+ }
2127
+ });
2023
2128
  }
2024
2129
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
2025
- if (!sub) throw new NotFoundException3(`Keine Subscription f\xFCr Tenant ${tenantId}`);
2130
+ if (!sub) {
2131
+ throw new NotFoundException3({
2132
+ code: BILLING_ERROR_CODES4.SUBSCRIPTION_NOT_FOUND,
2133
+ message: `No subscription for tenant ${tenantId}`,
2134
+ params: {
2135
+ tenantId
2136
+ }
2137
+ });
2138
+ }
2026
2139
  const blockers = await this.planPreview.assertChangeAllowed(tenantId, dto.plan, dto.billingCycle);
2027
2140
  if (blockers.length > 0) {
2028
2141
  throw new BadRequestException({
2029
- message: "Plan-Wechsel blockiert.",
2142
+ code: BILLING_ERROR_CODES4.PLAN_CHANGE_BLOCKED,
2143
+ message: "Plan change is blocked.",
2030
2144
  blockers
2031
2145
  });
2032
2146
  }
@@ -2096,14 +2210,29 @@ var TenantBillingController = class _TenantBillingController {
2096
2210
  const warnings = [];
2097
2211
  const blockedTargets = this.blockedPlans?.asTarget ?? [];
2098
2212
  if (blockedTargets.includes(dto.plan)) {
2099
- throw new ForbiddenException4(`${dto.plan} wird nicht per Self-Service aktiviert.`);
2213
+ throw new ForbiddenException4({
2214
+ code: BILLING_ERROR_CODES4.PLAN_NOT_SELF_SERVICE,
2215
+ message: `${dto.plan} is not activated via self-service.`,
2216
+ params: {
2217
+ planKey: dto.plan
2218
+ }
2219
+ });
2100
2220
  }
2101
2221
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
2102
- if (!sub) throw new NotFoundException3(`Keine Subscription f\xFCr Tenant ${tenantId}`);
2222
+ if (!sub) {
2223
+ throw new NotFoundException3({
2224
+ code: BILLING_ERROR_CODES4.SUBSCRIPTION_NOT_FOUND,
2225
+ message: `No subscription for tenant ${tenantId}`,
2226
+ params: {
2227
+ tenantId
2228
+ }
2229
+ });
2230
+ }
2103
2231
  const blockers = await this.planPreview.assertChangeAllowed(tenantId, dto.plan, dto.billingCycle);
2104
2232
  if (blockers.length > 0) {
2105
2233
  throw new BadRequestException({
2106
- message: "Plan-Wechsel im Onboarding blockiert.",
2234
+ code: BILLING_ERROR_CODES4.PLAN_CHANGE_BLOCKED,
2235
+ message: "Plan change during onboarding is blocked.",
2107
2236
  blockers
2108
2237
  });
2109
2238
  }
@@ -2137,8 +2266,13 @@ var TenantBillingController = class _TenantBillingController {
2137
2266
  warnings.push(...this.collectPromoSkipReasons(dto.promoCode, sub));
2138
2267
  }
2139
2268
  } catch (err) {
2269
+ const reason = err instanceof Error ? err.message : null;
2140
2270
  throw new BadRequestException({
2141
- message: err instanceof Error ? `Onboarding-Anlage fehlgeschlagen: ${err.message}` : "Onboarding-Anlage fehlgeschlagen"
2271
+ code: BILLING_ERROR_CODES4.ONBOARDING_CREATE_FAILED,
2272
+ message: reason ? `Onboarding creation failed: ${reason}` : "Onboarding creation failed",
2273
+ params: {
2274
+ reason
2275
+ }
2142
2276
  });
2143
2277
  }
2144
2278
  } else {
@@ -2151,9 +2285,9 @@ var TenantBillingController = class _TenantBillingController {
2151
2285
  });
2152
2286
  if (dto.promoCode) {
2153
2287
  if (!this.promoCodes) {
2154
- warnings.push("Promo-Code wurde gesendet, aber PromoCodesModule ist nicht geladen \u2014 der Code wurde NICHT eingel\xF6st.");
2288
+ warnings.push("A promo code was sent, but PromoCodesModule is not loaded \u2014 the code was NOT redeemed.");
2155
2289
  } else if (!sub.id) {
2156
- warnings.push("Promo-Code wurde gesendet, aber der Adapter liefert keine SubscriptionUsageRecord.id \u2014 der Code wurde NICHT eingel\xF6st.");
2290
+ warnings.push("A promo code was sent, but the adapter returns no SubscriptionUsageRecord.id \u2014 the code was NOT redeemed.");
2157
2291
  } else {
2158
2292
  try {
2159
2293
  const redemption = await this.promoCodes.redeem({
@@ -2164,8 +2298,8 @@ var TenantBillingController = class _TenantBillingController {
2164
2298
  });
2165
2299
  promoRedemption = this.toResponseRedemption(redemption, dto.promoCode);
2166
2300
  } catch (err) {
2167
- const message = err instanceof Error ? err.message : "Unbekannter Fehler";
2168
- warnings.push(`Promo-Code konnte nicht eingel\xF6st werden: ${message}`);
2301
+ const message = err instanceof Error ? err.message : "Unknown error";
2302
+ warnings.push(`The promo code could not be redeemed: ${message}`);
2169
2303
  }
2170
2304
  }
2171
2305
  }
@@ -2191,11 +2325,11 @@ var TenantBillingController = class _TenantBillingController {
2191
2325
  });
2192
2326
  bundlesAdded += 1;
2193
2327
  } catch (err) {
2194
- warnings.push(`Bundle '${bundleVersionId}' konnte nicht gebucht werden: ` + (err instanceof Error ? err.message : String(err)));
2328
+ warnings.push(`Bundle '${bundleVersionId}' could not be booked: ` + (err instanceof Error ? err.message : String(err)));
2195
2329
  }
2196
2330
  }
2197
2331
  } else if (bundleVersionIds.length > 0 && !this.subscriptionBundles) {
2198
- warnings.push("Bundle-Buchungen im Onboarding angefordert, aber SubscriptionBundleModule ist nicht im Konsumenten registriert. Bundles wurden nicht angelegt.");
2332
+ warnings.push("Bundle bookings were requested during onboarding, but SubscriptionBundleModule is not registered in the consumer. No bundles were created.");
2199
2333
  }
2200
2334
  return {
2201
2335
  plan: planResult.plan,
@@ -2215,9 +2349,20 @@ var TenantBillingController = class _TenantBillingController {
2215
2349
  const tenantId = this.requireTenantId(req);
2216
2350
  const userId = this.requireUserId(req);
2217
2351
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
2218
- if (!sub) throw new NotFoundException3(`Keine Subscription f\xFCr Tenant ${tenantId}`);
2352
+ if (!sub) {
2353
+ throw new NotFoundException3({
2354
+ code: BILLING_ERROR_CODES4.SUBSCRIPTION_NOT_FOUND,
2355
+ message: `No subscription for tenant ${tenantId}`,
2356
+ params: {
2357
+ tenantId
2358
+ }
2359
+ });
2360
+ }
2219
2361
  if (!sub.pendingPlanVersion) {
2220
- throw new BadRequestException("Es liegt aktuell keine Pending-Plan-Version zur Best\xE4tigung vor.");
2362
+ throw new BadRequestException({
2363
+ code: BILLING_ERROR_CODES4.NO_PENDING_PLAN_VERSION,
2364
+ message: "There is no pending plan version awaiting confirmation."
2365
+ });
2221
2366
  }
2222
2367
  const result = await this.subscriptionWrite.acceptPendingPlanVersion(tenantId, userId, /* @__PURE__ */ new Date());
2223
2368
  if (!result.alreadyAccepted) {
@@ -2262,7 +2407,10 @@ var TenantBillingController = class _TenantBillingController {
2262
2407
  const resolver = this.tenantIdResolver ?? ((r) => r.user?.tenantId ?? null);
2263
2408
  const tenantId = resolver(req);
2264
2409
  if (!tenantId) {
2265
- throw new NotFoundException3("Tenant-ID nicht im Request gefunden");
2410
+ throw new NotFoundException3({
2411
+ code: AUTH_ERROR_CODES4.TENANT_CONTEXT_MISSING,
2412
+ message: "No tenant ID found on the request"
2413
+ });
2266
2414
  }
2267
2415
  return tenantId;
2268
2416
  }
@@ -2270,7 +2418,10 @@ var TenantBillingController = class _TenantBillingController {
2270
2418
  const resolver = this.userIdResolver ?? ((r) => r.user?.sub ?? r.user?.id ?? null);
2271
2419
  const userId = resolver(req);
2272
2420
  if (!userId) {
2273
- throw new NotFoundException3("User-ID nicht im Request gefunden");
2421
+ throw new NotFoundException3({
2422
+ code: AUTH_ERROR_CODES4.TENANT_CONTEXT_MISSING,
2423
+ message: "No user ID found on the request"
2424
+ });
2274
2425
  }
2275
2426
  return userId;
2276
2427
  }
@@ -2310,10 +2461,10 @@ var TenantBillingController = class _TenantBillingController {
2310
2461
  collectPromoSkipReasons(promoCode, sub) {
2311
2462
  const reasons = [];
2312
2463
  if (!this.promoCodes) {
2313
- reasons.push(`Promo-Code "${promoCode}" wurde gesendet, aber PromoCodesModule ist nicht geladen \u2014 der Code wurde NICHT eingel\xF6st.`);
2464
+ reasons.push(`Promo-Code "${promoCode}" was sent, but PromoCodesModule is not loaded \u2014 the code was NOT redeemed.`);
2314
2465
  }
2315
2466
  if (!sub.id) {
2316
- reasons.push(`Promo-Code "${promoCode}" wurde gesendet, aber der Adapter liefert keine SubscriptionUsageRecord.id \u2014 der Code wurde NICHT eingel\xF6st.`);
2467
+ reasons.push(`Promo-Code "${promoCode}" was sent, but the adapter returns no SubscriptionUsageRecord.id \u2014 the code was NOT redeemed.`);
2317
2468
  }
2318
2469
  return reasons;
2319
2470
  }
@@ -2328,7 +2479,7 @@ var TenantBillingController = class _TenantBillingController {
2328
2479
  try {
2329
2480
  await this.contractFreeze.freezeOnPlanChange(tenantId, plan, cycle, /* @__PURE__ */ new Date());
2330
2481
  } catch (err) {
2331
- this.logger.error(`Contract-Freeze nach Plan-Wechsel fehlgeschlagen (tenant ${tenantId}): ${String(err)}`);
2482
+ this.logger.error(`Contract freeze after plan change failed (tenant ${tenantId}): ${String(err)}`);
2332
2483
  }
2333
2484
  }
2334
2485
  /**
@@ -2639,7 +2790,7 @@ __name(normalizeQuotas, "normalizeQuotas");
2639
2790
 
2640
2791
  // src/billing/subscription-bundle-preview.service.ts
2641
2792
  import { Inject as Inject12, Injectable as Injectable11, NotFoundException as NotFoundException4, Optional as Optional9 } from "@nestjs/common";
2642
- import { buildFeatureRequiresIndex as buildFeatureRequiresIndex3, collectUnsatisfiedRequires as collectUnsatisfiedRequires3 } from "@saasicat/types";
2793
+ import { BILLING_ERROR_CODES as BILLING_ERROR_CODES5, CATALOG_ERROR_CODES as CATALOG_ERROR_CODES2, buildFeatureRequiresIndex as buildFeatureRequiresIndex3, collectUnsatisfiedRequires as collectUnsatisfiedRequires3 } from "@saasicat/types";
2643
2794
  function _ts_decorate20(decorators, target, key, desc) {
2644
2795
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2645
2796
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2678,7 +2829,13 @@ var SubscriptionBundlePreviewService = class {
2678
2829
  async previewAdd(ctx, input, now = /* @__PURE__ */ new Date()) {
2679
2830
  const bundleVersion = await this.bundles.findVersionById(input.bundleVersionId);
2680
2831
  if (!bundleVersion) {
2681
- throw new NotFoundException4(`BundleVersion '${input.bundleVersionId}' nicht gefunden`);
2832
+ throw new NotFoundException4({
2833
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_NOT_FOUND,
2834
+ message: `BundleVersion '${input.bundleVersionId}' not found`,
2835
+ params: {
2836
+ bundleVersionId: input.bundleVersionId
2837
+ }
2838
+ });
2682
2839
  }
2683
2840
  const blockers = [];
2684
2841
  const warnings = [];
@@ -2687,7 +2844,7 @@ var SubscriptionBundlePreviewService = class {
2687
2844
  if (activeBundleVersions.some((bv) => bv.id === bundleVersion.id)) {
2688
2845
  blockers.push({
2689
2846
  code: "BUNDLE_ALREADY_SUBSCRIBED",
2690
- message: "Dieses Bundle ist bereits aktiv gebucht."
2847
+ message: "This bundle is already actively booked."
2691
2848
  });
2692
2849
  }
2693
2850
  const planFeatures = new Set(await this.resolvePlanFeatures(ctx.currentPlanKey, now));
@@ -2695,14 +2852,14 @@ var SubscriptionBundlePreviewService = class {
2695
2852
  if (redundantFeatures.length > 0) {
2696
2853
  warnings.push({
2697
2854
  code: "REDUNDANT_FEATURES",
2698
- 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.`
2855
+ 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.`
2699
2856
  });
2700
2857
  }
2701
2858
  const missingRequires = await this.collectMissingRequires(bundleVersion, planFeatures, activeBundleVersions);
2702
2859
  if (missingRequires.length > 0) {
2703
2860
  blockers.push({
2704
2861
  code: "BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED",
2705
- message: `Das Bundle ben\xF6tigt [${missingRequires.join(", ")}] \u2014 weder im Plan noch in den aktiven Bundles enthalten.`
2862
+ message: `The bundle requires [${missingRequires.join(", ")}] \u2014 present neither in the plan nor in the active bundles.`
2706
2863
  });
2707
2864
  }
2708
2865
  const priceNet = resolveBundlePriceNet(bundleVersion, ctx.currentPlanKey, ctx.billingCycle);
@@ -2731,18 +2888,30 @@ var SubscriptionBundlePreviewService = class {
2731
2888
  async previewCancel(ctx, input, now = /* @__PURE__ */ new Date()) {
2732
2889
  const existing = await this.subscriptionBundles.findById(input.subscriptionBundleId);
2733
2890
  if (!existing || existing.subscriptionId !== ctx.subscriptionId) {
2734
- throw new NotFoundException4(`SubscriptionBundle '${input.subscriptionBundleId}' nicht gefunden`);
2891
+ throw new NotFoundException4({
2892
+ code: BILLING_ERROR_CODES5.SUBSCRIPTION_BUNDLE_NOT_FOUND,
2893
+ message: `SubscriptionBundle '${input.subscriptionBundleId}' not found`,
2894
+ params: {
2895
+ subscriptionBundleId: input.subscriptionBundleId
2896
+ }
2897
+ });
2735
2898
  }
2736
2899
  const bundleVersion = await this.bundles.findVersionById(existing.bundleVersionId);
2737
2900
  if (!bundleVersion) {
2738
- throw new NotFoundException4(`BundleVersion '${existing.bundleVersionId}' nicht gefunden`);
2901
+ throw new NotFoundException4({
2902
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_NOT_FOUND,
2903
+ message: `BundleVersion '${existing.bundleVersionId}' not found`,
2904
+ params: {
2905
+ bundleVersionId: existing.bundleVersionId
2906
+ }
2907
+ });
2739
2908
  }
2740
2909
  const blockers = [];
2741
2910
  const warnings = [];
2742
2911
  if (existing.canceledAt !== null) {
2743
2912
  blockers.push({
2744
2913
  code: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELED",
2745
- message: "Diese Bundle-Buchung ist bereits gek\xFCndigt."
2914
+ message: "This bundle booking is already cancelled."
2746
2915
  });
2747
2916
  }
2748
2917
  const effectiveAt = resolveBundleCancelEffectiveAt({
@@ -2754,7 +2923,7 @@ var SubscriptionBundlePreviewService = class {
2754
2923
  if (existing.minimumTermEndsAt && existing.minimumTermEndsAt.getTime() > periodEnd.getTime()) {
2755
2924
  warnings.push({
2756
2925
  code: "MINIMUM_TERM_BINDS",
2757
- message: "Die Mindestlaufzeit bindet \xFCber das Periodenende hinaus \u2014 die K\xFCndigung wirkt erst zum Mindestlaufzeit-Ende."
2926
+ message: "The minimum term extends beyond the end of the period \u2014 the cancellation only takes effect when the minimum term ends."
2758
2927
  });
2759
2928
  }
2760
2929
  return {
@@ -2773,26 +2942,26 @@ var SubscriptionBundlePreviewService = class {
2773
2942
  if (bundleVersion.publishedAt === null) {
2774
2943
  blockers.push({
2775
2944
  code: "BUNDLE_VERSION_NOT_PUBLISHED",
2776
- message: "Diese BundleVersion ist nicht ver\xF6ffentlicht und nicht buchbar."
2945
+ message: "This bundle version is not published and cannot be booked."
2777
2946
  });
2778
2947
  }
2779
2948
  if (bundleVersion.supersededAt !== null) {
2780
2949
  blockers.push({
2781
2950
  code: "BUNDLE_VERSION_SUPERSEDED",
2782
- message: "Diese BundleVersion wurde durch eine Nachfolge-Version abgel\xF6st."
2951
+ message: "This bundle version has been superseded by a newer one."
2783
2952
  });
2784
2953
  }
2785
2954
  const planIds = bundleVersion.compatibility?.planIds ?? [];
2786
2955
  if (planIds.length > 0 && !planIds.includes(currentPlanKey)) {
2787
2956
  blockers.push({
2788
2957
  code: "BUNDLE_INCOMPATIBLE_WITH_PLAN",
2789
- message: `Das Bundle ist nicht mit Plan '${currentPlanKey}' kompatibel. Erlaubt: [${planIds.join(", ")}].`
2958
+ message: `The bundle is not compatible with plan '${currentPlanKey}'. Allowed: [${planIds.join(", ")}].`
2790
2959
  });
2791
2960
  }
2792
2961
  if (this.blockedBundles?.bundleKeys?.includes(bundleVersion.bundleKey)) {
2793
2962
  blockers.push({
2794
2963
  code: "BUNDLE_NOT_SELF_SERVICE",
2795
- message: `Bundle '${bundleVersion.bundleKey}' wird nur per Sondervertrag aktiviert. Bitte den Vertragsbetreuer kontaktieren.`
2964
+ message: `Bundle '${bundleVersion.bundleKey}' is only activated via a special contract. Please contact the contract manager.`
2796
2965
  });
2797
2966
  }
2798
2967
  }
@@ -2905,6 +3074,7 @@ import { Module as Module4 } from "@nestjs/common";
2905
3074
 
2906
3075
  // src/billing/tenant-subscription-bundles.controller.ts
2907
3076
  import { BadRequestException as BadRequestException2, Body as Body3, Controller as Controller4, Delete, Get as Get3, HttpCode as HttpCode2, HttpStatus as HttpStatus3, Inject as Inject13, Logger as Logger5, NotFoundException as NotFoundException5, Optional as Optional10, Param, ParseUUIDPipe, Post as Post3, Req as Req2, UseGuards as UseGuards3 } from "@nestjs/common";
3077
+ import { AUTH_ERROR_CODES as AUTH_ERROR_CODES5, BILLING_ERROR_CODES as BILLING_ERROR_CODES6 } from "@saasicat/types";
2908
3078
 
2909
3079
  // src/billing/dto/subscription-bundles.dto.ts
2910
3080
  import { IsInt, IsOptional as IsOptional4, IsUUID as IsUUID2, Matches as Matches3, Max, Min } from "class-validator";
@@ -2985,7 +3155,7 @@ var CancelSubscriptionBundleDto = class {
2985
3155
  _ts_decorate21([
2986
3156
  IsOptional4(),
2987
3157
  Matches3(ISO_DATE_PATTERN, {
2988
- message: "canceledAt muss ISO-Datum sein (YYYY-MM-DD)"
3158
+ message: "canceledAt must be an ISO date (YYYY-MM-DD)"
2989
3159
  }),
2990
3160
  _ts_metadata15("design:type", String)
2991
3161
  ], CancelSubscriptionBundleDto.prototype, "canceledAt", void 0);
@@ -3049,7 +3219,10 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
3049
3219
  const hasAdd = Boolean(dto.bundleVersionId);
3050
3220
  const hasCancel = Boolean(dto.subscriptionBundleId);
3051
3221
  if (hasAdd === hasCancel) {
3052
- throw new BadRequestException2("Genau eines von bundleVersionId (Add-Preview) oder subscriptionBundleId (Cancel-Preview) angeben.");
3222
+ throw new BadRequestException2({
3223
+ code: BILLING_ERROR_CODES6.BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS,
3224
+ message: "Exactly one of bundleVersionId (add preview) or subscriptionBundleId (cancel preview) must be given."
3225
+ });
3053
3226
  }
3054
3227
  const sub = await this.requireSubscription(this.requireTenantId(req));
3055
3228
  const ctx = {
@@ -3089,21 +3262,30 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
3089
3262
  }
3090
3263
  requireSubscriptionPk(sub) {
3091
3264
  if (!sub.id) {
3092
- throw new NotFoundException5("SubscriptionUsageRecord ohne `id` \u2014 Adapter muss die Subscription-PK durchreichen (siehe SubscriptionUsageRecord.id).");
3265
+ throw new NotFoundException5("SubscriptionUsageRecord without `id` \u2014 the adapter must return the subscription primary key (see SubscriptionUsageRecord.id).");
3093
3266
  }
3094
3267
  return sub.id;
3095
3268
  }
3096
3269
  requireTenantId(req) {
3097
3270
  const tenantId = this.tenantIdResolver(req);
3098
3271
  if (!tenantId) {
3099
- throw new NotFoundException5("Tenant-ID nicht im Request gefunden");
3272
+ throw new NotFoundException5({
3273
+ code: AUTH_ERROR_CODES5.TENANT_CONTEXT_MISSING,
3274
+ message: "No tenant ID found on the request"
3275
+ });
3100
3276
  }
3101
3277
  return tenantId;
3102
3278
  }
3103
3279
  async requireSubscription(tenantId) {
3104
3280
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
3105
3281
  if (!sub) {
3106
- throw new NotFoundException5(`Keine Subscription f\xFCr Tenant ${tenantId}`);
3282
+ throw new NotFoundException5({
3283
+ code: BILLING_ERROR_CODES6.SUBSCRIPTION_NOT_FOUND,
3284
+ message: `No subscription for tenant ${tenantId}`,
3285
+ params: {
3286
+ tenantId
3287
+ }
3288
+ });
3107
3289
  }
3108
3290
  return sub;
3109
3291
  }
@@ -3122,7 +3304,7 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
3122
3304
  try {
3123
3305
  await this.contractFreeze.freezeOnPlanChange(tenantId, sub.planVersion.planId, sub.billingCycle, /* @__PURE__ */ new Date());
3124
3306
  } catch (err) {
3125
- this.logger.error(`Contract-Freeze nach Bundle-Mutation fehlgeschlagen (tenant ${tenantId}): ${String(err)}`);
3307
+ this.logger.error(`Contract freeze after bundle mutation failed (tenant ${tenantId}): ${String(err)}`);
3126
3308
  }
3127
3309
  }
3128
3310
  };