@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
@@ -3,7 +3,7 @@ import {
3
3
  computeIncludedVat,
4
4
  getPlanPriceGross,
5
5
  round2
6
- } from "./chunk-R2QOSVAA.js";
6
+ } from "./chunk-DQKCK7DX.js";
7
7
  import {
8
8
  __name
9
9
  } from "./chunk-SHUYVCID.js";
@@ -50,15 +50,15 @@ function buildLabel(code, cycle) {
50
50
  maximumFractionDigits: 2
51
51
  })} \u20AC`;
52
52
  if (code.durationType === "ONCE") {
53
- return `${valueStr} einmalig`;
53
+ return `${valueStr} once`;
54
54
  }
55
55
  if (code.durationType === "MONTHS") {
56
56
  const m = code.durationValue ?? 0;
57
- return m === 1 ? `${valueStr} im ersten Monat` : `${valueStr} f\xFCr ${m} Monate`;
57
+ return m === 1 ? `${valueStr} for the first month` : `${valueStr} for ${m} months`;
58
58
  }
59
59
  const n = code.durationValue ?? 0;
60
- if (n === 1) return cycle === "YEARLY" ? `${valueStr} im ersten Jahr` : `${valueStr} im ersten Monat`;
61
- return cycle === "YEARLY" ? `${valueStr} in den ersten ${n} Jahren` : `${valueStr} in den ersten ${n} Monaten`;
60
+ if (n === 1) return cycle === "YEARLY" ? `${valueStr} for the first year` : `${valueStr} for the first month`;
61
+ return cycle === "YEARLY" ? `${valueStr} for the first ${n} years` : `${valueStr} for the first ${n} months`;
62
62
  }
63
63
  __name(buildLabel, "buildLabel");
64
64
 
@@ -74,6 +74,7 @@ var PROMO_SERVICE_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/Promo
74
74
 
75
75
  // src/promo/service.ts
76
76
  import { BadRequestException, Inject, Injectable, NotFoundException } from "@nestjs/common";
77
+ import { BILLING_ERROR_CODES, PROMO_ERROR_CODES } from "@saasicat/types";
77
78
  function _ts_decorate(decorators, target, key, desc) {
78
79
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
79
80
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -91,7 +92,23 @@ function _ts_param(paramIndex, decorator) {
91
92
  };
92
93
  }
93
94
  __name(_ts_param, "_ts_param");
94
- var CODE_PATTERN = /^[A-Z0-9_-]{4,32}$/;
95
+ var CODE_MIN_LENGTH = 4;
96
+ var CODE_MAX_LENGTH = 32;
97
+ var CODE_PATTERN = new RegExp(`^[A-Z0-9_-]{${CODE_MIN_LENGTH},${CODE_MAX_LENGTH}}$`);
98
+ var PERCENT_MIN_EXCLUSIVE = 0;
99
+ var PERCENT_MAX = 100;
100
+ var DURATION_MIN = 1;
101
+ var DURATION_MAX = 24;
102
+ function notRedeemable(reason) {
103
+ return new BadRequestException({
104
+ code: PROMO_ERROR_CODES.PROMO_CODE_NOT_REDEEMABLE,
105
+ message: `Code cannot be redeemed: ${reason}`,
106
+ params: {
107
+ reason
108
+ }
109
+ });
110
+ }
111
+ __name(notRedeemable, "notRedeemable");
95
112
  var PromoCodesService = class {
96
113
  static {
97
114
  __name(this, "PromoCodesService");
@@ -120,46 +137,118 @@ var PromoCodesService = class {
120
137
  async create(input) {
121
138
  const code = input.code.trim().toUpperCase();
122
139
  if (!CODE_PATTERN.test(code)) {
123
- throw new BadRequestException('Code darf nur Gro\xDFbuchstaben, Ziffern, \u201E-" und \u201E_" enthalten (4\u201332 Zeichen).');
140
+ throw new BadRequestException({
141
+ code: PROMO_ERROR_CODES.PROMO_CODE_FORMAT_INVALID,
142
+ message: 'The code may only contain upper-case letters, digits, "-" and "_" (4\u201332 characters).',
143
+ params: {
144
+ promoCode: code,
145
+ minLength: CODE_MIN_LENGTH,
146
+ maxLength: CODE_MAX_LENGTH
147
+ }
148
+ });
124
149
  }
125
150
  if (input.valueType === "PERCENT") {
126
- if (!(input.value > 0 && input.value <= 100)) {
127
- throw new BadRequestException("Prozentsatz muss zwischen 0 und 100 liegen.");
151
+ if (!(input.value > PERCENT_MIN_EXCLUSIVE && input.value <= PERCENT_MAX)) {
152
+ throw new BadRequestException({
153
+ code: PROMO_ERROR_CODES.PROMO_PERCENT_OUT_OF_RANGE,
154
+ message: "The percentage must be between 0 and 100.",
155
+ params: {
156
+ value: input.value,
157
+ min: PERCENT_MIN_EXCLUSIVE,
158
+ max: PERCENT_MAX
159
+ }
160
+ });
128
161
  }
129
162
  } else if (!(input.value > 0)) {
130
- throw new BadRequestException("Betrag muss positiv sein.");
163
+ throw new BadRequestException({
164
+ code: PROMO_ERROR_CODES.PROMO_AMOUNT_NOT_POSITIVE,
165
+ message: "The amount must be positive.",
166
+ params: {
167
+ value: input.value,
168
+ min: 0
169
+ }
170
+ });
131
171
  }
132
172
  if (input.durationType === "ONCE") {
133
173
  if (input.durationValue != null) {
134
- throw new BadRequestException("Bei einmaligem Rabatt darf keine Laufzeit gesetzt sein.");
174
+ throw new BadRequestException({
175
+ code: PROMO_ERROR_CODES.PROMO_ONE_OFF_WITH_DURATION,
176
+ message: "A one-off discount must not set a duration.",
177
+ params: {
178
+ durationValue: input.durationValue
179
+ }
180
+ });
135
181
  }
136
182
  } else {
137
183
  const v = input.durationValue;
138
- if (v == null || v < 1 || v > 24) {
139
- throw new BadRequestException("Laufzeit ung\xFCltig (max. 24 Monate bzw. Abrechnungsperioden).");
184
+ if (v == null || v < DURATION_MIN || v > DURATION_MAX) {
185
+ throw new BadRequestException({
186
+ code: PROMO_ERROR_CODES.PROMO_DURATION_INVALID,
187
+ message: "Invalid duration (at most 24 months or billing periods).",
188
+ params: {
189
+ durationValue: v ?? null,
190
+ min: DURATION_MIN,
191
+ max: DURATION_MAX
192
+ }
193
+ });
140
194
  }
141
195
  }
142
196
  if (input.validFrom && input.validUntil && input.validFrom >= input.validUntil) {
143
- throw new BadRequestException("G\xFCltigkeits-Fenster ung\xFCltig.");
197
+ throw new BadRequestException({
198
+ code: PROMO_ERROR_CODES.PROMO_VALIDITY_WINDOW_INVALID,
199
+ message: "Invalid validity window.",
200
+ params: {
201
+ validFrom: input.validFrom.toISOString(),
202
+ validUntil: input.validUntil.toISOString()
203
+ }
204
+ });
144
205
  }
145
206
  const plans = input.appliesToPlans ?? [];
146
207
  const blocked = this.config.nonRedeemablePlans ?? [];
147
208
  for (const p of plans) {
148
209
  if (blocked.includes(p)) {
149
- throw new BadRequestException(`${p}-Plan kann nicht rabattiert werden.`);
210
+ throw new BadRequestException({
211
+ code: PROMO_ERROR_CODES.PROMO_PLAN_NOT_DISCOUNTABLE,
212
+ message: `${p}-Plan cannot be discounted.`,
213
+ params: {
214
+ plan: p
215
+ }
216
+ });
150
217
  }
151
218
  }
152
219
  if (input.minimumPlanAmountGross != null && input.minimumPlanAmountGross <= 0) {
153
- throw new BadRequestException("Mindest-Plan-Brutto muss positiv sein.");
220
+ throw new BadRequestException({
221
+ code: PROMO_ERROR_CODES.PROMO_MIN_AMOUNT_NOT_POSITIVE,
222
+ message: "The minimum plan gross amount must be positive.",
223
+ params: {
224
+ value: input.minimumPlanAmountGross,
225
+ min: 0
226
+ }
227
+ });
154
228
  }
155
229
  if (input.valueType === "ABSOLUTE" && !input.allowZeroInvoice) {
156
230
  const lowestApplicableGross = this.lowestApplicablePlanGross(plans);
157
231
  if (lowestApplicableGross != null && input.value >= lowestApplicableGross) {
158
- throw new BadRequestException("Bei Absolutbetr\xE4gen muss der Rabatt unter dem niedrigsten anwendbaren Plan-Preis liegen oder allowZeroInvoice aktiviert sein.");
232
+ throw new BadRequestException({
233
+ code: PROMO_ERROR_CODES.PROMO_WOULD_PRODUCE_ZERO_INVOICE,
234
+ message: "For absolute amounts the discount must stay below the lowest applicable plan price, or allowZeroInvoice must be enabled.",
235
+ params: {
236
+ value: input.value,
237
+ lowestApplicablePlanGross: lowestApplicableGross
238
+ }
239
+ });
159
240
  }
160
241
  }
161
242
  const exists = await this.promoRepo.findByCode(code);
162
- if (exists) throw new BadRequestException("Code existiert bereits.");
243
+ if (exists) {
244
+ throw new BadRequestException({
245
+ code: PROMO_ERROR_CODES.PROMO_CODE_ALREADY_EXISTS,
246
+ message: "The code already exists.",
247
+ params: {
248
+ promoCode: code
249
+ }
250
+ });
251
+ }
163
252
  return this.promoRepo.create({
164
253
  ...input,
165
254
  code
@@ -167,10 +256,26 @@ var PromoCodesService = class {
167
256
  }
168
257
  async update(id, input) {
169
258
  const existing = await this.promoRepo.findById(id);
170
- if (!existing) throw new NotFoundException("Code nicht gefunden");
259
+ if (!existing) {
260
+ throw new NotFoundException({
261
+ code: PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
262
+ message: "Code not found",
263
+ params: {
264
+ promoCodeId: id,
265
+ promoCode: null
266
+ }
267
+ });
268
+ }
171
269
  if (input.maxRedemptions != null && existing.maxRedemptions != null) {
172
270
  if (input.maxRedemptions < existing.maxRedemptions) {
173
- throw new BadRequestException("maxRedemptions kann nicht gesenkt werden.");
271
+ throw new BadRequestException({
272
+ code: PROMO_ERROR_CODES.PROMO_MAX_REDEMPTIONS_LOWERED,
273
+ message: "maxRedemptions cannot be lowered.",
274
+ params: {
275
+ current: existing.maxRedemptions,
276
+ requested: input.maxRedemptions
277
+ }
278
+ });
174
279
  }
175
280
  }
176
281
  return this.promoRepo.update(id, input);
@@ -178,7 +283,14 @@ var PromoCodesService = class {
178
283
  async softDelete(id) {
179
284
  const redemptions = await this.redemptionRepo.countByPromoCode(id);
180
285
  if (redemptions > 0) {
181
- throw new BadRequestException("Code hat bereits Einl\xF6sungen \u2014 Soft-Delete nicht m\xF6glich. Stattdessen pausieren.");
286
+ throw new BadRequestException({
287
+ code: PROMO_ERROR_CODES.PROMO_CODE_HAS_REDEMPTIONS,
288
+ message: "The code already has redemptions \u2014 it cannot be soft-deleted. Pause it instead.",
289
+ params: {
290
+ promoCodeId: id,
291
+ redemptions
292
+ }
293
+ });
182
294
  }
183
295
  await this.promoRepo.softDelete(id);
184
296
  }
@@ -189,7 +301,16 @@ var PromoCodesService = class {
189
301
  async findOne(id) {
190
302
  await this.lazyExpire();
191
303
  const code = await this.promoRepo.findById(id);
192
- if (!code || code.deletedAt) throw new NotFoundException("Code nicht gefunden");
304
+ if (!code || code.deletedAt) {
305
+ throw new NotFoundException({
306
+ code: PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
307
+ message: "Code not found",
308
+ params: {
309
+ promoCodeId: id,
310
+ promoCode: null
311
+ }
312
+ });
313
+ }
193
314
  return code;
194
315
  }
195
316
  async stats(id) {
@@ -317,28 +438,51 @@ var PromoCodesService = class {
317
438
  const code = input.code.trim().toUpperCase();
318
439
  const promo = await this.promoRepo.findByCode(code, tx);
319
440
  if (!promo || promo.deletedAt) {
320
- throw new BadRequestException("Code nicht gefunden");
441
+ throw new BadRequestException({
442
+ code: PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
443
+ // The lookup runs on the code string, so there is no id to report.
444
+ message: "Code not found",
445
+ params: {
446
+ promoCodeId: null,
447
+ promoCode: code
448
+ }
449
+ });
321
450
  }
322
451
  const sub = await this.subscriptionLookup.findById(input.subscriptionId, tx);
323
- if (!sub) throw new NotFoundException("Subscription nicht gefunden");
452
+ if (!sub) {
453
+ throw new NotFoundException({
454
+ code: BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
455
+ message: "Subscription not found",
456
+ params: {
457
+ tenantId: input.tenantId
458
+ }
459
+ });
460
+ }
324
461
  if (sub.tenantId !== input.tenantId) {
325
- throw new BadRequestException("Subscription geh\xF6rt nicht zum Tenant");
462
+ throw new BadRequestException({
463
+ code: BILLING_ERROR_CODES.SUBSCRIPTION_TENANT_MISMATCH,
464
+ message: "Subscription does not belong to the tenant",
465
+ params: {
466
+ subscriptionId: sub.id,
467
+ tenantId: input.tenantId
468
+ }
469
+ });
326
470
  }
327
471
  const reason = this.checkEligibility(promo, {
328
472
  code,
329
473
  planId: sub.plan,
330
474
  billingCycle: sub.billingCycle
331
475
  });
332
- if (reason) throw new BadRequestException(`Code nicht einl\xF6sbar: ${reason}`);
476
+ if (reason) throw notRedeemable(reason);
333
477
  const firstTimeReason = await this.checkFirstTimeCustomer(promo, input.email, {
334
478
  requireEmail: true
335
479
  });
336
480
  if (firstTimeReason) {
337
- throw new BadRequestException(`Code nicht einl\xF6sbar: ${firstTimeReason}`);
481
+ throw notRedeemable(firstTimeReason);
338
482
  }
339
483
  const claimed = await this.promoRepo.claimSlot(promo.id, tx);
340
484
  if (!claimed) {
341
- throw new BadRequestException("Code nicht einl\xF6sbar: EXHAUSTED");
485
+ throw notRedeemable("EXHAUSTED");
342
486
  }
343
487
  await this.promoRepo.markExhaustedIfFull(promo.id, tx);
344
488
  const startsAt = sub.startedAt ?? /* @__PURE__ */ new Date();
@@ -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
  SaaSiCatPublicRoute
6
6
  } from "./chunk-YSC46BGM.js";
@@ -21,7 +21,7 @@ var CHECKOUT_OFFER_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/
21
21
 
22
22
  // src/checkout-offer/checkout-offer.service.ts
23
23
  import { ConflictException, Inject, Injectable, NotFoundException, Optional, UnprocessableEntityException } from "@nestjs/common";
24
- import { buildFeatureRequiresIndex, collectUnsatisfiedRequires, startOfUtcDay } from "@saasicat/types";
24
+ import { CONTRACT_ERROR_CODES, buildFeatureRequiresIndex, collectUnsatisfiedRequires, startOfUtcDay } from "@saasicat/types";
25
25
  function _ts_decorate(decorators, target, key, desc) {
26
26
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
27
27
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -59,7 +59,13 @@ var CheckoutOfferService = class {
59
59
  async getById(id) {
60
60
  const row = await this.repo.findById(id);
61
61
  if (!row) {
62
- throw new NotFoundException(`CheckoutOffer '${id}' nicht gefunden`);
62
+ throw new NotFoundException({
63
+ code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_NOT_FOUND,
64
+ message: `CheckoutOffer '${id}' not found`,
65
+ params: {
66
+ offerId: id
67
+ }
68
+ });
63
69
  }
64
70
  return row;
65
71
  }
@@ -77,7 +83,7 @@ var CheckoutOfferService = class {
77
83
  /** Customization in onboarding — only while the offer is `open`. */
78
84
  async update(id, data) {
79
85
  const existing = await this.getById(id);
80
- this.assertOpen(existing, "\xE4ndern");
86
+ this.assertOpen(existing, "changed");
81
87
  const next = this.normalizeUpdateData(existing, data);
82
88
  await this.assertFeatureRequiresSatisfied({
83
89
  projectKey: existing.projectKey,
@@ -95,7 +101,7 @@ var CheckoutOfferService = class {
95
101
  */
96
102
  async consume(id) {
97
103
  const existing = await this.getById(id);
98
- this.assertOpen(existing, "verbrauchen");
104
+ this.assertOpen(existing, "consumed");
99
105
  await this.assertBundleVersionsStillBookable(existing);
100
106
  return this.repo.consume(id);
101
107
  }
@@ -124,8 +130,8 @@ var CheckoutOfferService = class {
124
130
  ], requiresIndex);
125
131
  if (missingRequires.length > 0) {
126
132
  throw new UnprocessableEntityException({
127
- code: "CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED",
128
- message: `Die Paket-Auswahl deckt nicht alle Feature-Abh\xE4ngigkeiten: [${missingRequires.join(", ")}] fehlen in Plan + gew\xE4hlten Bundles.`,
133
+ code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED,
134
+ message: `The selected plan does not cover all feature dependencies: [${missingRequires.join(", ")}] are missing from the plan + selected bundles.`,
129
135
  missingRequires
130
136
  });
131
137
  }
@@ -205,8 +211,8 @@ var CheckoutOfferService = class {
205
211
  }
206
212
  if (violations.length > 0) {
207
213
  throw new UnprocessableEntityException({
208
- code: "CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE",
209
- message: "Mindestens eine BundleVersion aus dem CheckoutOffer ist nicht mehr buchbar.",
214
+ code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE,
215
+ message: "At least one bundle version from the checkout offer is no longer bookable.",
210
216
  violations
211
217
  });
212
218
  }
@@ -260,15 +266,15 @@ var CheckoutOfferService = class {
260
266
  const hasPlan = lineItems.some((item) => item.kind === "plan");
261
267
  if (!hasPlan) {
262
268
  throw new UnprocessableEntityException({
263
- code: "CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED",
264
- message: "CheckoutOffer ben\xF6tigt eine eingefrorene Plan-LineItem."
269
+ code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED,
270
+ message: "A checkout offer requires a frozen plan line item."
265
271
  });
266
272
  }
267
273
  const missingBundleVersionIds = input.bundleVersionIds.filter((versionId) => !lineItems.some((item) => item.kind === "bundle" && item.sourceVersionId === versionId));
268
274
  if (missingBundleVersionIds.length > 0) {
269
275
  throw new UnprocessableEntityException({
270
- code: "CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED",
271
- message: "Jede ausgew\xE4hlte BundleVersion ben\xF6tigt eine eingefrorene Bundle-LineItem.",
276
+ code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED,
277
+ message: "Every selected bundle version requires a frozen bundle line item.",
272
278
  bundleVersionIds: missingBundleVersionIds
273
279
  });
274
280
  }
@@ -300,13 +306,36 @@ var CheckoutOfferService = class {
300
306
  }
301
307
  assertOpen(existing, action) {
302
308
  if (existing.status === "consumed") {
303
- throw new ConflictException(`CheckoutOffer '${existing.id}' ist bereits verbraucht und kann nicht ${action} werden`);
309
+ throw new ConflictException({
310
+ code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_ALREADY_CONSUMED,
311
+ message: `Checkout offer '${existing.id}' has already been consumed and cannot be ${action}`,
312
+ params: {
313
+ offerId: existing.id,
314
+ action
315
+ }
316
+ });
304
317
  }
305
318
  if (existing.status === "expired") {
306
- throw new ConflictException(`CheckoutOffer '${existing.id}' ist abgelaufen und kann nicht ${action} werden`);
319
+ throw new ConflictException({
320
+ code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_EXPIRED,
321
+ message: `Checkout offer '${existing.id}' has expired and cannot be ${action}`,
322
+ params: {
323
+ offerId: existing.id,
324
+ action,
325
+ validUntil: existing.validUntil ?? null
326
+ }
327
+ });
307
328
  }
308
329
  if (this.isExpired(existing)) {
309
- throw new ConflictException(`CheckoutOffer '${existing.id}' ist abgelaufen und kann nicht ${action} werden`);
330
+ throw new ConflictException({
331
+ code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_EXPIRED,
332
+ message: `CheckoutOffer '${existing.id}' has expired and cannot be ${action}`,
333
+ params: {
334
+ offerId: existing.id,
335
+ action,
336
+ validUntil: existing.validUntil ?? null
337
+ }
338
+ });
310
339
  }
311
340
  }
312
341
  isExpired(row) {
@@ -385,7 +414,7 @@ var CreateCheckoutOfferDto = class {
385
414
  _ts_decorate2([
386
415
  IsString(),
387
416
  Matches(PROJECT_KEY_PATTERN, {
388
- message: "projectKey muss kebab-case sein"
417
+ message: "projectKey must be kebab-case"
389
418
  }),
390
419
  MaxLength(64),
391
420
  _ts_metadata2("design:type", String)
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  DiscoveryScanner
3
- } from "./chunk-Z3FWN3HJ.js";
3
+ } from "./chunk-5ZXBXYXU.js";
4
4
  import {
5
5
  DEFINES_QUOTA_KEY,
6
6
  DISCOVERY_SNAPSHOT_TOKEN,
@@ -44,7 +44,7 @@ async function runHeadlessDiscoveryScan(appModule, options = {}) {
44
44
  await app.close();
45
45
  }
46
46
  } catch (err) {
47
- console.error("[discovery-scan] FEHLER:", err instanceof Error ? err.message : err);
47
+ console.error("[discovery-scan] ERROR:", err instanceof Error ? err.message : err);
48
48
  return exit(4);
49
49
  }
50
50
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PromoCodesModule
3
- } from "./chunk-GLLCB4BP.js";
3
+ } from "./chunk-4O6R6ARQ.js";
4
4
  import {
5
5
  AdminManifestModule,
6
6
  AdminManifestService,
@@ -9,36 +9,39 @@ import {
9
9
  AdminStatsModule,
10
10
  MfaService,
11
11
  SuperAdminGuard
12
- } from "./chunk-PLJEAUFU.js";
12
+ } from "./chunk-FYWLPYBK.js";
13
13
  import {
14
14
  PublicCatalogModule,
15
15
  REQUIRE_FEATURE_KEY,
16
16
  SubscriptionBundleModule,
17
17
  TenantBillingModule
18
- } from "./chunk-5KH2O4SF.js";
18
+ } from "./chunk-NZLAYCEX.js";
19
19
  import {
20
20
  SubscriptionContractModule
21
- } from "./chunk-32EMXPU2.js";
21
+ } from "./chunk-B7NRY2LV.js";
22
22
  import {
23
23
  CatalogModule
24
- } from "./chunk-P3DP6GJE.js";
24
+ } from "./chunk-GC5AKN44.js";
25
25
  import {
26
26
  CheckoutOfferModule
27
- } from "./chunk-GJBXOCRZ.js";
27
+ } from "./chunk-XTVAFJ3P.js";
28
28
  import {
29
29
  SaaSiCatPublicRoute
30
30
  } from "./chunk-YSC46BGM.js";
31
31
  import {
32
32
  DiscoveryModule
33
- } from "./chunk-V34AA7C7.js";
33
+ } from "./chunk-6WL6VQ7Z.js";
34
34
  import {
35
35
  EntitlementModule,
36
36
  LimitExceededError
37
- } from "./chunk-EXOLOJFZ.js";
37
+ } from "./chunk-AGOG5B77.js";
38
38
  import {
39
39
  PLAN_CATALOG_TOKEN,
40
40
  PlanCatalogModule
41
- } from "./chunk-R2QOSVAA.js";
41
+ } from "./chunk-DQKCK7DX.js";
42
+ import {
43
+ codedError
44
+ } from "./chunk-EBJJNTW6.js";
42
45
  import {
43
46
  asProvider
44
47
  } from "./chunk-SABTXESR.js";
@@ -105,9 +108,7 @@ var SetupService = class {
105
108
  await this.assertNoSuperAdmin();
106
109
  const email = req.email.trim().toLowerCase();
107
110
  if (!email.includes("@")) {
108
- throw new BadRequestException({
109
- code: SETUP_ERROR_CODES.INVALID_EMAIL
110
- });
111
+ throw new BadRequestException(codedError(SETUP_ERROR_CODES.INVALID_EMAIL));
111
112
  }
112
113
  const generated = !req.password;
113
114
  const password = req.password ?? randomBytes(GENERATED_PASSWORD_BYTES).toString("base64url");
@@ -157,20 +158,19 @@ var SetupService = class {
157
158
  if (!expected) {
158
159
  throw new ForbiddenException({
159
160
  code: SETUP_ERROR_CODES.SETUP_DISABLED,
160
- message: `${this.config.setupTokenEnvVar} ist nicht gesetzt \u2014 Setup ist deaktiviert.`
161
+ message: `${this.config.setupTokenEnvVar} is not set \u2014 setup is disabled.`,
162
+ params: {
163
+ envVar: this.config.setupTokenEnvVar
164
+ }
161
165
  });
162
166
  }
163
167
  if (!token || !timingSafeEqualStr(token, expected)) {
164
- throw new UnauthorizedException({
165
- code: SETUP_ERROR_CODES.INVALID_SETUP_TOKEN
166
- });
168
+ throw new UnauthorizedException(codedError(SETUP_ERROR_CODES.INVALID_SETUP_TOKEN));
167
169
  }
168
170
  }
169
171
  async assertNoSuperAdmin() {
170
172
  if (await this.users.countSuperAdmins() > 0) {
171
- throw new ConflictException({
172
- code: SETUP_ERROR_CODES.SETUP_ALREADY_DONE
173
- });
173
+ throw new ConflictException(codedError(SETUP_ERROR_CODES.SETUP_ALREADY_DONE));
174
174
  }
175
175
  }
176
176
  };
@@ -448,6 +448,7 @@ StaticEntitlementService = _ts_decorate4([
448
448
  // src/platform/static-feature.guard.ts
449
449
  import { ForbiddenException as ForbiddenException2, Inject as Inject3, Injectable as Injectable3, Optional } from "@nestjs/common";
450
450
  import { Reflector } from "@nestjs/core";
451
+ import { AUTH_ERROR_CODES, FEATURE_NOT_LICENSED } from "@saasicat/types";
451
452
  function _ts_decorate5(decorators, target, key, desc) {
452
453
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
453
454
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -486,15 +487,32 @@ var StaticFeatureGuard = class {
486
487
  if (!required || required.length === 0) return true;
487
488
  const request = context.switchToHttp().getRequest();
488
489
  const user = request.user;
489
- if (!user) throw new ForbiddenException2("Nicht authentifiziert");
490
+ if (!user) {
491
+ throw new ForbiddenException2({
492
+ code: AUTH_ERROR_CODES.NOT_AUTHENTICATED,
493
+ message: "Not authenticated"
494
+ });
495
+ }
490
496
  const role = this.config?.userRoleResolver ? this.config.userRoleResolver(user) : user.role ?? user.platformRole;
491
497
  if (role === "SUPER_ADMIN") return true;
492
498
  const tenantId = this.config?.tenantIdResolver ? this.config.tenantIdResolver(request) : request.tenantId ?? user.tenantId;
493
- if (!tenantId) throw new ForbiddenException2("Kein Mandant zugeordnet");
499
+ if (!tenantId) {
500
+ throw new ForbiddenException2({
501
+ code: AUTH_ERROR_CODES.NO_TENANT_ASSIGNED,
502
+ message: "No tenant assigned"
503
+ });
504
+ }
494
505
  const snap = await this.entitlements.snapshot(tenantId);
495
506
  const allowed = required.some((f) => snap.features.includes(f));
496
507
  if (!allowed) {
497
- throw new ForbiddenException2(`Feature ${required.join(" / ")} nicht im aktuellen Plan enthalten.`);
508
+ const body = {
509
+ code: FEATURE_NOT_LICENSED,
510
+ featureKey: required[0],
511
+ featureKeys: required,
512
+ offers: [],
513
+ message: `Feature ${required.join(" / ")} is not included in the current plan.`
514
+ };
515
+ throw new ForbiddenException2(body);
498
516
  }
499
517
  return true;
500
518
  }
@@ -737,7 +755,7 @@ function buildTenantManifestController(options) {
737
755
  async get(req) {
738
756
  const tenantId = req.user?.tenantId ?? req.tenantId;
739
757
  if (!tenantId) {
740
- throw new Error("Kein Mandant im Request \u2014 TenantManifest braucht user.tenantId.");
758
+ throw new Error("No tenant on the request \u2014 TenantManifest requires user.tenantId.");
741
759
  }
742
760
  return this.svc.getManifest(tenantId);
743
761
  }
@@ -3,7 +3,7 @@ import { ExecutionContext } from '@nestjs/common';
3
3
  import { Reflector } from '@nestjs/core';
4
4
  import { SuperAdminProvisioningPort, SetupStatusResponse, SetupRequest, SetupResult, SetupConfirmMfaRequest, SetupConfirmMfaResponse } from '@saasicat/types';
5
5
  import { h as MfaService } from './admin-stats.service-Db2exT_d.js';
6
- import { f as SaasPlatformModuleOptions } from './saas-platform.module-DLhoqKNA.js';
6
+ import { f as SaasPlatformModuleOptions } from './saas-platform.module-DChhDrR-.js';
7
7
 
8
8
  /**
9
9
  * Shared metadata key for endpoints that intentionally run without application
@@ -3,7 +3,7 @@ import { ExecutionContext } from '@nestjs/common';
3
3
  import { Reflector } from '@nestjs/core';
4
4
  import { SuperAdminProvisioningPort, SetupStatusResponse, SetupRequest, SetupResult, SetupConfirmMfaRequest, SetupConfirmMfaResponse } from '@saasicat/types';
5
5
  import { h as MfaService } from './admin-stats.service-HUZIU448.cjs';
6
- import { f as SaasPlatformModuleOptions } from './saas-platform.module-D3rUTiho.cjs';
6
+ import { f as SaasPlatformModuleOptions } from './saas-platform.module-BfaYfL-G.cjs';
7
7
 
8
8
  /**
9
9
  * Shared metadata key for endpoints that intentionally run without application
@@ -117,7 +117,7 @@ interface ImplementsCapabilityMetadata extends ImplementsCapabilityOptions {
117
117
  * ```ts
118
118
  * @Post()
119
119
  * @ImplementsCapability('invoice.create', {
120
- * label: 'Rechnung erstellen',
120
+ * label: 'Create invoice',
121
121
  * feature: 'INVOICE_MANAGEMENT',
122
122
  * kind: 'endpoint',
123
123
  * owner: 'accounting',
@@ -157,7 +157,7 @@ declare const RequiresCapability: (...capabilityKeys: string[]) => _nestjs_commo
157
157
  * @Injectable()
158
158
  * @DefinesQuota({
159
159
  * key: 'invoicesPerMonth',
160
- * label: 'Rechnungen pro Monat',
160
+ * label: 'Invoices per month',
161
161
  * unit: 'invoices',
162
162
  * policy: 'monthlyReset',
163
163
  * feature: 'INVOICE_MANAGEMENT',
@@ -117,7 +117,7 @@ interface ImplementsCapabilityMetadata extends ImplementsCapabilityOptions {
117
117
  * ```ts
118
118
  * @Post()
119
119
  * @ImplementsCapability('invoice.create', {
120
- * label: 'Rechnung erstellen',
120
+ * label: 'Create invoice',
121
121
  * feature: 'INVOICE_MANAGEMENT',
122
122
  * kind: 'endpoint',
123
123
  * owner: 'accounting',
@@ -157,7 +157,7 @@ declare const RequiresCapability: (...capabilityKeys: string[]) => _nestjs_commo
157
157
  * @Injectable()
158
158
  * @DefinesQuota({
159
159
  * key: 'invoicesPerMonth',
160
- * label: 'Rechnungen pro Monat',
160
+ * label: 'Invoices per month',
161
161
  * unit: 'invoices',
162
162
  * policy: 'monthlyReset',
163
163
  * feature: 'INVOICE_MANAGEMENT',