@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
@@ -8,13 +8,16 @@ import {
8
8
  PROMO_SUBSCRIPTION_LOOKUP_TOKEN,
9
9
  PROMO_TRANSACTION_RUNNER_TOKEN,
10
10
  PromoCodesService
11
- } from "./chunk-E7AJ42AB.js";
11
+ } from "./chunk-VQQWF7MW.js";
12
12
  import {
13
13
  AdminAuditService
14
14
  } from "./chunk-E56W4U2P.js";
15
15
  import {
16
16
  SaaSiCatPublicRoute
17
17
  } from "./chunk-YSC46BGM.js";
18
+ import {
19
+ codedError
20
+ } from "./chunk-EBJJNTW6.js";
18
21
  import {
19
22
  asProvider
20
23
  } from "./chunk-SABTXESR.js";
@@ -58,7 +61,7 @@ var PromoCodeExpirer = class _PromoCodeExpirer {
58
61
  const codes = await this.promoRepo.expireDueCodes(now);
59
62
  const redemptions = await this.redemptionRepo.expireDueRedemptions(now);
60
63
  if (codes > 0 || redemptions > 0) {
61
- this.logger.log(`PromoCodeExpirer: ${codes} Codes, ${redemptions} Redemptions abgelaufen.`);
64
+ this.logger.log(`PromoCodeExpirer: ${codes} Codes, ${redemptions} redemptions expired.`);
62
65
  }
63
66
  }
64
67
  };
@@ -84,6 +87,7 @@ PromoCodeExpirer = _ts_decorate([
84
87
 
85
88
  // src/promo/rate-limit.guard.ts
86
89
  import { HttpException, HttpStatus, Injectable as Injectable2 } from "@nestjs/common";
90
+ import { REGISTRATION_ERROR_CODES } from "@saasicat/types";
87
91
  function _ts_decorate2(decorators, target, key, desc) {
88
92
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
89
93
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -95,6 +99,18 @@ var IP_WINDOW_MS = 6e4;
95
99
  var IP_LIMIT = 20;
96
100
  var SESSION_WINDOW_MS = 60 * 6e4;
97
101
  var SESSION_LIMIT = 50;
102
+ function rateLimited(windowMs) {
103
+ return new HttpException({
104
+ ...codedError(REGISTRATION_ERROR_CODES.RATE_LIMITED, {
105
+ retryAfterSeconds: Math.ceil(windowMs / 1e3)
106
+ }),
107
+ retryAfterSeconds: Math.ceil(windowMs / 1e3),
108
+ valid: false,
109
+ // Superseded by `code` — kept until all consumers read `code`.
110
+ reason: "RATE_LIMITED"
111
+ }, HttpStatus.TOO_MANY_REQUESTS);
112
+ }
113
+ __name(rateLimited, "rateLimited");
98
114
  var PromoCodeRateLimitGuard = class {
99
115
  static {
100
116
  __name(this, "PromoCodeRateLimitGuard");
@@ -106,16 +122,10 @@ var PromoCodeRateLimitGuard = class {
106
122
  const ipKey = ipFingerprint(req);
107
123
  const sessionKey = req.user?.id ?? null;
108
124
  if (this.exceeded(this.ipBuckets, ipKey, IP_WINDOW_MS, IP_LIMIT)) {
109
- throw new HttpException({
110
- valid: false,
111
- reason: "RATE_LIMITED"
112
- }, HttpStatus.TOO_MANY_REQUESTS);
125
+ throw rateLimited(IP_WINDOW_MS);
113
126
  }
114
127
  if (sessionKey && this.exceeded(this.sessionBuckets, sessionKey, SESSION_WINDOW_MS, SESSION_LIMIT)) {
115
- throw new HttpException({
116
- valid: false,
117
- reason: "RATE_LIMITED"
118
- }, HttpStatus.TOO_MANY_REQUESTS);
128
+ throw rateLimited(SESSION_WINDOW_MS);
119
129
  }
120
130
  return true;
121
131
  }
@@ -179,21 +189,21 @@ var PreviewPromoCodeDto = class {
179
189
  _ts_decorate3([
180
190
  IsString(),
181
191
  Matches(CODE_PATTERN, {
182
- message: 'code muss aus A\u2013Z, 0\u20139, "-" und "_" bestehen (4\u201332 Zeichen)'
192
+ message: 'code must consist of A\u2013Z, 0\u20139, "-" and "_" (4\u201332 characters)'
183
193
  }),
184
194
  _ts_metadata2("design:type", String)
185
195
  ], PreviewPromoCodeDto.prototype, "code", void 0);
186
196
  _ts_decorate3([
187
197
  IsString(),
188
198
  Matches(PLAN_OR_CYCLE_PATTERN, {
189
- message: "plan muss SCREAMING_SNAKE_CASE sein"
199
+ message: "plan must be SCREAMING_SNAKE_CASE"
190
200
  }),
191
201
  _ts_metadata2("design:type", String)
192
202
  ], PreviewPromoCodeDto.prototype, "plan", void 0);
193
203
  _ts_decorate3([
194
204
  IsString(),
195
205
  Matches(PLAN_OR_CYCLE_PATTERN, {
196
- message: "billingCycle muss SCREAMING_SNAKE_CASE sein (z. B. MONTHLY, YEARLY)"
206
+ message: "billingCycle must be SCREAMING_SNAKE_CASE (e.g. MONTHLY, YEARLY)"
197
207
  }),
198
208
  _ts_metadata2("design:type", String)
199
209
  ], PreviewPromoCodeDto.prototype, "billingCycle", void 0);
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  DiscoverySnapshotNotFoundError,
3
3
  loadDiscoverySnapshotFromFile
4
- } from "./chunk-ABB2CRAS.js";
4
+ } from "./chunk-74SCJ4HF.js";
5
5
  import {
6
6
  ADVISORY_STRICT_MODE_CODES,
7
7
  validateBundleDraft,
8
8
  validatePlanDraft
9
- } from "./chunk-P3DP6GJE.js";
9
+ } from "./chunk-GC5AKN44.js";
10
10
  import {
11
11
  __name
12
12
  } from "./chunk-SHUYVCID.js";
@@ -66,9 +66,9 @@ function runPreflight(input) {
66
66
  __name(runPreflight, "runPreflight");
67
67
  function formatPreflightReport(report) {
68
68
  const lines = [];
69
- lines.push(`Preflight (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7 Gesamt: ${report.counts.total}`, "");
69
+ lines.push(`Preflight (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7 Total: ${report.counts.total}`, "");
70
70
  if (report.findings.length === 0) {
71
- lines.push(" \u2713 Keine Strict-Mode-Verst\xF6\xDFe gefunden.");
71
+ lines.push(" \u2713 No strict-mode violations found.");
72
72
  return lines.join("\n");
73
73
  }
74
74
  let lastEntity = "";
@@ -138,9 +138,9 @@ function validateSeedAgainstSnapshot(input) {
138
138
  __name(validateSeedAgainstSnapshot, "validateSeedAgainstSnapshot");
139
139
  function formatSeedGateReport(report) {
140
140
  const lines = [];
141
- lines.push(`Seed-Gate (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7 Gesamt: ${report.counts.total}`, "");
141
+ lines.push(`Seed-Gate (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7 Total: ${report.counts.total}`, "");
142
142
  if (report.findings.length === 0) {
143
- lines.push(" \u2713 Alle geseedeten Plan-/Bundle-Features sind discovered (und, sofern gepr\xFCft, approved).");
143
+ lines.push(" \u2713 Every seeded plan/bundle feature is discovered (and approved where checked).");
144
144
  return lines.join("\n");
145
145
  }
146
146
  let lastEntity = "";
@@ -174,10 +174,10 @@ function runSeedGateFromFile(options) {
174
174
  } catch (err) {
175
175
  if (err instanceof DiscoverySnapshotNotFoundError) {
176
176
  if (mode === "blocking") {
177
- error(`[seed-gate] FEHLER: Kein Discovery-Snapshot unter ${options.snapshotPath} \u2014 im blocking-Modus Pflicht (Headless-Scan vor dem Seed ausf\xFChren, #23).`);
177
+ error(`[seed-gate] ERROR: no discovery snapshot at ${options.snapshotPath} \u2014 required in blocking mode (run the headless scan before seeding, #23).`);
178
178
  return exit(4);
179
179
  }
180
- warn(`[seed-gate] Kein Discovery-Snapshot unter ${options.snapshotPath} \u2014 \xFCbersprungen (report-only).`);
180
+ warn(`[seed-gate] No discovery snapshot at ${options.snapshotPath} \u2014 skipped (report-only).`);
181
181
  return null;
182
182
  }
183
183
  throw err;
@@ -191,10 +191,10 @@ function runSeedGateFromFile(options) {
191
191
  log(formatSeedGateReport(report));
192
192
  if (report.overall === "error") {
193
193
  if (mode === "blocking") {
194
- error("[seed-gate] blocking: Verst\xF6\xDFe gefunden \u2014 Seed wird abgebrochen (#23).");
194
+ error("[seed-gate] blocking: violations found \u2014 aborting the seed (#23).");
195
195
  return exit(seedGateExitCode(report));
196
196
  }
197
- warn("[seed-gate] report-only: Verst\xF6\xDFe gefunden, Seed l\xE4uft trotzdem weiter.");
197
+ warn("[seed-gate] report-only: violations found, seeding continues anyway.");
198
198
  }
199
199
  return report;
200
200
  }
@@ -74,7 +74,7 @@ var DiscoveryScanner = class _DiscoveryScanner {
74
74
  writeFileSync(path, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
75
75
  this.logger.log(`Discovery snapshot persisted to ${path}`);
76
76
  } catch (err) {
77
- this.logger.warn(`Discovery snapshot konnte nicht nach ${path} geschrieben werden: ${err.message}`);
77
+ this.logger.warn(`The discovery snapshot could not be written to ${path}: ${err.message}`);
78
78
  }
79
79
  }
80
80
  /**
@@ -159,7 +159,7 @@ var DiscoveryScanner = class _DiscoveryScanner {
159
159
  registerCapability(target, meta, declaredAt) {
160
160
  const existing = target.get(meta.capabilityKey);
161
161
  if (existing) {
162
- this.logger.warn(`Capability '${meta.capabilityKey}' wird an mehreren Stellen deklariert (${existing.declaredAt} und ${declaredAt}); erste Deklaration gewinnt`);
162
+ this.logger.warn(`Capability '${meta.capabilityKey}' is declared in several places (${existing.declaredAt} and ${declaredAt}); the first declaration wins`);
163
163
  return;
164
164
  }
165
165
  target.set(meta.capabilityKey, {
@@ -180,7 +180,7 @@ var DiscoveryScanner = class _DiscoveryScanner {
180
180
  registerQuota(target, opts, declaredAt) {
181
181
  const existing = target.get(opts.key);
182
182
  if (existing) {
183
- this.logger.warn(`Quota '${opts.key}' wird an mehreren Stellen definiert (${existing.declaredAt} und ${declaredAt}); erste Definition gewinnt`);
183
+ this.logger.warn(`Quota '${opts.key}' is defined in several places (${existing.declaredAt} and ${declaredAt}); the first definition wins`);
184
184
  return;
185
185
  }
186
186
  target.set(opts.key, {
@@ -2,7 +2,7 @@ import {
2
2
  DISCOVERY_APP_INFO_TOKEN,
3
3
  DISCOVERY_SNAPSHOT_PATH_TOKEN,
4
4
  DiscoveryScanner
5
- } from "./chunk-Z3FWN3HJ.js";
5
+ } from "./chunk-5ZXBXYXU.js";
6
6
  import {
7
7
  DISCOVERY_SNAPSHOT_TOKEN
8
8
  } from "./chunk-L4SUWH5B.js";
@@ -11,7 +11,7 @@ var DiscoverySnapshotNotFoundError = class extends Error {
11
11
  }
12
12
  path;
13
13
  constructor(path) {
14
- super(`Discovery-Snapshot-Datei nicht gefunden: ${path}. Booten Sie zuerst die App mit DISCOVERY_SNAPSHOT_PATH oder DiscoveryModule.forRoot({ snapshotPath: '\u2026' }), damit der Scanner die Datei beim OnApplicationBootstrap-Hook schreibt.`), this.path = path;
14
+ super(`Discovery snapshot file not found: ${path}. Boot the app first with DISCOVERY_SNAPSHOT_PATH or DiscoveryModule.forRoot({ snapshotPath: '\u2026' }) so that the scanner writes the file on the OnApplicationBootstrap hook.`), this.path = path;
15
15
  this.name = "DiscoverySnapshotNotFoundError";
16
16
  }
17
17
  };
@@ -1,10 +1,13 @@
1
1
  import {
2
2
  PLAN_CATALOG_TOKEN,
3
3
  isFeaturePlannedOnly
4
- } from "./chunk-R2QOSVAA.js";
4
+ } from "./chunk-DQKCK7DX.js";
5
5
  import {
6
6
  SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN
7
7
  } from "./chunk-NYLON2VC.js";
8
+ import {
9
+ codedError
10
+ } from "./chunk-EBJJNTW6.js";
8
11
  import {
9
12
  ENTITLEMENT_RESOLUTION_CONFIG_TOKEN,
10
13
  ENTITLEMENT_SERVICE_TOKEN,
@@ -250,7 +253,7 @@ function assertNoReplacesCycle(index) {
250
253
  ...path.slice(cycleStart),
251
254
  key
252
255
  ].join(" \u2192 ");
253
- throw new Error(`replaces-Zyklus in den Discovery-Metadaten: ${cycle}. Feature-Ersetzungen m\xFCssen azyklisch sein \u2014 replaces-Deklarationen im Code korrigieren (#39).`);
256
+ throw new Error(`replaces cycle in the discovery metadata: ${cycle}. Feature replacements must be acyclic \u2014 fix the replaces declarations in code (#39).`);
254
257
  }
255
258
  state.set(key, IN_PROGRESS);
256
259
  for (const successor of index.get(key) ?? []) {
@@ -294,7 +297,7 @@ var LimitExceededError = class extends Error {
294
297
  used;
295
298
  code = LIMIT_EXCEEDED;
296
299
  constructor(dimension, max, used) {
297
- super(`Limit f\xFCr ${dimension} erreicht: ${used}/${max}.`), this.dimension = dimension, this.max = max, this.used = used;
300
+ super(`Limit for ${dimension} reached: ${used}/${max}.`), this.dimension = dimension, this.max = max, this.used = used;
298
301
  this.name = "LimitExceededError";
299
302
  }
300
303
  };
@@ -304,7 +307,8 @@ function isLimitExceededError(err) {
304
307
  __name(isLimitExceededError, "isLimitExceededError");
305
308
 
306
309
  // src/entitlement/service.ts
307
- import { Inject, Injectable, NotFoundException, Optional } from "@nestjs/common";
310
+ import { Inject, Injectable, InternalServerErrorException, NotFoundException, Optional } from "@nestjs/common";
311
+ import { BILLING_ERROR_CODES } from "@saasicat/types";
308
312
 
309
313
  // src/billing/subscription-bundles.tokens.ts
310
314
  var SUBSCRIPTION_BUNDLE_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/SubscriptionBundleRepository");
@@ -371,7 +375,13 @@ var EntitlementService = class {
371
375
  if (cached) return cached;
372
376
  const sub = await this.subscriptions.findByTenantId(tenantId);
373
377
  if (!sub) {
374
- throw new NotFoundException(`Keine Subscription f\xFCr Tenant ${tenantId}`);
378
+ throw new NotFoundException({
379
+ code: BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
380
+ message: `No subscription for tenant ${tenantId}`,
381
+ params: {
382
+ tenantId
383
+ }
384
+ });
375
385
  }
376
386
  const limits = await this.deriveLimits(sub, now);
377
387
  this.writeCache(tenantId, limits, now.getTime());
@@ -444,7 +454,7 @@ var EntitlementService = class {
444
454
  return Promise.all(active.map(async (booking) => {
445
455
  const bv = await this.bundles.findVersionById(booking.bundleVersionId, tx);
446
456
  if (!bv) {
447
- throw new Error(`BundleVersion '${booking.bundleVersionId}' aus aktiver SubscriptionBundle nicht gefunden`);
457
+ throw new Error(`BundleVersion '${booking.bundleVersionId}' from an active subscription bundle not found`);
448
458
  }
449
459
  return {
450
460
  bundleKey: bv.bundleKey,
@@ -466,12 +476,21 @@ var EntitlementService = class {
466
476
  return this.tx.run(async (tx) => {
467
477
  const sub = await this.subscriptions.findByTenantIdLocked(input.tenantId, tx);
468
478
  if (!sub) {
469
- throw new NotFoundException(`Keine Subscription f\xFCr Tenant ${input.tenantId}`);
479
+ throw new NotFoundException({
480
+ code: BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
481
+ message: `No subscription for tenant ${input.tenantId}`,
482
+ params: {
483
+ tenantId: input.tenantId
484
+ }
485
+ });
470
486
  }
471
487
  const limits = await this.deriveLimits(sub, now, tx);
472
488
  const max = limits.quotas[input.dimension];
473
489
  if (max === void 0) {
474
- throw new Error(`Quota-Dimension "${input.dimension}" nicht im PlanCatalog (${this.catalog.projectKey}).`);
490
+ throw new InternalServerErrorException(codedError(BILLING_ERROR_CODES.QUOTA_DIMENSION_UNKNOWN, {
491
+ dimension: input.dimension,
492
+ projectKey: this.catalog.projectKey
493
+ }));
475
494
  }
476
495
  const used = await input.currentUsage(tx, input.tenantId);
477
496
  if (max !== -1 && used + delta > max) {
@@ -492,7 +511,15 @@ var EntitlementService = class {
492
511
  async findActivePlanVersionOrFallback(planId, asOf, tx) {
493
512
  const v = this.planVersions.findActive ? await this.planVersions.findActive(planId, asOf, tx) : await this.planVersions.findLatestLive(planId, tx);
494
513
  if (!v) {
495
- throw new NotFoundException(`Keine zu ${asOf.toISOString().slice(0, 10)} aktive PlanVersion f\xFCr ${planId} \u2014 weder validFrom-window erf\xFCllt noch latest-live verf\xFCgbar.`);
514
+ const asOfDate = asOf.toISOString().slice(0, 10);
515
+ throw new NotFoundException({
516
+ code: BILLING_ERROR_CODES.NO_ACTIVE_PLAN_VERSION,
517
+ message: `No plan version for ${planId} active as of ${asOfDate} \u2014 neither the validFrom window is satisfied nor is a latest-live version available.`,
518
+ params: {
519
+ planId,
520
+ asOf: asOfDate
521
+ }
522
+ });
496
523
  }
497
524
  return v;
498
525
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  appendImplicitDiscountLineItem
3
- } from "./chunk-MDIZUVIK.js";
3
+ } from "./chunk-BSK6YBLI.js";
4
4
  import {
5
5
  SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN
6
6
  } from "./chunk-NYLON2VC.js";
@@ -13,6 +13,7 @@ import {
13
13
 
14
14
  // src/subscription-contract/subscription-contract.service.ts
15
15
  import { ConflictException, Inject, Injectable, NotFoundException, UnprocessableEntityException } from "@nestjs/common";
16
+ import { CONTRACT_ERROR_CODES } from "@saasicat/types";
16
17
  function _ts_decorate(decorators, target, key, desc) {
17
18
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
18
19
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -44,7 +45,13 @@ var SubscriptionContractService = class {
44
45
  async getById(contractId) {
45
46
  const row = await this.repo.findById(contractId);
46
47
  if (!row) {
47
- throw new NotFoundException(`SubscriptionContract '${contractId}' nicht gefunden`);
48
+ throw new NotFoundException({
49
+ code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_NOT_FOUND,
50
+ message: `SubscriptionContract '${contractId}' not found`,
51
+ params: {
52
+ contractId
53
+ }
54
+ });
48
55
  }
49
56
  return row;
50
57
  }
@@ -54,7 +61,14 @@ var SubscriptionContractService = class {
54
61
  async getActiveInvoiceSnapshotForTenant(tenantId, asOf = /* @__PURE__ */ new Date()) {
55
62
  const contract = await this.repo.findActiveByTenantId(tenantId, asOf);
56
63
  if (!contract) {
57
- throw new NotFoundException(`Kein aktiver SubscriptionContract f\xFCr Tenant ${tenantId}`);
64
+ throw new NotFoundException({
65
+ code: CONTRACT_ERROR_CODES.NO_ACTIVE_SUBSCRIPTION_CONTRACT,
66
+ message: `No active subscription contract for tenant ${tenantId}`,
67
+ params: {
68
+ tenantId,
69
+ asOf: asOf.toISOString()
70
+ }
71
+ });
58
72
  }
59
73
  return subscriptionContractToInvoiceSnapshot(contract);
60
74
  }
@@ -87,7 +101,14 @@ var SubscriptionContractService = class {
87
101
  }
88
102
  createDataFromOffer(offer, options) {
89
103
  if (offer.status !== "consumed") {
90
- throw new ConflictException(`CheckoutOffer '${offer.id}' muss vor Contract-Erstellung consumed sein`);
104
+ throw new ConflictException({
105
+ code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_NOT_CONSUMED,
106
+ message: `CheckoutOffer '${offer.id}' must be consumed before the contract is created`,
107
+ params: {
108
+ offerId: offer.id,
109
+ status: offer.status
110
+ }
111
+ });
91
112
  }
92
113
  const lineItems = this.lineItemsFromOffer(offer);
93
114
  return {
@@ -120,8 +141,8 @@ var SubscriptionContractService = class {
120
141
  const source = offer.lineItems ?? [];
121
142
  if (source.length === 0) {
122
143
  throw new UnprocessableEntityException({
123
- code: "CHECKOUT_OFFER_LINE_ITEMS_REQUIRED",
124
- message: "Aus einem CheckoutOffer kann nur ein Contract entstehen, wenn LineItems eingefroren sind."
144
+ code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_LINE_ITEMS_REQUIRED,
145
+ message: "A checkout offer can yield only one contract, and only once its line items are frozen."
125
146
  });
126
147
  }
127
148
  return appendImplicitDiscountLineItem({
@@ -159,8 +180,11 @@ var SubscriptionContractService = class {
159
180
  const date = value instanceof Date ? new Date(value) : new Date(value);
160
181
  if (Number.isNaN(date.getTime())) {
161
182
  throw new UnprocessableEntityException({
162
- code: "SUBSCRIPTION_CONTRACT_INVALID_DATE",
163
- message: `${field} muss ein g\xFCltiges Datum sein.`
183
+ code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_DATE,
184
+ message: `${field} must be a valid date.`,
185
+ params: {
186
+ field
187
+ }
164
188
  });
165
189
  }
166
190
  return date;
@@ -180,32 +204,39 @@ var SubscriptionContractService = class {
180
204
  assertCreateData(data) {
181
205
  if (data.lineItems.length === 0) {
182
206
  throw new UnprocessableEntityException({
183
- code: "SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED",
184
- message: "SubscriptionContract ben\xF6tigt mindestens eine LineItem."
207
+ code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED,
208
+ message: "A subscription contract requires at least one line item."
185
209
  });
186
210
  }
187
211
  if (data.effectiveUntil && data.effectiveUntil <= data.effectiveFrom) {
188
212
  throw new UnprocessableEntityException({
189
- code: "SUBSCRIPTION_CONTRACT_INVALID_WINDOW",
190
- message: "effectiveUntil muss nach effectiveFrom liegen."
213
+ code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_WINDOW,
214
+ message: "effectiveUntil must be after effectiveFrom."
191
215
  });
192
216
  }
193
217
  const planLineItems = data.lineItems.filter((item) => item.kind === "plan");
194
218
  if (planLineItems.length !== 1) {
195
219
  throw new UnprocessableEntityException({
196
- code: "SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED",
197
- message: "SubscriptionContract ben\xF6tigt genau eine Plan-Grundposition."
220
+ code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED,
221
+ message: "A subscription contract requires exactly one plan base item."
198
222
  });
199
223
  }
200
224
  }
201
225
  assertTerminable(existing, data) {
202
226
  if (existing.status === "terminated" || existing.status === "superseded") {
203
- throw new ConflictException(`SubscriptionContract '${existing.id}' ist bereits geschlossen`);
227
+ throw new ConflictException({
228
+ code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_ALREADY_CLOSED,
229
+ message: `SubscriptionContract '${existing.id}' is already closed`,
230
+ params: {
231
+ contractId: existing.id,
232
+ status: existing.status
233
+ }
234
+ });
204
235
  }
205
236
  if (data.effectiveUntil <= existing.effectiveFrom) {
206
237
  throw new UnprocessableEntityException({
207
- code: "SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START",
208
- message: "effectiveUntil muss nach effectiveFrom des Contracts liegen."
238
+ code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START,
239
+ message: "effectiveUntil must be after the effectiveFrom of the contract."
209
240
  });
210
241
  }
211
242
  }
@@ -40,7 +40,7 @@ function createDiscountLineItem(input, discountNet) {
40
40
  kind: "discount",
41
41
  sourceKey: promoCode?.code ?? firstPromotion?.id ?? "price-discount",
42
42
  sourceVersionId: null,
43
- titleSnapshot: promoCode?.label ?? firstPromotion?.label ?? "Rabatt",
43
+ titleSnapshot: promoCode?.label ?? firstPromotion?.label ?? "Discount",
44
44
  descriptionSnapshot: promoCode ? `Promo-Code ${promoCode.code}` : null,
45
45
  quantity: 1,
46
46
  unit: null,
@@ -152,7 +152,7 @@ __name(findPlan, "findPlan");
152
152
  function getPlanOrThrow(catalog, planId) {
153
153
  const plan = findPlan(catalog, planId);
154
154
  if (!plan) {
155
- throw new Error(`Plan "${planId}" nicht im Catalog (${catalog.projectKey}) gefunden`);
155
+ throw new Error(`Plan "${planId}" not found in the catalog (${catalog.projectKey})`);
156
156
  }
157
157
  return plan;
158
158
  }
@@ -0,0 +1,22 @@
1
+ import {
2
+ __name
3
+ } from "./chunk-SHUYVCID.js";
4
+
5
+ // src/errors/coded-error.ts
6
+ import { ERROR_MESSAGES_EN, formatErrorMessage } from "@saasicat/types";
7
+ function codedError(code, params) {
8
+ const message = formatErrorMessage(ERROR_MESSAGES_EN[code], params ?? {});
9
+ return params ? {
10
+ code,
11
+ message,
12
+ params
13
+ } : {
14
+ code,
15
+ message
16
+ };
17
+ }
18
+ __name(codedError, "codedError");
19
+
20
+ export {
21
+ codedError
22
+ };