@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
package/dist/_entries.cjs CHANGED
@@ -124,7 +124,22 @@ SuperAdminGuard = _ts_decorate([
124
124
  // src/admin/mfa.guard.ts
125
125
  var import_common3 = require("@nestjs/common");
126
126
  var import_core = require("@nestjs/core");
127
+ var import_types3 = require("@saasicat/types");
128
+
129
+ // src/errors/coded-error.ts
127
130
  var import_types2 = require("@saasicat/types");
131
+ function codedError(code, params) {
132
+ const message = (0, import_types2.formatErrorMessage)(import_types2.ERROR_MESSAGES_EN[code], params ?? {});
133
+ return params ? {
134
+ code,
135
+ message,
136
+ params
137
+ } : {
138
+ code,
139
+ message
140
+ };
141
+ }
142
+ __name(codedError, "codedError");
128
143
 
129
144
  // src/admin/mfa.ts
130
145
  var import_common2 = require("@nestjs/common");
@@ -272,7 +287,7 @@ var MfaGuard = class _MfaGuard {
272
287
  const user = request.user;
273
288
  if (!user) {
274
289
  throw new import_common3.UnauthorizedException({
275
- code: import_types2.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
290
+ ...codedError(import_types3.AUTH_ERROR_CODES.NOT_AUTHENTICATED),
276
291
  // `reason` is superseded by `code` and will be removed.
277
292
  reason: "NOT_AUTHENTICATED"
278
293
  });
@@ -280,7 +295,7 @@ var MfaGuard = class _MfaGuard {
280
295
  const enabled = await this.mfaService.isEnabled(user.id);
281
296
  if (!enabled) {
282
297
  throw new import_common3.UnauthorizedException({
283
- code: import_types2.AUTH_ERROR_CODES.MFA_NOT_SET_UP,
298
+ code: import_types3.AUTH_ERROR_CODES.MFA_NOT_SET_UP,
284
299
  // `reason` is superseded by `code` and will be removed.
285
300
  reason: "MFA_NOT_SET_UP",
286
301
  message: "Run the MFA setup via the CLI first."
@@ -290,7 +305,7 @@ var MfaGuard = class _MfaGuard {
290
305
  const code = Array.isArray(headerVal) ? headerVal[0] : headerVal;
291
306
  if (!code) {
292
307
  throw new import_common3.UnauthorizedException({
293
- code: import_types2.AUTH_ERROR_CODES.MFA_REQUIRED,
308
+ code: import_types3.AUTH_ERROR_CODES.MFA_REQUIRED,
294
309
  // `reason` is superseded by `code` and will be removed.
295
310
  reason: "MFA_REQUIRED",
296
311
  message: "TOTP code required in the X-Mfa-Code header."
@@ -302,7 +317,7 @@ var MfaGuard = class _MfaGuard {
302
317
  });
303
318
  if (!valid) {
304
319
  throw new import_common3.UnauthorizedException({
305
- code: import_types2.AUTH_ERROR_CODES.MFA_FAILED,
320
+ code: import_types3.AUTH_ERROR_CODES.MFA_FAILED,
306
321
  // `reason` is superseded by `code` and will be removed.
307
322
  reason: "MFA_FAILED",
308
323
  message: "Invalid TOTP code."
@@ -756,7 +771,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
756
771
  enabled: true,
757
772
  requiredCapability: "dashboard.read"
758
773
  },
759
- // SPEC_V2 §3.3 — discovery snapshot of the running backend.
774
+ // — discovery snapshot of the running backend.
760
775
  // Apps must set `discovery.read` in their manifest capabilities
761
776
  // to `true` so that the NavBuilder wires the page into the
762
777
  // sidebar; otherwise it is filtered out automatically.
@@ -764,7 +779,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
764
779
  enabled: true,
765
780
  requiredCapability: "discovery.read"
766
781
  },
767
- // SPEC_V2 §11.1 M3 — Bundle editor in SuperAdmin.
782
+ // — Bundle editor in SuperAdmin.
768
783
  // Apps with a DB-driven catalog set `bundles.read = true`.
769
784
  bundles: {
770
785
  enabled: true,
@@ -774,7 +789,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
774
789
  enabled: true,
775
790
  requiredCapability: "plans.read"
776
791
  },
777
- // SPEC_V2 §11.1 M3 — marketing catalog (locale pivot).
792
+ // — marketing catalog (locale pivot).
778
793
  // Apps that market a public catalog set
779
794
  // `marketingProjections.read = true`.
780
795
  marketingCatalog: {
@@ -1338,7 +1353,7 @@ AdminStatsModule = _ts_decorate12([
1338
1353
  // src/admin/admin-resources.module.ts
1339
1354
  var import_common14 = require("@nestjs/common");
1340
1355
  var import_class_validator = require("class-validator");
1341
- var import_types3 = require("@saasicat/types");
1356
+ var import_types4 = require("@saasicat/types");
1342
1357
  function _ts_decorate13(decorators, target, key, desc) {
1343
1358
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1344
1359
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1386,7 +1401,7 @@ var AdminResourcesService = class {
1386
1401
  const tenant = await this.resources.getTenantDetail(slug);
1387
1402
  if (!tenant) {
1388
1403
  throw new import_common14.NotFoundException({
1389
- code: import_types3.BILLING_ERROR_CODES.TENANT_NOT_FOUND,
1404
+ code: import_types4.BILLING_ERROR_CODES.TENANT_NOT_FOUND,
1390
1405
  message: `Tenant ${slug} not found`,
1391
1406
  params: {
1392
1407
  slug
@@ -1420,7 +1435,7 @@ var AdminResourcesService = class {
1420
1435
  const result = await this.resources.setTenantActive(slug, active, subscriptionStatus);
1421
1436
  if (!result) {
1422
1437
  throw new import_common14.NotFoundException({
1423
- code: import_types3.BILLING_ERROR_CODES.TENANT_NOT_FOUND,
1438
+ code: import_types4.BILLING_ERROR_CODES.TENANT_NOT_FOUND,
1424
1439
  message: `Tenant ${slug} not found`,
1425
1440
  params: {
1426
1441
  slug
@@ -1668,8 +1683,8 @@ __export(billing_exports, {
1668
1683
  buildPlanCatalogFromSnapshot: () => buildPlanCatalogFromSnapshot,
1669
1684
  buildPlanCatalogImporterController: () => buildPlanCatalogImporterController,
1670
1685
  buildTenantSubscriptionBundlesController: () => buildTenantSubscriptionBundlesController,
1671
- classifyBundleVersionDiff: () => import_types4.classifyBundleVersionDiff,
1672
- classifyPlanDiff: () => import_types4.classifyPlanDiff,
1686
+ classifyBundleVersionDiff: () => import_types5.classifyBundleVersionDiff,
1687
+ classifyPlanDiff: () => import_types5.classifyPlanDiff,
1673
1688
  clearPendingPlanVersionFields: () => clearPendingPlanVersionFields,
1674
1689
  computeCarriedTrialEndsAt: () => computeCarriedTrialEndsAt,
1675
1690
  computeNextPeriod: () => computeNextPeriod,
@@ -1730,7 +1745,7 @@ function periodEndWithMinLead(startedAt, cycle, now, minLeadDays = 42) {
1730
1745
  __name(periodEndWithMinLead, "periodEndWithMinLead");
1731
1746
 
1732
1747
  // src/billing/version-diff.ts
1733
- var import_types4 = require("@saasicat/types");
1748
+ var import_types5 = require("@saasicat/types");
1734
1749
 
1735
1750
  // src/billing/version-publish.ts
1736
1751
  var PublishValidationError = class extends Error {
@@ -1825,11 +1840,11 @@ var FEATURE_GUARD_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/Featu
1825
1840
  // src/billing/feature.guard.ts
1826
1841
  var import_common18 = require("@nestjs/common");
1827
1842
  var import_core2 = require("@nestjs/core");
1828
- var import_types6 = require("@saasicat/types");
1843
+ var import_types7 = require("@saasicat/types");
1829
1844
 
1830
1845
  // src/entitlement/service.ts
1831
1846
  var import_common17 = require("@nestjs/common");
1832
- var import_types5 = require("@saasicat/types");
1847
+ var import_types6 = require("@saasicat/types");
1833
1848
 
1834
1849
  // src/catalog/tokens.ts
1835
1850
  var PLAN_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/PlanRepository");
@@ -2396,7 +2411,7 @@ var EntitlementService = class {
2396
2411
  const sub = await this.subscriptions.findByTenantId(tenantId);
2397
2412
  if (!sub) {
2398
2413
  throw new import_common17.NotFoundException({
2399
- code: import_types5.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
2414
+ code: import_types6.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
2400
2415
  message: `No subscription for tenant ${tenantId}`,
2401
2416
  params: {
2402
2417
  tenantId
@@ -2497,7 +2512,7 @@ var EntitlementService = class {
2497
2512
  const sub = await this.subscriptions.findByTenantIdLocked(input.tenantId, tx);
2498
2513
  if (!sub) {
2499
2514
  throw new import_common17.NotFoundException({
2500
- code: import_types5.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
2515
+ code: import_types6.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
2501
2516
  message: `No subscription for tenant ${input.tenantId}`,
2502
2517
  params: {
2503
2518
  tenantId: input.tenantId
@@ -2507,7 +2522,10 @@ var EntitlementService = class {
2507
2522
  const limits = await this.deriveLimits(sub, now, tx);
2508
2523
  const max = limits.quotas[input.dimension];
2509
2524
  if (max === void 0) {
2510
- throw new Error(`Quota-Dimension "${input.dimension}" is not in the plan catalog (${this.catalog.projectKey}).`);
2525
+ throw new import_common17.InternalServerErrorException(codedError(import_types6.BILLING_ERROR_CODES.QUOTA_DIMENSION_UNKNOWN, {
2526
+ dimension: input.dimension,
2527
+ projectKey: this.catalog.projectKey
2528
+ }));
2511
2529
  }
2512
2530
  const used = await input.currentUsage(tx, input.tenantId);
2513
2531
  if (max !== -1 && used + delta > max) {
@@ -2521,7 +2539,7 @@ var EntitlementService = class {
2521
2539
  // ---------------------------------------------------------------------
2522
2540
  /**
2523
2541
  * Plan fallback for TRIAL/PENDING_SALES: returns the PlanVersion active
2524
- * as of `asOf` (SPEC_V2 §4.2). If the repo does not implement
2542
+ * as of `asOf`. If the repo does not implement
2525
2543
  * `findActive`, we fall back to `findLatestLive` (backward compat for
2526
2544
  * adapters without `validFrom` columns).
2527
2545
  */
@@ -2530,7 +2548,7 @@ var EntitlementService = class {
2530
2548
  if (!v) {
2531
2549
  const asOfDate = asOf.toISOString().slice(0, 10);
2532
2550
  throw new import_common17.NotFoundException({
2533
- code: import_types5.BILLING_ERROR_CODES.NO_ACTIVE_PLAN_VERSION,
2551
+ code: import_types6.BILLING_ERROR_CODES.NO_ACTIVE_PLAN_VERSION,
2534
2552
  message: `No plan version for ${planId} active as of ${asOfDate} \u2014 neither the validFrom window is satisfied nor is a latest-live version available.`,
2535
2553
  params: {
2536
2554
  planId,
@@ -2641,7 +2659,7 @@ var FeatureGuard = class _FeatureGuard {
2641
2659
  const user = request.user;
2642
2660
  if (!user) {
2643
2661
  throw new import_common18.ForbiddenException({
2644
- code: import_types6.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
2662
+ code: import_types7.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
2645
2663
  message: "Not authenticated"
2646
2664
  });
2647
2665
  }
@@ -2650,7 +2668,7 @@ var FeatureGuard = class _FeatureGuard {
2650
2668
  const tenantId = this.config?.tenantIdResolver ? this.config.tenantIdResolver(request) : request.tenantId ?? user.tenantId;
2651
2669
  if (!tenantId) {
2652
2670
  throw new import_common18.ForbiddenException({
2653
- code: import_types6.AUTH_ERROR_CODES.NO_TENANT_ASSIGNED,
2671
+ code: import_types7.AUTH_ERROR_CODES.NO_TENANT_ASSIGNED,
2654
2672
  message: "No tenant assigned"
2655
2673
  });
2656
2674
  }
@@ -2663,10 +2681,11 @@ var FeatureGuard = class _FeatureGuard {
2663
2681
  return true;
2664
2682
  }
2665
2683
  /**
2666
- * Upsell response (#36): with a registered `UpsellOfferResolver` the 403
2667
- * becomes machine-readable (`FeatureNotLicensedBody`), so consumer UIs can
2668
- * render a purchase offer. Without a resolver the body carries the same
2669
- * `code` but no offers.
2684
+ * Upsell response (#36): the 403 body is always the full
2685
+ * `FeatureNotLicensedBody`, so a consumer that matches on `code` can read
2686
+ * `featureKey`/`offers` unconditionally. Without a registered
2687
+ * `UpsellOfferResolver` there is simply nothing to offer — `offers` is
2688
+ * empty, the same degradation a failing resolver produces.
2670
2689
  *
2671
2690
  * Deliberately 403 + `code` field instead of 402 — rationale in
2672
2691
  * `@saasicat/types` upsell.types.ts (402 is reserved / inconsistently
@@ -2677,22 +2696,12 @@ var FeatureGuard = class _FeatureGuard {
2677
2696
  * classify the response by that prefix.
2678
2697
  */
2679
2698
  async buildNotLicensedException(required, tenantId) {
2680
- const message = `Feature ${required.join(" / ")} is not included in the current plan.`;
2681
- if (!this.upsellResolver) {
2682
- return new import_common18.ForbiddenException({
2683
- code: import_types6.FEATURE_NOT_LICENSED,
2684
- message,
2685
- params: {
2686
- featureKeys: required
2687
- }
2688
- });
2689
- }
2690
2699
  const body = {
2691
- code: import_types6.FEATURE_NOT_LICENSED,
2700
+ code: import_types7.FEATURE_NOT_LICENSED,
2692
2701
  featureKey: required[0],
2693
2702
  featureKeys: required,
2694
- offers: await this.resolveOffersSafe(required, tenantId),
2695
- message
2703
+ offers: this.upsellResolver ? await this.resolveOffersSafe(required, tenantId) : [],
2704
+ message: `Feature ${required.join(" / ")} is not included in the current plan.`
2696
2705
  };
2697
2706
  return new import_common18.ForbiddenException(body);
2698
2707
  }
@@ -2728,7 +2737,7 @@ FeatureGuard = _ts_decorate16([
2728
2737
 
2729
2738
  // src/billing/catalog-bundle-upsell-resolver.ts
2730
2739
  var import_common19 = require("@nestjs/common");
2731
- var import_types7 = require("@saasicat/types");
2740
+ var import_types8 = require("@saasicat/types");
2732
2741
 
2733
2742
  // src/billing/public-catalog.tokens.ts
2734
2743
  var PUBLIC_CATALOG_PROJECT_KEY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/PublicCatalogProjectKey");
@@ -2798,7 +2807,7 @@ var CatalogBundleUpsellResolver = class {
2798
2807
  const rows = await this.catalogEntryRepo.listFeatures({
2799
2808
  projectKey: this.projectKey
2800
2809
  });
2801
- return (0, import_types7.collectUnsatisfiedRequires)(featureKeys, (0, import_types7.buildFeatureRequiresIndex)(rows));
2810
+ return (0, import_types8.collectUnsatisfiedRequires)(featureKeys, (0, import_types8.buildFeatureRequiresIndex)(rows));
2802
2811
  }
2803
2812
  /** Only published-and-live AND marketed — non-marketed bundles are not an offer. */
2804
2813
  async listLiveMarketedBundles() {
@@ -2857,7 +2866,7 @@ __name(compareRankedOffers, "compareRankedOffers");
2857
2866
  // src/billing/limit-exceeded.filter.ts
2858
2867
  var import_common20 = require("@nestjs/common");
2859
2868
  var import_core3 = require("@nestjs/core");
2860
- var import_types8 = require("@saasicat/types");
2869
+ var import_types9 = require("@saasicat/types");
2861
2870
  function _ts_decorate18(decorators, target, key, desc) {
2862
2871
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2863
2872
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -2882,10 +2891,10 @@ var LimitExceededFilter = class _LimitExceededFilter extends import_core3.BaseEx
2882
2891
  response.status(import_common20.HttpStatus.PAYMENT_REQUIRED).send({
2883
2892
  statusCode: import_common20.HttpStatus.PAYMENT_REQUIRED,
2884
2893
  error: "PaymentRequired",
2885
- code: import_types8.BILLING_ERROR_CODES.LIMIT_EXCEEDED,
2894
+ code: import_types9.BILLING_ERROR_CODES.LIMIT_EXCEEDED,
2886
2895
  // Deprecated: superseded by `code`, kept until consumers have
2887
2896
  // migrated. Same value, only under the older field name.
2888
- reason: import_types8.BILLING_ERROR_CODES.LIMIT_EXCEEDED,
2897
+ reason: import_types9.BILLING_ERROR_CODES.LIMIT_EXCEEDED,
2889
2898
  dimension: exception.dimension,
2890
2899
  used: exception.used,
2891
2900
  max: exception.max,
@@ -2993,7 +3002,7 @@ function _ts_param14(paramIndex, decorator) {
2993
3002
  }
2994
3003
  __name(_ts_param14, "_ts_param");
2995
3004
  var PLAN_CATALOG_IMPORT_SINK_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/PlanCatalogImportSink");
2996
- var IMPORT_CHANGE_NOTE = "Initial import from saas.yaml (SPEC_V2 \xA711.1 M6 Pack 2c)";
3005
+ var IMPORT_CHANGE_NOTE = "Initial import from saas.yaml";
2997
3006
  var PlanCatalogImporterService = class {
2998
3007
  static {
2999
3008
  __name(this, "PlanCatalogImporterService");
@@ -3197,7 +3206,7 @@ var FEATURE_UI_REGISTRY_TOKEN2 = /* @__PURE__ */ Symbol.for("saas-platform/Featu
3197
3206
 
3198
3207
  // src/billing/public-catalog.controller.ts
3199
3208
  var import_common24 = require("@nestjs/common");
3200
- var import_types9 = require("@saasicat/types");
3209
+ var import_types10 = require("@saasicat/types");
3201
3210
  function _ts_decorate22(decorators, target, key, desc) {
3202
3211
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3203
3212
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -3284,7 +3293,7 @@ var PublicCatalogController = class {
3284
3293
  return merged;
3285
3294
  }
3286
3295
  /**
3287
- * SPEC_V2 §11.1 M6 Pack 2c — public catalog endpoint for bundles
3296
+ * — public catalog endpoint for bundles
3288
3297
  * (stem list with live versions). If a `MarketingProjection` with
3289
3298
  * `targetType=BUNDLE` + matching locale exists, it is merged in.
3290
3299
  */
@@ -3317,7 +3326,7 @@ var PublicCatalogController = class {
3317
3326
  const rows = await this.catalogEntryRepo.listFeatures({
3318
3327
  projectKey: this.projectKey
3319
3328
  });
3320
- return (0, import_types9.buildFeatureRequiresIndex)(rows);
3329
+ return (0, import_types10.buildFeatureRequiresIndex)(rows);
3321
3330
  } catch {
3322
3331
  return /* @__PURE__ */ new Map();
3323
3332
  }
@@ -3357,7 +3366,7 @@ var PublicCatalogController = class {
3357
3366
  quotas: live.quotas,
3358
3367
  monthlyNet: live.monthlyNet,
3359
3368
  yearlyNet: live.yearlyNet,
3360
- requiresFeatures: (0, import_types9.collectUnsatisfiedRequires)(features, requiresIndex),
3369
+ requiresFeatures: (0, import_types10.collectUnsatisfiedRequires)(features, requiresIndex),
3361
3370
  marketing
3362
3371
  };
3363
3372
  }
@@ -3481,7 +3490,7 @@ var PENDING_PLAN_QUERY_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/Pe
3481
3490
 
3482
3491
  // src/billing/composed-tenant-auth.guard.ts
3483
3492
  var import_common26 = require("@nestjs/common");
3484
- var import_types10 = require("@saasicat/types");
3493
+ var import_types11 = require("@saasicat/types");
3485
3494
  function _ts_decorate24(decorators, target, key, desc) {
3486
3495
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3487
3496
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -3510,7 +3519,7 @@ var ComposedTenantAuthGuard = class {
3510
3519
  async canActivate(context) {
3511
3520
  if (!this.guards || this.guards.length === 0) {
3512
3521
  throw new import_common26.ForbiddenException({
3513
- code: import_types10.AUTH_ERROR_CODES.AUTH_GUARDS_NOT_CONFIGURED,
3522
+ code: import_types11.AUTH_ERROR_CODES.AUTH_GUARDS_NOT_CONFIGURED,
3514
3523
  message: "TenantBillingModule.forRoot.authGuards is not configured."
3515
3524
  });
3516
3525
  }
@@ -3533,7 +3542,7 @@ ComposedTenantAuthGuard = _ts_decorate24([
3533
3542
 
3534
3543
  // src/billing/tenant-admin.guard.ts
3535
3544
  var import_common27 = require("@nestjs/common");
3536
- var import_types11 = require("@saasicat/types");
3545
+ var import_types12 = require("@saasicat/types");
3537
3546
  function _ts_decorate25(decorators, target, key, desc) {
3538
3547
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3539
3548
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -3553,14 +3562,14 @@ var TenantAdminGuard = class {
3553
3562
  const request = context.switchToHttp().getRequest();
3554
3563
  if (!request.user) {
3555
3564
  throw new import_common27.ForbiddenException({
3556
- code: import_types11.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
3565
+ code: import_types12.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
3557
3566
  message: "Not authenticated"
3558
3567
  });
3559
3568
  }
3560
3569
  const role = request.user.platformRole ?? request.user.role;
3561
3570
  if (!role || !ADMIN_ROLES.has(role)) {
3562
3571
  throw new import_common27.ForbiddenException({
3563
- code: import_types11.AUTH_ERROR_CODES.TENANT_ADMIN_REQUIRED,
3572
+ code: import_types12.AUTH_ERROR_CODES.TENANT_ADMIN_REQUIRED,
3564
3573
  message: "This action requires the TENANT_ADMIN role."
3565
3574
  });
3566
3575
  }
@@ -3600,7 +3609,7 @@ __name(round22, "round2");
3600
3609
 
3601
3610
  // src/billing/plan-change-preview.service.ts
3602
3611
  var import_common29 = require("@nestjs/common");
3603
- var import_types12 = require("@saasicat/types");
3612
+ var import_types13 = require("@saasicat/types");
3604
3613
 
3605
3614
  // src/entitlement/index.ts
3606
3615
  var entitlement_exports = {};
@@ -3730,7 +3739,7 @@ var PlanChangePreviewService = class {
3730
3739
  const sub = await this.subscriptions.findForTenant(tenantId);
3731
3740
  if (!sub) {
3732
3741
  throw new import_common29.NotFoundException({
3733
- code: import_types12.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
3742
+ code: import_types13.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
3734
3743
  message: `No subscription for tenant ${tenantId}`,
3735
3744
  params: {
3736
3745
  tenantId
@@ -3740,7 +3749,7 @@ var PlanChangePreviewService = class {
3740
3749
  const targetPlanDef = findPlan(this.catalog, targetPlan);
3741
3750
  if (!targetPlanDef) {
3742
3751
  throw new import_common29.NotFoundException({
3743
- code: import_types12.BILLING_ERROR_CODES.PLAN_NOT_IN_CATALOG,
3752
+ code: import_types13.BILLING_ERROR_CODES.PLAN_NOT_IN_CATALOG,
3744
3753
  message: `Plan "${targetPlan}" is not in the catalog (${this.catalog.projectKey})`,
3745
3754
  params: {
3746
3755
  planKey: targetPlan,
@@ -4129,7 +4138,7 @@ function cloneJsonValue(value) {
4129
4138
  __name(cloneJsonValue, "cloneJsonValue");
4130
4139
 
4131
4140
  // src/subscription-contract/subscription-contract.service.ts
4132
- var import_types13 = require("@saasicat/types");
4141
+ var import_types14 = require("@saasicat/types");
4133
4142
  function _ts_decorate29(decorators, target, key, desc) {
4134
4143
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4135
4144
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -4162,7 +4171,7 @@ var SubscriptionContractService = class {
4162
4171
  const row = await this.repo.findById(contractId);
4163
4172
  if (!row) {
4164
4173
  throw new import_common31.NotFoundException({
4165
- code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_NOT_FOUND,
4174
+ code: import_types14.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_NOT_FOUND,
4166
4175
  message: `SubscriptionContract '${contractId}' not found`,
4167
4176
  params: {
4168
4177
  contractId
@@ -4178,7 +4187,7 @@ var SubscriptionContractService = class {
4178
4187
  const contract = await this.repo.findActiveByTenantId(tenantId, asOf);
4179
4188
  if (!contract) {
4180
4189
  throw new import_common31.NotFoundException({
4181
- code: import_types13.CONTRACT_ERROR_CODES.NO_ACTIVE_SUBSCRIPTION_CONTRACT,
4190
+ code: import_types14.CONTRACT_ERROR_CODES.NO_ACTIVE_SUBSCRIPTION_CONTRACT,
4182
4191
  message: `No active subscription contract for tenant ${tenantId}`,
4183
4192
  params: {
4184
4193
  tenantId,
@@ -4218,7 +4227,7 @@ var SubscriptionContractService = class {
4218
4227
  createDataFromOffer(offer, options) {
4219
4228
  if (offer.status !== "consumed") {
4220
4229
  throw new import_common31.ConflictException({
4221
- code: import_types13.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_NOT_CONSUMED,
4230
+ code: import_types14.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_NOT_CONSUMED,
4222
4231
  message: `CheckoutOffer '${offer.id}' must be consumed before the contract is created`,
4223
4232
  params: {
4224
4233
  offerId: offer.id,
@@ -4257,7 +4266,7 @@ var SubscriptionContractService = class {
4257
4266
  const source = offer.lineItems ?? [];
4258
4267
  if (source.length === 0) {
4259
4268
  throw new import_common31.UnprocessableEntityException({
4260
- code: import_types13.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_LINE_ITEMS_REQUIRED,
4269
+ code: import_types14.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_LINE_ITEMS_REQUIRED,
4261
4270
  message: "A checkout offer can yield only one contract, and only once its line items are frozen."
4262
4271
  });
4263
4272
  }
@@ -4296,7 +4305,7 @@ var SubscriptionContractService = class {
4296
4305
  const date = value instanceof Date ? new Date(value) : new Date(value);
4297
4306
  if (Number.isNaN(date.getTime())) {
4298
4307
  throw new import_common31.UnprocessableEntityException({
4299
- code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_DATE,
4308
+ code: import_types14.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_DATE,
4300
4309
  message: `${field} must be a valid date.`,
4301
4310
  params: {
4302
4311
  field
@@ -4320,20 +4329,20 @@ var SubscriptionContractService = class {
4320
4329
  assertCreateData(data) {
4321
4330
  if (data.lineItems.length === 0) {
4322
4331
  throw new import_common31.UnprocessableEntityException({
4323
- code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED,
4332
+ code: import_types14.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED,
4324
4333
  message: "A subscription contract requires at least one line item."
4325
4334
  });
4326
4335
  }
4327
4336
  if (data.effectiveUntil && data.effectiveUntil <= data.effectiveFrom) {
4328
4337
  throw new import_common31.UnprocessableEntityException({
4329
- code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_WINDOW,
4338
+ code: import_types14.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_WINDOW,
4330
4339
  message: "effectiveUntil must be after effectiveFrom."
4331
4340
  });
4332
4341
  }
4333
4342
  const planLineItems = data.lineItems.filter((item) => item.kind === "plan");
4334
4343
  if (planLineItems.length !== 1) {
4335
4344
  throw new import_common31.UnprocessableEntityException({
4336
- code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED,
4345
+ code: import_types14.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED,
4337
4346
  message: "A subscription contract requires exactly one plan base item."
4338
4347
  });
4339
4348
  }
@@ -4341,7 +4350,7 @@ var SubscriptionContractService = class {
4341
4350
  assertTerminable(existing, data) {
4342
4351
  if (existing.status === "terminated" || existing.status === "superseded") {
4343
4352
  throw new import_common31.ConflictException({
4344
- code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_ALREADY_CLOSED,
4353
+ code: import_types14.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_ALREADY_CLOSED,
4345
4354
  message: `SubscriptionContract '${existing.id}' is already closed`,
4346
4355
  params: {
4347
4356
  contractId: existing.id,
@@ -4351,7 +4360,7 @@ var SubscriptionContractService = class {
4351
4360
  }
4352
4361
  if (data.effectiveUntil <= existing.effectiveFrom) {
4353
4362
  throw new import_common31.UnprocessableEntityException({
4354
- code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START,
4363
+ code: import_types14.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START,
4355
4364
  message: "effectiveUntil must be after the effectiveFrom of the contract."
4356
4365
  });
4357
4366
  }
@@ -4746,11 +4755,11 @@ _ts_decorate33([
4746
4755
 
4747
4756
  // src/billing/tenant-billing.controller.ts
4748
4757
  var import_common36 = require("@nestjs/common");
4749
- var import_types16 = require("@saasicat/types");
4758
+ var import_types17 = require("@saasicat/types");
4750
4759
 
4751
4760
  // src/promo/service.ts
4752
4761
  var import_common34 = require("@nestjs/common");
4753
- var import_types14 = require("@saasicat/types");
4762
+ var import_types15 = require("@saasicat/types");
4754
4763
 
4755
4764
  // src/promo/tokens.ts
4756
4765
  var PROMO_CODE_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/PromoCodeRepository");
@@ -4843,7 +4852,7 @@ var DURATION_MIN = 1;
4843
4852
  var DURATION_MAX = 24;
4844
4853
  function notRedeemable(reason) {
4845
4854
  return new import_common34.BadRequestException({
4846
- code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_NOT_REDEEMABLE,
4855
+ code: import_types15.PROMO_ERROR_CODES.PROMO_CODE_NOT_REDEEMABLE,
4847
4856
  message: `Code cannot be redeemed: ${reason}`,
4848
4857
  params: {
4849
4858
  reason
@@ -4880,7 +4889,7 @@ var PromoCodesService = class {
4880
4889
  const code = input.code.trim().toUpperCase();
4881
4890
  if (!CODE_PATTERN.test(code)) {
4882
4891
  throw new import_common34.BadRequestException({
4883
- code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_FORMAT_INVALID,
4892
+ code: import_types15.PROMO_ERROR_CODES.PROMO_CODE_FORMAT_INVALID,
4884
4893
  message: 'The code may only contain upper-case letters, digits, "-" and "_" (4\u201332 characters).',
4885
4894
  params: {
4886
4895
  promoCode: code,
@@ -4892,7 +4901,7 @@ var PromoCodesService = class {
4892
4901
  if (input.valueType === "PERCENT") {
4893
4902
  if (!(input.value > PERCENT_MIN_EXCLUSIVE && input.value <= PERCENT_MAX)) {
4894
4903
  throw new import_common34.BadRequestException({
4895
- code: import_types14.PROMO_ERROR_CODES.PROMO_PERCENT_OUT_OF_RANGE,
4904
+ code: import_types15.PROMO_ERROR_CODES.PROMO_PERCENT_OUT_OF_RANGE,
4896
4905
  message: "The percentage must be between 0 and 100.",
4897
4906
  params: {
4898
4907
  value: input.value,
@@ -4903,7 +4912,7 @@ var PromoCodesService = class {
4903
4912
  }
4904
4913
  } else if (!(input.value > 0)) {
4905
4914
  throw new import_common34.BadRequestException({
4906
- code: import_types14.PROMO_ERROR_CODES.PROMO_AMOUNT_NOT_POSITIVE,
4915
+ code: import_types15.PROMO_ERROR_CODES.PROMO_AMOUNT_NOT_POSITIVE,
4907
4916
  message: "The amount must be positive.",
4908
4917
  params: {
4909
4918
  value: input.value,
@@ -4914,7 +4923,7 @@ var PromoCodesService = class {
4914
4923
  if (input.durationType === "ONCE") {
4915
4924
  if (input.durationValue != null) {
4916
4925
  throw new import_common34.BadRequestException({
4917
- code: import_types14.PROMO_ERROR_CODES.PROMO_ONE_OFF_WITH_DURATION,
4926
+ code: import_types15.PROMO_ERROR_CODES.PROMO_ONE_OFF_WITH_DURATION,
4918
4927
  message: "A one-off discount must not set a duration.",
4919
4928
  params: {
4920
4929
  durationValue: input.durationValue
@@ -4925,7 +4934,7 @@ var PromoCodesService = class {
4925
4934
  const v = input.durationValue;
4926
4935
  if (v == null || v < DURATION_MIN || v > DURATION_MAX) {
4927
4936
  throw new import_common34.BadRequestException({
4928
- code: import_types14.PROMO_ERROR_CODES.PROMO_DURATION_INVALID,
4937
+ code: import_types15.PROMO_ERROR_CODES.PROMO_DURATION_INVALID,
4929
4938
  message: "Invalid duration (at most 24 months or billing periods).",
4930
4939
  params: {
4931
4940
  durationValue: v ?? null,
@@ -4937,7 +4946,7 @@ var PromoCodesService = class {
4937
4946
  }
4938
4947
  if (input.validFrom && input.validUntil && input.validFrom >= input.validUntil) {
4939
4948
  throw new import_common34.BadRequestException({
4940
- code: import_types14.PROMO_ERROR_CODES.PROMO_VALIDITY_WINDOW_INVALID,
4949
+ code: import_types15.PROMO_ERROR_CODES.PROMO_VALIDITY_WINDOW_INVALID,
4941
4950
  message: "Invalid validity window.",
4942
4951
  params: {
4943
4952
  validFrom: input.validFrom.toISOString(),
@@ -4950,7 +4959,7 @@ var PromoCodesService = class {
4950
4959
  for (const p of plans) {
4951
4960
  if (blocked.includes(p)) {
4952
4961
  throw new import_common34.BadRequestException({
4953
- code: import_types14.PROMO_ERROR_CODES.PROMO_PLAN_NOT_DISCOUNTABLE,
4962
+ code: import_types15.PROMO_ERROR_CODES.PROMO_PLAN_NOT_DISCOUNTABLE,
4954
4963
  message: `${p}-Plan cannot be discounted.`,
4955
4964
  params: {
4956
4965
  plan: p
@@ -4960,7 +4969,7 @@ var PromoCodesService = class {
4960
4969
  }
4961
4970
  if (input.minimumPlanAmountGross != null && input.minimumPlanAmountGross <= 0) {
4962
4971
  throw new import_common34.BadRequestException({
4963
- code: import_types14.PROMO_ERROR_CODES.PROMO_MIN_AMOUNT_NOT_POSITIVE,
4972
+ code: import_types15.PROMO_ERROR_CODES.PROMO_MIN_AMOUNT_NOT_POSITIVE,
4964
4973
  message: "The minimum plan gross amount must be positive.",
4965
4974
  params: {
4966
4975
  value: input.minimumPlanAmountGross,
@@ -4972,7 +4981,7 @@ var PromoCodesService = class {
4972
4981
  const lowestApplicableGross = this.lowestApplicablePlanGross(plans);
4973
4982
  if (lowestApplicableGross != null && input.value >= lowestApplicableGross) {
4974
4983
  throw new import_common34.BadRequestException({
4975
- code: import_types14.PROMO_ERROR_CODES.PROMO_WOULD_PRODUCE_ZERO_INVOICE,
4984
+ code: import_types15.PROMO_ERROR_CODES.PROMO_WOULD_PRODUCE_ZERO_INVOICE,
4976
4985
  message: "For absolute amounts the discount must stay below the lowest applicable plan price, or allowZeroInvoice must be enabled.",
4977
4986
  params: {
4978
4987
  value: input.value,
@@ -4984,7 +4993,7 @@ var PromoCodesService = class {
4984
4993
  const exists = await this.promoRepo.findByCode(code);
4985
4994
  if (exists) {
4986
4995
  throw new import_common34.BadRequestException({
4987
- code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_ALREADY_EXISTS,
4996
+ code: import_types15.PROMO_ERROR_CODES.PROMO_CODE_ALREADY_EXISTS,
4988
4997
  message: "The code already exists.",
4989
4998
  params: {
4990
4999
  promoCode: code
@@ -5000,17 +5009,18 @@ var PromoCodesService = class {
5000
5009
  const existing = await this.promoRepo.findById(id);
5001
5010
  if (!existing) {
5002
5011
  throw new import_common34.NotFoundException({
5003
- code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
5012
+ code: import_types15.PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
5004
5013
  message: "Code not found",
5005
5014
  params: {
5006
- promoCodeId: id
5015
+ promoCodeId: id,
5016
+ promoCode: null
5007
5017
  }
5008
5018
  });
5009
5019
  }
5010
5020
  if (input.maxRedemptions != null && existing.maxRedemptions != null) {
5011
5021
  if (input.maxRedemptions < existing.maxRedemptions) {
5012
5022
  throw new import_common34.BadRequestException({
5013
- code: import_types14.PROMO_ERROR_CODES.PROMO_MAX_REDEMPTIONS_LOWERED,
5023
+ code: import_types15.PROMO_ERROR_CODES.PROMO_MAX_REDEMPTIONS_LOWERED,
5014
5024
  message: "maxRedemptions cannot be lowered.",
5015
5025
  params: {
5016
5026
  current: existing.maxRedemptions,
@@ -5025,7 +5035,7 @@ var PromoCodesService = class {
5025
5035
  const redemptions = await this.redemptionRepo.countByPromoCode(id);
5026
5036
  if (redemptions > 0) {
5027
5037
  throw new import_common34.BadRequestException({
5028
- code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_HAS_REDEMPTIONS,
5038
+ code: import_types15.PROMO_ERROR_CODES.PROMO_CODE_HAS_REDEMPTIONS,
5029
5039
  message: "The code already has redemptions \u2014 it cannot be soft-deleted. Pause it instead.",
5030
5040
  params: {
5031
5041
  promoCodeId: id,
@@ -5044,10 +5054,11 @@ var PromoCodesService = class {
5044
5054
  const code = await this.promoRepo.findById(id);
5045
5055
  if (!code || code.deletedAt) {
5046
5056
  throw new import_common34.NotFoundException({
5047
- code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
5057
+ code: import_types15.PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
5048
5058
  message: "Code not found",
5049
5059
  params: {
5050
- promoCodeId: id
5060
+ promoCodeId: id,
5061
+ promoCode: null
5051
5062
  }
5052
5063
  });
5053
5064
  }
@@ -5179,9 +5190,11 @@ var PromoCodesService = class {
5179
5190
  const promo = await this.promoRepo.findByCode(code, tx);
5180
5191
  if (!promo || promo.deletedAt) {
5181
5192
  throw new import_common34.BadRequestException({
5182
- code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
5193
+ code: import_types15.PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
5194
+ // The lookup runs on the code string, so there is no id to report.
5183
5195
  message: "Code not found",
5184
5196
  params: {
5197
+ promoCodeId: null,
5185
5198
  promoCode: code
5186
5199
  }
5187
5200
  });
@@ -5189,16 +5202,16 @@ var PromoCodesService = class {
5189
5202
  const sub = await this.subscriptionLookup.findById(input.subscriptionId, tx);
5190
5203
  if (!sub) {
5191
5204
  throw new import_common34.NotFoundException({
5192
- code: import_types14.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5205
+ code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5193
5206
  message: "Subscription not found",
5194
5207
  params: {
5195
- subscriptionId: input.subscriptionId
5208
+ tenantId: input.tenantId
5196
5209
  }
5197
5210
  });
5198
5211
  }
5199
5212
  if (sub.tenantId !== input.tenantId) {
5200
5213
  throw new import_common34.BadRequestException({
5201
- code: import_types14.BILLING_ERROR_CODES.SUBSCRIPTION_TENANT_MISMATCH,
5214
+ code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_TENANT_MISMATCH,
5202
5215
  message: "Subscription does not belong to the tenant",
5203
5216
  params: {
5204
5217
  subscriptionId: sub.id,
@@ -5331,7 +5344,7 @@ PromoCodesService = _ts_decorate34([
5331
5344
 
5332
5345
  // src/billing/subscription-bundles.service.ts
5333
5346
  var import_common35 = require("@nestjs/common");
5334
- var import_types15 = require("@saasicat/types");
5347
+ var import_types16 = require("@saasicat/types");
5335
5348
  function _ts_decorate35(decorators, target, key, desc) {
5336
5349
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5337
5350
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -5380,7 +5393,7 @@ var SubscriptionBundlesService = class {
5380
5393
  const bundleVersion = await this.bundles.findVersionById(input.bundleVersionId);
5381
5394
  if (!bundleVersion) {
5382
5395
  throw new import_common35.NotFoundException({
5383
- code: import_types15.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
5396
+ code: import_types16.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
5384
5397
  message: `BundleVersion '${input.bundleVersionId}' not found`,
5385
5398
  params: {
5386
5399
  bundleVersionId: input.bundleVersionId
@@ -5389,7 +5402,7 @@ var SubscriptionBundlesService = class {
5389
5402
  }
5390
5403
  if (bundleVersion.publishedAt === null) {
5391
5404
  throw new import_common35.UnprocessableEntityException({
5392
- code: import_types15.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_PUBLISHED,
5405
+ code: import_types16.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_PUBLISHED,
5393
5406
  message: `BundleVersion '${input.bundleVersionId}' is not published and cannot be booked.`,
5394
5407
  params: {
5395
5408
  bundleVersionId: input.bundleVersionId
@@ -5398,7 +5411,7 @@ var SubscriptionBundlesService = class {
5398
5411
  }
5399
5412
  if (bundleVersion.supersededAt !== null) {
5400
5413
  throw new import_common35.UnprocessableEntityException({
5401
- code: import_types15.CATALOG_ERROR_CODES.BUNDLE_VERSION_SUPERSEDED,
5414
+ code: import_types16.CATALOG_ERROR_CODES.BUNDLE_VERSION_SUPERSEDED,
5402
5415
  message: `BundleVersion '${input.bundleVersionId}' has been superseded by a newer version.`,
5403
5416
  params: {
5404
5417
  bundleVersionId: input.bundleVersionId
@@ -5407,7 +5420,7 @@ var SubscriptionBundlesService = class {
5407
5420
  }
5408
5421
  if (this.blockedBundles?.bundleKeys?.includes(bundleVersion.bundleKey)) {
5409
5422
  throw new import_common35.UnprocessableEntityException({
5410
- code: import_types15.BILLING_ERROR_CODES.BUNDLE_NOT_SELF_SERVICE,
5423
+ code: import_types16.BILLING_ERROR_CODES.BUNDLE_NOT_SELF_SERVICE,
5411
5424
  message: `Bundle '${bundleVersion.bundleKey}' is only activated via a special contract. Please contact the contract manager.`,
5412
5425
  params: {
5413
5426
  bundleKey: bundleVersion.bundleKey
@@ -5417,7 +5430,7 @@ var SubscriptionBundlesService = class {
5417
5430
  const planIds = bundleVersion.compatibility?.planIds ?? [];
5418
5431
  if (planIds.length > 0 && !planIds.includes(input.currentPlanKey)) {
5419
5432
  throw new import_common35.UnprocessableEntityException({
5420
- code: import_types15.BILLING_ERROR_CODES.BUNDLE_INCOMPATIBLE_WITH_PLAN,
5433
+ code: import_types16.BILLING_ERROR_CODES.BUNDLE_INCOMPATIBLE_WITH_PLAN,
5421
5434
  message: `BundleVersion '${input.bundleVersionId}' is not compatible with plan '${input.currentPlanKey}'. Allowed: [${planIds.join(", ")}].`,
5422
5435
  params: {
5423
5436
  bundleVersionId: input.bundleVersionId,
@@ -5429,7 +5442,7 @@ var SubscriptionBundlesService = class {
5429
5442
  const active = await this.repo.listActiveBySubscription(input.subscriptionId);
5430
5443
  if (active.some((b) => b.bundleVersionId === input.bundleVersionId)) {
5431
5444
  throw new import_common35.UnprocessableEntityException({
5432
- code: import_types15.BILLING_ERROR_CODES.BUNDLE_ALREADY_SUBSCRIBED,
5445
+ code: import_types16.BILLING_ERROR_CODES.BUNDLE_ALREADY_SUBSCRIBED,
5433
5446
  message: `Subscription '${input.subscriptionId}' has already actively booked this bundle.`,
5434
5447
  params: {
5435
5448
  subscriptionId: input.subscriptionId
@@ -5450,7 +5463,7 @@ var SubscriptionBundlesService = class {
5450
5463
  const existing = await this.repo.findById(input.subscriptionBundleId);
5451
5464
  if (!existing) {
5452
5465
  throw new import_common35.NotFoundException({
5453
- code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_FOUND,
5466
+ code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_FOUND,
5454
5467
  message: `SubscriptionBundle '${input.subscriptionBundleId}' not found`,
5455
5468
  params: {
5456
5469
  subscriptionBundleId: input.subscriptionBundleId
@@ -5459,7 +5472,7 @@ var SubscriptionBundlesService = class {
5459
5472
  }
5460
5473
  if (existing.canceledAt !== null) {
5461
5474
  throw new import_common35.UnprocessableEntityException({
5462
- code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_ALREADY_CANCELED,
5475
+ code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED,
5463
5476
  message: `SubscriptionBundle '${input.subscriptionBundleId}' is already cancelled.`,
5464
5477
  params: {
5465
5478
  subscriptionBundleId: input.subscriptionBundleId
@@ -5485,7 +5498,7 @@ var SubscriptionBundlesService = class {
5485
5498
  const existing = await this.repo.findById(subscriptionBundleId);
5486
5499
  if (!existing) {
5487
5500
  throw new import_common35.NotFoundException({
5488
- code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_FOUND,
5501
+ code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_FOUND,
5489
5502
  message: `SubscriptionBundle '${subscriptionBundleId}' not found`,
5490
5503
  params: {
5491
5504
  subscriptionBundleId
@@ -5494,7 +5507,7 @@ var SubscriptionBundlesService = class {
5494
5507
  }
5495
5508
  if (existing.canceledAt === null) {
5496
5509
  throw new import_common35.UnprocessableEntityException({
5497
- code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_CANCELED,
5510
+ code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_CANCELLED,
5498
5511
  message: `SubscriptionBundle '${subscriptionBundleId}' is not cancelled.`,
5499
5512
  params: {
5500
5513
  subscriptionBundleId
@@ -5503,7 +5516,7 @@ var SubscriptionBundlesService = class {
5503
5516
  }
5504
5517
  if (existing.canceledEffectiveAt !== null && existing.canceledEffectiveAt <= /* @__PURE__ */ new Date()) {
5505
5518
  throw new import_common35.UnprocessableEntityException({
5506
- code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE,
5519
+ code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE,
5507
5520
  message: "Cancellation already in effect \u2014 book the bundle again."
5508
5521
  });
5509
5522
  }
@@ -5611,7 +5624,7 @@ var TenantBillingController = class _TenantBillingController {
5611
5624
  ]);
5612
5625
  if (!sub) {
5613
5626
  throw new import_common36.NotFoundException({
5614
- code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5627
+ code: import_types17.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5615
5628
  message: `No subscription for tenant ${tenantId}`,
5616
5629
  params: {
5617
5630
  tenantId
@@ -5666,7 +5679,7 @@ var TenantBillingController = class _TenantBillingController {
5666
5679
  const blockedTargets = this.blockedPlans?.asTarget ?? [];
5667
5680
  if (blockedTargets.includes(dto.plan)) {
5668
5681
  throw new import_common36.ForbiddenException({
5669
- code: import_types16.BILLING_ERROR_CODES.PLAN_NOT_SELF_SERVICE,
5682
+ code: import_types17.BILLING_ERROR_CODES.PLAN_NOT_SELF_SERVICE,
5670
5683
  message: `${dto.plan} is not activated via self-service.`,
5671
5684
  params: {
5672
5685
  planKey: dto.plan
@@ -5676,7 +5689,7 @@ var TenantBillingController = class _TenantBillingController {
5676
5689
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
5677
5690
  if (!sub) {
5678
5691
  throw new import_common36.NotFoundException({
5679
- code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5692
+ code: import_types17.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5680
5693
  message: `No subscription for tenant ${tenantId}`,
5681
5694
  params: {
5682
5695
  tenantId
@@ -5686,7 +5699,7 @@ var TenantBillingController = class _TenantBillingController {
5686
5699
  const blockers = await this.planPreview.assertChangeAllowed(tenantId, dto.plan, dto.billingCycle);
5687
5700
  if (blockers.length > 0) {
5688
5701
  throw new import_common36.BadRequestException({
5689
- code: import_types16.BILLING_ERROR_CODES.PLAN_CHANGE_BLOCKED,
5702
+ code: import_types17.BILLING_ERROR_CODES.PLAN_CHANGE_BLOCKED,
5690
5703
  message: "Plan change is blocked.",
5691
5704
  blockers
5692
5705
  });
@@ -5758,7 +5771,7 @@ var TenantBillingController = class _TenantBillingController {
5758
5771
  const blockedTargets = this.blockedPlans?.asTarget ?? [];
5759
5772
  if (blockedTargets.includes(dto.plan)) {
5760
5773
  throw new import_common36.ForbiddenException({
5761
- code: import_types16.BILLING_ERROR_CODES.PLAN_NOT_SELF_SERVICE,
5774
+ code: import_types17.BILLING_ERROR_CODES.PLAN_NOT_SELF_SERVICE,
5762
5775
  message: `${dto.plan} is not activated via self-service.`,
5763
5776
  params: {
5764
5777
  planKey: dto.plan
@@ -5768,7 +5781,7 @@ var TenantBillingController = class _TenantBillingController {
5768
5781
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
5769
5782
  if (!sub) {
5770
5783
  throw new import_common36.NotFoundException({
5771
- code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5784
+ code: import_types17.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5772
5785
  message: `No subscription for tenant ${tenantId}`,
5773
5786
  params: {
5774
5787
  tenantId
@@ -5778,7 +5791,7 @@ var TenantBillingController = class _TenantBillingController {
5778
5791
  const blockers = await this.planPreview.assertChangeAllowed(tenantId, dto.plan, dto.billingCycle);
5779
5792
  if (blockers.length > 0) {
5780
5793
  throw new import_common36.BadRequestException({
5781
- code: import_types16.BILLING_ERROR_CODES.PLAN_CHANGE_BLOCKED,
5794
+ code: import_types17.BILLING_ERROR_CODES.PLAN_CHANGE_BLOCKED,
5782
5795
  message: "Plan change during onboarding is blocked.",
5783
5796
  blockers
5784
5797
  });
@@ -5815,7 +5828,7 @@ var TenantBillingController = class _TenantBillingController {
5815
5828
  } catch (err) {
5816
5829
  const reason = err instanceof Error ? err.message : null;
5817
5830
  throw new import_common36.BadRequestException({
5818
- code: import_types16.BILLING_ERROR_CODES.ONBOARDING_CREATE_FAILED,
5831
+ code: import_types17.BILLING_ERROR_CODES.ONBOARDING_CREATE_FAILED,
5819
5832
  message: reason ? `Onboarding creation failed: ${reason}` : "Onboarding creation failed",
5820
5833
  params: {
5821
5834
  reason
@@ -5898,7 +5911,7 @@ var TenantBillingController = class _TenantBillingController {
5898
5911
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
5899
5912
  if (!sub) {
5900
5913
  throw new import_common36.NotFoundException({
5901
- code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5914
+ code: import_types17.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
5902
5915
  message: `No subscription for tenant ${tenantId}`,
5903
5916
  params: {
5904
5917
  tenantId
@@ -5907,7 +5920,7 @@ var TenantBillingController = class _TenantBillingController {
5907
5920
  }
5908
5921
  if (!sub.pendingPlanVersion) {
5909
5922
  throw new import_common36.BadRequestException({
5910
- code: import_types16.BILLING_ERROR_CODES.NO_PENDING_PLAN_VERSION,
5923
+ code: import_types17.BILLING_ERROR_CODES.NO_PENDING_PLAN_VERSION,
5911
5924
  message: "There is no pending plan version awaiting confirmation."
5912
5925
  });
5913
5926
  }
@@ -5955,7 +5968,7 @@ var TenantBillingController = class _TenantBillingController {
5955
5968
  const tenantId = resolver(req);
5956
5969
  if (!tenantId) {
5957
5970
  throw new import_common36.NotFoundException({
5958
- code: import_types16.AUTH_ERROR_CODES.TENANT_CONTEXT_MISSING,
5971
+ code: import_types17.AUTH_ERROR_CODES.TENANT_CONTEXT_MISSING,
5959
5972
  message: "No tenant ID found on the request"
5960
5973
  });
5961
5974
  }
@@ -5966,7 +5979,7 @@ var TenantBillingController = class _TenantBillingController {
5966
5979
  const userId = resolver(req);
5967
5980
  if (!userId) {
5968
5981
  throw new import_common36.NotFoundException({
5969
- code: import_types16.AUTH_ERROR_CODES.TENANT_CONTEXT_MISSING,
5982
+ code: import_types17.AUTH_ERROR_CODES.TENANT_CONTEXT_MISSING,
5970
5983
  message: "No user ID found on the request"
5971
5984
  });
5972
5985
  }
@@ -6337,7 +6350,7 @@ __name(normalizeQuotas, "normalizeQuotas");
6337
6350
 
6338
6351
  // src/billing/subscription-bundle-preview.service.ts
6339
6352
  var import_common39 = require("@nestjs/common");
6340
- var import_types17 = require("@saasicat/types");
6353
+ var import_types18 = require("@saasicat/types");
6341
6354
  function _ts_decorate39(decorators, target, key, desc) {
6342
6355
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
6343
6356
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -6377,7 +6390,7 @@ var SubscriptionBundlePreviewService = class {
6377
6390
  const bundleVersion = await this.bundles.findVersionById(input.bundleVersionId);
6378
6391
  if (!bundleVersion) {
6379
6392
  throw new import_common39.NotFoundException({
6380
- code: import_types17.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
6393
+ code: import_types18.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
6381
6394
  message: `BundleVersion '${input.bundleVersionId}' not found`,
6382
6395
  params: {
6383
6396
  bundleVersionId: input.bundleVersionId
@@ -6436,7 +6449,7 @@ var SubscriptionBundlePreviewService = class {
6436
6449
  const existing = await this.subscriptionBundles.findById(input.subscriptionBundleId);
6437
6450
  if (!existing || existing.subscriptionId !== ctx.subscriptionId) {
6438
6451
  throw new import_common39.NotFoundException({
6439
- code: import_types17.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_FOUND,
6452
+ code: import_types18.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_FOUND,
6440
6453
  message: `SubscriptionBundle '${input.subscriptionBundleId}' not found`,
6441
6454
  params: {
6442
6455
  subscriptionBundleId: input.subscriptionBundleId
@@ -6446,7 +6459,7 @@ var SubscriptionBundlePreviewService = class {
6446
6459
  const bundleVersion = await this.bundles.findVersionById(existing.bundleVersionId);
6447
6460
  if (!bundleVersion) {
6448
6461
  throw new import_common39.NotFoundException({
6449
- code: import_types17.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
6462
+ code: import_types18.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
6450
6463
  message: `BundleVersion '${existing.bundleVersionId}' not found`,
6451
6464
  params: {
6452
6465
  bundleVersionId: existing.bundleVersionId
@@ -6457,7 +6470,7 @@ var SubscriptionBundlePreviewService = class {
6457
6470
  const warnings = [];
6458
6471
  if (existing.canceledAt !== null) {
6459
6472
  blockers.push({
6460
- code: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELED",
6473
+ code: import_types18.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_ALREADY_CANCELLED,
6461
6474
  message: "This bundle booking is already cancelled."
6462
6475
  });
6463
6476
  }
@@ -6557,12 +6570,12 @@ var SubscriptionBundlePreviewService = class {
6557
6570
  const entries = await this.catalogEntries.listFeatures({
6558
6571
  projectKey
6559
6572
  });
6560
- const requiresIndex = (0, import_types17.buildFeatureRequiresIndex)(entries);
6573
+ const requiresIndex = (0, import_types18.buildFeatureRequiresIndex)(entries);
6561
6574
  const covered = /* @__PURE__ */ new Set([
6562
6575
  ...planFeatures,
6563
6576
  ...activeBundleVersions.flatMap((bv) => bv.features ?? [])
6564
6577
  ]);
6565
- return (0, import_types17.collectUnsatisfiedRequires)(bundleVersion.features ?? [], requiresIndex).filter((key) => !covered.has(key));
6578
+ return (0, import_types18.collectUnsatisfiedRequires)(bundleVersion.features ?? [], requiresIndex).filter((key) => !covered.has(key));
6566
6579
  }
6567
6580
  /** `projectKey` lives on the bundle stem, not on the version. */
6568
6581
  async resolveProjectKey(bundleVersion) {
@@ -6621,7 +6634,7 @@ var import_common41 = require("@nestjs/common");
6621
6634
 
6622
6635
  // src/billing/tenant-subscription-bundles.controller.ts
6623
6636
  var import_common40 = require("@nestjs/common");
6624
- var import_types18 = require("@saasicat/types");
6637
+ var import_types19 = require("@saasicat/types");
6625
6638
 
6626
6639
  // src/billing/dto/subscription-bundles.dto.ts
6627
6640
  var import_class_validator5 = require("class-validator");
@@ -6767,7 +6780,7 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
6767
6780
  const hasCancel = Boolean(dto.subscriptionBundleId);
6768
6781
  if (hasAdd === hasCancel) {
6769
6782
  throw new import_common40.BadRequestException({
6770
- code: import_types18.BILLING_ERROR_CODES.BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS,
6783
+ code: import_types19.BILLING_ERROR_CODES.BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS,
6771
6784
  message: "Exactly one of bundleVersionId (add preview) or subscriptionBundleId (cancel preview) must be given."
6772
6785
  });
6773
6786
  }
@@ -6809,7 +6822,7 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
6809
6822
  }
6810
6823
  requireSubscriptionPk(sub) {
6811
6824
  if (!sub.id) {
6812
- throw new import_common40.NotFoundException("SubscriptionUsageRecord without `id` \u2014 the adapter must return the subscription primary key (see SubscriptionUsageRecord.id).");
6825
+ throw new import_common40.NotFoundException(codedError(import_types19.BILLING_ERROR_CODES.SUBSCRIPTION_PK_MISSING));
6813
6826
  }
6814
6827
  return sub.id;
6815
6828
  }
@@ -6817,7 +6830,7 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
6817
6830
  const tenantId = this.tenantIdResolver(req);
6818
6831
  if (!tenantId) {
6819
6832
  throw new import_common40.NotFoundException({
6820
- code: import_types18.AUTH_ERROR_CODES.TENANT_CONTEXT_MISSING,
6833
+ code: import_types19.AUTH_ERROR_CODES.TENANT_CONTEXT_MISSING,
6821
6834
  message: "No tenant ID found on the request"
6822
6835
  });
6823
6836
  }
@@ -6827,7 +6840,7 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
6827
6840
  const sub = await this.subscriptionUsage.findForTenant(tenantId);
6828
6841
  if (!sub) {
6829
6842
  throw new import_common40.NotFoundException({
6830
- code: import_types18.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
6843
+ code: import_types19.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
6831
6844
  message: `No subscription for tenant ${tenantId}`,
6832
6845
  params: {
6833
6846
  tenantId
@@ -7070,7 +7083,7 @@ __export(catalog_exports, {
7070
7083
 
7071
7084
  // src/catalog/bundles.service.ts
7072
7085
  var import_common43 = require("@nestjs/common");
7073
- var import_types19 = require("@saasicat/types");
7086
+ var import_types20 = require("@saasicat/types");
7074
7087
 
7075
7088
  // src/discovery/discovery.scanner.ts
7076
7089
  var import_node_crypto2 = require("crypto");
@@ -7594,7 +7607,7 @@ var BundlesService = class _BundlesService {
7594
7607
  const bundle = await this.repo.findById(bundleId);
7595
7608
  if (!bundle) {
7596
7609
  throw new import_common43.NotFoundException({
7597
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
7610
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
7598
7611
  message: `Bundle '${bundleId}' not found`,
7599
7612
  params: {
7600
7613
  bundleId
@@ -7611,7 +7624,7 @@ var BundlesService = class _BundlesService {
7611
7624
  const existing = await this.repo.findByKey(data.projectKey, data.bundleKey);
7612
7625
  if (existing) {
7613
7626
  throw new import_common43.UnprocessableEntityException({
7614
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_ALREADY_EXISTS,
7627
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_ALREADY_EXISTS,
7615
7628
  message: `Bundle '${data.bundleKey}' already exists in project '${data.projectKey}'`,
7616
7629
  params: {
7617
7630
  bundleKey: data.bundleKey,
@@ -7625,7 +7638,7 @@ var BundlesService = class _BundlesService {
7625
7638
  const existing = await this.repo.findById(bundleId);
7626
7639
  if (!existing) {
7627
7640
  throw new import_common43.NotFoundException({
7628
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
7641
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
7629
7642
  message: `Bundle '${bundleId}' not found`,
7630
7643
  params: {
7631
7644
  bundleId
@@ -7638,7 +7651,7 @@ var BundlesService = class _BundlesService {
7638
7651
  const existing = await this.repo.findById(bundleId);
7639
7652
  if (!existing) {
7640
7653
  throw new import_common43.NotFoundException({
7641
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
7654
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
7642
7655
  message: `Bundle '${bundleId}' not found`,
7643
7656
  params: {
7644
7657
  bundleId
@@ -7657,16 +7670,16 @@ var BundlesService = class _BundlesService {
7657
7670
  const existing = await this.repo.findVersionById(versionId);
7658
7671
  if (!existing) {
7659
7672
  throw new import_common43.NotFoundException({
7660
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
7673
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
7661
7674
  message: `BundleVersion '${versionId}' not found`,
7662
7675
  params: {
7663
- versionId
7676
+ bundleVersionId: versionId
7664
7677
  }
7665
7678
  });
7666
7679
  }
7667
7680
  if (existing.publishedAt !== null) {
7668
7681
  throw new import_common43.UnprocessableEntityException({
7669
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_ALREADY_PUBLISHED,
7682
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_ALREADY_PUBLISHED,
7670
7683
  message: `BundleVersion '${versionId}' is already published and cannot be discarded.`,
7671
7684
  params: {
7672
7685
  versionId
@@ -7675,7 +7688,7 @@ var BundlesService = class _BundlesService {
7675
7688
  }
7676
7689
  if (typeof this.repo.deleteDraft !== "function") {
7677
7690
  throw new import_common43.UnprocessableEntityException({
7678
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED,
7691
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED,
7679
7692
  message: "Discard is not implemented in the current repository. Implement BundleRepository.deleteDraft."
7680
7693
  });
7681
7694
  }
@@ -7691,10 +7704,10 @@ var BundlesService = class _BundlesService {
7691
7704
  const version = await this.repo.findVersionById(versionId);
7692
7705
  if (!version) {
7693
7706
  throw new import_common43.NotFoundException({
7694
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
7707
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
7695
7708
  message: `BundleVersion '${versionId}' not found`,
7696
7709
  params: {
7697
- versionId
7710
+ bundleVersionId: versionId
7698
7711
  }
7699
7712
  });
7700
7713
  }
@@ -7739,7 +7752,7 @@ var BundlesService = class _BundlesService {
7739
7752
  const bundle = await this.repo.findById(data.bundleId);
7740
7753
  if (!bundle) {
7741
7754
  throw new import_common43.NotFoundException({
7742
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
7755
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
7743
7756
  message: `Bundle '${data.bundleId}' not found`,
7744
7757
  params: {
7745
7758
  bundleId: data.bundleId
@@ -7749,7 +7762,7 @@ var BundlesService = class _BundlesService {
7749
7762
  const existingDraft = await this.repo.findCurrentDraft(data.bundleId);
7750
7763
  if (existingDraft) {
7751
7764
  throw new import_common43.UnprocessableEntityException({
7752
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_DRAFT_ALREADY_EXISTS,
7765
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_DRAFT_ALREADY_EXISTS,
7753
7766
  message: `Bundle '${bundle.bundleKey}' already has a draft version v${existingDraft.version}; publish or discard it first`,
7754
7767
  params: {
7755
7768
  bundleKey: bundle.bundleKey,
@@ -7788,20 +7801,20 @@ var BundlesService = class _BundlesService {
7788
7801
  const existing = await this.repo.findVersionById(versionId);
7789
7802
  if (!existing) {
7790
7803
  throw new import_common43.NotFoundException({
7791
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
7804
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
7792
7805
  message: `BundleVersion '${versionId}' not found`,
7793
7806
  params: {
7794
- versionId
7807
+ bundleVersionId: versionId
7795
7808
  }
7796
7809
  });
7797
7810
  }
7798
7811
  const [annotated] = await this.annotateEditability([
7799
7812
  existing
7800
7813
  ]);
7801
- const { editable } = (0, import_types19.isVersionEditable)(annotated);
7814
+ const { editable } = (0, import_types20.isVersionEditable)(annotated);
7802
7815
  if (!editable) {
7803
7816
  throw new import_common43.UnprocessableEntityException({
7804
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_EDITABLE,
7817
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_EDITABLE,
7805
7818
  message: `BundleVersion '${versionId}' is not editable. Only drafts and published versions are editable that are latest-in-chain, bind no subscription yet, and whose validFrom lies in the future.`,
7806
7819
  params: {
7807
7820
  versionId
@@ -7827,7 +7840,7 @@ var BundlesService = class _BundlesService {
7827
7840
  * Publishes a draft version atomically. Steps:
7828
7841
  * 1. Strict check on the draft definition (warn-only or blocking)
7829
7842
  * 2. Enforce validFrom requirement + ordering against the predecessor
7830
- * (SPEC_V2 §4.2 + §11.1 M6 Pack 2c, analogous to PlanVersion)
7843
+ * (analogous to PlanVersion)
7831
7844
  * 3. Compute the diff against the predecessor version (latest live) →
7832
7845
  * `publishedChanges` + `nonRegressive` via `classifyBundleVersionDiff`
7833
7846
  * 4. Call Repository.publishDraft with the diff result + validity dates
@@ -7837,16 +7850,16 @@ var BundlesService = class _BundlesService {
7837
7850
  const draft = await this.repo.findVersionById(versionId);
7838
7851
  if (!draft) {
7839
7852
  throw new import_common43.NotFoundException({
7840
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
7853
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
7841
7854
  message: `BundleVersion '${versionId}' not found`,
7842
7855
  params: {
7843
- versionId
7856
+ bundleVersionId: versionId
7844
7857
  }
7845
7858
  });
7846
7859
  }
7847
7860
  if (draft.publishedAt !== null) {
7848
7861
  throw new import_common43.UnprocessableEntityException({
7849
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_ALREADY_PUBLISHED,
7862
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_ALREADY_PUBLISHED,
7850
7863
  message: `BundleVersion '${versionId}' is already published`,
7851
7864
  params: {
7852
7865
  versionId
@@ -7864,7 +7877,7 @@ var BundlesService = class _BundlesService {
7864
7877
  const explicitZero = /* @__PURE__ */ __name((v) => v !== null && v !== void 0 && Number.parseFloat(String(v)) <= 0, "explicitZero");
7865
7878
  if (explicitZero(draft.monthlyNet) || explicitZero(draft.yearlyNet)) {
7866
7879
  throw new import_common43.UnprocessableEntityException({
7867
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_ZERO_PRICE,
7880
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_ZERO_PRICE,
7868
7881
  message: "A bundle version cannot be published with an explicit price of 0.00 (guard against seed placeholders). For free bundles leave it null, or set allowZeroPrice.",
7869
7882
  monthlyNet: draft.monthlyNet,
7870
7883
  yearlyNet: draft.yearlyNet
@@ -7875,14 +7888,14 @@ var BundlesService = class _BundlesService {
7875
7888
  const validFromInput = publishMeta.validFrom ?? draft.validFrom;
7876
7889
  if (!validFromInput) {
7877
7890
  throw new import_common43.UnprocessableEntityException({
7878
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
7879
- message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
7891
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
7892
+ message: "validFrom must be set when publishing (on the draft or the publish call).."
7880
7893
  });
7881
7894
  }
7882
7895
  const validFrom = new Date(validFromInput);
7883
7896
  if (Number.isNaN(validFrom.getTime())) {
7884
7897
  throw new import_common43.UnprocessableEntityException({
7885
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_INVALID,
7898
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_INVALID,
7886
7899
  message: `validFrom '${validFromInput}' is not a valid date`,
7887
7900
  params: {
7888
7901
  validFrom: validFromInput
@@ -7893,7 +7906,7 @@ var BundlesService = class _BundlesService {
7893
7906
  const prevFrom = new Date(previous.validFrom);
7894
7907
  if (validFrom <= prevFrom) {
7895
7908
  throw new import_common43.UnprocessableEntityException({
7896
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
7909
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
7897
7910
  message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
7898
7911
  params: {
7899
7912
  validFrom: validFrom.toISOString(),
@@ -7907,7 +7920,7 @@ var BundlesService = class _BundlesService {
7907
7920
  const requiredStart = new Date(prevUntil.getTime() + dayMs);
7908
7921
  if (validFrom.getTime() !== requiredStart.getTime()) {
7909
7922
  throw new import_common43.UnprocessableEntityException({
7910
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
7923
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
7911
7924
  message: `The predecessor has validUntil=${previous.validUntil.slice(0, 10)} \u2014 the successor must start seamlessly on the next day (${requiredStart.toISOString().slice(0, 10)}). Received: ${validFrom.toISOString().slice(0, 10)}.`,
7912
7925
  params: {
7913
7926
  received: validFrom.toISOString().slice(0, 10)
@@ -7922,7 +7935,7 @@ var BundlesService = class _BundlesService {
7922
7935
  const validUntil = validUntilInput ? new Date(validUntilInput) : null;
7923
7936
  if (validUntil && Number.isNaN(validUntil.getTime())) {
7924
7937
  throw new import_common43.UnprocessableEntityException({
7925
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_INVALID,
7938
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_INVALID,
7926
7939
  message: `validUntil '${validUntilInput}' is not a valid date`,
7927
7940
  params: {
7928
7941
  validUntil: validUntilInput
@@ -7931,7 +7944,7 @@ var BundlesService = class _BundlesService {
7931
7944
  }
7932
7945
  if (validUntil && validUntil <= validFrom) {
7933
7946
  throw new import_common43.UnprocessableEntityException({
7934
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM,
7947
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM,
7935
7948
  message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
7936
7949
  params: {
7937
7950
  validUntil: validUntil.toISOString(),
@@ -7939,7 +7952,7 @@ var BundlesService = class _BundlesService {
7939
7952
  }
7940
7953
  });
7941
7954
  }
7942
- const diff = previous ? (0, import_types4.classifyBundleVersionDiff)({
7955
+ const diff = previous ? (0, import_types5.classifyBundleVersionDiff)({
7943
7956
  features: previous.features,
7944
7957
  quotas: previous.quotas,
7945
7958
  monthlyNet: previous.monthlyNet,
@@ -7955,7 +7968,7 @@ var BundlesService = class _BundlesService {
7955
7968
  };
7956
7969
  if (!diff.nonRegressive && !publishMeta.forceRegressive) {
7957
7970
  throw new import_common43.UnprocessableEntityException({
7958
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_REGRESSION,
7971
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_REGRESSION,
7959
7972
  message: "This bundle version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
7960
7973
  changes: diff.changes
7961
7974
  });
@@ -7995,7 +8008,7 @@ var BundlesService = class _BundlesService {
7995
8008
  const hasHardBlockingWarning = blocking.some((w) => w.code === "BUNDLE_PLAN_KEY_UNKNOWN");
7996
8009
  if (this.mode === "blocking" || hasHardBlockingWarning) {
7997
8010
  throw new import_common43.UnprocessableEntityException({
7998
- code: import_types19.CATALOG_ERROR_CODES.STRICT_MODE_VIOLATIONS,
8011
+ code: import_types20.CATALOG_ERROR_CODES.STRICT_MODE_VIOLATIONS,
7999
8012
  message: "The strict-mode check found drift against the discovery snapshot.",
8000
8013
  warnings
8001
8014
  });
@@ -8009,7 +8022,7 @@ var BundlesService = class _BundlesService {
8009
8022
  if (!validFromInput) {
8010
8023
  if (existing.publishedAt !== null) {
8011
8024
  throw new import_common43.UnprocessableEntityException({
8012
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
8025
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
8013
8026
  message: "Published bundle versions must keep a validFrom. Set a new future date, or edit the draft before publishing."
8014
8027
  });
8015
8028
  }
@@ -8018,7 +8031,7 @@ var BundlesService = class _BundlesService {
8018
8031
  const validFrom = this.parseRequiredBundleDate(validFromInput, "validFrom", "BUNDLE_VERSION_VALID_FROM_INVALID");
8019
8032
  if (existing.publishedAt !== null && validFrom.getTime() <= Date.now()) {
8020
8033
  throw new import_common43.UnprocessableEntityException({
8021
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_FUTURE,
8034
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_FUTURE,
8022
8035
  message: `validFrom (${validFrom.toISOString()}) must, for a published-but-future bundle version, still lie in the future.`,
8023
8036
  params: {
8024
8037
  validFrom: validFrom.toISOString()
@@ -8027,7 +8040,7 @@ var BundlesService = class _BundlesService {
8027
8040
  }
8028
8041
  if (validUntil && validUntil <= validFrom) {
8029
8042
  throw new import_common43.UnprocessableEntityException({
8030
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM,
8043
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM,
8031
8044
  message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
8032
8045
  params: {
8033
8046
  validUntil: validUntil.toISOString(),
@@ -8041,7 +8054,7 @@ var BundlesService = class _BundlesService {
8041
8054
  if (Number.isNaN(previousValidFrom.getTime())) return;
8042
8055
  if (validFrom <= previousValidFrom) {
8043
8056
  throw new import_common43.UnprocessableEntityException({
8044
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
8057
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
8045
8058
  message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
8046
8059
  params: {
8047
8060
  validFrom: validFrom.toISOString(),
@@ -8056,7 +8069,7 @@ var BundlesService = class _BundlesService {
8056
8069
  const requiredStart = new Date(previousValidUntil.getTime() + dayMs);
8057
8070
  if (validFrom.getTime() !== requiredStart.getTime()) {
8058
8071
  throw new import_common43.UnprocessableEntityException({
8059
- code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
8072
+ code: import_types20.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
8060
8073
  message: `The predecessor has validUntil=${previous.validUntil.slice(0, 10)} \u2014 the successor must start seamlessly on the next day (${requiredStart.toISOString().slice(0, 10)}). Received: ${validFrom.toISOString().slice(0, 10)}.`,
8061
8074
  params: {
8062
8075
  received: validFrom.toISOString().slice(0, 10)
@@ -8080,9 +8093,10 @@ var BundlesService = class _BundlesService {
8080
8093
  throw new import_common43.UnprocessableEntityException({
8081
8094
  code,
8082
8095
  message: `${field} '${input}' is not a valid date`,
8096
+ // Keyed by the field name so these params match the publish
8097
+ // path, where the same codes carry `validFrom` / `validUntil`.
8083
8098
  params: {
8084
- field,
8085
- value: input
8099
+ [field]: input
8086
8100
  }
8087
8101
  });
8088
8102
  }
@@ -8118,7 +8132,7 @@ BundlesService = _ts_decorate44([
8118
8132
 
8119
8133
  // src/catalog/marketing-projections.service.ts
8120
8134
  var import_common44 = require("@nestjs/common");
8121
- var import_types20 = require("@saasicat/types");
8135
+ var import_types21 = require("@saasicat/types");
8122
8136
  function _ts_decorate45(decorators, target, key, desc) {
8123
8137
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
8124
8138
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -8151,7 +8165,7 @@ var MarketingProjectionsService = class {
8151
8165
  const row = await this.repo.findById(id);
8152
8166
  if (!row) {
8153
8167
  throw new import_common44.NotFoundException({
8154
- code: import_types20.CATALOG_ERROR_CODES.MARKETING_PROJECTION_NOT_FOUND,
8168
+ code: import_types21.CATALOG_ERROR_CODES.MARKETING_PROJECTION_NOT_FOUND,
8155
8169
  message: `MarketingProjection '${id}' not found`,
8156
8170
  params: {
8157
8171
  projectionId: id
@@ -8165,7 +8179,7 @@ var MarketingProjectionsService = class {
8165
8179
  const existing = await this.repo.findByTarget(data.targetType, data.targetVersionId, locale);
8166
8180
  if (existing) {
8167
8181
  throw new import_common44.ConflictException({
8168
- code: import_types20.CATALOG_ERROR_CODES.MARKETING_PROJECTION_ALREADY_EXISTS,
8182
+ code: import_types21.CATALOG_ERROR_CODES.MARKETING_PROJECTION_ALREADY_EXISTS,
8169
8183
  message: `Marketing projection for ${data.targetType}/${data.targetVersionId}/${locale} already exists \u2014 use PATCH to edit it`,
8170
8184
  params: {
8171
8185
  targetType: data.targetType,
@@ -8183,7 +8197,7 @@ var MarketingProjectionsService = class {
8183
8197
  const existing = await this.repo.findById(id);
8184
8198
  if (!existing) {
8185
8199
  throw new import_common44.NotFoundException({
8186
- code: import_types20.CATALOG_ERROR_CODES.MARKETING_PROJECTION_NOT_FOUND,
8200
+ code: import_types21.CATALOG_ERROR_CODES.MARKETING_PROJECTION_NOT_FOUND,
8187
8201
  message: `MarketingProjection '${id}' not found`,
8188
8202
  params: {
8189
8203
  projectionId: id
@@ -8196,7 +8210,7 @@ var MarketingProjectionsService = class {
8196
8210
  const existing = await this.repo.findById(id);
8197
8211
  if (!existing) {
8198
8212
  throw new import_common44.NotFoundException({
8199
- code: import_types20.CATALOG_ERROR_CODES.MARKETING_PROJECTION_NOT_FOUND,
8213
+ code: import_types21.CATALOG_ERROR_CODES.MARKETING_PROJECTION_NOT_FOUND,
8200
8214
  message: `MarketingProjection '${id}' not found`,
8201
8215
  params: {
8202
8216
  projectionId: id
@@ -8217,7 +8231,7 @@ MarketingProjectionsService = _ts_decorate45([
8217
8231
 
8218
8232
  // src/catalog/catalog-entries.service.ts
8219
8233
  var import_common45 = require("@nestjs/common");
8220
- var import_types21 = require("@saasicat/types");
8234
+ var import_types22 = require("@saasicat/types");
8221
8235
 
8222
8236
  // src/catalog/approval-signature.ts
8223
8237
  function featureApprovalSignature(featureKey, snapshot) {
@@ -8368,7 +8382,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
8368
8382
  const existing = await this.repo.findFeature(projectKey, featureKey);
8369
8383
  if (!existing) {
8370
8384
  throw new import_common45.NotFoundException({
8371
- code: import_types21.CATALOG_ERROR_CODES.FEATURE_NOT_FOUND,
8385
+ code: import_types22.CATALOG_ERROR_CODES.FEATURE_NOT_FOUND,
8372
8386
  message: `Feature '${featureKey}' not found in project '${projectKey}'`,
8373
8387
  params: {
8374
8388
  featureKey,
@@ -8383,7 +8397,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
8383
8397
  const existing = await this.repo.findQuota(projectKey, quotaKey);
8384
8398
  if (!existing) {
8385
8399
  throw new import_common45.NotFoundException({
8386
- code: import_types21.CATALOG_ERROR_CODES.QUOTA_NOT_FOUND,
8400
+ code: import_types22.CATALOG_ERROR_CODES.QUOTA_NOT_FOUND,
8387
8401
  message: `Quota '${quotaKey}' not found in project '${projectKey}'`,
8388
8402
  params: {
8389
8403
  quotaKey,
@@ -8395,7 +8409,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
8395
8409
  const discovered = snapshot.quotas.find((q) => q.quotaKey === quotaKey);
8396
8410
  if (!discovered) {
8397
8411
  throw new import_common45.UnprocessableEntityException({
8398
- code: import_types21.CATALOG_ERROR_CODES.QUOTA_NOT_IN_DISCOVERY_SNAPSHOT,
8412
+ code: import_types22.CATALOG_ERROR_CODES.QUOTA_NOT_IN_DISCOVERY_SNAPSHOT,
8399
8413
  message: `Quota '${quotaKey}' is not in the discovery snapshot \u2014 a quota that is not declared in code cannot be approved`,
8400
8414
  params: {
8401
8415
  quotaKey
@@ -8408,7 +8422,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
8408
8422
  resolveReviewUpdate(existing, target, reviewedBy, signatureOf) {
8409
8423
  if (!REVIEW_TRANSITIONS[existing.discoveryStatus].includes(target)) {
8410
8424
  throw new import_common45.UnprocessableEntityException({
8411
- code: import_types21.CATALOG_ERROR_CODES.DISCOVERY_STATUS_TRANSITION_INVALID,
8425
+ code: import_types22.CATALOG_ERROR_CODES.DISCOVERY_STATUS_TRANSITION_INVALID,
8412
8426
  message: `Transition '${existing.discoveryStatus}' \u2192 '${target}' is not allowed`,
8413
8427
  params: {
8414
8428
  from: existing.discoveryStatus,
@@ -8420,7 +8434,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
8420
8434
  const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
8421
8435
  if (!snapshot) {
8422
8436
  throw new import_common45.UnprocessableEntityException({
8423
- code: import_types21.CATALOG_ERROR_CODES.DISCOVERY_NOT_INITIALIZED,
8437
+ code: import_types22.CATALOG_ERROR_CODES.DISCOVERY_NOT_INITIALIZED,
8424
8438
  message: "Approval requires a discovery snapshot \u2014 discovery is not initialised (#25)"
8425
8439
  });
8426
8440
  }
@@ -9122,7 +9136,7 @@ __name(buildCatalogEntriesController, "buildCatalogEntriesController");
9122
9136
 
9123
9137
  // src/catalog/promotions.service.ts
9124
9138
  var import_common48 = require("@nestjs/common");
9125
- var import_types22 = require("@saasicat/types");
9139
+ var import_types23 = require("@saasicat/types");
9126
9140
  function _ts_decorate50(decorators, target, key, desc) {
9127
9141
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
9128
9142
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -9157,7 +9171,7 @@ var PromotionsService = class {
9157
9171
  const row = await this.repo.findById(id);
9158
9172
  if (!row) {
9159
9173
  throw new import_common48.NotFoundException({
9160
- code: import_types22.CATALOG_ERROR_CODES.PROMOTION_NOT_FOUND,
9174
+ code: import_types23.CATALOG_ERROR_CODES.PROMOTION_NOT_FOUND,
9161
9175
  message: `Promotion '${id}' not found`,
9162
9176
  params: {
9163
9177
  promotionId: id
@@ -9173,7 +9187,7 @@ var PromotionsService = class {
9173
9187
  const existing = await this.repo.findById(id);
9174
9188
  if (!existing) {
9175
9189
  throw new import_common48.NotFoundException({
9176
- code: import_types22.CATALOG_ERROR_CODES.PROMOTION_NOT_FOUND,
9190
+ code: import_types23.CATALOG_ERROR_CODES.PROMOTION_NOT_FOUND,
9177
9191
  message: `Promotion '${id}' not found`,
9178
9192
  params: {
9179
9193
  promotionId: id
@@ -9186,7 +9200,7 @@ var PromotionsService = class {
9186
9200
  const existing = await this.repo.findById(id);
9187
9201
  if (!existing) {
9188
9202
  throw new import_common48.NotFoundException({
9189
- code: import_types22.CATALOG_ERROR_CODES.PROMOTION_NOT_FOUND,
9203
+ code: import_types23.CATALOG_ERROR_CODES.PROMOTION_NOT_FOUND,
9190
9204
  message: `Promotion '${id}' not found`,
9191
9205
  params: {
9192
9206
  promotionId: id
@@ -9745,7 +9759,7 @@ __name(buildMarketingSettingsController, "buildMarketingSettingsController");
9745
9759
 
9746
9760
  // src/catalog/public-marketing-catalog.service.ts
9747
9761
  var import_common52 = require("@nestjs/common");
9748
- var import_types23 = require("@saasicat/types");
9762
+ var import_types24 = require("@saasicat/types");
9749
9763
  function _ts_decorate56(decorators, target, key, desc) {
9750
9764
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
9751
9765
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -9848,7 +9862,7 @@ var PublicMarketingCatalogService = class {
9848
9862
  });
9849
9863
  }
9850
9864
  out.sort((a, b) => b.priority - a.priority);
9851
- const requiresIndex = (0, import_types23.buildFeatureRequiresIndex)([
9865
+ const requiresIndex = (0, import_types24.buildFeatureRequiresIndex)([
9852
9866
  ...labelMeta.features
9853
9867
  ].map(([featureKey, meta]) => ({
9854
9868
  featureKey,
@@ -9902,7 +9916,7 @@ var PublicMarketingCatalogService = class {
9902
9916
  const features = [
9903
9917
  ...bv.features ?? []
9904
9918
  ];
9905
- const requiresFeatures = (0, import_types23.collectUnsatisfiedRequires)(features, requiresIndex);
9919
+ const requiresFeatures = (0, import_types24.collectUnsatisfiedRequires)(features, requiresIndex);
9906
9920
  return {
9907
9921
  bundleKey: bv.bundleKey,
9908
9922
  label: marketing?.displayLabel || bv.label,
@@ -10046,12 +10060,12 @@ var PublicMarketingCatalogService = class {
10046
10060
  };
10047
10061
  }
10048
10062
  resolvePromo(promotions, targetKey, targetType, locale, monthlyNet, yearlyNet, asOf) {
10049
- const monthlyPromo = (0, import_types23.pickActivePromo)(promotions, targetKey, locale, "monthly", asOf, targetType);
10050
- const yearlyPromo = (0, import_types23.pickActivePromo)(promotions, targetKey, locale, "yearly", asOf, targetType);
10063
+ const monthlyPromo = (0, import_types24.pickActivePromo)(promotions, targetKey, locale, "monthly", asOf, targetType);
10064
+ const yearlyPromo = (0, import_types24.pickActivePromo)(promotions, targetKey, locale, "yearly", asOf, targetType);
10051
10065
  const promo = monthlyPromo ?? yearlyPromo;
10052
10066
  if (!promo) return null;
10053
- const monthlyResult = (0, import_types23.applyPromo)(monthlyPromo, monthlyNet);
10054
- const yearlyResult = (0, import_types23.applyPromo)(yearlyPromo, yearlyNet);
10067
+ const monthlyResult = (0, import_types24.applyPromo)(monthlyPromo, monthlyNet);
10068
+ const yearlyResult = (0, import_types24.applyPromo)(yearlyPromo, yearlyNet);
10055
10069
  const i18n = promo.i18n?.[locale] ?? promo.i18n?.[DEFAULT_LOCALE] ?? {};
10056
10070
  return {
10057
10071
  type: promo.type,
@@ -10111,7 +10125,7 @@ function buildPublicMarketingCatalogController(guards, projectKey, currency, vat
10111
10125
  this.service = service;
10112
10126
  }
10113
10127
  /**
10114
- * `?lang=de|tr|en` — alias `locale` accepted (SPEC_V2 §9).
10128
+ * `?lang=de|tr|en` — alias `locale` accepted.
10115
10129
  * `?asOf=YYYY-MM-DD` — preview of a future or past catalog variant.
10116
10130
  * Default = today. Returns the PlanVersion whose `validFrom`/`validUntil`
10117
10131
  * span `asOf` inclusively by day (the validUntil day still counts,
@@ -10158,7 +10172,7 @@ __name(parseAsOf, "parseAsOf");
10158
10172
 
10159
10173
  // src/catalog/plans.service.ts
10160
10174
  var import_common54 = require("@nestjs/common");
10161
- var import_types24 = require("@saasicat/types");
10175
+ var import_types25 = require("@saasicat/types");
10162
10176
  function _ts_decorate58(decorators, target, key, desc) {
10163
10177
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
10164
10178
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -10207,7 +10221,7 @@ var PlansService = class {
10207
10221
  const plan = await this.repo.findById(planId);
10208
10222
  if (!plan) {
10209
10223
  throw new import_common54.NotFoundException({
10210
- code: import_types24.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10224
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10211
10225
  message: `Plan '${planId}' not found`,
10212
10226
  params: {
10213
10227
  planId
@@ -10220,7 +10234,7 @@ var PlansService = class {
10220
10234
  const existing = await this.repo.findByKey(data.projectKey, data.planKey);
10221
10235
  if (existing) {
10222
10236
  throw new import_common54.UnprocessableEntityException({
10223
- code: import_types24.CATALOG_ERROR_CODES.PLAN_ALREADY_EXISTS,
10237
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_ALREADY_EXISTS,
10224
10238
  message: `Plan '${data.planKey}' already exists in project '${data.projectKey}'`,
10225
10239
  params: {
10226
10240
  planKey: data.planKey,
@@ -10234,7 +10248,7 @@ var PlansService = class {
10234
10248
  const existing = await this.repo.findById(planId);
10235
10249
  if (!existing) {
10236
10250
  throw new import_common54.NotFoundException({
10237
- code: import_types24.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10251
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10238
10252
  message: `Plan '${planId}' not found`,
10239
10253
  params: {
10240
10254
  planId
@@ -10247,7 +10261,7 @@ var PlansService = class {
10247
10261
  const existing = await this.repo.findById(planId);
10248
10262
  if (!existing) {
10249
10263
  throw new import_common54.NotFoundException({
10250
- code: import_types24.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10264
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10251
10265
  message: `Plan '${planId}' not found`,
10252
10266
  params: {
10253
10267
  planId
@@ -10272,7 +10286,7 @@ var PlansService = class {
10272
10286
  const existing = await this.repo.findById(planId);
10273
10287
  if (!existing) {
10274
10288
  throw new import_common54.NotFoundException({
10275
- code: import_types24.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10289
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10276
10290
  message: `Plan '${planId}' not found`,
10277
10291
  params: {
10278
10292
  planId
@@ -10283,7 +10297,7 @@ var PlansService = class {
10283
10297
  await this.assertNoDrafts(existing.planKey);
10284
10298
  if (typeof this.repo.hardDelete !== "function") {
10285
10299
  throw new import_common54.UnprocessableEntityException({
10286
- code: import_types24.CATALOG_ERROR_CODES.PLAN_HARD_DELETE_NOT_IMPLEMENTED,
10300
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_HARD_DELETE_NOT_IMPLEMENTED,
10287
10301
  message: "Hard delete is not implemented in the current repository. Implement PlanRepository.hardDelete."
10288
10302
  });
10289
10303
  }
@@ -10304,7 +10318,7 @@ var PlansService = class {
10304
10318
  const live = published.filter((v) => v.supersededAt === null).length;
10305
10319
  const superseded = published.length - live;
10306
10320
  throw new import_common54.UnprocessableEntityException({
10307
- code: import_types24.CATALOG_ERROR_CODES.PLAN_HAS_PUBLISHED_VERSIONS,
10321
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_HAS_PUBLISHED_VERSIONS,
10308
10322
  message: `Plan '${planKey}' cannot be ${op === "hard-delete" ? "deleted" : "archived"} \u2014 it has ${published.length} published version(s) (${live} live, ${superseded} superseded). Existing subscriptions reference those versions (contract protection P1), so the plan record must be preserved.`,
10309
10323
  params: {
10310
10324
  planKey,
@@ -10321,7 +10335,7 @@ var PlansService = class {
10321
10335
  const drafts = versions.filter((v) => v.publishedAt === null);
10322
10336
  if (drafts.length === 0) return;
10323
10337
  throw new import_common54.UnprocessableEntityException({
10324
- code: import_types24.CATALOG_ERROR_CODES.PLAN_HAS_DRAFTS,
10338
+ code: import_types25.CATALOG_ERROR_CODES.PLAN_HAS_DRAFTS,
10325
10339
  message: `Plan '${planKey}' still has ${drafts.length} open draft version(s). Discard them first (DELETE /admin/catalog/plan-versions/:id) or publish them.`,
10326
10340
  params: {
10327
10341
  planKey
@@ -10344,7 +10358,7 @@ PlansService = _ts_decorate58([
10344
10358
 
10345
10359
  // src/catalog/plan-versions.service.ts
10346
10360
  var import_common55 = require("@nestjs/common");
10347
- var import_types25 = require("@saasicat/types");
10361
+ var import_types26 = require("@saasicat/types");
10348
10362
  function _ts_decorate59(decorators, target, key, desc) {
10349
10363
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
10350
10364
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -10414,7 +10428,7 @@ var PlanVersionsService = class _PlanVersionsService {
10414
10428
  const existing = await this.repo.findVersionById(versionId);
10415
10429
  if (!existing) {
10416
10430
  throw new import_common55.NotFoundException({
10417
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10431
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10418
10432
  message: `PlanVersion '${versionId}' not found`,
10419
10433
  params: {
10420
10434
  versionId
@@ -10423,7 +10437,7 @@ var PlanVersionsService = class _PlanVersionsService {
10423
10437
  }
10424
10438
  if (existing.publishedAt !== null) {
10425
10439
  throw new import_common55.UnprocessableEntityException({
10426
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_ALREADY_PUBLISHED,
10440
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_ALREADY_PUBLISHED,
10427
10441
  message: `PlanVersion '${versionId}' is already published and cannot be discarded.`,
10428
10442
  params: {
10429
10443
  versionId
@@ -10432,7 +10446,7 @@ var PlanVersionsService = class _PlanVersionsService {
10432
10446
  }
10433
10447
  if (typeof this.repo.deletePlanVersionDraft !== "function") {
10434
10448
  throw new import_common55.UnprocessableEntityException({
10435
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_DISCARD_NOT_IMPLEMENTED,
10449
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_DISCARD_NOT_IMPLEMENTED,
10436
10450
  message: "Discard is not implemented in the current repository. Implement PlanRepository.deletePlanVersionDraft."
10437
10451
  });
10438
10452
  }
@@ -10447,7 +10461,7 @@ var PlanVersionsService = class _PlanVersionsService {
10447
10461
  const plan = await this.repo.findById(planUuid);
10448
10462
  if (!plan) {
10449
10463
  throw new import_common55.NotFoundException({
10450
- code: import_types25.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10464
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10451
10465
  message: `Plan '${planUuid}' not found`,
10452
10466
  params: {
10453
10467
  planId: planUuid
@@ -10461,7 +10475,7 @@ var PlanVersionsService = class _PlanVersionsService {
10461
10475
  const version = await this.repo.findVersionById(versionId);
10462
10476
  if (!version) {
10463
10477
  throw new import_common55.NotFoundException({
10464
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10478
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10465
10479
  message: `PlanVersion '${versionId}' not found`,
10466
10480
  params: {
10467
10481
  versionId
@@ -10505,7 +10519,7 @@ var PlanVersionsService = class _PlanVersionsService {
10505
10519
  const plan = await this.repo.findById(data.planId);
10506
10520
  if (!plan) {
10507
10521
  throw new import_common55.NotFoundException({
10508
- code: import_types25.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10522
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
10509
10523
  message: `Plan '${data.planId}' not found`,
10510
10524
  params: {
10511
10525
  planId: data.planId
@@ -10516,7 +10530,7 @@ var PlanVersionsService = class _PlanVersionsService {
10516
10530
  const existingDraft = await this.repo.findCurrentDraft(planKey);
10517
10531
  if (existingDraft) {
10518
10532
  throw new import_common55.UnprocessableEntityException({
10519
- code: import_types25.CATALOG_ERROR_CODES.PLAN_DRAFT_ALREADY_EXISTS,
10533
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_DRAFT_ALREADY_EXISTS,
10520
10534
  message: `Plan '${planKey}' already has a draft version v${existingDraft.version}; publish or discard it first`,
10521
10535
  params: {
10522
10536
  planKey,
@@ -10548,7 +10562,7 @@ var PlanVersionsService = class _PlanVersionsService {
10548
10562
  const existing = await this.repo.findVersionById(versionId);
10549
10563
  if (!existing) {
10550
10564
  throw new import_common55.NotFoundException({
10551
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10565
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10552
10566
  message: `PlanVersion '${versionId}' not found`,
10553
10567
  params: {
10554
10568
  versionId
@@ -10558,10 +10572,10 @@ var PlanVersionsService = class _PlanVersionsService {
10558
10572
  const [annotated] = await this.annotateEditability([
10559
10573
  existing
10560
10574
  ]);
10561
- const { editable } = (0, import_types25.isVersionEditable)(annotated);
10575
+ const { editable } = (0, import_types26.isVersionEditable)(annotated);
10562
10576
  if (!editable) {
10563
10577
  throw new import_common55.UnprocessableEntityException({
10564
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_EDITABLE,
10578
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_EDITABLE,
10565
10579
  message: `PlanVersion '${versionId}' is not editable. Only drafts and published versions are editable that are latest-in-chain, bind no subscription yet, and whose validFrom lies in the future.`,
10566
10580
  params: {
10567
10581
  versionId
@@ -10584,7 +10598,7 @@ var PlanVersionsService = class _PlanVersionsService {
10584
10598
  const draft = await this.repo.findVersionById(versionId);
10585
10599
  if (!draft) {
10586
10600
  throw new import_common55.NotFoundException({
10587
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10601
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10588
10602
  message: `PlanVersion '${versionId}' not found`,
10589
10603
  params: {
10590
10604
  versionId
@@ -10593,7 +10607,7 @@ var PlanVersionsService = class _PlanVersionsService {
10593
10607
  }
10594
10608
  if (draft.publishedAt !== null) {
10595
10609
  throw new import_common55.UnprocessableEntityException({
10596
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_ALREADY_PUBLISHED,
10610
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_ALREADY_PUBLISHED,
10597
10611
  message: `PlanVersion '${versionId}' is already published`,
10598
10612
  params: {
10599
10613
  versionId
@@ -10610,7 +10624,7 @@ var PlanVersionsService = class _PlanVersionsService {
10610
10624
  const yearly = Number.parseFloat(String(draft.yearlyNet ?? "0"));
10611
10625
  if (monthly <= 0 || yearly <= 0) {
10612
10626
  throw new import_common55.UnprocessableEntityException({
10613
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_ZERO_PRICE,
10627
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_ZERO_PRICE,
10614
10628
  message: "A plan version cannot be published with a price of 0.00 (guard against seed placeholder). For deliberately free special contracts, set allowZeroPrice.",
10615
10629
  monthlyNet: String(draft.monthlyNet),
10616
10630
  yearlyNet: String(draft.yearlyNet)
@@ -10621,14 +10635,14 @@ var PlanVersionsService = class _PlanVersionsService {
10621
10635
  const validFromInput = publishMeta.validFrom ?? draft.validFrom;
10622
10636
  if (!validFromInput) {
10623
10637
  throw new import_common55.UnprocessableEntityException({
10624
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_REQUIRED,
10625
- message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
10638
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_REQUIRED,
10639
+ message: "validFrom must be set when publishing (on the draft or the publish call).."
10626
10640
  });
10627
10641
  }
10628
10642
  const validFrom = new Date(validFromInput);
10629
10643
  if (Number.isNaN(validFrom.getTime())) {
10630
10644
  throw new import_common55.UnprocessableEntityException({
10631
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_INVALID,
10645
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_INVALID,
10632
10646
  message: `validFrom '${validFromInput}' is not a valid date`,
10633
10647
  params: {
10634
10648
  validFrom: validFromInput
@@ -10639,7 +10653,7 @@ var PlanVersionsService = class _PlanVersionsService {
10639
10653
  const prevFrom = new Date(previous.validFrom);
10640
10654
  if (validFrom <= prevFrom) {
10641
10655
  throw new import_common55.UnprocessableEntityException({
10642
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
10656
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
10643
10657
  message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
10644
10658
  params: {
10645
10659
  validFrom: validFrom.toISOString(),
@@ -10653,7 +10667,7 @@ var PlanVersionsService = class _PlanVersionsService {
10653
10667
  const requiredStart = new Date(prevUntil.getTime() + dayMs);
10654
10668
  if (validFrom.getTime() !== requiredStart.getTime()) {
10655
10669
  throw new import_common55.UnprocessableEntityException({
10656
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_NOT_GAPLESS,
10670
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_NOT_GAPLESS,
10657
10671
  message: `The predecessor has validUntil=${previous.validUntil.slice(0, 10)} \u2014 the successor must start seamlessly on the next day (${requiredStart.toISOString().slice(0, 10)}). Received: ${validFrom.toISOString().slice(0, 10)}.`,
10658
10672
  params: {
10659
10673
  received: validFrom.toISOString().slice(0, 10)
@@ -10668,7 +10682,7 @@ var PlanVersionsService = class _PlanVersionsService {
10668
10682
  const validUntil = validUntilInput ? new Date(validUntilInput) : null;
10669
10683
  if (validUntil && Number.isNaN(validUntil.getTime())) {
10670
10684
  throw new import_common55.UnprocessableEntityException({
10671
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_UNTIL_INVALID,
10685
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_UNTIL_INVALID,
10672
10686
  message: `validUntil '${validUntilInput}' is not a valid date`,
10673
10687
  params: {
10674
10688
  validUntil: validUntilInput
@@ -10677,7 +10691,7 @@ var PlanVersionsService = class _PlanVersionsService {
10677
10691
  }
10678
10692
  if (validUntil && validUntil <= validFrom) {
10679
10693
  throw new import_common55.UnprocessableEntityException({
10680
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_UNTIL_BEFORE_FROM,
10694
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_UNTIL_BEFORE_FROM,
10681
10695
  message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
10682
10696
  params: {
10683
10697
  validUntil: validUntil.toISOString(),
@@ -10693,13 +10707,13 @@ var PlanVersionsService = class _PlanVersionsService {
10693
10707
  monthlyNet: v.monthlyNet,
10694
10708
  yearlyNet: v.yearlyNet
10695
10709
  }), "fieldsFor");
10696
- const diff = previous ? (0, import_types4.classifyPlanDiff)(fieldsFor(previous), fieldsFor(draft)) : {
10710
+ const diff = previous ? (0, import_types5.classifyPlanDiff)(fieldsFor(previous), fieldsFor(draft)) : {
10697
10711
  changes: [],
10698
10712
  nonRegressive: true
10699
10713
  };
10700
10714
  if (!diff.nonRegressive && !publishMeta.forceRegressive) {
10701
10715
  throw new import_common55.UnprocessableEntityException({
10702
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_REGRESSION,
10716
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_REGRESSION,
10703
10717
  message: "This plan version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
10704
10718
  changes: diff.changes
10705
10719
  });
@@ -10736,7 +10750,7 @@ var PlanVersionsService = class _PlanVersionsService {
10736
10750
  const existing = await this.repo.findVersionById(versionId);
10737
10751
  if (!existing) {
10738
10752
  throw new import_common55.NotFoundException({
10739
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10753
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
10740
10754
  message: `PlanVersion '${versionId}' not found`,
10741
10755
  params: {
10742
10756
  versionId
@@ -10745,7 +10759,7 @@ var PlanVersionsService = class _PlanVersionsService {
10745
10759
  }
10746
10760
  if (existing.publishedAt === null) {
10747
10761
  throw new import_common55.UnprocessableEntityException({
10748
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_LIVE,
10762
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_PUBLISHED,
10749
10763
  message: `PlanVersion '${versionId}' is not published and cannot be terminated.`,
10750
10764
  params: {
10751
10765
  versionId
@@ -10754,7 +10768,7 @@ var PlanVersionsService = class _PlanVersionsService {
10754
10768
  }
10755
10769
  if (existing.supersededAt !== null) {
10756
10770
  throw new import_common55.UnprocessableEntityException({
10757
- code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_LIVE,
10771
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_VERSION_SUPERSEDED,
10758
10772
  message: `PlanVersion '${versionId}' has already been superseded by a newer version (supersededAt) and cannot be terminated.`,
10759
10773
  params: {
10760
10774
  versionId
@@ -10763,13 +10777,13 @@ var PlanVersionsService = class _PlanVersionsService {
10763
10777
  }
10764
10778
  if (Number.isNaN(endsAt.getTime())) {
10765
10779
  throw new import_common55.UnprocessableEntityException({
10766
- code: import_types25.CATALOG_ERROR_CODES.PLAN_TERMINATE_INVALID_DATE,
10780
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_TERMINATE_INVALID_DATE,
10767
10781
  message: "endsAt is not a valid date."
10768
10782
  });
10769
10783
  }
10770
10784
  if (endsAt.getTime() <= Date.now()) {
10771
10785
  throw new import_common55.UnprocessableEntityException({
10772
- code: import_types25.CATALOG_ERROR_CODES.PLAN_TERMINATE_DATE_NOT_FUTURE,
10786
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_TERMINATE_DATE_NOT_FUTURE,
10773
10787
  message: `endsAt (${endsAt.toISOString()}) must lie strictly in the future.`,
10774
10788
  params: {
10775
10789
  endsAt: endsAt.toISOString()
@@ -10778,7 +10792,7 @@ var PlanVersionsService = class _PlanVersionsService {
10778
10792
  }
10779
10793
  if (typeof this.repo.terminate !== "function") {
10780
10794
  throw new import_common55.UnprocessableEntityException({
10781
- code: import_types25.CATALOG_ERROR_CODES.PLAN_TERMINATE_NOT_IMPLEMENTED,
10795
+ code: import_types26.CATALOG_ERROR_CODES.PLAN_TERMINATE_NOT_IMPLEMENTED,
10782
10796
  message: "Terminate is not implemented in the current repository. Implement PlanRepository.terminate."
10783
10797
  });
10784
10798
  }
@@ -10797,7 +10811,7 @@ var PlanVersionsService = class _PlanVersionsService {
10797
10811
  if (blockingStrictModeWarnings(warnings).length === 0) return;
10798
10812
  if (this.mode === "blocking") {
10799
10813
  throw new import_common55.UnprocessableEntityException({
10800
- code: import_types25.CATALOG_ERROR_CODES.STRICT_MODE_VIOLATIONS,
10814
+ code: import_types26.CATALOG_ERROR_CODES.STRICT_MODE_VIOLATIONS,
10801
10815
  message: "The strict-mode check found drift against the discovery snapshot.",
10802
10816
  warnings
10803
10817
  });
@@ -11139,7 +11153,7 @@ var PublishBundleVersionDto = class {
11139
11153
  allowZeroPrice;
11140
11154
  /**
11141
11155
  * Required on publish if the draft carries no `validFrom`
11142
- * (SPEC_V2 §4.2 + §11.1 M6 Pack 2c, analogous to PublishPlanVersionDto).
11156
+ * (analogous to PublishPlanVersionDto).
11143
11157
  * Service strictly checks > `validFrom` of the predecessor version.
11144
11158
  */
11145
11159
  validFrom;
@@ -11925,7 +11939,7 @@ var CreatePlanVersionDraftDto = class {
11925
11939
  marketed;
11926
11940
  changeNote;
11927
11941
  baseVersionId;
11928
- /** Optional in the draft (required at publish). ISO date string. SPEC_V2 §4.2. */
11942
+ /** Optional in the draft (required at publish). ISO date string.. */
11929
11943
  validFrom;
11930
11944
  /** Optional; null = unlimited. ISO date string. */
11931
11945
  validUntil;
@@ -12100,7 +12114,7 @@ var PublishPlanVersionDto = class {
12100
12114
  allowZeroPrice;
12101
12115
  /**
12102
12116
  * Required at publish (on the DTO or draft). The service strictly checks
12103
- * `validFrom > predecessor.validFrom`. SPEC_V2 §4.2.
12117
+ * `validFrom > predecessor.validFrom`..
12104
12118
  */
12105
12119
  validFrom;
12106
12120
  validUntil;
@@ -12744,7 +12758,7 @@ __export(checkout_offer_exports, {
12744
12758
 
12745
12759
  // src/checkout-offer/checkout-offer.service.ts
12746
12760
  var import_common60 = require("@nestjs/common");
12747
- var import_types26 = require("@saasicat/types");
12761
+ var import_types27 = require("@saasicat/types");
12748
12762
 
12749
12763
  // src/checkout-offer/tokens.ts
12750
12764
  var CHECKOUT_OFFER_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/CheckoutOfferRepository");
@@ -12788,7 +12802,7 @@ var CheckoutOfferService = class {
12788
12802
  const row = await this.repo.findById(id);
12789
12803
  if (!row) {
12790
12804
  throw new import_common60.NotFoundException({
12791
- code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_NOT_FOUND,
12805
+ code: import_types27.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_NOT_FOUND,
12792
12806
  message: `CheckoutOffer '${id}' not found`,
12793
12807
  params: {
12794
12808
  offerId: id
@@ -12847,18 +12861,18 @@ var CheckoutOfferService = class {
12847
12861
  const entries = await this.catalogEntries.listFeatures({
12848
12862
  projectKey: input.projectKey
12849
12863
  });
12850
- const requiresIndex = (0, import_types26.buildFeatureRequiresIndex)(entries);
12864
+ const requiresIndex = (0, import_types27.buildFeatureRequiresIndex)(entries);
12851
12865
  if (requiresIndex.size === 0) return;
12852
12866
  const selected = /* @__PURE__ */ new Set([
12853
12867
  ...await this.resolvePlanFeatures(input),
12854
12868
  ...await this.resolveBundleFeatures(input)
12855
12869
  ]);
12856
- const missingRequires = (0, import_types26.collectUnsatisfiedRequires)([
12870
+ const missingRequires = (0, import_types27.collectUnsatisfiedRequires)([
12857
12871
  ...selected
12858
12872
  ], requiresIndex);
12859
12873
  if (missingRequires.length > 0) {
12860
12874
  throw new import_common60.UnprocessableEntityException({
12861
- code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED,
12875
+ code: import_types27.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED,
12862
12876
  message: `The selected plan does not cover all feature dependencies: [${missingRequires.join(", ")}] are missing from the plan + selected bundles.`,
12863
12877
  missingRequires
12864
12878
  });
@@ -12939,7 +12953,7 @@ var CheckoutOfferService = class {
12939
12953
  }
12940
12954
  if (violations.length > 0) {
12941
12955
  throw new import_common60.UnprocessableEntityException({
12942
- code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE,
12956
+ code: import_types27.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE,
12943
12957
  message: "At least one bundle version from the checkout offer is no longer bookable.",
12944
12958
  violations
12945
12959
  });
@@ -12994,14 +13008,14 @@ var CheckoutOfferService = class {
12994
13008
  const hasPlan = lineItems.some((item) => item.kind === "plan");
12995
13009
  if (!hasPlan) {
12996
13010
  throw new import_common60.UnprocessableEntityException({
12997
- code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED,
13011
+ code: import_types27.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED,
12998
13012
  message: "A checkout offer requires a frozen plan line item."
12999
13013
  });
13000
13014
  }
13001
13015
  const missingBundleVersionIds = input.bundleVersionIds.filter((versionId) => !lineItems.some((item) => item.kind === "bundle" && item.sourceVersionId === versionId));
13002
13016
  if (missingBundleVersionIds.length > 0) {
13003
13017
  throw new import_common60.UnprocessableEntityException({
13004
- code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED,
13018
+ code: import_types27.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED,
13005
13019
  message: "Every selected bundle version requires a frozen bundle line item.",
13006
13020
  bundleVersionIds: missingBundleVersionIds
13007
13021
  });
@@ -13035,7 +13049,7 @@ var CheckoutOfferService = class {
13035
13049
  assertOpen(existing, action) {
13036
13050
  if (existing.status === "consumed") {
13037
13051
  throw new import_common60.ConflictException({
13038
- code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_ALREADY_CONSUMED,
13052
+ code: import_types27.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_ALREADY_CONSUMED,
13039
13053
  message: `Checkout offer '${existing.id}' has already been consumed and cannot be ${action}`,
13040
13054
  params: {
13041
13055
  offerId: existing.id,
@@ -13045,17 +13059,18 @@ var CheckoutOfferService = class {
13045
13059
  }
13046
13060
  if (existing.status === "expired") {
13047
13061
  throw new import_common60.ConflictException({
13048
- code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_EXPIRED,
13062
+ code: import_types27.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_EXPIRED,
13049
13063
  message: `Checkout offer '${existing.id}' has expired and cannot be ${action}`,
13050
13064
  params: {
13051
13065
  offerId: existing.id,
13052
- action
13066
+ action,
13067
+ validUntil: existing.validUntil ?? null
13053
13068
  }
13054
13069
  });
13055
13070
  }
13056
13071
  if (this.isExpired(existing)) {
13057
13072
  throw new import_common60.ConflictException({
13058
- code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_EXPIRED,
13073
+ code: import_types27.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_EXPIRED,
13059
13074
  message: `CheckoutOffer '${existing.id}' has expired and cannot be ${action}`,
13060
13075
  params: {
13061
13076
  offerId: existing.id,
@@ -13080,7 +13095,7 @@ var CheckoutOfferService = class {
13080
13095
  if (!value) return false;
13081
13096
  const validUntil = new Date(value).getTime();
13082
13097
  if (Number.isNaN(validUntil)) return true;
13083
- return validUntil < (0, import_types26.startOfUtcDay)(new Date(nowMs)).getTime();
13098
+ return validUntil < (0, import_types27.startOfUtcDay)(new Date(nowMs)).getTime();
13084
13099
  }
13085
13100
  };
13086
13101
  CheckoutOfferService = _ts_decorate68([
@@ -13711,7 +13726,7 @@ __export(platform_exports, {
13711
13726
  // src/platform/saas-platform.module.ts
13712
13727
  var import_common79 = require("@nestjs/common");
13713
13728
  var import_core9 = require("@nestjs/core");
13714
- var import_types30 = require("@saasicat/types");
13729
+ var import_types31 = require("@saasicat/types");
13715
13730
 
13716
13731
  // src/promo/module.ts
13717
13732
  var import_common70 = require("@nestjs/common");
@@ -13781,7 +13796,7 @@ var import_common68 = require("@nestjs/common");
13781
13796
 
13782
13797
  // src/promo/rate-limit.guard.ts
13783
13798
  var import_common67 = require("@nestjs/common");
13784
- var import_types27 = require("@saasicat/types");
13799
+ var import_types28 = require("@saasicat/types");
13785
13800
  function _ts_decorate75(decorators, target, key, desc) {
13786
13801
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
13787
13802
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -13793,12 +13808,15 @@ var IP_WINDOW_MS = 6e4;
13793
13808
  var IP_LIMIT = 20;
13794
13809
  var SESSION_WINDOW_MS = 60 * 6e4;
13795
13810
  var SESSION_LIMIT = 50;
13796
- function rateLimited() {
13811
+ function rateLimited(windowMs) {
13797
13812
  return new import_common67.HttpException({
13813
+ ...codedError(import_types28.REGISTRATION_ERROR_CODES.RATE_LIMITED, {
13814
+ retryAfterSeconds: Math.ceil(windowMs / 1e3)
13815
+ }),
13816
+ retryAfterSeconds: Math.ceil(windowMs / 1e3),
13798
13817
  valid: false,
13799
13818
  // Superseded by `code` — kept until all consumers read `code`.
13800
- reason: "RATE_LIMITED",
13801
- code: import_types27.REGISTRATION_ERROR_CODES.RATE_LIMITED
13819
+ reason: "RATE_LIMITED"
13802
13820
  }, import_common67.HttpStatus.TOO_MANY_REQUESTS);
13803
13821
  }
13804
13822
  __name(rateLimited, "rateLimited");
@@ -13813,10 +13831,10 @@ var PromoCodeRateLimitGuard = class {
13813
13831
  const ipKey = ipFingerprint(req);
13814
13832
  const sessionKey = req.user?.id ?? null;
13815
13833
  if (this.exceeded(this.ipBuckets, ipKey, IP_WINDOW_MS, IP_LIMIT)) {
13816
- throw rateLimited();
13834
+ throw rateLimited(IP_WINDOW_MS);
13817
13835
  }
13818
13836
  if (sessionKey && this.exceeded(this.sessionBuckets, sessionKey, SESSION_WINDOW_MS, SESSION_LIMIT)) {
13819
- throw rateLimited();
13837
+ throw rateLimited(SESSION_WINDOW_MS);
13820
13838
  }
13821
13839
  return true;
13822
13840
  }
@@ -14455,7 +14473,7 @@ var import_class_validator16 = require("class-validator");
14455
14473
 
14456
14474
  // src/setup/setup.service.ts
14457
14475
  var import_common71 = require("@nestjs/common");
14458
- var import_types28 = require("@saasicat/types");
14476
+ var import_types29 = require("@saasicat/types");
14459
14477
  var import_node_crypto3 = require("crypto");
14460
14478
  var import_qrcode = __toESM(require("qrcode"), 1);
14461
14479
 
@@ -14511,9 +14529,7 @@ var SetupService = class {
14511
14529
  await this.assertNoSuperAdmin();
14512
14530
  const email = req.email.trim().toLowerCase();
14513
14531
  if (!email.includes("@")) {
14514
- throw new import_common71.BadRequestException({
14515
- code: import_types28.SETUP_ERROR_CODES.INVALID_EMAIL
14516
- });
14532
+ throw new import_common71.BadRequestException(codedError(import_types29.SETUP_ERROR_CODES.INVALID_EMAIL));
14517
14533
  }
14518
14534
  const generated = !req.password;
14519
14535
  const password = req.password ?? (0, import_node_crypto3.randomBytes)(GENERATED_PASSWORD_BYTES).toString("base64url");
@@ -14524,9 +14540,9 @@ var SetupService = class {
14524
14540
  password
14525
14541
  });
14526
14542
  } catch (err) {
14527
- if ((0, import_types28.isPlatformUserExistsError)(err)) {
14543
+ if ((0, import_types29.isPlatformUserExistsError)(err)) {
14528
14544
  throw new import_common71.ConflictException({
14529
- code: import_types28.SETUP_ERROR_CODES.EMAIL_EXISTS,
14545
+ code: import_types29.SETUP_ERROR_CODES.EMAIL_EXISTS,
14530
14546
  message: err.message
14531
14547
  });
14532
14548
  }
@@ -14562,7 +14578,7 @@ var SetupService = class {
14562
14578
  const expected = process.env[this.config.setupTokenEnvVar]?.trim();
14563
14579
  if (!expected) {
14564
14580
  throw new import_common71.ForbiddenException({
14565
- code: import_types28.SETUP_ERROR_CODES.SETUP_DISABLED,
14581
+ code: import_types29.SETUP_ERROR_CODES.SETUP_DISABLED,
14566
14582
  message: `${this.config.setupTokenEnvVar} is not set \u2014 setup is disabled.`,
14567
14583
  params: {
14568
14584
  envVar: this.config.setupTokenEnvVar
@@ -14570,16 +14586,12 @@ var SetupService = class {
14570
14586
  });
14571
14587
  }
14572
14588
  if (!token || !timingSafeEqualStr(token, expected)) {
14573
- throw new import_common71.UnauthorizedException({
14574
- code: import_types28.SETUP_ERROR_CODES.INVALID_SETUP_TOKEN
14575
- });
14589
+ throw new import_common71.UnauthorizedException(codedError(import_types29.SETUP_ERROR_CODES.INVALID_SETUP_TOKEN));
14576
14590
  }
14577
14591
  }
14578
14592
  async assertNoSuperAdmin() {
14579
14593
  if (await this.users.countSuperAdmins() > 0) {
14580
- throw new import_common71.ConflictException({
14581
- code: import_types28.SETUP_ERROR_CODES.SETUP_ALREADY_DONE
14582
- });
14594
+ throw new import_common71.ConflictException(codedError(import_types29.SETUP_ERROR_CODES.SETUP_ALREADY_DONE));
14583
14595
  }
14584
14596
  }
14585
14597
  };
@@ -14854,7 +14866,7 @@ StaticEntitlementService = _ts_decorate84([
14854
14866
  // src/platform/static-feature.guard.ts
14855
14867
  var import_common75 = require("@nestjs/common");
14856
14868
  var import_core7 = require("@nestjs/core");
14857
- var import_types29 = require("@saasicat/types");
14869
+ var import_types30 = require("@saasicat/types");
14858
14870
  function _ts_decorate85(decorators, target, key, desc) {
14859
14871
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
14860
14872
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -14895,7 +14907,7 @@ var StaticFeatureGuard = class {
14895
14907
  const user = request.user;
14896
14908
  if (!user) {
14897
14909
  throw new import_common75.ForbiddenException({
14898
- code: import_types29.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
14910
+ code: import_types30.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
14899
14911
  message: "Not authenticated"
14900
14912
  });
14901
14913
  }
@@ -14904,20 +14916,21 @@ var StaticFeatureGuard = class {
14904
14916
  const tenantId = this.config?.tenantIdResolver ? this.config.tenantIdResolver(request) : request.tenantId ?? user.tenantId;
14905
14917
  if (!tenantId) {
14906
14918
  throw new import_common75.ForbiddenException({
14907
- code: import_types29.AUTH_ERROR_CODES.NO_TENANT_ASSIGNED,
14919
+ code: import_types30.AUTH_ERROR_CODES.NO_TENANT_ASSIGNED,
14908
14920
  message: "No tenant assigned"
14909
14921
  });
14910
14922
  }
14911
14923
  const snap = await this.entitlements.snapshot(tenantId);
14912
14924
  const allowed = required.some((f) => snap.features.includes(f));
14913
14925
  if (!allowed) {
14914
- throw new import_common75.ForbiddenException({
14915
- code: import_types29.FEATURE_NOT_LICENSED,
14916
- message: `Feature ${required.join(" / ")} is not included in the current plan.`,
14917
- params: {
14918
- featureKeys: required
14919
- }
14920
- });
14926
+ const body = {
14927
+ code: import_types30.FEATURE_NOT_LICENSED,
14928
+ featureKey: required[0],
14929
+ featureKeys: required,
14930
+ offers: [],
14931
+ message: `Feature ${required.join(" / ")} is not included in the current plan.`
14932
+ };
14933
+ throw new import_common75.ForbiddenException(body);
14921
14934
  }
14922
14935
  return true;
14923
14936
  }
@@ -15195,6 +15208,7 @@ function _ts_decorate89(decorators, target, key, desc) {
15195
15208
  return c > 3 && r && Object.defineProperty(target, key, r), r;
15196
15209
  }
15197
15210
  __name(_ts_decorate89, "_ts_decorate");
15211
+ var PLATFORM_LOGGER = new import_common79.Logger("SaaSiCat");
15198
15212
  var PLATFORM_SUBSCRIPTION_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-nest/PlatformSubscriptionRepository");
15199
15213
  var STANDARD_MANIFEST_REGISTRATION_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-nest/StandardManifestRegistration");
15200
15214
  function buildStandardManifestContribution(catalog, adminResources, promoCodes) {
@@ -15397,7 +15411,7 @@ var SaasPlatformModule = class _SaasPlatformModule {
15397
15411
  throw new Error(`SaasPlatformModule.forRoot: entitlement active or required by the standard stack, but adapters are missing: ${missing.join(", ")}`);
15398
15412
  }
15399
15413
  if (persistence) {
15400
- (0, import_types30.assertPersistenceCapabilities)(persistence.capabilities, {
15414
+ (0, import_types31.assertPersistenceCapabilities)(persistence.capabilities, {
15401
15415
  transactions: true,
15402
15416
  pessimisticLocking: true
15403
15417
  }, "SaasPlatformModule entitlement (transactional enforceLimit)");
@@ -15659,6 +15673,11 @@ var SaasPlatformModule = class _SaasPlatformModule {
15659
15673
  useExisting: EnforceQuotaInterceptor
15660
15674
  });
15661
15675
  lightweightExports.push(PLAN_RESOLVER_PORT_TOKEN, StaticEntitlementService, StaticFeatureGuard, EnforceQuotaInterceptor, QUOTA_PROVIDERS_TOKEN);
15676
+ if (options.globalFeatureGuard === false) {
15677
+ 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.");
15678
+ }
15679
+ } else {
15680
+ 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.");
15662
15681
  }
15663
15682
  const tenantControllers = [];
15664
15683
  if (options.tenantManifest) {
@@ -15853,8 +15872,8 @@ var REGISTRATION_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/Regist
15853
15872
 
15854
15873
  // src/registration/pending-registration.service.ts
15855
15874
  var import_common80 = require("@nestjs/common");
15856
- var import_types31 = require("@saasicat/types");
15857
15875
  var import_types32 = require("@saasicat/types");
15876
+ var import_types33 = require("@saasicat/types");
15858
15877
 
15859
15878
  // src/registration/pricing.ts
15860
15879
  function round24(value) {
@@ -15982,28 +16001,20 @@ var PendingRegistrationService = class _PendingRegistrationService {
15982
16001
  async selectPlan(input, context) {
15983
16002
  const pending = await this.repo.findById(input.pendingRegistrationId);
15984
16003
  if (!pending) {
15985
- throw new import_common80.NotFoundException({
15986
- code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
15987
- });
16004
+ throw new import_common80.NotFoundException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
15988
16005
  }
15989
16006
  if (pending.expiresAt.getTime() < Date.now()) {
15990
- throw new import_common80.BadRequestException({
15991
- code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
15992
- });
16007
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
15993
16008
  }
15994
16009
  if (pending.status !== "EMAIL_VERIFIED" && pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
15995
- throw new import_common80.BadRequestException({
15996
- code: import_types31.REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE
15997
- });
16010
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE));
15998
16011
  }
15999
16012
  const plan = await this.planCatalog.findPublicSignupPlan(input.planId);
16000
16013
  if (!plan) {
16001
- throw new import_common80.BadRequestException({
16002
- code: import_types31.REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE
16003
- });
16014
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE));
16004
16015
  }
16005
16016
  const now = /* @__PURE__ */ new Date();
16006
- const expiresAt = new Date(now.getTime() + import_types32.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
16017
+ const expiresAt = new Date(now.getTime() + import_types33.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
16007
16018
  const updated = await this.repo.update(pending.id, {
16008
16019
  status: "PLAN_SELECTED",
16009
16020
  currentStep: 4,
@@ -16016,37 +16027,27 @@ var PendingRegistrationService = class _PendingRegistrationService {
16016
16027
  return {
16017
16028
  pendingRegistrationId: updated.id,
16018
16029
  status: updated.status,
16019
- nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[updated.status],
16030
+ nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[updated.status],
16020
16031
  selectedPlanId: updated.selectedPlanId ?? plan.id
16021
16032
  };
16022
16033
  }
16023
16034
  async startCheckout(input, context) {
16024
16035
  const pending = await this.repo.findById(input.pendingRegistrationId);
16025
16036
  if (!pending) {
16026
- throw new import_common80.NotFoundException({
16027
- code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
16028
- });
16037
+ throw new import_common80.NotFoundException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
16029
16038
  }
16030
16039
  if (pending.expiresAt.getTime() < Date.now()) {
16031
- throw new import_common80.BadRequestException({
16032
- code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
16033
- });
16040
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
16034
16041
  }
16035
16042
  if (pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
16036
- throw new import_common80.BadRequestException({
16037
- code: import_types31.REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED
16038
- });
16043
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED));
16039
16044
  }
16040
16045
  if (!pending.selectedPlanId) {
16041
- throw new import_common80.BadRequestException({
16042
- code: import_types31.REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED
16043
- });
16046
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED));
16044
16047
  }
16045
16048
  const plan = await this.planCatalog.findPublicSignupPlan(pending.selectedPlanId);
16046
16049
  if (!plan) {
16047
- throw new import_common80.BadRequestException({
16048
- code: import_types31.REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE
16049
- });
16050
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE));
16050
16051
  }
16051
16052
  const session = await this.paymentProvider.createCheckoutSession({
16052
16053
  pendingRegistrationId: pending.id,
@@ -16056,7 +16057,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
16056
16057
  cancelUrl: input.cancelUrl
16057
16058
  });
16058
16059
  const now = /* @__PURE__ */ new Date();
16059
- const expiresAt = new Date(now.getTime() + import_types32.PENDING_CHECKOUT_TTL_DAYS * 24 * 60 * 60 * 1e3);
16060
+ const expiresAt = new Date(now.getTime() + import_types33.PENDING_CHECKOUT_TTL_DAYS * 24 * 60 * 60 * 1e3);
16060
16061
  const updated = await this.repo.update(pending.id, {
16061
16062
  status: "CHECKOUT_STARTED",
16062
16063
  currentStep: 4,
@@ -16071,7 +16072,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
16071
16072
  return {
16072
16073
  pendingRegistrationId: updated.id,
16073
16074
  status: updated.status,
16074
- nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[updated.status],
16075
+ nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[updated.status],
16075
16076
  checkoutSessionId: session.sessionId,
16076
16077
  checkoutUrl: session.checkoutUrl
16077
16078
  };
@@ -16227,8 +16228,8 @@ var PendingRegistrationService = class _PendingRegistrationService {
16227
16228
  const code = generateOtpCode();
16228
16229
  const otpHash = hashOtpCode(code);
16229
16230
  const now = /* @__PURE__ */ new Date();
16230
- const otpExpiresAt = new Date(now.getTime() + import_types32.OTP_TTL_MINUTES * 60 * 1e3);
16231
- const expiresAt = new Date(now.getTime() + import_types32.PENDING_EMAIL_TTL_HOURS * 60 * 60 * 1e3);
16231
+ const otpExpiresAt = new Date(now.getTime() + import_types33.OTP_TTL_MINUTES * 60 * 1e3);
16232
+ const expiresAt = new Date(now.getTime() + import_types33.PENDING_EMAIL_TTL_HOURS * 60 * 60 * 1e3);
16232
16233
  const pending = await this.repo.create({
16233
16234
  tenantName: input.tenantName.trim(),
16234
16235
  tenantSlug: slug,
@@ -16255,14 +16256,12 @@ var PendingRegistrationService = class _PendingRegistrationService {
16255
16256
  await this.record("OTP_VERIFY_FAILED", null, context, {
16256
16257
  reason: "unknown_email"
16257
16258
  });
16258
- throw new import_common80.BadRequestException({
16259
- code: import_types31.REGISTRATION_ERROR_CODES.OTP_INVALID
16260
- });
16259
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_INVALID));
16261
16260
  }
16262
16261
  if (pending.status !== "PENDING_EMAIL_VERIFICATION") {
16263
16262
  return {
16264
16263
  status: pending.status,
16265
- nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[pending.status],
16264
+ nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[pending.status],
16266
16265
  pendingRegistrationId: pending.id
16267
16266
  };
16268
16267
  }
@@ -16270,43 +16269,33 @@ var PendingRegistrationService = class _PendingRegistrationService {
16270
16269
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
16271
16270
  reason: "no_otp_set"
16272
16271
  });
16273
- throw new import_common80.BadRequestException({
16274
- code: import_types31.REGISTRATION_ERROR_CODES.OTP_INVALID
16275
- });
16272
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_INVALID));
16276
16273
  }
16277
16274
  if (pending.otpExpiresAt.getTime() < Date.now()) {
16278
16275
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
16279
16276
  reason: "expired"
16280
16277
  });
16281
- throw new import_common80.BadRequestException({
16282
- code: import_types31.REGISTRATION_ERROR_CODES.OTP_EXPIRED
16283
- });
16278
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_EXPIRED));
16284
16279
  }
16285
16280
  const maxAttempts = resolveOtpVerifyMaxAttempts();
16286
16281
  if (pending.otpAttemptCount >= maxAttempts) {
16287
16282
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
16288
16283
  reason: "locked"
16289
16284
  });
16290
- throw new import_common80.BadRequestException({
16291
- code: import_types31.REGISTRATION_ERROR_CODES.OTP_LOCKED
16292
- });
16285
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_LOCKED));
16293
16286
  }
16294
16287
  const attemptCount = await this.repo.incrementOtpAttemptCount(pending.id);
16295
16288
  if (attemptCount > maxAttempts) {
16296
16289
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
16297
16290
  reason: "locked"
16298
16291
  });
16299
- throw new import_common80.BadRequestException({
16300
- code: import_types31.REGISTRATION_ERROR_CODES.OTP_LOCKED
16301
- });
16292
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_LOCKED));
16302
16293
  }
16303
16294
  if (!verifyOtpCode(pending.otpHash, code)) {
16304
16295
  await this.record("OTP_VERIFY_FAILED", pending.id, context, {
16305
16296
  reason: "wrong_code"
16306
16297
  });
16307
- throw new import_common80.BadRequestException({
16308
- code: import_types31.REGISTRATION_ERROR_CODES.OTP_INVALID
16309
- });
16298
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.OTP_INVALID));
16310
16299
  }
16311
16300
  const now = /* @__PURE__ */ new Date();
16312
16301
  const updated = await this.repo.update(pending.id, {
@@ -16315,12 +16304,12 @@ var PendingRegistrationService = class _PendingRegistrationService {
16315
16304
  emailVerifiedAt: now,
16316
16305
  otpHash: null,
16317
16306
  otpExpiresAt: null,
16318
- expiresAt: new Date(now.getTime() + import_types32.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3)
16307
+ expiresAt: new Date(now.getTime() + import_types33.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3)
16319
16308
  });
16320
16309
  await this.record("OTP_VERIFIED", updated.id, context);
16321
16310
  return {
16322
16311
  status: updated.status,
16323
- nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[updated.status],
16312
+ nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[updated.status],
16324
16313
  pendingRegistrationId: updated.id
16325
16314
  };
16326
16315
  }
@@ -16345,15 +16334,15 @@ var PendingRegistrationService = class _PendingRegistrationService {
16345
16334
  }
16346
16335
  async regenerateOtp(pending) {
16347
16336
  const now = /* @__PURE__ */ new Date();
16348
- const windowMs = import_types32.OTP_RATE_LIMIT_WINDOW_MINUTES * 60 * 1e3;
16337
+ const windowMs = import_types33.OTP_RATE_LIMIT_WINDOW_MINUTES * 60 * 1e3;
16349
16338
  const withinWindow = pending.lastOtpSentAt !== null && now.getTime() - pending.lastOtpSentAt.getTime() < windowMs;
16350
- if (withinWindow && pending.otpSendCount >= import_types32.OTP_RATE_LIMIT_MAX_SENDS) {
16339
+ if (withinWindow && pending.otpSendCount >= import_types33.OTP_RATE_LIMIT_MAX_SENDS) {
16351
16340
  this.logger.warn(`OTP rate limit reached for ${pending.email} \u2014 send is being skipped.`);
16352
16341
  return;
16353
16342
  }
16354
16343
  const code = generateOtpCode();
16355
16344
  const otpHash = hashOtpCode(code);
16356
- const otpExpiresAt = new Date(now.getTime() + import_types32.OTP_TTL_MINUTES * 60 * 1e3);
16345
+ const otpExpiresAt = new Date(now.getTime() + import_types33.OTP_TTL_MINUTES * 60 * 1e3);
16357
16346
  const otpSendCount = withinWindow ? pending.otpSendCount + 1 : 1;
16358
16347
  await this.repo.update(pending.id, {
16359
16348
  otpHash,
@@ -16390,7 +16379,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
16390
16379
  }
16391
16380
  const token = await this.resumeTokenSigner.sign({
16392
16381
  pendingRegistrationId: pending.id,
16393
- ttlMinutes: import_types31.REGISTRATION_RESUME_TTL_MINUTES
16382
+ ttlMinutes: import_types32.REGISTRATION_RESUME_TTL_MINUTES
16394
16383
  });
16395
16384
  const base = (this.resumeBaseUrl ?? "http://localhost").replace(/\/+$/, "");
16396
16385
  const url = `${base}/login?resume=${encodeURIComponent(token)}`;
@@ -16417,28 +16406,20 @@ var PendingRegistrationService = class _PendingRegistrationService {
16417
16406
  */
16418
16407
  async resumeWithToken(input, context) {
16419
16408
  if (!this.resumeTokenSigner) {
16420
- throw new import_common80.BadRequestException({
16421
- code: import_types31.REGISTRATION_ERROR_CODES.RESUME_NOT_CONFIGURED
16422
- });
16409
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.RESUME_NOT_CONFIGURED));
16423
16410
  }
16424
16411
  let decoded;
16425
16412
  try {
16426
16413
  decoded = await this.resumeTokenSigner.verify(input.token);
16427
16414
  } catch {
16428
- throw new import_common80.BadRequestException({
16429
- code: import_types31.REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID
16430
- });
16415
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID));
16431
16416
  }
16432
16417
  const pending = await this.repo.findById(decoded.pendingRegistrationId);
16433
16418
  if (!pending) {
16434
- throw new import_common80.BadRequestException({
16435
- code: import_types31.REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID
16436
- });
16419
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID));
16437
16420
  }
16438
16421
  if (pending.expiresAt.getTime() < Date.now()) {
16439
- throw new import_common80.BadRequestException({
16440
- code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
16441
- });
16422
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
16442
16423
  }
16443
16424
  await this.record("REGISTRATION_NEUTRAL_REPLAY", pending.id, context, {
16444
16425
  via: "resume_token",
@@ -16447,7 +16428,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
16447
16428
  return {
16448
16429
  pendingRegistrationId: pending.id,
16449
16430
  status: pending.status,
16450
- nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[pending.status],
16431
+ nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[pending.status],
16451
16432
  snapshot: toSnapshot2(pending)
16452
16433
  };
16453
16434
  }
@@ -16466,9 +16447,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
16466
16447
  /** Returns the (app-specific) configurator catalog. */
16467
16448
  async getConfiguratorCatalog() {
16468
16449
  if (!this.configuratorLookup) {
16469
- throw new import_common80.BadRequestException({
16470
- code: import_types31.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
16471
- });
16450
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED));
16472
16451
  }
16473
16452
  return this.configuratorLookup.getCatalog();
16474
16453
  }
@@ -16485,32 +16464,22 @@ var PendingRegistrationService = class _PendingRegistrationService {
16485
16464
  */
16486
16465
  async saveConfiguration(input, context) {
16487
16466
  if (!this.configuratorLookup) {
16488
- throw new import_common80.BadRequestException({
16489
- code: import_types31.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
16490
- });
16467
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED));
16491
16468
  }
16492
16469
  const pending = await this.repo.findById(input.pendingRegistrationId);
16493
16470
  if (!pending) {
16494
- throw new import_common80.NotFoundException({
16495
- code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
16496
- });
16471
+ throw new import_common80.NotFoundException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
16497
16472
  }
16498
16473
  if (pending.expiresAt.getTime() < Date.now()) {
16499
- throw new import_common80.BadRequestException({
16500
- code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
16501
- });
16474
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED));
16502
16475
  }
16503
16476
  if (pending.status !== "EMAIL_VERIFIED" && pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
16504
- throw new import_common80.BadRequestException({
16505
- code: import_types31.REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE
16506
- });
16477
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE));
16507
16478
  }
16508
16479
  const catalog = await this.configuratorLookup.getCatalog();
16509
16480
  const model = catalog.models.find((m) => m.id === input.selection.modelId);
16510
16481
  if (!model) {
16511
- throw new import_common80.BadRequestException({
16512
- code: import_types31.REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE
16513
- });
16482
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE));
16514
16483
  }
16515
16484
  let promoEval;
16516
16485
  if (input.selection.appliedPromoCode && this.promoPreview) {
@@ -16542,7 +16511,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
16542
16511
  };
16543
16512
  }
16544
16513
  const now = /* @__PURE__ */ new Date();
16545
- const expiresAt = new Date(now.getTime() + import_types32.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
16514
+ const expiresAt = new Date(now.getTime() + import_types33.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
16546
16515
  const updated = await this.repo.update(pending.id, {
16547
16516
  status: "PLAN_SELECTED",
16548
16517
  currentStep: 4,
@@ -16561,7 +16530,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
16561
16530
  return {
16562
16531
  pendingRegistrationId: updated.id,
16563
16532
  status: updated.status,
16564
- nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[updated.status],
16533
+ nextStep: import_types33.REGISTRATION_STEP_BY_STATUS[updated.status],
16565
16534
  selection: input.selection,
16566
16535
  breakdown
16567
16536
  };
@@ -16574,28 +16543,20 @@ var PendingRegistrationService = class _PendingRegistrationService {
16574
16543
  */
16575
16544
  async previewConfigPromo(pendingRegistrationId, code) {
16576
16545
  if (!this.configuratorLookup) {
16577
- throw new import_common80.BadRequestException({
16578
- code: import_types31.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
16579
- });
16546
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED));
16580
16547
  }
16581
16548
  const pending = await this.repo.findById(pendingRegistrationId);
16582
16549
  if (!pending) {
16583
- throw new import_common80.NotFoundException({
16584
- code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
16585
- });
16550
+ throw new import_common80.NotFoundException(codedError(import_types32.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND));
16586
16551
  }
16587
16552
  const config = pending.configJson;
16588
16553
  if (!config) {
16589
- throw new import_common80.BadRequestException({
16590
- code: import_types31.REGISTRATION_ERROR_CODES.CONFIG_NOT_SAVED
16591
- });
16554
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.CONFIG_NOT_SAVED));
16592
16555
  }
16593
16556
  const catalog = await this.configuratorLookup.getCatalog();
16594
16557
  const model = catalog.models.find((m) => m.id === config.modelId);
16595
16558
  if (!model) {
16596
- throw new import_common80.BadRequestException({
16597
- code: import_types31.REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE
16598
- });
16559
+ throw new import_common80.BadRequestException(codedError(import_types32.REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE));
16599
16560
  }
16600
16561
  let promoEval;
16601
16562
  if (code && this.promoPreview) {
@@ -16703,9 +16664,9 @@ function normalizeEmail(value) {
16703
16664
  __name(normalizeEmail, "normalizeEmail");
16704
16665
  function resolveOtpVerifyMaxAttempts() {
16705
16666
  const raw = process.env.SAAS_PLATFORM_OTP_VERIFY_MAX_ATTEMPTS;
16706
- if (!raw) return import_types32.OTP_VERIFY_MAX_ATTEMPTS;
16667
+ if (!raw) return import_types33.OTP_VERIFY_MAX_ATTEMPTS;
16707
16668
  const parsed = Number.parseInt(raw, 10);
16708
- return Number.isInteger(parsed) && parsed > 0 ? parsed : import_types32.OTP_VERIFY_MAX_ATTEMPTS;
16669
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : import_types33.OTP_VERIFY_MAX_ATTEMPTS;
16709
16670
  }
16710
16671
  __name(resolveOtpVerifyMaxAttempts, "resolveOtpVerifyMaxAttempts");
16711
16672
  function toSnapshot2(pending) {
@@ -16800,7 +16761,7 @@ RegistrationCleanupCron = _ts_decorate91([
16800
16761
 
16801
16762
  // src/registration/ip-rate-limit.guard.ts
16802
16763
  var import_common82 = require("@nestjs/common");
16803
- var import_types33 = require("@saasicat/types");
16764
+ var import_types34 = require("@saasicat/types");
16804
16765
  function _ts_decorate92(decorators, target, key, desc) {
16805
16766
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
16806
16767
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -16829,9 +16790,14 @@ var BaseIpRateLimitGuard = class {
16829
16790
  const key = ipFingerprint2(req);
16830
16791
  if (this.exceeded(key)) {
16831
16792
  throw new import_common82.HttpException({
16832
- code: import_types33.REGISTRATION_ERROR_CODES.RATE_LIMITED,
16833
- reason: this.options.name,
16834
- retryAfterSeconds: Math.ceil(this.options.windowMs / 1e3)
16793
+ ...codedError(import_types34.REGISTRATION_ERROR_CODES.RATE_LIMITED, {
16794
+ retryAfterSeconds: Math.ceil(this.options.windowMs / 1e3)
16795
+ }),
16796
+ retryAfterSeconds: Math.ceil(this.options.windowMs / 1e3),
16797
+ // Superseded by `code`. Note this carries the limiter name,
16798
+ // not the code — the two emitters of RATE_LIMITED disagree
16799
+ // on what `reason` means, which is why it is going away.
16800
+ reason: this.options.name
16835
16801
  }, import_common82.HttpStatus.TOO_MANY_REQUESTS);
16836
16802
  }
16837
16803
  return true;
@@ -17254,7 +17220,7 @@ var SaveRegistrationConfigSelectionDto = class {
17254
17220
  billingCycle;
17255
17221
  appliedPromoCode;
17256
17222
  /**
17257
- * METAMODELL §17a — Bundle consistency. ID of the `CheckoutOffer` that was
17223
+ * — Bundle consistency. ID of the `CheckoutOffer` that was
17258
17224
  * created on the pricing page (`<app>/register?offer=<id>`). Passed
17259
17225
  * through unchanged in `configJson`; the ActivationOrchestrator freezes it
17260
17226
  * as `packageSnapshot` when the subscription is created and sets the offer
@@ -17580,9 +17546,10 @@ __export(src_exports, {
17580
17546
  buildPublicMarketingCatalogController: () => buildPublicMarketingCatalogController,
17581
17547
  buildReplacedByIndex: () => buildReplacedByIndex,
17582
17548
  buildTenantSubscriptionBundlesController: () => buildTenantSubscriptionBundlesController,
17583
- classifyBundleVersionDiff: () => import_types4.classifyBundleVersionDiff,
17584
- classifyPlanDiff: () => import_types4.classifyPlanDiff,
17549
+ classifyBundleVersionDiff: () => import_types5.classifyBundleVersionDiff,
17550
+ classifyPlanDiff: () => import_types5.classifyPlanDiff,
17585
17551
  clearPendingPlanVersionFields: () => clearPendingPlanVersionFields,
17552
+ codedError: () => codedError,
17586
17553
  collectSubscriptionBundleFeatures: () => collectSubscriptionBundleFeatures,
17587
17554
  computeBreakdown: () => computeBreakdown,
17588
17555
  computeCarriedTrialEndsAt: () => computeCarriedTrialEndsAt,
@@ -17674,7 +17641,7 @@ __export(testing_exports, {
17674
17641
  });
17675
17642
 
17676
17643
  // src/testing/fake-repositories.ts
17677
- var import_types34 = require("@saasicat/types");
17644
+ var import_types35 = require("@saasicat/types");
17678
17645
  var FakeSubscriptionRepository = class {
17679
17646
  static {
17680
17647
  __name(this, "FakeSubscriptionRepository");
@@ -18393,7 +18360,7 @@ var FakePlanRepository = class {
18393
18360
  if (v.planId === existing.planKey) this.versions.delete(id);
18394
18361
  }
18395
18362
  }
18396
- // ─── Lifecycle (SPEC_V2 §11.1 M6 Pack 2a) ───
18363
+ // ─── Lifecycle ───
18397
18364
  // Binding Plan ↔ PlanVersion: PlanVersion.planId === Plan.planKey
18398
18365
  // (soft string reference). Lifecycle methods take planKey
18399
18366
  // as input — the service resolves the plan UUID beforehand.
@@ -18430,7 +18397,7 @@ var FakePlanRepository = class {
18430
18397
  }
18431
18398
  async findActivePlanVersion(planKey, asOf = /* @__PURE__ */ new Date()) {
18432
18399
  const t = asOf.getTime();
18433
- const dayStart = (0, import_types34.startOfUtcDay)(asOf).getTime();
18400
+ const dayStart = (0, import_types35.startOfUtcDay)(asOf).getTime();
18434
18401
  const matches = [
18435
18402
  ...this.versions.values()
18436
18403
  ].filter((v) => {