@saasicat/nest 0.18.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/_entries.cjs +1341 -590
  2. package/dist/admin/index.js +2 -1
  3. package/dist/billing/index.d.cts +10 -6
  4. package/dist/billing/index.d.ts +10 -6
  5. package/dist/billing/index.js +7 -6
  6. package/dist/catalog/index.js +4 -4
  7. package/dist/checkout-offer/index.js +2 -2
  8. package/dist/{chunk-RZVSKKXX.js → chunk-3DRZ5GCP.js} +56 -105
  9. package/dist/{chunk-GLLCB4BP.js → chunk-4O6R6ARQ.js} +23 -13
  10. package/dist/{chunk-7RRWI5LH.js → chunk-55QGAZRQ.js} +10 -10
  11. package/dist/{chunk-Z3FWN3HJ.js → chunk-5ZXBXYXU.js} +3 -3
  12. package/dist/{chunk-V34AA7C7.js → chunk-6WL6VQ7Z.js} +1 -1
  13. package/dist/{chunk-ABB2CRAS.js → chunk-74SCJ4HF.js} +1 -1
  14. package/dist/{chunk-EXOLOJFZ.js → chunk-AGOG5B77.js} +36 -9
  15. package/dist/{chunk-32EMXPU2.js → chunk-B7NRY2LV.js} +48 -17
  16. package/dist/{chunk-MDIZUVIK.js → chunk-BSK6YBLI.js} +1 -1
  17. package/dist/{chunk-R2QOSVAA.js → chunk-DQKCK7DX.js} +1 -1
  18. package/dist/chunk-EBJJNTW6.js +22 -0
  19. package/dist/{chunk-PLJEAUFU.js → chunk-FYWLPYBK.js} +79 -39
  20. package/dist/{chunk-P3DP6GJE.js → chunk-GC5AKN44.js} +487 -160
  21. package/dist/{chunk-5KH2O4SF.js → chunk-NZLAYCEX.js} +286 -110
  22. package/dist/{chunk-E7AJ42AB.js → chunk-VQQWF7MW.js} +172 -28
  23. package/dist/{chunk-GJBXOCRZ.js → chunk-XTVAFJ3P.js} +46 -17
  24. package/dist/{chunk-BOADBIGB.js → chunk-YYUXKLXK.js} +2 -2
  25. package/dist/{chunk-YITTE646.js → chunk-ZH5PYEMI.js} +41 -23
  26. package/dist/{define-saasicat-Rc0rXrYG.d.ts → define-saasicat-B20Ms8UC.d.ts} +1 -1
  27. package/dist/{define-saasicat-CeDBX9ji.d.cts → define-saasicat-Cf9DpEsY.d.cts} +1 -1
  28. package/dist/discovery/index.d.cts +2 -2
  29. package/dist/discovery/index.d.ts +2 -2
  30. package/dist/discovery/index.js +4 -4
  31. package/dist/{enforce-quota.interceptor-CmbWX39L.d.ts → enforce-quota.interceptor-CUyfR4cQ.d.ts} +1 -1
  32. package/dist/{enforce-quota.interceptor-DMw-bPyk.d.cts → enforce-quota.interceptor-Dtg5WdFj.d.cts} +1 -1
  33. package/dist/entitlement/index.js +3 -2
  34. package/dist/index.d.cts +27 -8
  35. package/dist/index.d.ts +27 -8
  36. package/dist/index.js +21 -17
  37. package/dist/{module-DgYJur6f.d.ts → module-BE17XFLg.d.ts} +1 -1
  38. package/dist/{module-CSN8j8Tp.d.cts → module-CHMIyWlf.d.cts} +1 -1
  39. package/dist/platform/index.d.cts +8 -8
  40. package/dist/platform/index.d.ts +8 -8
  41. package/dist/platform/index.js +14 -13
  42. package/dist/promo/index.d.cts +3 -3
  43. package/dist/promo/index.d.ts +3 -3
  44. package/dist/promo/index.js +4 -3
  45. package/dist/registration/index.js +2 -1
  46. package/dist/{saas-platform.module-D3rUTiho.d.cts → saas-platform.module-BfaYfL-G.d.cts} +1 -1
  47. package/dist/{saas-platform.module-DLhoqKNA.d.ts → saas-platform.module-DChhDrR-.d.ts} +1 -1
  48. package/dist/{service-DX8KbGXl.d.cts → service-D48clObR.d.cts} +3 -3
  49. package/dist/{service-DX8KbGXl.d.ts → service-D48clObR.d.ts} +3 -3
  50. package/dist/subscription-contract/index.js +2 -2
  51. package/dist/{tenant-billing.controller-CG4CaFUP.d.cts → tenant-billing.controller-BIF-mstX.d.cts} +2 -2
  52. package/dist/{tenant-billing.controller-BwZRnJ3q.d.ts → tenant-billing.controller-DT5UrnDX.d.ts} +2 -2
  53. package/dist/testing/index.d.cts +5 -5
  54. package/dist/testing/index.d.ts +5 -5
  55. package/dist/testing/index.js +34 -33
  56. package/package.json +3 -3
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  PromoCodesService
3
- } from "./chunk-E7AJ42AB.js";
3
+ } from "./chunk-VQQWF7MW.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,16 +28,19 @@ import {
28
28
  SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN,
29
29
  isLimitExceededError,
30
30
  toEffectiveLimitsSnapshot
31
- } from "./chunk-EXOLOJFZ.js";
31
+ } from "./chunk-AGOG5B77.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";
41
+ import {
42
+ codedError
43
+ } from "./chunk-EBJJNTW6.js";
41
44
  import {
42
45
  ENTITLEMENT_SERVICE_TOKEN
43
46
  } from "./chunk-WKVBNTYC.js";
@@ -103,35 +106,35 @@ var PublishValidationError = class extends Error {
103
106
  function assertChangeNote(changeNote) {
104
107
  const trimmed = (changeNote ?? "").trim();
105
108
  if (trimmed.length === 0) {
106
- throw new PublishValidationError("CHANGE_NOTE_REQUIRED", "changeNote ist Pflicht beim Ver\xF6ffentlichen einer Version.");
109
+ throw new PublishValidationError("CHANGE_NOTE_REQUIRED", "changeNote is required when publishing a version.");
107
110
  }
108
111
  return trimmed;
109
112
  }
110
113
  __name(assertChangeNote, "assertChangeNote");
111
114
  function assertDraftPublishable(draft, draftId) {
112
115
  if (!draft) {
113
- throw new PublishValidationError("NOT_FOUND", `PlanVersion ${draftId} nicht gefunden`);
116
+ throw new PublishValidationError("NOT_FOUND", `PlanVersion ${draftId} not found`);
114
117
  }
115
118
  if (draft.publishedAt !== null) {
116
- throw new PublishValidationError("ALREADY_PUBLISHED", `PlanVersion ${draftId} ist bereits ver\xF6ffentlicht.`);
119
+ throw new PublishValidationError("ALREADY_PUBLISHED", `PlanVersion ${draftId} has already been published.`);
117
120
  }
118
121
  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.`);
122
+ 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
123
  }
121
124
  }
122
125
  __name(assertDraftPublishable, "assertDraftPublishable");
123
126
  function assertBaseVersionFresh(base, baseId, planContext) {
124
127
  if (!base) {
125
- throw new PublishValidationError("BASE_NOT_FOUND", `baseVersion ${baseId} nicht mehr vorhanden.`);
128
+ throw new PublishValidationError("BASE_NOT_FOUND", `baseVersion ${baseId} no longer exists.`);
126
129
  }
127
130
  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.`);
131
+ 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
132
  }
130
133
  }
131
134
  __name(assertBaseVersionFresh, "assertBaseVersionFresh");
132
135
  function assertOptimisticLockHeld(updateCount, draftId) {
133
136
  if (updateCount !== 1) {
134
- throw new PublishValidationError("OPTIMISTIC_LOCK_CONFLICT", `Draft ${draftId} wurde zwischenzeitlich von einem anderen Admin ver\xF6ffentlicht.`);
137
+ throw new PublishValidationError("OPTIMISTIC_LOCK_CONFLICT", `Draft ${draftId} has meanwhile been published by another admin.`);
135
138
  }
136
139
  }
137
140
  __name(assertOptimisticLockHeld, "assertOptimisticLockHeld");
@@ -182,7 +185,7 @@ var FEATURE_GUARD_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/Featu
182
185
  // src/billing/feature.guard.ts
183
186
  import { ForbiddenException, Inject, Injectable, Logger, Optional } from "@nestjs/common";
184
187
  import { Reflector } from "@nestjs/core";
185
- import { FEATURE_NOT_LICENSED } from "@saasicat/types";
188
+ import { AUTH_ERROR_CODES, FEATURE_NOT_LICENSED } from "@saasicat/types";
186
189
 
187
190
  // src/billing/upsell.tokens.ts
188
191
  var UPSELL_OFFER_RESOLVER_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/UpsellOfferResolver");
@@ -229,11 +232,21 @@ var FeatureGuard = class _FeatureGuard {
229
232
  if (!required || required.length === 0) return true;
230
233
  const request = context.switchToHttp().getRequest();
231
234
  const user = request.user;
232
- if (!user) throw new ForbiddenException("Nicht authentifiziert");
235
+ if (!user) {
236
+ throw new ForbiddenException({
237
+ code: AUTH_ERROR_CODES.NOT_AUTHENTICATED,
238
+ message: "Not authenticated"
239
+ });
240
+ }
233
241
  const role = this.config?.userRoleResolver ? this.config.userRoleResolver(user) : user.role ?? user.platformRole;
234
242
  if (role === "SUPER_ADMIN") return true;
235
243
  const tenantId = this.config?.tenantIdResolver ? this.config.tenantIdResolver(request) : request.tenantId ?? user.tenantId;
236
- if (!tenantId) throw new ForbiddenException("Kein Mandant zugeordnet");
244
+ if (!tenantId) {
245
+ throw new ForbiddenException({
246
+ code: AUTH_ERROR_CODES.NO_TENANT_ASSIGNED,
247
+ message: "No tenant assigned"
248
+ });
249
+ }
237
250
  const compute = /* @__PURE__ */ __name(() => this.entitlements.computeLimits(tenantId), "compute");
238
251
  const limits = this.config?.tenantContextRunner ? await this.config.tenantContextRunner(tenantId, compute) : await compute();
239
252
  const allowed = required.some((f) => limits.features.has(f));
@@ -243,25 +256,27 @@ var FeatureGuard = class _FeatureGuard {
243
256
  return true;
244
257
  }
245
258
  /**
246
- * Upsell response (#36): with a registered `UpsellOfferResolver` the 403
247
- * becomes machine-readable (`FeatureNotLicensedBody`), so consumer UIs can
248
- * render a purchase offer. Without a resolver the previous plain 403
249
- * remainsno breaking change.
259
+ * Upsell response (#36): the 403 body is always the full
260
+ * `FeatureNotLicensedBody`, so a consumer that matches on `code` can read
261
+ * `featureKey`/`offers` unconditionally. Without a registered
262
+ * `UpsellOfferResolver` there is simply nothing to offer `offers` is
263
+ * empty, the same degradation a failing resolver produces.
250
264
  *
251
265
  * Deliberately 403 + `code` field instead of 402 — rationale in
252
266
  * `@saasicat/types` upsell.types.ts (402 is reserved / inconsistently
253
267
  * supported; SPA interceptors must not treat the 403 as an auth error, the
254
268
  * distinction runs through `code`).
269
+ *
270
+ * The message must keep starting with the word `Feature` — consumers
271
+ * classify the response by that prefix.
255
272
  */
256
273
  async buildNotLicensedException(required, tenantId) {
257
- const message = `Feature ${required.join(" / ")} nicht im aktuellen Paket enthalten.`;
258
- if (!this.upsellResolver) return new ForbiddenException(message);
259
274
  const body = {
260
275
  code: FEATURE_NOT_LICENSED,
261
276
  featureKey: required[0],
262
277
  featureKeys: required,
263
- offers: await this.resolveOffersSafe(required, tenantId),
264
- message
278
+ offers: this.upsellResolver ? await this.resolveOffersSafe(required, tenantId) : [],
279
+ message: `Feature ${required.join(" / ")} is not included in the current plan.`
265
280
  };
266
281
  return new ForbiddenException(body);
267
282
  }
@@ -273,7 +288,7 @@ var FeatureGuard = class _FeatureGuard {
273
288
  try {
274
289
  return await this.upsellResolver.resolveOffers(required, tenantId);
275
290
  } catch (error) {
276
- this.logger.warn(`UpsellOfferResolver fehlgeschlagen f\xFCr [${required.join(", ")}]: ${String(error)}`);
291
+ this.logger.warn(`UpsellOfferResolver failed for [${required.join(", ")}]: ${String(error)}`);
277
292
  return [];
278
293
  }
279
294
  }
@@ -426,6 +441,7 @@ __name(compareRankedOffers, "compareRankedOffers");
426
441
  // src/billing/limit-exceeded.filter.ts
427
442
  import { Catch, HttpStatus, Logger as Logger2 } from "@nestjs/common";
428
443
  import { BaseExceptionFilter } from "@nestjs/core";
444
+ import { BILLING_ERROR_CODES } from "@saasicat/types";
429
445
  function _ts_decorate3(decorators, target, key, desc) {
430
446
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
431
447
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -446,11 +462,14 @@ var LimitExceededFilter = class _LimitExceededFilter extends BaseExceptionFilter
446
462
  const ctx = host.switchToHttp();
447
463
  const response = ctx.getResponse();
448
464
  const request = ctx.getRequest();
449
- this.logger.warn(`Limit ${exception.dimension} \xFCberschritten: ${exception.used}/${exception.max} bei ${request.method ?? "?"} ${request.url ?? ""}`);
465
+ this.logger.warn(`Limit ${exception.dimension} exceeded: ${exception.used}/${exception.max} on ${request.method ?? "?"} ${request.url ?? ""}`);
450
466
  response.status(HttpStatus.PAYMENT_REQUIRED).send({
451
467
  statusCode: HttpStatus.PAYMENT_REQUIRED,
452
468
  error: "PaymentRequired",
453
- reason: "LIMIT_EXCEEDED",
469
+ code: BILLING_ERROR_CODES.LIMIT_EXCEEDED,
470
+ // Deprecated: superseded by `code`, kept until consumers have
471
+ // migrated. Same value, only under the older field name.
472
+ reason: BILLING_ERROR_CODES.LIMIT_EXCEEDED,
454
473
  dimension: exception.dimension,
455
474
  used: exception.used,
456
475
  max: exception.max,
@@ -476,8 +495,8 @@ var PlanCatalogValidationError = class extends Error {
476
495
  source;
477
496
  errors;
478
497
  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}:
498
+ const messages = errors.map((e) => `${e.instancePath || "/"}: ${e.message ?? "unknown"}`).join("\n ");
499
+ super(`Plan catalog validation failed for ${source}:
481
500
  ${messages}`), this.source = source, this.errors = errors;
482
501
  this.name = "PlanCatalogValidationError";
483
502
  }
@@ -494,7 +513,7 @@ __name(loadPlanCatalogFromFile, "loadPlanCatalogFromFile");
494
513
  function loadPlanCatalogFromString(yamlContent, opts) {
495
514
  const parsed = yaml.load(yamlContent);
496
515
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
497
- throw new Error(`YAML-Inhalt von ${opts.source} ist kein Objekt`);
516
+ throw new Error(`The YAML content of ${opts.source} is not an object`);
498
517
  }
499
518
  const ajv = new Ajv2020({
500
519
  strict: false,
@@ -518,19 +537,19 @@ function validateConsistency(catalog, source) {
518
537
  const planIds = /* @__PURE__ */ new Set();
519
538
  for (const plan of catalog.plans ?? []) {
520
539
  if (planIds.has(plan.id)) {
521
- errors.push(`plans[].id: Doppelte Plan-ID "${plan.id}"`);
540
+ errors.push(`plans[].id: duplicate plan id "${plan.id}"`);
522
541
  }
523
542
  planIds.add(plan.id);
524
543
  if (catalog.features) {
525
544
  for (const fk of plan.features) {
526
545
  if (!declaredFeatureKeys.has(fk)) {
527
- errors.push(`plans[id=${plan.id}].features: Unbekannter featureKey "${fk}" \u2014 nicht in catalog.features deklariert`);
546
+ errors.push(`plans[id=${plan.id}].features: Unknown featureKey "${fk}" \u2014 not declared in catalog.features`);
528
547
  }
529
548
  }
530
549
  }
531
550
  }
532
551
  if (errors.length > 0) {
533
- const err = new Error(`Plan-Catalog-Konsistenz-Fehler in ${source}:
552
+ const err = new Error(`Plan catalog consistency error in ${source}:
534
553
  ${errors.join("\n ")}`);
535
554
  err.name = "PlanCatalogConsistencyError";
536
555
  throw err;
@@ -613,7 +632,7 @@ var PlanCatalogImporterService = class {
613
632
  if (planResult.created) report.plansCreated++;
614
633
  else report.plansSkipped++;
615
634
  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).`);
635
+ report.warnings.push(`Plan '${plan.id}' has no monthlyNet \u2014 skipped (on-request plans need a manual plan version).`);
617
636
  continue;
618
637
  }
619
638
  const versionResult = await this.sink.upsertPlanVersion({
@@ -1034,6 +1053,7 @@ PublicCatalogModule = _ts_decorate8([
1034
1053
 
1035
1054
  // src/billing/composed-tenant-auth.guard.ts
1036
1055
  import { ForbiddenException as ForbiddenException2, Inject as Inject6, Injectable as Injectable4, Optional as Optional4 } from "@nestjs/common";
1056
+ import { AUTH_ERROR_CODES as AUTH_ERROR_CODES2 } from "@saasicat/types";
1037
1057
  function _ts_decorate9(decorators, target, key, desc) {
1038
1058
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1039
1059
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1061,7 +1081,10 @@ var ComposedTenantAuthGuard = class {
1061
1081
  }
1062
1082
  async canActivate(context) {
1063
1083
  if (!this.guards || this.guards.length === 0) {
1064
- throw new ForbiddenException2("TenantBillingModule.forRoot.authGuards ist nicht konfiguriert.");
1084
+ throw new ForbiddenException2({
1085
+ code: AUTH_ERROR_CODES2.AUTH_GUARDS_NOT_CONFIGURED,
1086
+ message: "TenantBillingModule.forRoot.authGuards is not configured."
1087
+ });
1065
1088
  }
1066
1089
  for (const guard of this.guards) {
1067
1090
  const result = await Promise.resolve(guard.canActivate(context));
@@ -1082,6 +1105,7 @@ ComposedTenantAuthGuard = _ts_decorate9([
1082
1105
 
1083
1106
  // src/billing/tenant-admin.guard.ts
1084
1107
  import { ForbiddenException as ForbiddenException3, Injectable as Injectable5 } from "@nestjs/common";
1108
+ import { AUTH_ERROR_CODES as AUTH_ERROR_CODES3 } from "@saasicat/types";
1085
1109
  function _ts_decorate10(decorators, target, key, desc) {
1086
1110
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1087
1111
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1100,11 +1124,17 @@ var TenantAdminGuard = class {
1100
1124
  canActivate(context) {
1101
1125
  const request = context.switchToHttp().getRequest();
1102
1126
  if (!request.user) {
1103
- throw new ForbiddenException3("Nicht authentifiziert");
1127
+ throw new ForbiddenException3({
1128
+ code: AUTH_ERROR_CODES3.NOT_AUTHENTICATED,
1129
+ message: "Not authenticated"
1130
+ });
1104
1131
  }
1105
1132
  const role = request.user.platformRole ?? request.user.role;
1106
1133
  if (!role || !ADMIN_ROLES.has(role)) {
1107
- throw new ForbiddenException3("Diese Aktion erfordert die TENANT_ADMIN-Rolle.");
1134
+ throw new ForbiddenException3({
1135
+ code: AUTH_ERROR_CODES3.TENANT_ADMIN_REQUIRED,
1136
+ message: "This action requires the TENANT_ADMIN role."
1137
+ });
1108
1138
  }
1109
1139
  return true;
1110
1140
  }
@@ -1142,6 +1172,7 @@ __name(round2, "round2");
1142
1172
 
1143
1173
  // src/billing/plan-change-preview.service.ts
1144
1174
  import { Inject as Inject7, Injectable as Injectable6, NotFoundException, Optional as Optional5 } from "@nestjs/common";
1175
+ import { BILLING_ERROR_CODES as BILLING_ERROR_CODES2 } from "@saasicat/types";
1145
1176
  function _ts_decorate11(decorators, target, key, desc) {
1146
1177
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1147
1178
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1180,11 +1211,24 @@ var PlanChangePreviewService = class {
1180
1211
  async preview(tenantId, targetPlan, targetCycle, now = /* @__PURE__ */ new Date()) {
1181
1212
  const sub = await this.subscriptions.findForTenant(tenantId);
1182
1213
  if (!sub) {
1183
- throw new NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
1214
+ throw new NotFoundException({
1215
+ code: BILLING_ERROR_CODES2.SUBSCRIPTION_NOT_FOUND,
1216
+ message: `No subscription for tenant ${tenantId}`,
1217
+ params: {
1218
+ tenantId
1219
+ }
1220
+ });
1184
1221
  }
1185
1222
  const targetPlanDef = findPlan(this.catalog, targetPlan);
1186
1223
  if (!targetPlanDef) {
1187
- throw new NotFoundException(`Plan "${targetPlan}" nicht im Catalog (${this.catalog.projectKey})`);
1224
+ throw new NotFoundException({
1225
+ code: BILLING_ERROR_CODES2.PLAN_NOT_IN_CATALOG,
1226
+ message: `Plan "${targetPlan}" is not in the catalog (${this.catalog.projectKey})`,
1227
+ params: {
1228
+ planKey: targetPlan,
1229
+ projectKey: this.catalog.projectKey
1230
+ }
1231
+ });
1188
1232
  }
1189
1233
  const ctx = {
1190
1234
  currentPeriodStart: sub.currentPeriodStart,
@@ -1249,13 +1293,13 @@ var PlanChangePreviewService = class {
1249
1293
  if (blockedTargets.includes(targetPlan)) {
1250
1294
  blockers.push({
1251
1295
  code: `${targetPlan}_NOT_SELF_SERVICE`,
1252
- message: `${targetSnap.name} wird nur per Sondervertrag aktiviert. Bitte den Vertragsbetreuer kontaktieren.`
1296
+ message: `${targetSnap.name} is only activated via a special contract. Please contact the contract manager.`
1253
1297
  });
1254
1298
  }
1255
1299
  if (blockedSources.includes(sub.plan)) {
1256
1300
  blockers.push({
1257
1301
  code: `${sub.plan}_LOCKED`,
1258
- message: `Aktiver ${currentSnap.name}-Sondervertrag \u2014 bitte den Vertragsbetreuer f\xFCr Plan-Wechsel kontaktieren.`
1302
+ message: `Active ${currentSnap.name} special contract \u2014 please contact the contract manager to change plans.`
1259
1303
  });
1260
1304
  }
1261
1305
  for (const [key, row] of Object.entries(limitsCheck)) {
@@ -1263,19 +1307,19 @@ var PlanChangePreviewService = class {
1263
1307
  const usedDisplay = isFloatQuota(key) ? row.used.toFixed(1) : row.used.toString();
1264
1308
  blockers.push({
1265
1309
  code: `${key.toUpperCase()}_OVER_TARGET`,
1266
- message: `Aktueller Verbrauch ${usedDisplay} \xFCberschreitet das Ziel-Limit ${row.targetMax} (${key}) im ${targetSnap.name}-Paket. Bitte Verbrauch reduzieren.`
1310
+ message: `Current usage ${usedDisplay} exceeds the target limit ${row.targetMax} (${key}) in the ${targetSnap.name} plan. Please reduce usage first.`
1267
1311
  });
1268
1312
  }
1269
1313
  if (featuresLost.length > 0) {
1270
1314
  warnings.push({
1271
1315
  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.`
1316
+ 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
1317
  });
1274
1318
  }
1275
1319
  if (changeType === "NOOP") {
1276
1320
  warnings.push({
1277
1321
  code: "NO_CHANGE",
1278
- message: "Ziel-Paket und Abrechnungszyklus stimmen mit dem aktuellen Stand \xFCberein."
1322
+ message: "Target plan and billing cycle already match the current state."
1279
1323
  });
1280
1324
  }
1281
1325
  const projectedTrialEndsAt = this.trialProjection && sub.status === "TRIAL" ? await this.trialProjection.projectTrialEndsAt({
@@ -1433,13 +1477,13 @@ var PendingPlanMaterializationService = class _PendingPlanMaterializationService
1433
1477
  this.entitlements.invalidateTenant(change.tenantId);
1434
1478
  applied += 1;
1435
1479
  } catch (err) {
1436
- this.logger.error(`Pending-Plan-Materialisierung fehlgeschlagen (tenant ${change.tenantId}): ${String(err)}`);
1480
+ this.logger.error(`Pending plan materialisation failed (tenant ${change.tenantId}): ${String(err)}`);
1437
1481
  continue;
1438
1482
  }
1439
1483
  await this.tryFreeze(change.tenantId, change.pendingPlan, cycle, now);
1440
1484
  }
1441
1485
  if (applied > 0) {
1442
- this.logger.log(`Pending-Plan-Materialisierung: ${applied} geplante(r) Plan-Wechsel angewandt.`);
1486
+ this.logger.log(`Pending plan materialisation: applied ${applied} scheduled plan change(s).`);
1443
1487
  }
1444
1488
  return {
1445
1489
  applied
@@ -1450,7 +1494,7 @@ var PendingPlanMaterializationService = class _PendingPlanMaterializationService
1450
1494
  try {
1451
1495
  await this.contractFreeze.freezeOnPlanChange(tenantId, plan, cycle, now);
1452
1496
  } catch (err) {
1453
- this.logger.error(`Contract-Freeze nach Materialisierung fehlgeschlagen (tenant ${tenantId}): ${String(err)}`);
1497
+ this.logger.error(`Contract freeze after materialisation failed (tenant ${tenantId}): ${String(err)}`);
1454
1498
  }
1455
1499
  }
1456
1500
  };
@@ -1628,14 +1672,14 @@ var PreviewPlanChangeDto = class {
1628
1672
  _ts_decorate14([
1629
1673
  IsString2(),
1630
1674
  Matches(PLAN_OR_CYCLE_PATTERN, {
1631
- message: "plan muss SCREAMING_SNAKE_CASE sein"
1675
+ message: "plan must be SCREAMING_SNAKE_CASE"
1632
1676
  }),
1633
1677
  _ts_metadata10("design:type", String)
1634
1678
  ], PreviewPlanChangeDto.prototype, "plan", void 0);
1635
1679
  _ts_decorate14([
1636
1680
  IsString2(),
1637
1681
  Matches(PLAN_OR_CYCLE_PATTERN, {
1638
- message: "billingCycle muss SCREAMING_SNAKE_CASE sein (z. B. MONTHLY, YEARLY)"
1682
+ message: "billingCycle must be SCREAMING_SNAKE_CASE (e.g. MONTHLY, YEARLY)"
1639
1683
  }),
1640
1684
  _ts_metadata10("design:type", String)
1641
1685
  ], PreviewPlanChangeDto.prototype, "billingCycle", void 0);
@@ -1699,14 +1743,14 @@ var CompleteOnboardingSubscriptionDto = class {
1699
1743
  _ts_decorate15([
1700
1744
  IsString3(),
1701
1745
  Matches2(PLAN_OR_CYCLE_PATTERN2, {
1702
- message: "plan muss SCREAMING_SNAKE_CASE sein"
1746
+ message: "plan must be SCREAMING_SNAKE_CASE"
1703
1747
  }),
1704
1748
  _ts_metadata11("design:type", String)
1705
1749
  ], CompleteOnboardingSubscriptionDto.prototype, "plan", void 0);
1706
1750
  _ts_decorate15([
1707
1751
  IsString3(),
1708
1752
  Matches2(PLAN_OR_CYCLE_PATTERN2, {
1709
- message: "billingCycle muss SCREAMING_SNAKE_CASE sein (z. B. MONTHLY, YEARLY)"
1753
+ message: "billingCycle must be SCREAMING_SNAKE_CASE (e.g. MONTHLY, YEARLY)"
1710
1754
  }),
1711
1755
  _ts_metadata11("design:type", String)
1712
1756
  ], CompleteOnboardingSubscriptionDto.prototype, "billingCycle", void 0);
@@ -1714,7 +1758,7 @@ _ts_decorate15([
1714
1758
  IsOptional3(),
1715
1759
  IsString3(),
1716
1760
  Matches2(PROMO_CODE_PATTERN, {
1717
- message: 'promoCode muss aus A\u2013Z, 0\u20139, "-", "_" bestehen (4\u201332 Zeichen)'
1761
+ message: 'promoCode must consist of A\u2013Z, 0\u20139, "-", "_" (4\u201332 characters)'
1718
1762
  }),
1719
1763
  _ts_metadata11("design:type", String)
1720
1764
  ], CompleteOnboardingSubscriptionDto.prototype, "promoCode", void 0);
@@ -1730,9 +1774,11 @@ _ts_decorate15([
1730
1774
 
1731
1775
  // src/billing/tenant-billing.controller.ts
1732
1776
  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";
1777
+ import { AUTH_ERROR_CODES as AUTH_ERROR_CODES4, BILLING_ERROR_CODES as BILLING_ERROR_CODES4 } from "@saasicat/types";
1733
1778
 
1734
1779
  // src/billing/subscription-bundles.service.ts
1735
1780
  import { Inject as Inject10, Injectable as Injectable9, NotFoundException as NotFoundException2, Optional as Optional7, UnprocessableEntityException } from "@nestjs/common";
1781
+ import { BILLING_ERROR_CODES as BILLING_ERROR_CODES3, CATALOG_ERROR_CODES } from "@saasicat/types";
1736
1782
  function _ts_decorate16(decorators, target, key, desc) {
1737
1783
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1738
1784
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1780,38 +1826,61 @@ var SubscriptionBundlesService = class {
1780
1826
  async addBundleToSubscription(input) {
1781
1827
  const bundleVersion = await this.bundles.findVersionById(input.bundleVersionId);
1782
1828
  if (!bundleVersion) {
1783
- throw new NotFoundException2(`BundleVersion '${input.bundleVersionId}' nicht gefunden`);
1829
+ throw new NotFoundException2({
1830
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
1831
+ message: `BundleVersion '${input.bundleVersionId}' not found`,
1832
+ params: {
1833
+ bundleVersionId: input.bundleVersionId
1834
+ }
1835
+ });
1784
1836
  }
1785
1837
  if (bundleVersion.publishedAt === null) {
1786
1838
  throw new UnprocessableEntityException({
1787
- code: "BUNDLE_VERSION_NOT_PUBLISHED",
1788
- message: `BundleVersion '${input.bundleVersionId}' ist nicht published und kann nicht gebucht werden.`
1839
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_PUBLISHED,
1840
+ message: `BundleVersion '${input.bundleVersionId}' is not published and cannot be booked.`,
1841
+ params: {
1842
+ bundleVersionId: input.bundleVersionId
1843
+ }
1789
1844
  });
1790
1845
  }
1791
1846
  if (bundleVersion.supersededAt !== null) {
1792
1847
  throw new UnprocessableEntityException({
1793
- code: "BUNDLE_VERSION_SUPERSEDED",
1794
- message: `BundleVersion '${input.bundleVersionId}' wurde durch eine Nachfolge-Version abgel\xF6st.`
1848
+ code: CATALOG_ERROR_CODES.BUNDLE_VERSION_SUPERSEDED,
1849
+ message: `BundleVersion '${input.bundleVersionId}' has been superseded by a newer version.`,
1850
+ params: {
1851
+ bundleVersionId: input.bundleVersionId
1852
+ }
1795
1853
  });
1796
1854
  }
1797
1855
  if (this.blockedBundles?.bundleKeys?.includes(bundleVersion.bundleKey)) {
1798
1856
  throw new UnprocessableEntityException({
1799
- code: "BUNDLE_NOT_SELF_SERVICE",
1800
- message: `Bundle '${bundleVersion.bundleKey}' wird nur per Sondervertrag aktiviert. Bitte den Vertragsbetreuer kontaktieren.`
1857
+ code: BILLING_ERROR_CODES3.BUNDLE_NOT_SELF_SERVICE,
1858
+ message: `Bundle '${bundleVersion.bundleKey}' is only activated via a special contract. Please contact the contract manager.`,
1859
+ params: {
1860
+ bundleKey: bundleVersion.bundleKey
1861
+ }
1801
1862
  });
1802
1863
  }
1803
1864
  const planIds = bundleVersion.compatibility?.planIds ?? [];
1804
1865
  if (planIds.length > 0 && !planIds.includes(input.currentPlanKey)) {
1805
1866
  throw new UnprocessableEntityException({
1806
- code: "BUNDLE_INCOMPATIBLE_WITH_PLAN",
1807
- message: `BundleVersion '${input.bundleVersionId}' ist nicht mit Plan '${input.currentPlanKey}' kompatibel. Erlaubt: [${planIds.join(", ")}].`
1867
+ code: BILLING_ERROR_CODES3.BUNDLE_INCOMPATIBLE_WITH_PLAN,
1868
+ message: `BundleVersion '${input.bundleVersionId}' is not compatible with plan '${input.currentPlanKey}'. Allowed: [${planIds.join(", ")}].`,
1869
+ params: {
1870
+ bundleVersionId: input.bundleVersionId,
1871
+ planKey: input.currentPlanKey,
1872
+ allowedPlanKeys: planIds
1873
+ }
1808
1874
  });
1809
1875
  }
1810
1876
  const active = await this.repo.listActiveBySubscription(input.subscriptionId);
1811
1877
  if (active.some((b) => b.bundleVersionId === input.bundleVersionId)) {
1812
1878
  throw new UnprocessableEntityException({
1813
- code: "BUNDLE_ALREADY_SUBSCRIBED",
1814
- message: `Subscription '${input.subscriptionId}' hat dieses Bundle bereits aktiv gebucht.`
1879
+ code: BILLING_ERROR_CODES3.BUNDLE_ALREADY_SUBSCRIBED,
1880
+ message: `Subscription '${input.subscriptionId}' has already actively booked this bundle.`,
1881
+ params: {
1882
+ subscriptionId: input.subscriptionId
1883
+ }
1815
1884
  });
1816
1885
  }
1817
1886
  const startedAt = input.startedAt ?? /* @__PURE__ */ new Date();
@@ -1827,12 +1896,21 @@ var SubscriptionBundlesService = class {
1827
1896
  async cancelBundleFromSubscription(input) {
1828
1897
  const existing = await this.repo.findById(input.subscriptionBundleId);
1829
1898
  if (!existing) {
1830
- throw new NotFoundException2(`SubscriptionBundle '${input.subscriptionBundleId}' nicht gefunden`);
1899
+ throw new NotFoundException2({
1900
+ code: BILLING_ERROR_CODES3.SUBSCRIPTION_BUNDLE_NOT_FOUND,
1901
+ message: `SubscriptionBundle '${input.subscriptionBundleId}' not found`,
1902
+ params: {
1903
+ subscriptionBundleId: input.subscriptionBundleId
1904
+ }
1905
+ });
1831
1906
  }
1832
1907
  if (existing.canceledAt !== null) {
1833
1908
  throw new UnprocessableEntityException({
1834
- code: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELED",
1835
- message: `SubscriptionBundle '${input.subscriptionBundleId}' ist bereits gek\xFCndigt.`
1909
+ code: BILLING_ERROR_CODES3.SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED,
1910
+ message: `SubscriptionBundle '${input.subscriptionBundleId}' is already cancelled.`,
1911
+ params: {
1912
+ subscriptionBundleId: input.subscriptionBundleId
1913
+ }
1836
1914
  });
1837
1915
  }
1838
1916
  const canceledAt = input.canceledAt ?? /* @__PURE__ */ new Date();
@@ -1853,18 +1931,27 @@ var SubscriptionBundlesService = class {
1853
1931
  async reactivateBundle(subscriptionBundleId) {
1854
1932
  const existing = await this.repo.findById(subscriptionBundleId);
1855
1933
  if (!existing) {
1856
- throw new NotFoundException2(`SubscriptionBundle '${subscriptionBundleId}' nicht gefunden`);
1934
+ throw new NotFoundException2({
1935
+ code: BILLING_ERROR_CODES3.SUBSCRIPTION_BUNDLE_NOT_FOUND,
1936
+ message: `SubscriptionBundle '${subscriptionBundleId}' not found`,
1937
+ params: {
1938
+ subscriptionBundleId
1939
+ }
1940
+ });
1857
1941
  }
1858
1942
  if (existing.canceledAt === null) {
1859
1943
  throw new UnprocessableEntityException({
1860
- code: "SUBSCRIPTION_BUNDLE_NOT_CANCELED",
1861
- message: `SubscriptionBundle '${subscriptionBundleId}' ist nicht gek\xFCndigt.`
1944
+ code: BILLING_ERROR_CODES3.SUBSCRIPTION_BUNDLE_NOT_CANCELLED,
1945
+ message: `SubscriptionBundle '${subscriptionBundleId}' is not cancelled.`,
1946
+ params: {
1947
+ subscriptionBundleId
1948
+ }
1862
1949
  });
1863
1950
  }
1864
1951
  if (existing.canceledEffectiveAt !== null && existing.canceledEffectiveAt <= /* @__PURE__ */ new Date()) {
1865
1952
  throw new UnprocessableEntityException({
1866
- code: "SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE",
1867
- message: "K\xFCndigung bereits wirksam \u2014 bitte das Bundle neu buchen."
1953
+ code: BILLING_ERROR_CODES3.SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE,
1954
+ message: "Cancellation already in effect \u2014 book the bundle again."
1868
1955
  });
1869
1956
  }
1870
1957
  return this.repo.reactivate(subscriptionBundleId);
@@ -1970,7 +2057,13 @@ var TenantBillingController = class _TenantBillingController {
1970
2057
  this.usageSnapshot.snapshot(tenantId)
1971
2058
  ]);
1972
2059
  if (!sub) {
1973
- throw new NotFoundException3(`Keine Subscription f\xFCr Tenant ${tenantId}`);
2060
+ throw new NotFoundException3({
2061
+ code: BILLING_ERROR_CODES4.SUBSCRIPTION_NOT_FOUND,
2062
+ message: `No subscription for tenant ${tenantId}`,
2063
+ params: {
2064
+ tenantId
2065
+ }
2066
+ });
1974
2067
  }
1975
2068
  const usage = {};
1976
2069
  for (const key of /* @__PURE__ */ new Set([
@@ -2019,14 +2112,29 @@ var TenantBillingController = class _TenantBillingController {
2019
2112
  const userId = this.requireUserId(req);
2020
2113
  const blockedTargets = this.blockedPlans?.asTarget ?? [];
2021
2114
  if (blockedTargets.includes(dto.plan)) {
2022
- throw new ForbiddenException4(`${dto.plan} wird nicht per Self-Service aktiviert.`);
2115
+ throw new ForbiddenException4({
2116
+ code: BILLING_ERROR_CODES4.PLAN_NOT_SELF_SERVICE,
2117
+ message: `${dto.plan} is not activated via self-service.`,
2118
+ params: {
2119
+ planKey: dto.plan
2120
+ }
2121
+ });
2023
2122
  }
2024
2123
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
2025
- if (!sub) throw new NotFoundException3(`Keine Subscription f\xFCr Tenant ${tenantId}`);
2124
+ if (!sub) {
2125
+ throw new NotFoundException3({
2126
+ code: BILLING_ERROR_CODES4.SUBSCRIPTION_NOT_FOUND,
2127
+ message: `No subscription for tenant ${tenantId}`,
2128
+ params: {
2129
+ tenantId
2130
+ }
2131
+ });
2132
+ }
2026
2133
  const blockers = await this.planPreview.assertChangeAllowed(tenantId, dto.plan, dto.billingCycle);
2027
2134
  if (blockers.length > 0) {
2028
2135
  throw new BadRequestException({
2029
- message: "Plan-Wechsel blockiert.",
2136
+ code: BILLING_ERROR_CODES4.PLAN_CHANGE_BLOCKED,
2137
+ message: "Plan change is blocked.",
2030
2138
  blockers
2031
2139
  });
2032
2140
  }
@@ -2096,14 +2204,29 @@ var TenantBillingController = class _TenantBillingController {
2096
2204
  const warnings = [];
2097
2205
  const blockedTargets = this.blockedPlans?.asTarget ?? [];
2098
2206
  if (blockedTargets.includes(dto.plan)) {
2099
- throw new ForbiddenException4(`${dto.plan} wird nicht per Self-Service aktiviert.`);
2207
+ throw new ForbiddenException4({
2208
+ code: BILLING_ERROR_CODES4.PLAN_NOT_SELF_SERVICE,
2209
+ message: `${dto.plan} is not activated via self-service.`,
2210
+ params: {
2211
+ planKey: dto.plan
2212
+ }
2213
+ });
2100
2214
  }
2101
2215
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
2102
- if (!sub) throw new NotFoundException3(`Keine Subscription f\xFCr Tenant ${tenantId}`);
2216
+ if (!sub) {
2217
+ throw new NotFoundException3({
2218
+ code: BILLING_ERROR_CODES4.SUBSCRIPTION_NOT_FOUND,
2219
+ message: `No subscription for tenant ${tenantId}`,
2220
+ params: {
2221
+ tenantId
2222
+ }
2223
+ });
2224
+ }
2103
2225
  const blockers = await this.planPreview.assertChangeAllowed(tenantId, dto.plan, dto.billingCycle);
2104
2226
  if (blockers.length > 0) {
2105
2227
  throw new BadRequestException({
2106
- message: "Plan-Wechsel im Onboarding blockiert.",
2228
+ code: BILLING_ERROR_CODES4.PLAN_CHANGE_BLOCKED,
2229
+ message: "Plan change during onboarding is blocked.",
2107
2230
  blockers
2108
2231
  });
2109
2232
  }
@@ -2137,8 +2260,13 @@ var TenantBillingController = class _TenantBillingController {
2137
2260
  warnings.push(...this.collectPromoSkipReasons(dto.promoCode, sub));
2138
2261
  }
2139
2262
  } catch (err) {
2263
+ const reason = err instanceof Error ? err.message : null;
2140
2264
  throw new BadRequestException({
2141
- message: err instanceof Error ? `Onboarding-Anlage fehlgeschlagen: ${err.message}` : "Onboarding-Anlage fehlgeschlagen"
2265
+ code: BILLING_ERROR_CODES4.ONBOARDING_CREATE_FAILED,
2266
+ message: reason ? `Onboarding creation failed: ${reason}` : "Onboarding creation failed",
2267
+ params: {
2268
+ reason
2269
+ }
2142
2270
  });
2143
2271
  }
2144
2272
  } else {
@@ -2151,9 +2279,9 @@ var TenantBillingController = class _TenantBillingController {
2151
2279
  });
2152
2280
  if (dto.promoCode) {
2153
2281
  if (!this.promoCodes) {
2154
- warnings.push("Promo-Code wurde gesendet, aber PromoCodesModule ist nicht geladen \u2014 der Code wurde NICHT eingel\xF6st.");
2282
+ warnings.push("A promo code was sent, but PromoCodesModule is not loaded \u2014 the code was NOT redeemed.");
2155
2283
  } 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.");
2284
+ warnings.push("A promo code was sent, but the adapter returns no SubscriptionUsageRecord.id \u2014 the code was NOT redeemed.");
2157
2285
  } else {
2158
2286
  try {
2159
2287
  const redemption = await this.promoCodes.redeem({
@@ -2164,8 +2292,8 @@ var TenantBillingController = class _TenantBillingController {
2164
2292
  });
2165
2293
  promoRedemption = this.toResponseRedemption(redemption, dto.promoCode);
2166
2294
  } catch (err) {
2167
- const message = err instanceof Error ? err.message : "Unbekannter Fehler";
2168
- warnings.push(`Promo-Code konnte nicht eingel\xF6st werden: ${message}`);
2295
+ const message = err instanceof Error ? err.message : "Unknown error";
2296
+ warnings.push(`The promo code could not be redeemed: ${message}`);
2169
2297
  }
2170
2298
  }
2171
2299
  }
@@ -2191,11 +2319,11 @@ var TenantBillingController = class _TenantBillingController {
2191
2319
  });
2192
2320
  bundlesAdded += 1;
2193
2321
  } catch (err) {
2194
- warnings.push(`Bundle '${bundleVersionId}' konnte nicht gebucht werden: ` + (err instanceof Error ? err.message : String(err)));
2322
+ warnings.push(`Bundle '${bundleVersionId}' could not be booked: ` + (err instanceof Error ? err.message : String(err)));
2195
2323
  }
2196
2324
  }
2197
2325
  } 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.");
2326
+ warnings.push("Bundle bookings were requested during onboarding, but SubscriptionBundleModule is not registered in the consumer. No bundles were created.");
2199
2327
  }
2200
2328
  return {
2201
2329
  plan: planResult.plan,
@@ -2215,9 +2343,20 @@ var TenantBillingController = class _TenantBillingController {
2215
2343
  const tenantId = this.requireTenantId(req);
2216
2344
  const userId = this.requireUserId(req);
2217
2345
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
2218
- if (!sub) throw new NotFoundException3(`Keine Subscription f\xFCr Tenant ${tenantId}`);
2346
+ if (!sub) {
2347
+ throw new NotFoundException3({
2348
+ code: BILLING_ERROR_CODES4.SUBSCRIPTION_NOT_FOUND,
2349
+ message: `No subscription for tenant ${tenantId}`,
2350
+ params: {
2351
+ tenantId
2352
+ }
2353
+ });
2354
+ }
2219
2355
  if (!sub.pendingPlanVersion) {
2220
- throw new BadRequestException("Es liegt aktuell keine Pending-Plan-Version zur Best\xE4tigung vor.");
2356
+ throw new BadRequestException({
2357
+ code: BILLING_ERROR_CODES4.NO_PENDING_PLAN_VERSION,
2358
+ message: "There is no pending plan version awaiting confirmation."
2359
+ });
2221
2360
  }
2222
2361
  const result = await this.subscriptionWrite.acceptPendingPlanVersion(tenantId, userId, /* @__PURE__ */ new Date());
2223
2362
  if (!result.alreadyAccepted) {
@@ -2262,7 +2401,10 @@ var TenantBillingController = class _TenantBillingController {
2262
2401
  const resolver = this.tenantIdResolver ?? ((r) => r.user?.tenantId ?? null);
2263
2402
  const tenantId = resolver(req);
2264
2403
  if (!tenantId) {
2265
- throw new NotFoundException3("Tenant-ID nicht im Request gefunden");
2404
+ throw new NotFoundException3({
2405
+ code: AUTH_ERROR_CODES4.TENANT_CONTEXT_MISSING,
2406
+ message: "No tenant ID found on the request"
2407
+ });
2266
2408
  }
2267
2409
  return tenantId;
2268
2410
  }
@@ -2270,7 +2412,10 @@ var TenantBillingController = class _TenantBillingController {
2270
2412
  const resolver = this.userIdResolver ?? ((r) => r.user?.sub ?? r.user?.id ?? null);
2271
2413
  const userId = resolver(req);
2272
2414
  if (!userId) {
2273
- throw new NotFoundException3("User-ID nicht im Request gefunden");
2415
+ throw new NotFoundException3({
2416
+ code: AUTH_ERROR_CODES4.TENANT_CONTEXT_MISSING,
2417
+ message: "No user ID found on the request"
2418
+ });
2274
2419
  }
2275
2420
  return userId;
2276
2421
  }
@@ -2310,10 +2455,10 @@ var TenantBillingController = class _TenantBillingController {
2310
2455
  collectPromoSkipReasons(promoCode, sub) {
2311
2456
  const reasons = [];
2312
2457
  if (!this.promoCodes) {
2313
- reasons.push(`Promo-Code "${promoCode}" wurde gesendet, aber PromoCodesModule ist nicht geladen \u2014 der Code wurde NICHT eingel\xF6st.`);
2458
+ reasons.push(`Promo-Code "${promoCode}" was sent, but PromoCodesModule is not loaded \u2014 the code was NOT redeemed.`);
2314
2459
  }
2315
2460
  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.`);
2461
+ reasons.push(`Promo-Code "${promoCode}" was sent, but the adapter returns no SubscriptionUsageRecord.id \u2014 the code was NOT redeemed.`);
2317
2462
  }
2318
2463
  return reasons;
2319
2464
  }
@@ -2328,7 +2473,7 @@ var TenantBillingController = class _TenantBillingController {
2328
2473
  try {
2329
2474
  await this.contractFreeze.freezeOnPlanChange(tenantId, plan, cycle, /* @__PURE__ */ new Date());
2330
2475
  } catch (err) {
2331
- this.logger.error(`Contract-Freeze nach Plan-Wechsel fehlgeschlagen (tenant ${tenantId}): ${String(err)}`);
2476
+ this.logger.error(`Contract freeze after plan change failed (tenant ${tenantId}): ${String(err)}`);
2332
2477
  }
2333
2478
  }
2334
2479
  /**
@@ -2639,7 +2784,7 @@ __name(normalizeQuotas, "normalizeQuotas");
2639
2784
 
2640
2785
  // src/billing/subscription-bundle-preview.service.ts
2641
2786
  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";
2787
+ 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
2788
  function _ts_decorate20(decorators, target, key, desc) {
2644
2789
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2645
2790
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2678,7 +2823,13 @@ var SubscriptionBundlePreviewService = class {
2678
2823
  async previewAdd(ctx, input, now = /* @__PURE__ */ new Date()) {
2679
2824
  const bundleVersion = await this.bundles.findVersionById(input.bundleVersionId);
2680
2825
  if (!bundleVersion) {
2681
- throw new NotFoundException4(`BundleVersion '${input.bundleVersionId}' nicht gefunden`);
2826
+ throw new NotFoundException4({
2827
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_NOT_FOUND,
2828
+ message: `BundleVersion '${input.bundleVersionId}' not found`,
2829
+ params: {
2830
+ bundleVersionId: input.bundleVersionId
2831
+ }
2832
+ });
2682
2833
  }
2683
2834
  const blockers = [];
2684
2835
  const warnings = [];
@@ -2687,7 +2838,7 @@ var SubscriptionBundlePreviewService = class {
2687
2838
  if (activeBundleVersions.some((bv) => bv.id === bundleVersion.id)) {
2688
2839
  blockers.push({
2689
2840
  code: "BUNDLE_ALREADY_SUBSCRIBED",
2690
- message: "Dieses Bundle ist bereits aktiv gebucht."
2841
+ message: "This bundle is already actively booked."
2691
2842
  });
2692
2843
  }
2693
2844
  const planFeatures = new Set(await this.resolvePlanFeatures(ctx.currentPlanKey, now));
@@ -2695,14 +2846,14 @@ var SubscriptionBundlePreviewService = class {
2695
2846
  if (redundantFeatures.length > 0) {
2696
2847
  warnings.push({
2697
2848
  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.`
2849
+ 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
2850
  });
2700
2851
  }
2701
2852
  const missingRequires = await this.collectMissingRequires(bundleVersion, planFeatures, activeBundleVersions);
2702
2853
  if (missingRequires.length > 0) {
2703
2854
  blockers.push({
2704
2855
  code: "BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED",
2705
- message: `Das Bundle ben\xF6tigt [${missingRequires.join(", ")}] \u2014 weder im Plan noch in den aktiven Bundles enthalten.`
2856
+ message: `The bundle requires [${missingRequires.join(", ")}] \u2014 present neither in the plan nor in the active bundles.`
2706
2857
  });
2707
2858
  }
2708
2859
  const priceNet = resolveBundlePriceNet(bundleVersion, ctx.currentPlanKey, ctx.billingCycle);
@@ -2731,18 +2882,30 @@ var SubscriptionBundlePreviewService = class {
2731
2882
  async previewCancel(ctx, input, now = /* @__PURE__ */ new Date()) {
2732
2883
  const existing = await this.subscriptionBundles.findById(input.subscriptionBundleId);
2733
2884
  if (!existing || existing.subscriptionId !== ctx.subscriptionId) {
2734
- throw new NotFoundException4(`SubscriptionBundle '${input.subscriptionBundleId}' nicht gefunden`);
2885
+ throw new NotFoundException4({
2886
+ code: BILLING_ERROR_CODES5.SUBSCRIPTION_BUNDLE_NOT_FOUND,
2887
+ message: `SubscriptionBundle '${input.subscriptionBundleId}' not found`,
2888
+ params: {
2889
+ subscriptionBundleId: input.subscriptionBundleId
2890
+ }
2891
+ });
2735
2892
  }
2736
2893
  const bundleVersion = await this.bundles.findVersionById(existing.bundleVersionId);
2737
2894
  if (!bundleVersion) {
2738
- throw new NotFoundException4(`BundleVersion '${existing.bundleVersionId}' nicht gefunden`);
2895
+ throw new NotFoundException4({
2896
+ code: CATALOG_ERROR_CODES2.BUNDLE_VERSION_NOT_FOUND,
2897
+ message: `BundleVersion '${existing.bundleVersionId}' not found`,
2898
+ params: {
2899
+ bundleVersionId: existing.bundleVersionId
2900
+ }
2901
+ });
2739
2902
  }
2740
2903
  const blockers = [];
2741
2904
  const warnings = [];
2742
2905
  if (existing.canceledAt !== null) {
2743
2906
  blockers.push({
2744
- code: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELED",
2745
- message: "Diese Bundle-Buchung ist bereits gek\xFCndigt."
2907
+ code: BILLING_ERROR_CODES5.SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED,
2908
+ message: "This bundle booking is already cancelled."
2746
2909
  });
2747
2910
  }
2748
2911
  const effectiveAt = resolveBundleCancelEffectiveAt({
@@ -2754,7 +2917,7 @@ var SubscriptionBundlePreviewService = class {
2754
2917
  if (existing.minimumTermEndsAt && existing.minimumTermEndsAt.getTime() > periodEnd.getTime()) {
2755
2918
  warnings.push({
2756
2919
  code: "MINIMUM_TERM_BINDS",
2757
- message: "Die Mindestlaufzeit bindet \xFCber das Periodenende hinaus \u2014 die K\xFCndigung wirkt erst zum Mindestlaufzeit-Ende."
2920
+ message: "The minimum term extends beyond the end of the period \u2014 the cancellation only takes effect when the minimum term ends."
2758
2921
  });
2759
2922
  }
2760
2923
  return {
@@ -2773,26 +2936,26 @@ var SubscriptionBundlePreviewService = class {
2773
2936
  if (bundleVersion.publishedAt === null) {
2774
2937
  blockers.push({
2775
2938
  code: "BUNDLE_VERSION_NOT_PUBLISHED",
2776
- message: "Diese BundleVersion ist nicht ver\xF6ffentlicht und nicht buchbar."
2939
+ message: "This bundle version is not published and cannot be booked."
2777
2940
  });
2778
2941
  }
2779
2942
  if (bundleVersion.supersededAt !== null) {
2780
2943
  blockers.push({
2781
2944
  code: "BUNDLE_VERSION_SUPERSEDED",
2782
- message: "Diese BundleVersion wurde durch eine Nachfolge-Version abgel\xF6st."
2945
+ message: "This bundle version has been superseded by a newer one."
2783
2946
  });
2784
2947
  }
2785
2948
  const planIds = bundleVersion.compatibility?.planIds ?? [];
2786
2949
  if (planIds.length > 0 && !planIds.includes(currentPlanKey)) {
2787
2950
  blockers.push({
2788
2951
  code: "BUNDLE_INCOMPATIBLE_WITH_PLAN",
2789
- message: `Das Bundle ist nicht mit Plan '${currentPlanKey}' kompatibel. Erlaubt: [${planIds.join(", ")}].`
2952
+ message: `The bundle is not compatible with plan '${currentPlanKey}'. Allowed: [${planIds.join(", ")}].`
2790
2953
  });
2791
2954
  }
2792
2955
  if (this.blockedBundles?.bundleKeys?.includes(bundleVersion.bundleKey)) {
2793
2956
  blockers.push({
2794
2957
  code: "BUNDLE_NOT_SELF_SERVICE",
2795
- message: `Bundle '${bundleVersion.bundleKey}' wird nur per Sondervertrag aktiviert. Bitte den Vertragsbetreuer kontaktieren.`
2958
+ message: `Bundle '${bundleVersion.bundleKey}' is only activated via a special contract. Please contact the contract manager.`
2796
2959
  });
2797
2960
  }
2798
2961
  }
@@ -2905,6 +3068,7 @@ import { Module as Module4 } from "@nestjs/common";
2905
3068
 
2906
3069
  // src/billing/tenant-subscription-bundles.controller.ts
2907
3070
  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";
3071
+ import { AUTH_ERROR_CODES as AUTH_ERROR_CODES5, BILLING_ERROR_CODES as BILLING_ERROR_CODES6 } from "@saasicat/types";
2908
3072
 
2909
3073
  // src/billing/dto/subscription-bundles.dto.ts
2910
3074
  import { IsInt, IsOptional as IsOptional4, IsUUID as IsUUID2, Matches as Matches3, Max, Min } from "class-validator";
@@ -2985,7 +3149,7 @@ var CancelSubscriptionBundleDto = class {
2985
3149
  _ts_decorate21([
2986
3150
  IsOptional4(),
2987
3151
  Matches3(ISO_DATE_PATTERN, {
2988
- message: "canceledAt muss ISO-Datum sein (YYYY-MM-DD)"
3152
+ message: "canceledAt must be an ISO date (YYYY-MM-DD)"
2989
3153
  }),
2990
3154
  _ts_metadata15("design:type", String)
2991
3155
  ], CancelSubscriptionBundleDto.prototype, "canceledAt", void 0);
@@ -3049,7 +3213,10 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
3049
3213
  const hasAdd = Boolean(dto.bundleVersionId);
3050
3214
  const hasCancel = Boolean(dto.subscriptionBundleId);
3051
3215
  if (hasAdd === hasCancel) {
3052
- throw new BadRequestException2("Genau eines von bundleVersionId (Add-Preview) oder subscriptionBundleId (Cancel-Preview) angeben.");
3216
+ throw new BadRequestException2({
3217
+ code: BILLING_ERROR_CODES6.BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS,
3218
+ message: "Exactly one of bundleVersionId (add preview) or subscriptionBundleId (cancel preview) must be given."
3219
+ });
3053
3220
  }
3054
3221
  const sub = await this.requireSubscription(this.requireTenantId(req));
3055
3222
  const ctx = {
@@ -3089,21 +3256,30 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
3089
3256
  }
3090
3257
  requireSubscriptionPk(sub) {
3091
3258
  if (!sub.id) {
3092
- throw new NotFoundException5("SubscriptionUsageRecord ohne `id` \u2014 Adapter muss die Subscription-PK durchreichen (siehe SubscriptionUsageRecord.id).");
3259
+ throw new NotFoundException5(codedError(BILLING_ERROR_CODES6.SUBSCRIPTION_PK_MISSING));
3093
3260
  }
3094
3261
  return sub.id;
3095
3262
  }
3096
3263
  requireTenantId(req) {
3097
3264
  const tenantId = this.tenantIdResolver(req);
3098
3265
  if (!tenantId) {
3099
- throw new NotFoundException5("Tenant-ID nicht im Request gefunden");
3266
+ throw new NotFoundException5({
3267
+ code: AUTH_ERROR_CODES5.TENANT_CONTEXT_MISSING,
3268
+ message: "No tenant ID found on the request"
3269
+ });
3100
3270
  }
3101
3271
  return tenantId;
3102
3272
  }
3103
3273
  async requireSubscription(tenantId) {
3104
3274
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
3105
3275
  if (!sub) {
3106
- throw new NotFoundException5(`Keine Subscription f\xFCr Tenant ${tenantId}`);
3276
+ throw new NotFoundException5({
3277
+ code: BILLING_ERROR_CODES6.SUBSCRIPTION_NOT_FOUND,
3278
+ message: `No subscription for tenant ${tenantId}`,
3279
+ params: {
3280
+ tenantId
3281
+ }
3282
+ });
3107
3283
  }
3108
3284
  return sub;
3109
3285
  }
@@ -3122,7 +3298,7 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
3122
3298
  try {
3123
3299
  await this.contractFreeze.freezeOnPlanChange(tenantId, sub.planVersion.planId, sub.billingCycle, /* @__PURE__ */ new Date());
3124
3300
  } catch (err) {
3125
- this.logger.error(`Contract-Freeze nach Bundle-Mutation fehlgeschlagen (tenant ${tenantId}): ${String(err)}`);
3301
+ this.logger.error(`Contract freeze after bundle mutation failed (tenant ${tenantId}): ${String(err)}`);
3126
3302
  }
3127
3303
  }
3128
3304
  };