@saasicat/nest 0.19.0 → 0.22.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 (59) hide show
  1. package/dist/_entries.cjs +358 -391
  2. package/dist/admin/index.js +2 -1
  3. package/dist/{aggregation-o0LMenvl.d.ts → aggregation-DP5zfKbU.d.ts} +3 -3
  4. package/dist/{aggregation-SKfXu09x.d.cts → aggregation-mZ7WZROK.d.cts} +3 -3
  5. package/dist/billing/index.d.cts +13 -12
  6. package/dist/billing/index.d.ts +13 -12
  7. package/dist/billing/index.js +4 -3
  8. package/dist/catalog/index.d.cts +10 -10
  9. package/dist/catalog/index.d.ts +10 -10
  10. package/dist/catalog/index.js +2 -2
  11. package/dist/{catalog.module-BKObceo0.d.ts → catalog.module-CQr1FpkI.d.ts} +6 -6
  12. package/dist/{catalog.module-Dw2Hrbyu.d.cts → catalog.module-D-_pQDNA.d.cts} +6 -6
  13. package/dist/checkout-offer/index.js +1 -1
  14. package/dist/{chunk-EUU3B6NB.js → chunk-4O6R6ARQ.js} +12 -6
  15. package/dist/{chunk-LXUBCLRK.js → chunk-7X5MSAIL.js} +16 -15
  16. package/dist/{chunk-AFK2IE2L.js → chunk-7ZPFQXHM.js} +1 -1
  17. package/dist/chunk-EBJJNTW6.js +22 -0
  18. package/dist/{chunk-6WA4X5U3.js → chunk-MJ32VNZD.js} +41 -91
  19. package/dist/{chunk-F6DCLVBB.js → chunk-NNNXXUPT.js} +9 -3
  20. package/dist/{chunk-7JIHG53V.js → chunk-SN5UOC43.js} +7 -4
  21. package/dist/{chunk-KUDKD7ZG.js → chunk-UFS344JD.js} +26 -22
  22. package/dist/{chunk-I77VS3YH.js → chunk-UO64MK3Q.js} +18 -24
  23. package/dist/{chunk-QLJHIIOY.js → chunk-VQQWF7MW.js} +7 -3
  24. package/dist/{chunk-ORWIGSNN.js → chunk-XTVAFJ3P.js} +2 -1
  25. package/dist/{define-saasicat-D8c6Bs4T.d.cts → define-saasicat-ByPjtKPy.d.cts} +1 -1
  26. package/dist/{define-saasicat-CqkhnHn-.d.ts → define-saasicat-C99hWAUK.d.ts} +1 -1
  27. package/dist/discovery/index.d.cts +2 -2
  28. package/dist/discovery/index.d.ts +2 -2
  29. package/dist/{enforce-quota.interceptor-Dk0eEtzV.d.cts → enforce-quota.interceptor-BrsU1UYJ.d.cts} +1 -1
  30. package/dist/{enforce-quota.interceptor-Dp8wTYro.d.ts → enforce-quota.interceptor-reO-i71U.d.ts} +1 -1
  31. package/dist/entitlement/index.d.cts +1 -1
  32. package/dist/entitlement/index.d.ts +1 -1
  33. package/dist/entitlement/index.js +2 -1
  34. package/dist/index.d.cts +30 -11
  35. package/dist/index.d.ts +30 -11
  36. package/dist/index.js +14 -10
  37. package/dist/{module-BwD-wAor.d.ts → module-BE17XFLg.d.ts} +1 -1
  38. package/dist/{module-DsDkbawm.d.cts → module-CHMIyWlf.d.cts} +1 -1
  39. package/dist/{plan-versions.service-BB36Q1bg.d.ts → plan-versions.service-B-YoE4SZ.d.ts} +1 -1
  40. package/dist/{plan-versions.service-CUR2Whyr.d.cts → plan-versions.service-_saKzb50.d.cts} +1 -1
  41. package/dist/platform/index.d.cts +11 -11
  42. package/dist/platform/index.d.ts +11 -11
  43. package/dist/platform/index.js +9 -8
  44. package/dist/promo/index.d.cts +3 -3
  45. package/dist/promo/index.d.ts +3 -3
  46. package/dist/promo/index.js +3 -2
  47. package/dist/registration/index.d.cts +1 -1
  48. package/dist/registration/index.d.ts +1 -1
  49. package/dist/registration/index.js +2 -1
  50. package/dist/{saas-platform.module-BQmrDGVU.d.ts → saas-platform.module-DolE41YY.d.ts} +2 -2
  51. package/dist/{saas-platform.module-DducnQ9s.d.cts → saas-platform.module-HCDNZTiE.d.cts} +2 -2
  52. package/dist/{service-DPZ2W1E-.d.cts → service-D48clObR.d.cts} +1 -1
  53. package/dist/{service-DPZ2W1E-.d.ts → service-D48clObR.d.ts} +1 -1
  54. package/dist/{tenant-billing.controller-b1G1yB09.d.ts → tenant-billing.controller-7o4ObzGs.d.ts} +3 -3
  55. package/dist/{tenant-billing.controller-m42YQ_Ei.d.cts → tenant-billing.controller-DubP6-Md.d.cts} +3 -3
  56. package/dist/testing/index.d.cts +6 -6
  57. package/dist/testing/index.d.ts +6 -6
  58. package/dist/testing/index.js +10 -9
  59. package/package.json +3 -3
@@ -330,7 +330,7 @@ var BundlesService = class _BundlesService {
330
330
  code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
331
331
  message: `BundleVersion '${versionId}' not found`,
332
332
  params: {
333
- versionId
333
+ bundleVersionId: versionId
334
334
  }
335
335
  });
336
336
  }
@@ -364,7 +364,7 @@ var BundlesService = class _BundlesService {
364
364
  code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
365
365
  message: `BundleVersion '${versionId}' not found`,
366
366
  params: {
367
- versionId
367
+ bundleVersionId: versionId
368
368
  }
369
369
  });
370
370
  }
@@ -461,7 +461,7 @@ var BundlesService = class _BundlesService {
461
461
  code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
462
462
  message: `BundleVersion '${versionId}' not found`,
463
463
  params: {
464
- versionId
464
+ bundleVersionId: versionId
465
465
  }
466
466
  });
467
467
  }
@@ -497,7 +497,7 @@ var BundlesService = class _BundlesService {
497
497
  * Publishes a draft version atomically. Steps:
498
498
  * 1. Strict check on the draft definition (warn-only or blocking)
499
499
  * 2. Enforce validFrom requirement + ordering against the predecessor
500
- * (SPEC_V2 §4.2 + §11.1 M6 Pack 2c, analogous to PlanVersion)
500
+ * (analogous to PlanVersion)
501
501
  * 3. Compute the diff against the predecessor version (latest live) →
502
502
  * `publishedChanges` + `nonRegressive` via `classifyBundleVersionDiff`
503
503
  * 4. Call Repository.publishDraft with the diff result + validity dates
@@ -510,7 +510,7 @@ var BundlesService = class _BundlesService {
510
510
  code: CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
511
511
  message: `BundleVersion '${versionId}' not found`,
512
512
  params: {
513
- versionId
513
+ bundleVersionId: versionId
514
514
  }
515
515
  });
516
516
  }
@@ -546,7 +546,7 @@ var BundlesService = class _BundlesService {
546
546
  if (!validFromInput) {
547
547
  throw new UnprocessableEntityException({
548
548
  code: CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
549
- message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
549
+ message: "validFrom must be set when publishing (on the draft or the publish call).."
550
550
  });
551
551
  }
552
552
  const validFrom = new Date(validFromInput);
@@ -750,9 +750,10 @@ var BundlesService = class _BundlesService {
750
750
  throw new UnprocessableEntityException({
751
751
  code,
752
752
  message: `${field} '${input}' is not a valid date`,
753
+ // Keyed by the field name so these params match the publish
754
+ // path, where the same codes carry `validFrom` / `validUntil`.
753
755
  params: {
754
- field,
755
- value: input
756
+ [field]: input
756
757
  }
757
758
  });
758
759
  }
@@ -2775,7 +2776,7 @@ function buildPublicMarketingCatalogController(guards, projectKey, currency, vat
2775
2776
  this.service = service;
2776
2777
  }
2777
2778
  /**
2778
- * `?lang=de|tr|en` — alias `locale` accepted (SPEC_V2 §9).
2779
+ * `?lang=de|tr|en` — alias `locale` accepted.
2779
2780
  * `?asOf=YYYY-MM-DD` — preview of a future or past catalog variant.
2780
2781
  * Default = today. Returns the PlanVersion whose `validFrom`/`validUntil`
2781
2782
  * span `asOf` inclusively by day (the validUntil day still counts,
@@ -3286,7 +3287,7 @@ var PlanVersionsService = class _PlanVersionsService {
3286
3287
  if (!validFromInput) {
3287
3288
  throw new UnprocessableEntityException4({
3288
3289
  code: CATALOG_ERROR_CODES6.PLAN_VERSION_VALID_FROM_REQUIRED,
3289
- message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
3290
+ message: "validFrom must be set when publishing (on the draft or the publish call).."
3290
3291
  });
3291
3292
  }
3292
3293
  const validFrom = new Date(validFromInput);
@@ -3409,7 +3410,7 @@ var PlanVersionsService = class _PlanVersionsService {
3409
3410
  }
3410
3411
  if (existing.publishedAt === null) {
3411
3412
  throw new UnprocessableEntityException4({
3412
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_LIVE,
3413
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_PUBLISHED,
3413
3414
  message: `PlanVersion '${versionId}' is not published and cannot be terminated.`,
3414
3415
  params: {
3415
3416
  versionId
@@ -3418,7 +3419,7 @@ var PlanVersionsService = class _PlanVersionsService {
3418
3419
  }
3419
3420
  if (existing.supersededAt !== null) {
3420
3421
  throw new UnprocessableEntityException4({
3421
- code: CATALOG_ERROR_CODES6.PLAN_VERSION_NOT_LIVE,
3422
+ code: CATALOG_ERROR_CODES6.PLAN_VERSION_SUPERSEDED,
3422
3423
  message: `PlanVersion '${versionId}' has already been superseded by a newer version (supersededAt) and cannot be terminated.`,
3423
3424
  params: {
3424
3425
  versionId
@@ -3800,7 +3801,7 @@ var PublishBundleVersionDto = class {
3800
3801
  allowZeroPrice;
3801
3802
  /**
3802
3803
  * Required on publish if the draft carries no `validFrom`
3803
- * (SPEC_V2 §4.2 + §11.1 M6 Pack 2c, analogous to PublishPlanVersionDto).
3804
+ * (analogous to PublishPlanVersionDto).
3804
3805
  * Service strictly checks > `validFrom` of the predecessor version.
3805
3806
  */
3806
3807
  validFrom;
@@ -4582,7 +4583,7 @@ var CreatePlanVersionDraftDto = class {
4582
4583
  marketed;
4583
4584
  changeNote;
4584
4585
  baseVersionId;
4585
- /** Optional in the draft (required at publish). ISO date string. SPEC_V2 §4.2. */
4586
+ /** Optional in the draft (required at publish). ISO date string.. */
4586
4587
  validFrom;
4587
4588
  /** Optional; null = unlimited. ISO date string. */
4588
4589
  validUntil;
@@ -4757,7 +4758,7 @@ var PublishPlanVersionDto = class {
4757
4758
  allowZeroPrice;
4758
4759
  /**
4759
4760
  * Required at publish (on the DTO or draft). The service strictly checks
4760
- * `validFrom > predecessor.validFrom`. SPEC_V2 §4.2.
4761
+ * `validFrom > predecessor.validFrom`..
4761
4762
  */
4762
4763
  validFrom;
4763
4764
  validUntil;
@@ -6,7 +6,7 @@ import {
6
6
  ADVISORY_STRICT_MODE_CODES,
7
7
  validateBundleDraft,
8
8
  validatePlanDraft
9
- } from "./chunk-LXUBCLRK.js";
9
+ } from "./chunk-7X5MSAIL.js";
10
10
  import {
11
11
  __name
12
12
  } from "./chunk-SHUYVCID.js";
@@ -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
+ };
@@ -1,3 +1,6 @@
1
+ import {
2
+ codedError
3
+ } from "./chunk-EBJJNTW6.js";
1
4
  import {
2
5
  asProvider
3
6
  } from "./chunk-SABTXESR.js";
@@ -183,25 +186,17 @@ var PendingRegistrationService = class _PendingRegistrationService {
183
186
  async selectPlan(input, context) {
184
187
  const pending = await this.repo.findById(input.pendingRegistrationId);
185
188
  if (!pending) {
186
- throw new NotFoundException({
187
- code: REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
188
- });
189
+ throw new NotFoundException(codedError(REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
189
190
  }
190
191
  if (pending.expiresAt.getTime() < Date.now()) {
191
- throw new BadRequestException({
192
- code: REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
193
- });
192
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
194
193
  }
195
194
  if (pending.status !== "EMAIL_VERIFIED" && pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
196
- throw new BadRequestException({
197
- code: REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE
198
- });
195
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE));
199
196
  }
200
197
  const plan = await this.planCatalog.findPublicSignupPlan(input.planId);
201
198
  if (!plan) {
202
- throw new BadRequestException({
203
- code: REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE
204
- });
199
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE));
205
200
  }
206
201
  const now = /* @__PURE__ */ new Date();
207
202
  const expiresAt = new Date(now.getTime() + PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
@@ -224,30 +219,20 @@ var PendingRegistrationService = class _PendingRegistrationService {
224
219
  async startCheckout(input, context) {
225
220
  const pending = await this.repo.findById(input.pendingRegistrationId);
226
221
  if (!pending) {
227
- throw new NotFoundException({
228
- code: REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
229
- });
222
+ throw new NotFoundException(codedError(REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
230
223
  }
231
224
  if (pending.expiresAt.getTime() < Date.now()) {
232
- throw new BadRequestException({
233
- code: REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
234
- });
225
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
235
226
  }
236
227
  if (pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
237
- throw new BadRequestException({
238
- code: REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED
239
- });
228
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED));
240
229
  }
241
230
  if (!pending.selectedPlanId) {
242
- throw new BadRequestException({
243
- code: REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED
244
- });
231
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED));
245
232
  }
246
233
  const plan = await this.planCatalog.findPublicSignupPlan(pending.selectedPlanId);
247
234
  if (!plan) {
248
- throw new BadRequestException({
249
- code: REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE
250
- });
235
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE));
251
236
  }
252
237
  const session = await this.paymentProvider.createCheckoutSession({
253
238
  pendingRegistrationId: pending.id,
@@ -456,9 +441,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
456
441
  await this.record("OTP_VERIFY_FAILED", null, context, {
457
442
  reason: "unknown_email"
458
443
  });
459
- throw new BadRequestException({
460
- code: REGISTRATION_ERROR_CODES.OTP_INVALID
461
- });
444
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.OTP_INVALID));
462
445
  }
463
446
  if (pending.status !== "PENDING_EMAIL_VERIFICATION") {
464
447
  return {
@@ -471,43 +454,33 @@ var PendingRegistrationService = class _PendingRegistrationService {
471
454
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
472
455
  reason: "no_otp_set"
473
456
  });
474
- throw new BadRequestException({
475
- code: REGISTRATION_ERROR_CODES.OTP_INVALID
476
- });
457
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.OTP_INVALID));
477
458
  }
478
459
  if (pending.otpExpiresAt.getTime() < Date.now()) {
479
460
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
480
461
  reason: "expired"
481
462
  });
482
- throw new BadRequestException({
483
- code: REGISTRATION_ERROR_CODES.OTP_EXPIRED
484
- });
463
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.OTP_EXPIRED));
485
464
  }
486
465
  const maxAttempts = resolveOtpVerifyMaxAttempts();
487
466
  if (pending.otpAttemptCount >= maxAttempts) {
488
467
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
489
468
  reason: "locked"
490
469
  });
491
- throw new BadRequestException({
492
- code: REGISTRATION_ERROR_CODES.OTP_LOCKED
493
- });
470
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.OTP_LOCKED));
494
471
  }
495
472
  const attemptCount = await this.repo.incrementOtpAttemptCount(pending.id);
496
473
  if (attemptCount > maxAttempts) {
497
474
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
498
475
  reason: "locked"
499
476
  });
500
- throw new BadRequestException({
501
- code: REGISTRATION_ERROR_CODES.OTP_LOCKED
502
- });
477
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.OTP_LOCKED));
503
478
  }
504
479
  if (!verifyOtpCode(pending.otpHash, code)) {
505
480
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
506
481
  reason: "wrong_code"
507
482
  });
508
- throw new BadRequestException({
509
- code: REGISTRATION_ERROR_CODES.OTP_INVALID
510
- });
483
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.OTP_INVALID));
511
484
  }
512
485
  const now = /* @__PURE__ */ new Date();
513
486
  const updated = await this.repo.update(pending.id, {
@@ -618,28 +591,20 @@ var PendingRegistrationService = class _PendingRegistrationService {
618
591
  */
619
592
  async resumeWithToken(input, context) {
620
593
  if (!this.resumeTokenSigner) {
621
- throw new BadRequestException({
622
- code: REGISTRATION_ERROR_CODES.RESUME_NOT_CONFIGURED
623
- });
594
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.RESUME_NOT_CONFIGURED));
624
595
  }
625
596
  let decoded;
626
597
  try {
627
598
  decoded = await this.resumeTokenSigner.verify(input.token);
628
599
  } catch {
629
- throw new BadRequestException({
630
- code: REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID
631
- });
600
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID));
632
601
  }
633
602
  const pending = await this.repo.findById(decoded.pendingRegistrationId);
634
603
  if (!pending) {
635
- throw new BadRequestException({
636
- code: REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID
637
- });
604
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID));
638
605
  }
639
606
  if (pending.expiresAt.getTime() < Date.now()) {
640
- throw new BadRequestException({
641
- code: REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
642
- });
607
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
643
608
  }
644
609
  await this.record("REGISTRATION_NEUTRAL_REPLAY", pending.id, context, {
645
610
  via: "resume_token",
@@ -667,9 +632,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
667
632
  /** Returns the (app-specific) configurator catalog. */
668
633
  async getConfiguratorCatalog() {
669
634
  if (!this.configuratorLookup) {
670
- throw new BadRequestException({
671
- code: REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
672
- });
635
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED));
673
636
  }
674
637
  return this.configuratorLookup.getCatalog();
675
638
  }
@@ -686,32 +649,22 @@ var PendingRegistrationService = class _PendingRegistrationService {
686
649
  */
687
650
  async saveConfiguration(input, context) {
688
651
  if (!this.configuratorLookup) {
689
- throw new BadRequestException({
690
- code: REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
691
- });
652
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED));
692
653
  }
693
654
  const pending = await this.repo.findById(input.pendingRegistrationId);
694
655
  if (!pending) {
695
- throw new NotFoundException({
696
- code: REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
697
- });
656
+ throw new NotFoundException(codedError(REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
698
657
  }
699
658
  if (pending.expiresAt.getTime() < Date.now()) {
700
- throw new BadRequestException({
701
- code: REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
702
- });
659
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
703
660
  }
704
661
  if (pending.status !== "EMAIL_VERIFIED" && pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
705
- throw new BadRequestException({
706
- code: REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE
707
- });
662
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE));
708
663
  }
709
664
  const catalog = await this.configuratorLookup.getCatalog();
710
665
  const model = catalog.models.find((m) => m.id === input.selection.modelId);
711
666
  if (!model) {
712
- throw new BadRequestException({
713
- code: REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE
714
- });
667
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE));
715
668
  }
716
669
  let promoEval;
717
670
  if (input.selection.appliedPromoCode && this.promoPreview) {
@@ -775,28 +728,20 @@ var PendingRegistrationService = class _PendingRegistrationService {
775
728
  */
776
729
  async previewConfigPromo(pendingRegistrationId, code) {
777
730
  if (!this.configuratorLookup) {
778
- throw new BadRequestException({
779
- code: REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
780
- });
731
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED));
781
732
  }
782
733
  const pending = await this.repo.findById(pendingRegistrationId);
783
734
  if (!pending) {
784
- throw new NotFoundException({
785
- code: REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
786
- });
735
+ throw new NotFoundException(codedError(REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
787
736
  }
788
737
  const config = pending.configJson;
789
738
  if (!config) {
790
- throw new BadRequestException({
791
- code: REGISTRATION_ERROR_CODES.CONFIG_NOT_SAVED
792
- });
739
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.CONFIG_NOT_SAVED));
793
740
  }
794
741
  const catalog = await this.configuratorLookup.getCatalog();
795
742
  const model = catalog.models.find((m) => m.id === config.modelId);
796
743
  if (!model) {
797
- throw new BadRequestException({
798
- code: REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE
799
- });
744
+ throw new BadRequestException(codedError(REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE));
800
745
  }
801
746
  let promoEval;
802
747
  if (code && this.promoPreview) {
@@ -1030,9 +975,14 @@ var BaseIpRateLimitGuard = class {
1030
975
  const key = ipFingerprint(req);
1031
976
  if (this.exceeded(key)) {
1032
977
  throw new HttpException({
1033
- code: REGISTRATION_ERROR_CODES2.RATE_LIMITED,
1034
- reason: this.options.name,
1035
- retryAfterSeconds: Math.ceil(this.options.windowMs / 1e3)
978
+ ...codedError(REGISTRATION_ERROR_CODES2.RATE_LIMITED, {
979
+ retryAfterSeconds: Math.ceil(this.options.windowMs / 1e3)
980
+ }),
981
+ retryAfterSeconds: Math.ceil(this.options.windowMs / 1e3),
982
+ // Superseded by `code`. Note this carries the limiter name,
983
+ // not the code — the two emitters of RATE_LIMITED disagree
984
+ // on what `reason` means, which is why it is going away.
985
+ reason: this.options.name
1036
986
  }, HttpStatus.TOO_MANY_REQUESTS);
1037
987
  }
1038
988
  return true;
@@ -1455,7 +1405,7 @@ var SaveRegistrationConfigSelectionDto = class {
1455
1405
  billingCycle;
1456
1406
  appliedPromoCode;
1457
1407
  /**
1458
- * METAMODELL §17a — Bundle consistency. ID of the `CheckoutOffer` that was
1408
+ * — Bundle consistency. ID of the `CheckoutOffer` that was
1459
1409
  * created on the pricing page (`<app>/register?offer=<id>`). Passed
1460
1410
  * through unchanged in `configJson`; the ActivationOrchestrator freezes it
1461
1411
  * as `packageSnapshot` when the subscription is created and sets the offer
@@ -5,6 +5,9 @@ import {
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,
@@ -304,7 +307,7 @@ 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";
308
311
  import { BILLING_ERROR_CODES } from "@saasicat/types";
309
312
 
310
313
  // src/billing/subscription-bundles.tokens.ts
@@ -484,7 +487,10 @@ var EntitlementService = class {
484
487
  const limits = await this.deriveLimits(sub, now, tx);
485
488
  const max = limits.quotas[input.dimension];
486
489
  if (max === void 0) {
487
- throw new Error(`Quota-Dimension "${input.dimension}" is not in the plan catalog (${this.catalog.projectKey}).`);
490
+ throw new InternalServerErrorException(codedError(BILLING_ERROR_CODES.QUOTA_DIMENSION_UNKNOWN, {
491
+ dimension: input.dimension,
492
+ projectKey: this.catalog.projectKey
493
+ }));
488
494
  }
489
495
  const used = await input.currentUsage(tx, input.tenantId);
490
496
  if (max !== -1 && used + delta > max) {
@@ -498,7 +504,7 @@ var EntitlementService = class {
498
504
  // ---------------------------------------------------------------------
499
505
  /**
500
506
  * Plan fallback for TRIAL/PENDING_SALES: returns the PlanVersion active
501
- * as of `asOf` (SPEC_V2 §4.2). If the repo does not implement
507
+ * as of `asOf`. If the repo does not implement
502
508
  * `findActive`, we fall back to `findLatestLive` (backward compat for
503
509
  * adapters without `validFrom` columns).
504
510
  */
@@ -7,6 +7,9 @@ import {
7
7
  import {
8
8
  SaaSiCatPublicRoute
9
9
  } from "./chunk-YSC46BGM.js";
10
+ import {
11
+ codedError
12
+ } from "./chunk-EBJJNTW6.js";
10
13
  import {
11
14
  asProvider
12
15
  } from "./chunk-SABTXESR.js";
@@ -199,7 +202,7 @@ var MfaGuard = class _MfaGuard {
199
202
  const user = request.user;
200
203
  if (!user) {
201
204
  throw new UnauthorizedException({
202
- code: AUTH_ERROR_CODES2.NOT_AUTHENTICATED,
205
+ ...codedError(AUTH_ERROR_CODES2.NOT_AUTHENTICATED),
203
206
  // `reason` is superseded by `code` and will be removed.
204
207
  reason: "NOT_AUTHENTICATED"
205
208
  });
@@ -572,7 +575,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
572
575
  enabled: true,
573
576
  requiredCapability: "dashboard.read"
574
577
  },
575
- // SPEC_V2 §3.3 — discovery snapshot of the running backend.
578
+ // — discovery snapshot of the running backend.
576
579
  // Apps must set `discovery.read` in their manifest capabilities
577
580
  // to `true` so that the NavBuilder wires the page into the
578
581
  // sidebar; otherwise it is filtered out automatically.
@@ -580,7 +583,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
580
583
  enabled: true,
581
584
  requiredCapability: "discovery.read"
582
585
  },
583
- // SPEC_V2 §11.1 M3 — Bundle editor in SuperAdmin.
586
+ // — Bundle editor in SuperAdmin.
584
587
  // Apps with a DB-driven catalog set `bundles.read = true`.
585
588
  bundles: {
586
589
  enabled: true,
@@ -590,7 +593,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
590
593
  enabled: true,
591
594
  requiredCapability: "plans.read"
592
595
  },
593
- // SPEC_V2 §11.1 M3 — marketing catalog (locale pivot).
596
+ // — marketing catalog (locale pivot).
594
597
  // Apps that market a public catalog set
595
598
  // `marketingProjections.read = true`.
596
599
  marketingCatalog: {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PromoCodesModule
3
- } from "./chunk-EUU3B6NB.js";
3
+ } from "./chunk-4O6R6ARQ.js";
4
4
  import {
5
5
  AdminManifestModule,
6
6
  AdminManifestService,
@@ -9,22 +9,22 @@ import {
9
9
  AdminStatsModule,
10
10
  MfaService,
11
11
  SuperAdminGuard
12
- } from "./chunk-7JIHG53V.js";
12
+ } from "./chunk-SN5UOC43.js";
13
13
  import {
14
14
  PublicCatalogModule,
15
15
  REQUIRE_FEATURE_KEY,
16
16
  SubscriptionBundleModule,
17
17
  TenantBillingModule
18
- } from "./chunk-I77VS3YH.js";
18
+ } from "./chunk-UO64MK3Q.js";
19
19
  import {
20
20
  SubscriptionContractModule
21
21
  } from "./chunk-B7NRY2LV.js";
22
22
  import {
23
23
  CatalogModule
24
- } from "./chunk-LXUBCLRK.js";
24
+ } from "./chunk-7X5MSAIL.js";
25
25
  import {
26
26
  CheckoutOfferModule
27
- } from "./chunk-ORWIGSNN.js";
27
+ } from "./chunk-XTVAFJ3P.js";
28
28
  import {
29
29
  SaaSiCatPublicRoute
30
30
  } from "./chunk-YSC46BGM.js";
@@ -34,11 +34,14 @@ import {
34
34
  import {
35
35
  EntitlementModule,
36
36
  LimitExceededError
37
- } from "./chunk-F6DCLVBB.js";
37
+ } from "./chunk-NNNXXUPT.js";
38
38
  import {
39
39
  PLAN_CATALOG_TOKEN,
40
40
  PlanCatalogModule
41
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");
@@ -164,16 +165,12 @@ var SetupService = class {
164
165
  });
165
166
  }
166
167
  if (!token || !timingSafeEqualStr(token, expected)) {
167
- throw new UnauthorizedException({
168
- code: SETUP_ERROR_CODES.INVALID_SETUP_TOKEN
169
- });
168
+ throw new UnauthorizedException(codedError(SETUP_ERROR_CODES.INVALID_SETUP_TOKEN));
170
169
  }
171
170
  }
172
171
  async assertNoSuperAdmin() {
173
172
  if (await this.users.countSuperAdmins() > 0) {
174
- throw new ConflictException({
175
- code: SETUP_ERROR_CODES.SETUP_ALREADY_DONE
176
- });
173
+ throw new ConflictException(codedError(SETUP_ERROR_CODES.SETUP_ALREADY_DONE));
177
174
  }
178
175
  }
179
176
  };
@@ -508,13 +505,14 @@ var StaticFeatureGuard = class {
508
505
  const snap = await this.entitlements.snapshot(tenantId);
509
506
  const allowed = required.some((f) => snap.features.includes(f));
510
507
  if (!allowed) {
511
- throw new ForbiddenException2({
508
+ const body = {
512
509
  code: FEATURE_NOT_LICENSED,
513
- message: `Feature ${required.join(" / ")} is not included in the current plan.`,
514
- params: {
515
- featureKeys: required
516
- }
517
- });
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);
518
516
  }
519
517
  return true;
520
518
  }
@@ -785,7 +783,7 @@ function buildTenantManifestController(options) {
785
783
  __name(buildTenantManifestController, "buildTenantManifestController");
786
784
 
787
785
  // src/platform/saas-platform.module.ts
788
- import { Module as Module2 } from "@nestjs/common";
786
+ import { Logger, Module as Module2 } from "@nestjs/common";
789
787
  import { APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core";
790
788
  import { assertPersistenceCapabilities } from "@saasicat/types";
791
789
  function _ts_decorate9(decorators, target, key, desc) {
@@ -795,6 +793,7 @@ function _ts_decorate9(decorators, target, key, desc) {
795
793
  return c > 3 && r && Object.defineProperty(target, key, r), r;
796
794
  }
797
795
  __name(_ts_decorate9, "_ts_decorate");
796
+ var PLATFORM_LOGGER = new Logger("SaaSiCat");
798
797
  var PLATFORM_SUBSCRIPTION_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-nest/PlatformSubscriptionRepository");
799
798
  var STANDARD_MANIFEST_REGISTRATION_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-nest/StandardManifestRegistration");
800
799
  function buildStandardManifestContribution(catalog, adminResources, promoCodes) {
@@ -1259,6 +1258,11 @@ var SaasPlatformModule = class _SaasPlatformModule {
1259
1258
  useExisting: EnforceQuotaInterceptor
1260
1259
  });
1261
1260
  lightweightExports.push(PLAN_RESOLVER_PORT_TOKEN, StaticEntitlementService, StaticFeatureGuard, EnforceQuotaInterceptor, QUOTA_PROVIDERS_TOKEN);
1261
+ if (options.globalFeatureGuard === false) {
1262
+ PLATFORM_LOGGER.warn("globalFeatureGuard is off \u2014 the platform did NOT bind StaticFeatureGuard as an APP_GUARD. @RequireFeature only blocks on routes where you bound a feature guard yourself, behind your auth guard: @UseGuards(JwtAuthGuard, StaticFeatureGuard), or FeatureGuard from @saasicat/nest/billing on the V3 entitlement path. Any annotated route without one serves unlicensed traffic.");
1263
+ }
1264
+ } else {
1265
+ PLATFORM_LOGGER.warn("No plan resolver configured \u2014 @RequireFeature and @EnforceQuota are INERT. The platform registered neither StaticFeatureGuard nor EnforceQuotaInterceptor, so every annotated route serves unlicensed traffic and every quota is unlimited. Set one of: adapters.planResolver, defaultPlanId, or tenantBilling.");
1262
1266
  }
1263
1267
  const tenantControllers = [];
1264
1268
  if (options.tenantManifest) {